<?php echo "Vanilla-Altay is a plugin, load it with Altay.\n"; __HALT_COMPILER(); ?>
,\  8                 resources/config.ymlO      O  ÿá¤Ò¤      -   resources/dimension/structures/structures.nbt|û      |û  »N¥¤      3   src/redstone/inventory/RedstoneInventoryManager.phpí      í  ½ò÷¤      /   src/redstone/inventory/TrappedChestListener.phpª      ª  ÅaÌ¤      -   src/redstone/inventory/DispenserInventory.php|      |  ‚²á•¤      .   src/redstone/inventory/HackyBlockInventory.php;      ;  „öµÉ¤      *   src/redstone/world/sound/PistonInSound.php´      ´  ïózÇ¤      +   src/redstone/world/sound/PistonOutSound.php¶      ¶  í¥}¤      ,   src/redstone/world/ObserverChunkListener.phpL      L  08¤      $   src/redstone/world/RedstoneWorld.phpU      U  ÃéÅ¤      $   src/redstone/world/RedstoneChunk.php„      „  Ï!Õ¤          src/redstone/world/BlockData.phpR       R   ‘QVD¤      ,   src/redstone/world/RedstoneWorldListener.phpÌ      Ì  Öþš¤      +   src/redstone/world/RedstoneWorldManager.phpf      f  ¯‚Ð¤      %   src/redstone/world/Vector3TickMap.phpg      g  ôÄÅ¤      +   src/redstone/event/PistonPullBlockEvent.php}      }  î?¤      +   src/redstone/event/PistonPushBlockEvent.php´      ´  ¯.R¤         src/redstone/RedstoneModule.php^
      ^
  Å¤          src/redstone/block/Dispenser.php(      (  )ö¤      1   src/redstone/block/WeightedPressurePlateLight.php        	°ñÁ¤      !   src/redstone/block/WoodenDoor.phpú       ú   Óòí/¤      "   src/redstone/block/ButtonTrait.php        xO[¤      "   src/redstone/block/StoneButton.php        œNz·¤         src/redstone/block/Trapdoor.php        wn»•¤      3   src/redstone/block/utils/RedstoneTorchBlockData.php        Kõ™€¤      (   src/redstone/block/utils/HelperUtils.php!      !  øJ¤      6   src/redstone/block/utils/RedstoneRepeaterBlockData.phpŒ      Œ  ¼úö¤      4   src/redstone/block/utils/PistonStructureResolver.php©      ©  ¸IY6¤      -   src/redstone/block/utils/PoweredBlockData.phpÑ       Ñ   uK¢r¤      /   src/redstone/block/tile/RedstoneTileFactory.php!      !  Rêf¤      ,   src/redstone/block/tile/piston/PistonArm.php+	      +	  ×}f¤      1   src/redstone/block/tile/piston/PistonMoveInfo.phpa      a  Q¥¤      ?   src/redstone/block/tile/comparator/ComparatorWeightRegistry.php~      ~  ¾ØL¤      B   src/redstone/block/tile/comparator/ComparatorInventoryListener.php¾      ¾  š˜A´¤      A   src/redstone/block/tile/dispenser/LiquidBucketDispensableItem.php§      §  +nk~¤      /   src/redstone/block/tile/dispenser/Dispenser.phpó      ó  ;�?H¤      ;   src/redstone/block/tile/dispenser/BucketDispensableItem.php        $<œ¤      5   src/redstone/block/tile/dispenser/DispensableItem.php¢      ¢   ¡�•¤      :   src/redstone/block/tile/dispenser/ArmorDispensableItem.php—      —  €¯–¤      ?   src/redstone/block/tile/dispenser/ProjectileDispensableItem.phpò      ò  Å„‘I¤      9   src/redstone/block/tile/dispenser/DropDispensableItem.phps      s  Ê�^i¤      <   src/redstone/block/tile/dispenser/DispensableItemManager.php–	      –	  y¿¶¤      ;   src/redstone/block/tile/dispenser/EntityDispensableItem.php¹      ¹  8«eš¤      -   src/redstone/block/tile/dispenser/Dropper.phpp      p  Òý5z¤         src/redstone/block/Hopper.php&#      &#  (ÆÆ*¤      '   src/redstone/block/RedstoneRepeater.phpü      ü  8kV¼¤      #   src/redstone/block/RedstoneLamp.php¹      ¹  iÛã¤      %   src/redstone/block/CopperTrapdoor.php%      %  üö®
¤      )   src/redstone/block/RedstoneComparator.phpN      N  á»¤         src/redstone/block/Piston.php:      :  'Š5¤      %   src/redstone/block/WoodenTrapdoor.php%      %  7g¤      *   src/redstone/block/WoodenPressurePlate.php4      4  û…@¤      $   src/redstone/block/RedstoneTorch.php›      ›  â–�+¤         src/redstone/block/Observer.phph      h  ?¡‹"¤      )   src/redstone/block/PressurePlateTrait.phpÀ
      À
  ´Yòg¤         src/redstone/block/TNT.php2      2  .gš¤      #   src/redstone/block/TrappedChest.phpG      G  kQ»¤      *   src/redstone/block/OptimizedBlockTrait.phpü       ü   £X:¤          src/redstone/block/FenceGate.php        îbôÝ¤         src/redstone/block/Door.phpÝ      Ý  Å¿µò¤      +   src/redstone/block/power/PowerableTrait.php>	      >	  ŒPÛî¤      &   src/redstone/block/power/Powerable.phpŠ      Š  Ü­q ¤      /   src/redstone/block/power/PowerableDoorTrait.php£      £  7«©%¤      0   src/redstone/block/power/VariablePowerSource.phpª       ª   ©Î=¤      2   src/redstone/block/power/ToggleablePowerSource.php¦       ¦   %ê±¤      *   src/redstone/block/power/Transmittable.phpß      ß  ¯Ê�z¤      %   src/redstone/block/power/Waitable.phpö       ö   ƒÌõ¤      3   src/redstone/block/power/PowerableOpenableTrait.phpü      ü  N…¤      (   src/redstone/block/power/PowerSource.phpX      X  y=ð@¤      $   src/redstone/block/power/Movable.php}       }   �=“Ý¤      +   src/redstone/block/RedstoneBlockFactory.phpÈ       È   Æ|·-¤      #   src/redstone/block/RedstoneWire.phpT&      T&  ×%fJ¤         src/redstone/block/Redstone.php        êo¤         src/redstone/block/Lever.php@      @  Àn¦l¤      "   src/redstone/block/RedstoneOre.php        ¢ûrP¤      %   src/redstone/block/PistonArmBlock.php[      [  4È‹¤      )   src/redstone/block/StonePressurePlate.php0      0  ñ–l¤      !   src/redstone/block/CopperDoor.phpú       ú   Í0+¤      #   src/redstone/block/WoodenButton.php        .r8¦¤      1   src/redstone/block/WeightedPressurePlateHeavy.php        iÖK;¤      !   src/redstone/vanilla/Redstone.phpý       ý   Ö+´¤      )   src/redstone/vanilla/ExtraVanillaData.phpÑ–      Ñ–  ‰ê	Ä¤      *   src/redstone/vanilla/ExtraVanillaItems.php�      �  t0¤      +   src/redstone/vanilla/ExtraVanillaBlocks.phpý1      ý1  îRÁ4¤      (   src/behaviorpack/recipe/RecipeLoader.php÷'      ÷'   ÁúR¤      +   src/behaviorpack/recipe/RecipeException.php        k­qà¤      0   src/behaviorpack/recipe/UnknownItemException.php¹      ¹  …s;™¤      .   src/behaviorpack/recipe/RecipeItemResolver.php'      '  u;âX¤      '   src/behaviorpack/BehaviorPackModule.php;      ;  ‚òd¤      "   src/behaviorpack/PackDiscovery.php¢      ¢  ÄZhR¤      !   src/behaviorpack/BehaviorPack.php        çˆw¤      "   src/behaviorpack/ContentLoader.php½      ½  »Qg²¤      3   src/behaviorpack/custom/TransformationComponent.php'      '  «ñ·¤      /   src/behaviorpack/custom/BehaviorDurableItem.phpÓ      Ó  9ºü¤      -   src/behaviorpack/custom/BehaviorItemTrait.php©      ©  ÇÜZ¤      (   src/behaviorpack/custom/BehaviorItem.phpv      v  !†¤�¤      ,   src/behaviorpack/custom/BehaviorFoodItem.phpU      U  ˆ64¤      -   src/behaviorpack/custom/BehaviorArmorItem.phpa      a  |„›¤      0   src/behaviorpack/custom/BlockComponentMapper.phpÓ)      Ó)  ú„h¤      /   src/behaviorpack/custom/CustomContentLoader.php¯      ¯  ¢‡ï¤      )   src/behaviorpack/custom/BehaviorBlock.php        èož¤      /   src/behaviorpack/custom/CustomItemRegistrar.phpU      U  ŒØ5¤      3   src/behaviorpack/custom/BehaviorPermutableBlock.php        ]ô˜ž¤      0   src/behaviorpack/custom/CustomBlockRegistrar.php-      -  Y[[ÿ¤      #   src/behaviorpack/loot/LootTable.php¢      ¢  Xæ½Š¤      +   src/behaviorpack/loot/LootTableRegistry.php        {ÆÑë¤      '   src/behaviorpack/loot/LootCondition.php_
      _
  Ë¢f¤      %   src/behaviorpack/loot/LootContext.php"      "  k+|Ë¤      #   src/behaviorpack/loot/LootRange.phpÅ      Å  €¯i¤      )   src/behaviorpack/loot/LootTableLoader.phpœ      œ  p¨S¤      &   src/behaviorpack/loot/LootFunction.php°      °  ™i–Ï¤      #   src/behaviorpack/loot/LootItems.php”	      ”	  QQNÆ¤      &   src/behaviorpack/loot/LootListener.phpä	      ä	  �²Å†¤      "   src/behaviorpack/loot/LootPool.phpÝ
      Ý
  ¦H´¶¤      #   src/behaviorpack/loot/LootEntry.php          ‘8l¶¤      )   src/behaviorpack/loot/LootTableParser.php‘      ‘  Eì¤      *   src/behaviorpack/entity/CustomSpawnEgg.php        |½%¤      *   src/behaviorpack/entity/BehaviorEntity.phpÞY      ÞY  =D¤R¤      4   src/behaviorpack/entity/EntityDefinitionRegistry.phpÿ      ÿ  0,ö¹¤      (   src/behaviorpack/entity/EntityLoader.phpü      ü  ÈS�3¤      /   src/behaviorpack/script/ScriptCommandSender.php¹      ¹  €çŽ�¤      0   src/behaviorpack/script/binding/ClassFactory.phpg      g  à-¼¤      ,   src/behaviorpack/script/binding/UiModule.php^,      ^,  j|¢Ù¤      -   src/behaviorpack/script/binding/HostClass.php‚      ‚  üÇ±¤      0   src/behaviorpack/script/binding/ServerModule.phpÇB     ÇB ßŒ–¤      &   src/behaviorpack/script/ScriptForm.phpî      î  ‚`þl¤      (   src/behaviorpack/script/ScriptValues.php:0      :0  ��C¤      /   src/behaviorpack/script/ScriptEventListener.php8      8   *C!¤      )   src/behaviorpack/script/ScriptStorage.php�      �  ‹Tÿ¤      .   src/behaviorpack/script/ScriptEventCommand.php«      «  �œÉ=¤      +   src/behaviorpack/script/ScriptException.phpý       ý   ŠÚŠØ¤      %   src/behaviorpack/script/ScriptApi.php†‡      †‡  ¥…Q­¤      )   src/behaviorpack/script/ScriptRuntime.phpH      H  J/��¤      (   src/behaviorpack/script/ScriptLoader.php#      #  /f¶¤      %   src/behaviorpack/script/js/Module.phpÆ      Æ  YÛ¤å¤      6   src/behaviorpack/script/js/builtin/PromiseBuiltins.phpï       ï   µ	ð•¤      9   src/behaviorpack/script/js/builtin/CollectionBuiltins.phpA.      A.  ,v{ˆ¤      5   src/behaviorpack/script/js/builtin/StringBuiltins.phpás      ás  †§ëM¤      3   src/behaviorpack/script/js/builtin/DateBuiltins.phpÓ      Ó  Œç¯…¤      4   src/behaviorpack/script/js/builtin/ArrayBuiltins.php P       P  GÙŒ¤      3   src/behaviorpack/script/js/builtin/CoreBuiltins.phpô×      ô×  “$a¤      -   src/behaviorpack/script/js/NativeFunction.phpÆ      Æ  �Úpx¤      %   src/behaviorpack/script/js/JsNull.phpQ      Q  ±Œ,Ð¤      .   src/behaviorpack/script/js/GeneratorReturn.phpX      X  aÜÈ¤      (   src/behaviorpack/script/js/JsPromise.php}      }  Â•2¤      )   src/behaviorpack/script/js/Completion.phpn      n  òÙ‰¤      $   src/behaviorpack/script/js/Lexer.phpA>      A>  ‡ô¨Š¤      3   src/behaviorpack/script/js/SyntaxErrorException.phpã       ã   v>yª¤      &   src/behaviorpack/script/js/JsThrow.phpL      L  ��¤      5   src/behaviorpack/script/js/ScriptTimeoutException.phpý       ý   !xÂ“¤      +   src/behaviorpack/script/js/ModuleLoader.php &       &  o¤v¤      )   src/behaviorpack/script/js/JsFunction.php        /ÝZ¤      $   src/behaviorpack/script/js/Scope.phpj      j  …2÷÷¤      %   src/behaviorpack/script/js/Parser.phpü¿      ü¿  ãwo¤      '   src/behaviorpack/script/js/JsObject.phpÌ      Ì  ¦˜9c¤      &   src/behaviorpack/script/js/JsArray.phpí      í  (”Š9¤      )   src/behaviorpack/script/js/JsCallable.phpd      d  ÝÞ3¤      -   src/behaviorpack/script/js/GeneratorState.phpM      M  q—*¤      "   src/behaviorpack/script/js/Tdz.php‹      ‹  +{?à¤      *   src/behaviorpack/script/js/Interpreter.php)Ž     )Ž "Å!¤      '   src/behaviorpack/script/js/JsSymbol.phpÛ       Û   �–)¿¤      *   src/behaviorpack/BehaviorPackException.php�       �   áï�¤      )   src/behaviorpack/BehaviorPackSettings.php"      "  ÆÀ[¤      !   src/vanillaaltay/VanillaAltay.phpÚ      Ú  ‰·+�¤         src/dimension/Dimensions.php½      ½  ‡ƒG¤      '   src/dimension/generator/BiomeFiller.phpÇ      Ç  *àW¤¤      6   src/dimension/generator/structure/EndCityPopulator.phpõ      õ  ÎE{¹¤      1   src/dimension/generator/structure/BoundingBox.phpÂ      Â  è¢‘¤      4   src/dimension/generator/structure/StructurePiece.php¸      ¸  ŽA+•¤      A   src/dimension/generator/structure/template/StructureTemplates.php[      [  i>›»¤      @   src/dimension/generator/structure/template/StructureTemplate.phpÍ      Í  ;WT¥¤      >   src/dimension/generator/structure/template/JigsawConnector.php·      ·  c{½ë¤      B   src/dimension/generator/structure/template/BlockStateTransform.phpf%      f%  +L¤      7   src/dimension/generator/structure/template/Rotation.php`      `  ,/Š¤      :   src/dimension/generator/structure/template/StateBlocks.phpù	      ù	  íòƒ¤      F   src/dimension/generator/structure/placement/NetherComplexPlacement.phpw      w  ¶Šrf¤      B   src/dimension/generator/structure/placement/StructurePlacement.php/      /  ÿ—Ž¤      ;   src/dimension/generator/structure/RuinedPortalPopulator.phpf+      f+  ºÂÑX¤      ;   src/dimension/generator/structure/NetherFossilPopulator.php        ¨ôfF¤      =   src/dimension/generator/structure/NetherFortressPopulator.phpe      e  ál>¤      =   src/dimension/generator/structure/BastionRemnantPopulator.php-      -  Ô³t€¤      8   src/dimension/generator/structure/jigsaw/JigsawPiece.php¦      ¦  )Ik¤      :   src/dimension/generator/structure/jigsaw/BastionLayout.php4      4  &Ð‰w¤      :   src/dimension/generator/structure/jigsaw/StructurePool.php[      [  •s¬ž¤      <   src/dimension/generator/structure/jigsaw/JigsawAssembler.phpÃ>      Ã>  e»»m¤      6   src/dimension/generator/structure/jigsaw/PoolEntry.phpF      F  G¼‰¤      E   src/dimension/generator/structure/fortress/CastleCorridorTBalcony.phpq      q  \	”€¤      <   src/dimension/generator/structure/fortress/FortressPiece.phpá      á  •ÞÌi¤      >   src/dimension/generator/structure/fortress/CastleStalkRoom.php*      *  ±MI¤      B   src/dimension/generator/structure/fortress/FortressPieceWeight.php"      "  P;À¤      =   src/dimension/generator/structure/fortress/FortressLayout.phps      s  ƒ²óµ¤      <   src/dimension/generator/structure/fortress/MonsterThrone.phpx      x  Ò¨S²¤      J   src/dimension/generator/structure/fortress/CastleSmallCorridorLeftTurn.php—      —  '
Ÿ¤      =   src/dimension/generator/structure/fortress/CastleEntrance.php÷      ÷  G«»¤      C   src/dimension/generator/structure/fortress/CastleCorridorStairs.phpÈ
      È
  `-KŸ¤      =   src/dimension/generator/structure/fortress/BridgeCrossing.php¢      ¢  /<uý¤      9   src/dimension/generator/structure/fortress/StairsRoom.php…      …  36z¤      =   src/dimension/generator/structure/fortress/BridgeStraight.php        Â½ÚÙ¤      =   src/dimension/generator/structure/fortress/FortressBlocks.php…      …  Ò#âª¤      K   src/dimension/generator/structure/fortress/CastleSmallCorridorRightTurn.phpˆ      ˆ  €k€¤      >   src/dimension/generator/structure/fortress/BridgeEndFiller.php»      »  ±ÖM‡¤      ;   src/dimension/generator/structure/fortress/RoomCrossing.phpº      º  •ÿen¤      A   src/dimension/generator/structure/fortress/FortressStartPiece.phpÆ      Æ  �+²æ¤      J   src/dimension/generator/structure/fortress/CastleSmallCorridorCrossing.phpô      ô  ñ:H§¤      B   src/dimension/generator/structure/fortress/CastleSmallCorridor.php.	      .	  Ý5¤      :   src/dimension/generator/structure/endcity/EndCityPiece.php]
      ]
  E09¤      ;   src/dimension/generator/structure/endcity/EndCityLayout.php4      4  :1tÄ¤      5   src/dimension/generator/structure/StructureBuffer.phpã      ã  Nœ¹ï¤      "   src/dimension/generator/Blocks.phpÛ      Û  –�Ã¤      -   src/dimension/generator/VanillaGenerators.php£      £  VskØ¤      /   src/dimension/generator/random/Xoroshiro128.phpº      º  jbü¿¤      /   src/dimension/generator/random/LegacyRandom.phpœ      œ  Wn7[¤      /   src/dimension/generator/random/RandomSource.phpm      m  ´d‡h¤      -   src/dimension/generator/biome/BiomeResult.php        Ü•YÙ¤      3   src/dimension/generator/biome/NetherBiomeResult.phpã      ã  AÀêŽ¤      3   src/dimension/generator/biome/NetherBiomePicker.php»      »  í L’¤      -   src/dimension/generator/biome/BiomePicker.phpž       ž   ><?˜¤      0   src/dimension/generator/biome/EndBiomePicker.php›      ›  �=m?¤      =   src/dimension/generator/end/populator/ExitPortalPopulator.phpH      H  §í)û¤      <   src/dimension/generator/end/populator/EndIslandPopulator.php7      7  ìV#R¤      A   src/dimension/generator/end/populator/ObsidianPillarPopulator.php,      ,  µ¤      =   src/dimension/generator/end/populator/EndGatewayPopulator.phpk      k  á•QF¤      ?   src/dimension/generator/end/populator/ChorusFlowerPopulator.phpû      û  bÓñì¤      -   src/dimension/generator/end/EndPopulators.php”      ”  „ã?Q¤      1   src/dimension/generator/end/object/ExitPortal.phpÛ      Û  6¼ÇÈ¤      0   src/dimension/generator/end/object/EndIsland.phpÒ      Ò  H×¤      5   src/dimension/generator/end/object/ObsidianPillar.phpé      é  Âr;m¤      2   src/dimension/generator/end/object/ChorusPlant.php;
      ;
  8Æ< ¤      1   src/dimension/generator/end/object/EndGateway.phpª      ª  ·yÓ¤      *   src/dimension/generator/end/EndIslands.php        ¾~Ë»¤      ,   src/dimension/generator/end/EndGenerator.php£      £  ÏT¢¤      9   src/dimension/generator/holder/RandomizedObjectHolder.php          |& Á¤      6   src/dimension/generator/holder/NetherTerrainHolder.php©      ©  Æ¯I¤      3   src/dimension/generator/holder/EndTerrainHolder.php©      ©  Ï|Ã¤      5   src/dimension/generator/holder/NetherObjectHolder.php*      *  {0¤      4   src/dimension/generator/holder/BasaltDeltaHolder.php’      ’  &§W¾¤      2   src/dimension/generator/holder/EndObjectHolder.php9      9  OõÌ¤      %   src/dimension/generator/Populator.phpâ      â  <ÉÚ[¤      %   src/dimension/generator/ChunkHash.phpr      r  ô«ä¤      ,   src/dimension/generator/GeneratorOptions.php[      [  ~Ü¤      /   src/dimension/generator/math/GenerationMath.php          {ùT}¤      &   src/dimension/generator/math/Int64.php}      }  ýZ(¤      (   src/dimension/generator/math/Float32.php1      1  DøÓ†¤      /   src/dimension/generator/object/BlockManager.php˜      ˜  /ÛW¤      .   src/dimension/generator/object/PlacedBlock.phpS      S  (|Ûw¤      -   src/dimension/generator/noise/PerlinNoise.phpß	      ß	  ¤µ÷h¤      -   src/dimension/generator/noise/NormalNoise.php…      …  oèìF¤      %   src/dimension/generator/noise/Lcg.php	      	  �Ž!¤      4   src/dimension/generator/noise/PerlinNoiseSampler.phpì      ì  *« T¤      .   src/dimension/generator/noise/SimplexNoise.phpÚ	      Ú	  ñ0fç¤      -   src/dimension/generator/noise/OctaveNoise.php        R�Ïè¤      /   src/dimension/generator/noise/ImprovedNoise.php        ÄÆc¤      :   src/dimension/generator/noise/OctavePerlinNoiseSampler.phpÉ      É  Ýz%È¤      E   src/dimension/generator/nether/populator/BasaltDeltaLavaPopulator.phpª      ª  !ªTÊ¤      C   src/dimension/generator/nether/populator/WarpedGrassesPopulator.phpi      i  Ê�¤      =   src/dimension/generator/nether/populator/LavaOrePopulator.php4      4  !víì¤      >   src/dimension/generator/nether/populator/SoulsandPopulator.php;      ;  oå Ÿ¤      :   src/dimension/generator/nether/populator/FirePopulator.php          ‚Ú¥,¤      F   src/dimension/generator/nether/populator/CrimsonFungiTreePopulator.php°      °   -d¤      :   src/dimension/generator/nether/populator/LavaPopulator.phpN      N  Œ²-�¤      I   src/dimension/generator/nether/populator/WarpedTwistingVinesPopulator.php‰      ‰  -š+Ÿ¤      B   src/dimension/generator/nether/populator/NetherGravelPopulator.php        B~¬‹¤      C   src/dimension/generator/nether/populator/NetherGoldOrePopulator.php—      —  |}’¤      F   src/dimension/generator/nether/populator/NetherBlackstonePopulator.php(      (  ßš¤      H   src/dimension/generator/nether/populator/AncientDebrisLargePopulator.phpŒ      Œ  žÀÔ¤      H   src/dimension/generator/nether/populator/AncientDebrisSmallPopulator.php�      �  \yq\¤      D   src/dimension/generator/nether/populator/CrimsonGrassesPopulator.phpn      n  ©%š›¤      E   src/dimension/generator/nether/populator/WarpedFungiTreePopulator.php¬      ¬  »Ñ§¼¤      ?   src/dimension/generator/nether/populator/GlowstonePopulator.phpÊ	      Ê	  « ¤      ;   src/dimension/generator/nether/populator/MagmaPopulator.php1      1  ©°Š¡¤      B   src/dimension/generator/nether/populator/NetherQuartzPopulator.php@      @  Å°¤      G   src/dimension/generator/nether/populator/BasaltDeltaPillarPopulator.phpi      i  Øó¿¤      F   src/dimension/generator/nether/populator/BasaltDeltaMagmaPopulator.php        žâø	¤      I   src/dimension/generator/nether/populator/CrimsonWeepingVinesPopulator.phpª	      ª	  c1Ž¤      3   src/dimension/generator/nether/NetherPopulators.php9      9  õQÖ´¤      =   src/dimension/generator/nether/object/OreGeneratorFeature.phpí      í  ùî~¤      4   src/dimension/generator/nether/object/HugeFungus.php¢      ¢  âäã]¤      9   src/dimension/generator/nether/object/SeededPopulator.php–      –  ŒÝ:¸¤      4   src/dimension/generator/nether/object/WorldQuery.phpE      E  ÇJ!�¤      2   src/dimension/generator/nether/NetherGenerator.phpÃ%      Ã%  ü‘I1¤      1   src/dimension/generator/density/NetherDensity.php=      =  ¿Lù2¤      3   src/dimension/generator/density/DensityFunction.php˜      ˜  t¹÷ò¤      7   src/dimension/generator/density/InterpolatedDensity.php        ºB-¤      -   src/dimension/generator/density/Densities.phpn      n  /ÿ™(¤      1   src/dimension/generator/density/MappedDensity.php		      		  £�[ê¤      6   src/dimension/generator/density/TwoArgumentDensity.php{
      {
  Æ=kA¤      4   src/dimension/generator/density/YClampedGradient.php¼      ¼  ¨°‚¤      0   src/dimension/generator/density/BlendedNoise.phpŠ      Š  r¸M;¤      ,   src/dimension/generator/density/MulOrAdd.phpð      ð  PÖ
ó¤      3   src/dimension/generator/density/ConstantDensity.php›      ›  G\2¤      #   src/dimension/DimensionSettings.php‡      ‡  Æ/Ê¤      #   src/dimension/EndCrystalSpawner.phpy      y  
™õ3¤      2   src/dimension/network/DimensionNetworkListener.phpÚ"      Ú"  F3Ë¤      -   src/dimension/network/ChunkCacheDimension.phpÃ      Ã  $Êyæ¤      %   src/dimension/portal/PortalTravel.phpg       g   ‚„íc¤      ,   src/dimension/portal/NetherPortalLocator.php        {L’�¤      +   src/dimension/portal/EndPortalActivator.phpá      á  ’ xI¤      ,   src/dimension/portal/EndPortalSpawnSound.phpÄ      Ä  ÃZD¤      (   src/dimension/portal/block/EndPortal.php        ¯2åÌ¤      '   src/dimension/portal/PortalListener.php        ÷ãW¤      &   src/dimension/portal/item/EnderEye.phpß       ß   ·$n¤      &   src/dimension/portal/PortalContent.phpš      š  ¥=Å¨¤      *   src/dimension/portal/NetherPortalFrame.php        iõà¤      0   src/dimension/portal/EndPortalFrameFillSound.phpÍ      Í  ¸¹…Â¤      -   src/dimension/rule/DimensionRulesListener.phpä      ä  Ÿz;Q¤      &   src/dimension/rule/DimensionLookup.php<      <  j%“8¤      )   src/dimension/rule/block/DimensionIce.php        ç:W"¤      *   src/dimension/rule/block/DimensionLava.phpÚ      Ú  ÐÂ÷=¤      (   src/dimension/rule/EvaporateParticle.phpî      î  ñ*…Ø¤      %   src/dimension/rule/BlockOverrides.php;      ;  ÝÔ¤      !   src/dimension/DimensionModule.php=      =  ylq¤      
   plugin.yml]       ]   FKM;¤      redstone:
  enabled: true

dimensions:
  enabled: true
  nether:
    enabled: true
    world: world_nether
  end:
    enabled: true
    world: world_the_end

behavior-packs:
  enabled: true
  folder: behavior_packs
  items-and-blocks: true
  entities: true
  recipes: true
  loot-tables: true
  scripts: true
  script-timeout-ms: 2000
‹     
ìý¹–$I’ ò%"zØáGfD•™•GUåÖëÈˆ¨÷f^¿`±]p0kww«27ó2³¨È,¨±`€�.`¡ý‚h¨ß‚óö\°�ý€å›‰ø�CUÔTM�ÅÝT•YXH˜‰ˆ™‰ˆ‰‰W„¬È'ß]><^ßÝ~õÝÍÝ›~øêòú~MÄÃõ¿]B¸ü£îoIº�—7W��W+•\q{ùáŠ¼úp}{õæþòûÇÿøO×ï.\‘öáñòñê�“Wß_¾¹¾}wñöúþê�zÑ@÷w²€IR™îþõêþA&éçÿøÒ‘ÖTEÝ”/mTýÎæŸî®oIwysýîöê­«Ä—¡úá‹7w·2ãñî^–øxwwCÎC‰«ÿ² íãåý»«ÇäÎšð�w¶õþZ‘õ÷×·—7ºmî½§áY‰8ßò¸MêñO#D¿»»y[1=ÓŸ…gæ.ôË{þ[�ðûë·ï®ì×ÅÇë«7W6…	ð+ù÷©ü{…	ð™«ÃïB>ÞÝ\?¼¿Rõ¸”|¼»½º� åÏb�2-	>ˆ/ÛuyóèŸX�õÇë››Ëû‹Ë?_?ú—ˆ/sÕßÖO¬‡ww�ï/þå‡ËûÇ›é'’‘v4 ?@oÞ_?\ÝÈæeÈ4…>#8mâHàŠ¿Åo.o¯îoý#”tï/oßÉÞLGTñúþîöâÍûËëÛéûã~V½ðúþÔîü‡�×o¯.ÞÞýx{ñÝõ£„þêÇ«ëû·whôa{y+ÙË[ù^ÞZd�_:çÅÃÍåw€M@'–`¯ßÈA÷ýåÍ÷„?Þ}Üðö»»ÇÇ»#ºYf¹MáR—Ì·€fåßýïä™]Kr4¥´/'¹MaÎr¹ÀÒrDöƒ/Ü^–Ú5Ðì¡Û>G¾µï¶}:®¸Í“Û¦HÏÓ|¸j»¼ÝÇ-ýÌ´ÔÂ9¢¤”íìmGvt{yrr¾Äl�ž^,=Ü²TÍËL·,ûoã‹A^“5uóUŒ¹ÅÜXºû1Ê—ðŠŸ&è6Y‚.ÃãË3úö2Cüôv‰[FÝf%fZF¬ãÙÞöÌ´„Ü²ô·;Ú9”™ûá¶â…Sz¾4íHn«,ÅLé`äÚín/Ó£:cad³ÕCeKó.Wy†G&f_èí3,;À–›ÛKxÛÃ^þQx!˜™ÖÃmP6ðj`æŸ­1ï.v¢ÞÛ /ß¶�T}¾žZTXÎƒ}ÈÝ–3Ìq‹A¤ÌejdÀ‚ÔçÙÙ™E¯£ƒX
Ép€ z—aàBÝµ°‹„ÄâÉb	‚øÛê•šYI3ÀiÛ2™˜ßé×¯lbåÞ½ZÂémo»ïåÚ]òŽú$Ô0Óçæ�sPÎÜÖ!óÕbî3f˜@ã›©»ú2.cæ†™Ôãò³Xä–ëégˆ1H‚Ï�p½›,³·óèÒ÷_-_ƒ‘!a¦å"šJ°…˜	s‹šâ‘‰¥¬’½í‰àÆ7Ø˜l¢pËücŒÙ¯hêÚÅåQQDªFIÌLKÀ-ºÇáûö‡Áq·?˜½¡Gßµ#ŽZÕðB¿©\ÎØØNµåí!`’ƒÓË2æ%’‘—é<ÆÒÛˆ×¤hBÐPÀÌm‡e;ÍÙ�]}ª±h°ëðDíOÕEéÆCÑ$fêCªawÈC)·Ç$x3	ÜÂ;1–Þ^úÄô—æB“Å m9B�F‹°óÇ¨Hïa&,Æ24Îh‹ïCîÃ(ÖÈÁ¹Ì¢ÙàÍâN!Tþ Ûõ±å¶=ÔÍkŽÕ#n�¼eFÌL€Å«ÀÛjÐa˜—˜¹ˆ£³ÌÄ–s¶›�Þ‘É�IÚK\Ëhà"ä tšÜOrÌ$¾˜™¼nh fr¢¦cæ2ˆÚM.õÈdúké¥Sš Í‚é”Ø©´t;ðü²ÌL°DÊ-=·—ÉmÈm`P¢@0³%›•™)3;à7d€»~FŠG¦DÃª]ž™âÁ.f&0!°€8b{­<5åeŒ½1ƒ!—öá5ÇLKÌL f¢Éý~fbŽ™X˜à`íÒq¥ìí9e¦,3!s	%i`›Z¤<Í%Æ@(Í²¬Ï0ÅÒ‘)ØM,ê3)r‚¨ùÇÈNç¹~ 3Åz]¸­”±¥ü§/ÇAöVwµn[Ù:½ÍÜm²´ƒµí(ÜOwŒ¶{pþî-U·%dd@	Ò’3]íL©e�™1+:#
`&‡yâ¬†�,L‰ëKL(NéÞ-3‘ñÌ”¹03ù+ËLñm7t÷ÞfJj—£.×¢$§ZOz²¬�¡h3elÜËÈê”Y�YŽZ&B#S�É|ÝF^�& —«Ñ›iß©föaXs dò0C°á;6Ç”T—ÈJMfv”
·!7è)å-â5–r‹-`¬Lð¶·b;né¡ËWÁ·C”õ±œ¨�a&’¸HbÇÍf‘™©¼À²¸]z?€)‹^Áø–>ŒeÅÈìW«øé•WÔ×:†½6¦õx0ò™~ìÖì`ä@ÉKŠoËú¶Ylç,¹M�ÁÒ,ºqÍ®•àaðÜ·%ccù†à"É”Eðx“¶{£qkÇ}yv˜å6lHñv
!¹Ý³’»ì™ ËU[¡¥´*=a�3-˜Y­C·›¶¥¹e{™å”ˆ-Àí×ÀÛbÙëJ †G¦¾–oç^Qäñ,3õ°Ã’ô-Îáâ0ßm¶Íí1ÌÔó8Z÷OûKîvÄŠ·‡y	9‹¢“qêBl\‹‰œ`¡sïÍÕ»‡àh|Ïä�à¢ŒŽ[ç‘w8ÞÎÙ—ì·m_<MÛ÷w77—ßÝ\mÐ¸wß=|õñúÝÍõm¶qò~¡q?s|âÆý4Âø’ÏåûþWòïTõˆ�´HÊ]±”¯øÃWßI8¶=Æ3;Å/ìß§Ø)^‚SüD§ë©®šb¼«¦ø¯‡â·È£ïmï•+Ýv"µw°?¸½ÅÓ‰Ó‰êÚbÏ%ß„óÜóÚšÂÓ÷ÂobëÁãç™×7ŒöéžóªftT8kˆõ  ‚1>îyóÍµºÙú‡¹E Çí×¦nÎb¼óè[?Hð'pÛ(ô~};üóFô­$€þ|˜¿´S}ŽÍûM¶„?¶_’ŸO;þÞÞu;»ÿ+™ÿ §ò0uøÅN"3ìc;`äŒøwãfÄÇ»�ét¸®ÓáÀtØþ/“',¾Y7Úwù¡KPŠTRw¾Yw¦®Ì{áñ‘h6k ¾vUŸ{øpÒš—1øÍìƒLó}ð{àrëd§‹Ú¯¼‰pÝ´Ì=\õÃÎêh6œß-™P‹3Z†Áã½|BÃ4nëKs³vî;Ë�	ŸY¯—��k`öÀµ>E,ÀÏ·;ÂÏ†Ón—ô·;'Gòô™8½Å’B¿—“IãßPìHøqÃtîqwªXóÛ!±FJ3ípç»’i¾ÌŽÔÇÅŸ°£þ~¢¾±ÃlŸû&Û4y_�ƒÿÖ·Êm=Îˆql Ö9T�«Î´w‘ÜÜÕO1{û_;ðZ¥²·jµXCP¯6æTä‘‘J†Â¸øsõâ­#bÙõëÊ®1»*v¢D³¨ž+5·0#O2íÐdÿ­Á+W““kNÕÅŸÁæ)û¸YU-0:qž.ú·öqð½†2ï?kà»þ&b×ûË/ïÜ�x”}eÙ6Š&rºÃÍÃìbeÜË‡ó¡2ÆB›!6ÉÏAÐ�ë›·Ó"vLÔ{÷
âç9r?³ˆì°â\h¨ˆ"Üs¡æêvUãÍåý[=P…Wˆ+9ôôL?!k¼3ƒE;5Ð¥T©6ÊkÛ8�2Dt_DÅ…ÀÏ¶Å´ª’J»ŒV'eÚdè„M‹�l…)Ñ‚K¦õmbÿZ¡A™!_— `A[aÛ¸+ŒÇï˜«íO‹èžˆhÓö¥i„otß!”m=¾M†Â¬Á¯Î ½DSùsÉ"dü&ËN.ñ×ÅøuÛQ
ø¥}éßãÛaØ¦©cUá“Î�B&WÈ½‚­v‚ßY0, >Y‚Ï8M£´$Â§èIãññ/HÛRÔâ×`ï¤$"q˜Ú~Çô÷Þ««‡ð�ÓéðÝfÓmé¾âoƒfKïKCÛ�‡\íP‹úÆâ�ñ4‡oÞ3^GÓa	¿à-.Ýú
æðkìÂã·äáµæ|NÅ/LŸLÇ/ïŸ“ñ%Áo6ñK"ü\i52¸…ûÍX„P¶³ñw:ÿvÝjdñÉ&¥5>›(}ÚÆøö†/ƒE†Ó£QÒ¤ÅæØÂo¿üÕ-ÚIøŒÓœO‘7ÆŠˆ=/Õø…÷GØ7Á/y*üž06+>9ŸŠN¾	~[ô>? «EáùØ¬ôˆN±[ùlXF›¿¼�“ÙŸá¿š/%9À·¶9p-'KPƒ®5 =¿CóUn|ÝBý˜¿ýô”óÃ„dT³Œí2>Ëø�Ól"~‡çÿvñx|oó¼ÉRyi}w~I8Æè–øì«ð’�á��Ágø@:ØH„ß£Zwc‰”pøM¬e˜��pMi7ÝáÓôR�ö“¶ïÔàÊÄêô{!Sª®¯Ìj¡òÂ…ç×ÝénCó[‚Oœá…K”WŸæFÍo=S…‡½	‹è²ÖÈ`F0¡­°áf3‘›M‚<à„ØAÓ¦Í;ÃVaR"Øi8J1°­fn¼¸ÈöI03»å«'Û?bAgß;KÎ,Å—Ç¸mæÔšÚ]¹Ÿ.�~�.�þ:] =;„Ò–4ç]c=šRº›#ö³îùÜV wÞ•Í'pPÿ5YMXÑS“xQ~fð¨<�Ês]\x�$akñÌW,XÈ´�N;H- ‘`¢Qš„´À+Lzí5¤ J±jG�Dh–V[`AÒ’SÐÙµ&(J™ØQž
 …š4Gi*8–(9Úp†jÏñö®D‘ÝvEw¢Íóƒ¬Z´DC"8J�‹š	ÜB[`ÓÔ­%<2	òhÅQ§;@i•4¿ì'üåÞR ;2-b0ëOC‹fD`ç˜ˆÍFÒ³õÐ!Š¤0ZÂÄuÛò^úŠÓ²Ûâf’®8óhÅ™ã›öI“îZ”É–Dßq>PVIà×ˆÞ±Eš…_¸¥ËüÀ'ª¯s¯ˆ‹‹Oè=”nyäa@Ê²pÆã ˜¦8M!ý´:ÝBË7Hwˆî.(ï­¾¿C‹ãæôÇéÎ7Ý¼+ö[]ÃsC/ýS‹åT?N‚=TH‘?ÆÓŸ°ˆ˜_óƒý•az32”Æôæû ÿd&ˆüéxÆ 5´º;`1åÛñP¨¡y4 Äã¿„°Î�ÿñ•ìë¡ÿ@:úéW"8yBú¯9ë#xÎrâŠ=Ÿ¶b7À$MÛÖz]ßUÓ?êñ%úÇócË)ô'ýi^ wBËiz¾þá{¨Ã�èŸÓV'ò)òC³–£O�ù)ÿÜ@ ÝÛ>�ŒÙ	6¢4X§Lä¿ÝÒ?�·›�S¼é+žbÒýb}Tïo@ÿ…ôo°<Ðõôû˜¿ýâ4éã‡HÞg£åÿúŸmCÿTÙk§õ1DÁvzEïÂÏÿÍ	ÓÖ-4ÿK²«ø7cç€¡ÅðxÅu,�çƒ)ôïõ
é]�Äôo3úÙ¶ôŸè�—>0ÍåA`…Rà´pé.xO+Šu-ô¦†[/ŠôßÌ¹ò�æ‹8è�í?séÿjçåµc}mn•¹éßïp!?*0X·¹§/ÔMºCô�ä¾-ñû”ÒÚ¨Ç˜9Nët;ý¯Åôoé�wÇ®ìŸ7¡´ói'ý“Œü7nþ£<¸äÙˆÞ¤”vUŸ[ÿo1ýûÈv‹/[1ÑŸph|èÃ/�ÀÄ�sôoìBð-
FéÖÍf�Gu( JGôç�þ>ÐO¢î�þýú=k4®´ð¼µÀŒÂ �ùMa–zÃAÜ¾ÂgqÌ
OÂz{úÖÚ4!†�°GŒH ‚s6–þbù9Ô8‡ë„s<¿sRL©.k"åt,öoxIå9à�ÈCp°¶çw±ƒ»Öûã™Â‚át0ºQT3
7C®Š4m³)g¿`oW�
A‚ÝNQÕnÔä:¥z”Y±S%uZ/Öµ^fA '~ÙÆ¥ÍøNðx/b¹ßtÍ‚ÜO”†i¡'"sô&vIOÇ^
|‹í}Œ”Ò<¢o”æ<¦7ëÓÿ‘ÇÜ¢zÌmîT¶�XÌ¥Æý¶·q®n®®&6±³®jGÜÄ…mbwüM\£kçÒ6î(ýVÕß'ò¯9Œ8ýSŒÓïÜ\?ÞÏ7×Êß$Sˆ)Rh¯º~U}»…gªšr~ß[é¯LÅ¢HFjZý4uÔm�<&ÑAámÆû8²ÿÜÐð/¶mKéH™¨—-Ø¨à]Üý+•
%\SUJ©”é©9­Û–òešL„2åÉga%oôÑÙá“R~LãÂ¥L4àM
ÂàA©Ä)¾Œ‹Jµé•m6ÖXœòþÚS*ä?]û…|)1TJ„R…7êÊÕsD©2&ö­252´Ûð ¸¥HMí±›!óo¢!S>zÿ¨úåÇë«7rÌ¼º}¼¿¼}s…Í×vwÃ¯ñ ùËCˆZžzww£Þ+GŒç7@ÿz�_?H©0[£Áª|™#Å3d÷“Püû›»KÞ\þë%˜ANn®ÿå‡ë·o¯>>¾×~y³�ó�bó�ZÌŠìe«	9¬¨…ûÙVóe>V¢Ô=@‡Êƒ„ñxýFûï/o¾'üñîã1mm£õõýÝíÅ›÷—×·ÓG¦CÙ«ÔŒÜS4whÝ]ï�š{·ÒLõeÏ&Êeóp÷ƒyË
ÍÿD~B'_,ãØƒÒM”–t(ãgQº‰Ï;~mßm=^¦G´o�‘=š;‚œËÀUÍdD¯%´”a¼Ç}†võ4å6­€Wœ«ô¾Œùòv¡.X·¶~MhÎ§ŸãÙP¶½Sïþ,‘²ÊÀ°$…t	…)Ë¯—áhïÑü3.M2ì‘àåÅ¬/£iî0�?ô�>†QmôÁñÝCË�a· 1{èˆvAq<
}0*+íMÌgäÂ$Äô˜žÑòËˆ9–Zª>±tüaP²\.!Ã 6H¨?&£!ë“SD(É§LÁÔC”ÍX–2¹úiÓ4!ƒÒOõ�ÂÌfÈï%³§ÔŸºƒ±“wŽýIœÁ¨ßþæ“À;¤q7Šf²;,'Í(vÐó”ëzò‘S¶Ò¸Ó—Ìø!PÕã‚�Js�Ë(cg�,xÚâŒO#þP´E=;Ø+ð‡¯¼ãÆ ±vÎ8£Ö/ÈmŽ4æ†Y‚ÛÐ]LñQiFÜ‘»�ÇøŠ?NÖ«eHêã‘?¢þë”lÆÏíŸç�Sº`ËŸþ�/þe4\˜ëU< ,Sþˆ�ü±Ü’?‚ë€Æ&dƒ9džØG#¤ÏAþˆ¡8Cb,*‘f¤oYâI[òÇ2æ�eÌñ{qÆr™d4RJ3<´`aü�œ©iyî2àHïùÃÖ#Ã%þXnÇ'ˆ?â#æ²+þXÆì°ìÑ,´<†Ñžd,—)ÃÐ³3ø¿ÑŠ	øß0‡~—±\fø½7’Hãš~±Ï§z2hü,õ©§-“Álo·³Œ2$¬_8þP¤<�e‡8ƒÅ˜éÆð;s%)2Ý€?NONNO©ú"[¥ÆuJm†šÀÕ0~ÚŸA¢Œ¥ÊXÂŒ%(!+©JœÈ·Ùÿò·îÜÿ%¾]FCiÌ0cÉ"ê'°‘2)œ2÷zÀI&àô!‡‹!þ`1°˜XQþ€=›E]�åNR„*íTþXÉù÷ääd­þŸÊª›úzI"ë~{3Ð5GÆZþq!�55eÄìÀâ1Ì2ÎÉgÈ¡âÜsPx*Ú RIa>ƒybÓ˜ú8ƒfÐhJ¡1ƒd&b�Éü±”²^¯WkÆä�“u³„rTÁv}u�ý_Èˆx³[æØ¡Oø`0ƒáGd{Ïq†g˜aFg¡±J‹1°õ�™òËñG*ÄdÃ(e«Ÿ?$ƒ¬VZ‘”?Ö+ËOÁy.‰øe°ÁŒÆ²h‰?¢›ñ‡5eÁžåøƒ–Ç�”?–ýüÁòì�(I‰iüw®Ÿ&øü™¶� Q~ƒK7”Q&vœáÍ˜[,�P¨¶Ž~Êpzm’¿«^{£.éÆ2ÈdpÂÄe	7°t¸ˆ6Dl ž’¢æ—'�î2"{¢±fSþˆC«¢Ë~vèãšò‡?8ÖŒ.zõÄûÕ«^éb8Ã9N˜ë-˜?âÝö[ñ”üXÄ1¥,ÃO<~Ä£C†a0°Pådì£9þ`°Mýã‡bÄ0,,•8v`Kõ±Ñ¬À4æ�L†57bþ`›Ú? Hø‚fFÓØŸï‹?4ml4?lÅÈ2å�¬¸‘0]”‘e‡TÆÅËn0Ù˜F0°ŽA~`1gØ“ˆahæèª¦ç˜E`¬Ô¿¿ _€þÇhAþ`O)Ë†€G©,Pº@´É#2R:Zž2ÕÒAU¶>�å"êS2>‹K|ŽJP­³þ%Dop¾,ßÒx•†¥‡œÑxMS{Ð/à ÊôHjæTšïÚOÌŒ±Ô“—Æö�hÕOI4?i%/`ñ€â2~¡…Ó¬PSâfùÁ//0ËŽŸÃGþ0‘É`nÉ“úÅ|Ë^ŠÉœÉ–è·,’¯ÌtÁB×’‡ªç—t_ª¯e]ëAÃØ°Jÿ
OÖÀÚ¿^CûK0Ëd;œ_ôª-cÐA$ákm×¶bæ­ðú zÀ†?’á"µ,­iZû--)Öc]�Ýw_ü�$Î¸ÅëÈK½›#£_>�Â0…—4Ôù0EË-2ðR©6ìj“‹_ÍXc¼ØØ¶kþzÔµÀ˜çŠå—h$àÂñG÷t†UÀ±p8`ØŒ±eQ¨^…�ñZôTþ0\Æë‰êl¼>—èÐ¡£¨mh!­öjSKxlºÇXü³x·JÓÓ­çfYh7ÂéXýÖMz,ò€Í¬¥”Ù"“;ä�eÌÀ>ÖÄÔÎÀN‘ÄÎ4ÞcÆó‡öEµ†Q/Ïå¤—gèj½^©?eegËÝ§a½ƒFõ8#ÞÚDìùc�Ø›è/£3šüøÁ‚­3XÄËö1ç5†Ð‹ýÎõŽ7¢h—ÂX>!,îj6êç�õÉ	Sj•Ž­Ö{ãÌÊe$ÊèõZR³K3©«'î/&jpÉXž �>•Å—Ä®Ñn/ªŒ¦~ïd©&¤–æòÅæ
K,ñâ<‹+Ëæf--nhââœ’R×_NNµÈé)“LÂN˜qnP^¼ KŸqg¨Ÿqrâ³-J–ÙŒõ:ÊðÓ
YöHŽ£–a3f�Ø ôé„u.'÷-u`´ð±Ä’¡?,SÓwâ¼�ëžzx7Tïp·Š‹¢|°nXXFl�h?=9ÑžA'ÆGËÚ«gizL3ìôÔ(šêóÚCÆ¸-u†~ôT—ƒŠOPí$.jò�;1Ì˜æ(‰(ùñ£ÞÉ#§Qš¬ã»¶Ñ(çÄÆeœ.9w”8=AS»I´ë@GÓ2èY4f3H4j~i”ïOc×d)Ð^›°òÒl¸þÂ,
NNO6œziÕúg�
óu¢8F2‡ò‚2Îr§f€°¾Þ'Ë(C3|¤À¹ðD~²%ö/t«„a°A¦’½-™1§ñ¸|me!Ð�ÝPÿlsì Y]J†�›HdúD/o,ÏYØÁ27lGà€²°0À4ÞÕò�züˆ634pFÆÿT
!"2·‘]I´ÖFIÎÞtÓŽÆš¯_èCJ*0bhv8GÄ5¾z¥ÿSúÉ'±)5æ6°÷	«8ÑÍX†a€aN9%!@95«_vX‘ENýüaÙã4B|’qrk	ÃœÁa¬ÑãWàÃÌˆ"ñÒm³énŒ0.D÷£�±,¢!]Þû6Õü"ÎXbƒ·_ÅˆøƒE%’]žôUdN')…Ò˜aNbSu™�Jêáxp™´â�/›‡?HÌ*¢},øø 5^\1Q^4Ç4Ûø�öG	¯8ŒØPÇ6)ñÇá=KÏ2Ð’ë”Æ¡ñ#qÄˆRY’‘ˆyqs\p
Fûs‡u+Ãë°VdHÛ-ùƒañSŸÕdE7Ÿ_fØ_Ù·¦™'Uï¡h"/Šfiwéµ&¶{¦�Æ[[Z7{õ:,'™íñ€3˜ÏP²—^÷,£<»‰Ù;mÜr
Âš�ÎÀüqóGÂ1ëÈ‚*çG‡&áêìeÍÛ¥6 þWºU:Ú×D¶Š¦q�pnä_BfÄÖ¿Ò2´º‚¤Ã¡$) VÊ�{o³+evÿ­µwk}œŠ® šuzá[ÚP†Š˜÷^m5¡pŒŠÄ�¦�ÑxÁ®‰œÖY3èí1ËþýÁ-wËá˜æ�Fo[ŠL™`ÀhšØ$²›Lr²(ó‡#f(ïX›6YÙf‚65EÄKÄJ¢Ä³U6õÃúëÜ�aÌÌØ®>‚?PÀ¹ÏÇœsÁÙT@¯ùÎÄp>„€¹6X�o1HŠJ¿ùçS4 çâñî#jî+g[9ðø²ïï6ˆ/ÛXƒËÙ1Ïílã>=ÆÆ-lã^cãV¶qçÇØ¸“kÜŠã§M¨ÏÐÊŸ�¢Ñ3™(Ø±Žœg¶q¯�‘EÏmãŽrZ`vjøÅ16NsãV¶q??�ÆýmqäŒÂ$gÛé¥óï/Uüähà\�„=ûô·8VíìÌ Ž±qëkÜNJ9±“Ÿ8Ö“R>)æ32 ÷!t±O�Uñ9µ�ÛÍ'ç“¿Ï_›ŒpPÿWù þ?Ù.ÿ¼�õçŠŠ¿Ù!ók0Ãá céDˆî½Æ»gy6@ûÜúÏ›í(^8¯ñØ"núO‚úO„*Ÿªüúß,ø¶°°ó:Úˆm:£ûÊ1žÒ®…™Lv�qq�7ÔiÍ�. Ó„�wÈ|¼ògÒÌEÍóåPšÔÝýÔ û ¹ÝÄÈWHæuú}þ®_gÓÌ½T§ý>_“A)ÎhðÕ1ûÃ~+ÿæïª5þ>…wãXž¡@—¹ŸPüÂ¹—U£ýW¿žþqgÄãÅÒ—âÄôOø�vSkKÒ‘kÞÕu]TTŸ¡4�žW9,"GœnîÇéä��4™ˆŸ4ÍËÃf÷¦=¾º±ôŸZ_¶Õó.ä¸£ªß0ÿ’ˆ<ýéÁòÍäôÖôOÒ]œ†\oÒyú«o8^ø4è>;¨ïÓ#ø5ê¾ôJÓ©ü1·‹¡]§;�š4›£gÇ0NêO&�Žf‘]�ÿ3À‚?m|—MŽøƒNºßtt€þ¤›šîât§»~@ý=¤UNçÓtjúpÇ‡íèË],?Ð;á‡tüŸNÿnR:²øê?äèmù*Ÿ0’ùv×ôfËO¯ûç‡Džè†æ‹1ôŸFßnÿ'{h‹øc(ÝedñxVÁú�a¡!~ ÛÒ›Mä‡YéßÑ¿aëæd½nìíä>óü¿Ýüákˆ^]Loÿ|Ñ»ËÑ¿›N¶]zhüoú¯ø~—-µò¿Z¶Z7tÕ¬h³’Ÿ«–�5�9êAÿßšþñf»!~Ÿv�Ð¯X!mÈéŸ¦é°=á‰éŸ\¬ÙèZyÀLM;Cÿæl­(/_¼ú³˜þl¨ý¬˜î:l?‰ÓÎ¾6À%z[•#NÔ/�&Ž÷[öÿqõ† Ötûfµ¦ŽþgºÿÓõ<ô§t;þ/—7]DšðÃ ý‹ãIžþñ£I&ëŸ?º-åÿÆÑËýß:mgúû£Ùµþ?‘þ=ò_$¿wiº’÷’þ\¦—¥?‹èÏ¢ñƒîšþ–^Ôþè&^Så‡m7Q±-å¿Íá˜•º¼yh´¾fÊh8â§õÿ”ÙÔÎOîG~ŸœnIÿ(M»©ÝŸmG¯d¾Î²³ð“_CDïb,â‡¸¿“¨ ‰jËzj¿…ýg…è¯\ö™îTþS”:�Lþ©ý¿Ù’þ+ËçYù�ö¦SÞZÏ‰‡ïÈ>àåÏ.¢œaQÈ(óÚÿý?o,?§šž–¬ž¾ö§›õÉÀrà¬ý¿ÃòQn}<"WâÚ¥ýq>	îî1ýY<¾“Òû3ÛÒÞ—£ÿ§’nŸYòèþß}Ò¬¥Þ®ìyžð'úó'’Tñl÷w?#ó¯‚G·’ÿúù¡ëh®¿³¶óã5 ßDû\¦ÿ£õ¤8Mé"ªÑrý·ÓÿÞöd­¥�qã¿çÏæŽJ‡Wja`Ÿö¿ÞáÕÍÏébùŒ€�Œ÷™ñŸl×ÿÙ�¿ËÑÿ“h€îÂºMçäÎÕÚZ0íµ.Ñ˜ÌüBÍ#_@»†vû¤¤ÿu(½)ýY‰zzÓ˜Þ‰ OggP6H6DÿO°œ.uvª§ùãDê«•­ÔÊP\¦Í·e€õñC ¿!¼ûÓÔ­ÿ…©b×ôŽñ1�­¬�ÑQôçzèŸÑ(:FzÅ›þñ·ÑõŒÿaX¯ìØ¯¬¶R- «ÕÚñC·êTZ¿É¥W–!V>íøD1�zÌóKÓI�òl­Iß­Í;wLù{©þ %-—ôg›�'Câ{^žd½ö€§%ù¡}±ÓiDœÆú/ÖO]z’‘—áéÜ¤»xÍg¥o®èÊŽ'k=ë+þ R¦\+A²Q+@Ê¸´‹ù?öïB’uDšÒãù$¢÷†þ }ãQìÊ°½¹CöÀx}É»0Àñ‚¡ÅÚHÓöî>÷ž’¾oØ"½wvÖ¨9…jó?=ÓƒÀöWêßƒèßõZæ&¯—ã÷ÆøìâzM§?ë©¾Ù±_Êâõ¡úc} ¢÷ ½/s­íÊ¯–2Îš�^”ÈQƒþ]cü½6×Ÿº˜þ=iÚÍîïÃXBß^z'ôgþM¿?h3Õ¿?¿Z1KyËëtüžJ_œfú{m’¢’žùýãøw+ÿŸŒ?W×ë/„Ë¯hB”ÎÓ(�èMý=ŸLŸ4ôfÑøÏ&ÎlËúÍëÿ‘úëv=÷WY÷¤@›¸ÿ#òŸ·gÀÿs[ýn{B2ÿwEúwsÐoÊxÁ§Óp‡öôo)ó3ŠnŸ1D~ÙÿŸ×~ŠÉóÃ”ôöïg¬w‘
Ó*Šz4¾Ó­ö÷Äó‰”P	NâñŸž�?=º1å»ýµ»A~Àö^Êfo?æVñÃ¶û·¶LÏ¦ø§ë5ÙtDž®wI`?Ðžñ[ã›AuáŸ±ýÙ¸e¯�ø!gûšÿ›d¿n×»ás²C1}=�ÀÃö5ÑŸÅ�3ìc¹éFa‚ÉÒEç"›ë·5³Üú�%YxÒoâi
Ngˆ¾ŒEúËÐ?:­•1Î7ß‡i;Èÿf8ÒÔ¡Ðm˜¥ý=¾Pàäu1pr)ÀÏ_£¼ýxys“-¬îDQ—/î¯ß½L">A$åÙú×ý½¹ú>ßÎ_k8É/Ž5Ê°°�;Ê`[�mÜ'Çúh)§úÜ—Çºµ�{u¬1$‰rÓøÃÐáßÝ=>Þ}Hb)ÿl÷ä;Éo³vøD‰$zí¯ÊRÍ´0�“bš@º0f¡â£˜…�«Ì—9ìotï`¢ÿmˆ<u{õøþê^Ec|jb¤ÁÆù:×÷;²«K±‚<ùÅÈ~®}½÷Y\9»Eoñâò¿
ÉMðFø1íâñ‰£„€¸uÙ‹psÒ¨9'Š�|/Ë|„Ç”°×¶î�ùÐíœ�÷ê1é½¸÷Ò¶Õ�9W¯äòK~äO5¨DˆáA°ØTýZõ^bñÌy#ÿF¼WŒ9ÒµØT÷^ß«^¬ÿJï•<&Ø¶ïmÍ{}…�ü0DŽx}È‚‰ÌÈû?\_pï%¦	BBåÞk˜óÚˆórµ×¼ò®ëØþ�Ü|pÀÙø—f¸ìÛzÞ:l†Z\u0Íòî½ú‚ ÷šKxd^®Êµ-…|S`eû^!¦ô_‘á+ð^>â½zTQŒ5EÎàçðò¾Ñ[¾W¾˜M™REñ½éÄ€†JÝoI©9gSÚ›i´y¯å�òÄ@Ü{É&ïå=ïeÅ÷º×L�ûÂxEKãäŽåçÌø¼Ã‹?ý+ëU¯=\h�‹–ma¥3ˆ¶<Tô¯f�=^D>þëÜòÓWa©éOéÁÚÔ��<ËC:˜b³%æ:ÎâIÏƒxa1øi�Mê‹1®|08mlÒehï¥í}ë[� Åþ. 	nÛ0÷ÔÆßtæ#wŸdÞ‘C¿ß¿Ü×<yþá†Útƒ[CuŒw2æ*H›¬W	v†hh¨Ä"iÅ€oêPºÁípi–o0mô¦3äjXïóýõeM_Ò¥=7±¦Á®¦ �Ÿ€ÿñ;*ðŸM—8.‹6LïrÛ&ÖŸMLÓ'N7³ã?n_Ì�Êc'ö†i†IÖ°½á'Gât›Ž3Í >›~ôÇ½ßŽ·ðŸ„®`;Y‚ï¤‘4šp-§òŸ�aã{þ{ªžííCìßô0cx�ûlbü³Aüõÿ†Æø§)þ›¸?NŸá-6ÀŒIU2Ü8e¶ÚBËm"üWAÒAhzÜOûÇÛh€i"™ªa¸YÍ¤ÚzÇÌcð?mü`Sñ?HsÅ±ÚƒG§k’§ÍwÍfóOÓ›…Ï¸Â|§øg¥�1Â’^`88®>¡)ñl/8º7sÈå-]˜ÿÝû›¨È¾é4ùiÿtKü'ò2£kOÓ4˜ç@ºx÷6ó%H‡‘1*›hÖˆÇŸ¡­Óøa:þÇáª‚QºÈ8žßšl]ØÐüÆp³›HP@]ÎÃkü,Îš1Ã@Óƒ��4’gÆñ?w·ÌôÆ¢4å¬‰ð�ø-SŸþF k°¬Õ€ñ½	âo	Ý(ËC0�ÚDF�
Cã%7,Fép€ŸN/ë/‹LàŸñßD5m"Å«‰$ƒ†¢x<¬ƒ‡CÂdâßh¼—ÊOÏ|éÍ@šm®_ÐA…*[¿¨=Ù4Æ7£ø�Ò,À³»TPÚ3Fƒ’”E÷Áü:#ôåñÏšñMÁn¶�Þ�«–Or&5¨/eÓE-°ÿ},¶·¦1�â4¥±]Æ?‹'¨¦¿ÖVÝo-kÊéÜŠñÏ0¾ ˜.Óc´}3²0gÓÐ¼Ù›ˆOë6‚4�¶~x«]c³A%YŸå ­É‰¢·ö˜­c[.Ï¹ë|·ìØº£ÜNŽu_’ÞÂs;~SÜ1pùáãåý#Ø*ðI(ën&LøÓ9÷-=j©¸Ûn©xÏÿyœzÉ—íiÉ-ÍŽ8FE3á.¤‰°I—¡B[È,œ¦&­þ„Íp€`”w·mÚ@3�sjþ™»*ŸÖéPC™äáå*ƒS�¥üv]Ì¦EÔ5d*H)DOŠ9�3á�©tT�ò%òiÿ|’èÕqƒ§¦#|SC”ÆM¼·þ\ô%]ÚswìÀQ>ˆŸ(OLÅïæøz~ÿÂû‹K|‹mÚÃ'¦áóº. ÝˆÍñ9¾½M½éñí�R`ŒÆ}Up<„ðû.Óñ8’Ž+"mûá�‚@òÃ��ãð�“œã„'øN‰[¥~¢ûéO{Ò¢Ÿß³ø§“ð/Ù_ä™Qÿ€°éÁþ�à¨ÿãŸ¦øQÄ¤ñÞâÌ˜T%b:€ïü÷ŽÖˆàB`úÛTT¿^þ�Mz¼AøŽ„*A3i€¶�ñœÓiøŸ6~ð©øï#GŒïþEI"›0ÿ�X|)‡4Ÿ…O!2ýahüæCÒÅDü÷0°&x”ÿ2±�ü‘ƒ‘K»wsu]Uoßtšü4ˆº%þyƒ—Ú“ûÄÀ»‡Ú7V<´øæ"šºð_P¤¦ÉûÛà<¾‡ä¿üÔÃ‹£“U-Ž†y„^(.8ts�ßÇ§¸|¤ù�<3ßø3­>üóHÒQÕ¢ñ=ˆ¿wnîáÿ4Âÿ|Mµðið¦ê3¦9n?Çøð’�ðØm\‹]û¾Ó Á£›?¡>¶�>û„ø�íChb€q)l£dzšþ÷Ôú2�íb8-¦‡t¢ü˜“À2J»JCù=R;z«ÛÛÓýêÉüó¬@·¢°k«.€oÌËÅtÙ$m¤ ½ H�¡Ï€}3²0çÓÜUÇuÃ`îìÑMýpûÕÄ#[Om¡hi–—ÅF»œie–Ì»2{ë{åªôþN­¤*pßÝ\}uy}¯	�W÷þÊ®îý	¯îñ'
Vµ«U-1>ÖÿeÇ[ŒuœœòMp;)	ï¢’¾¼Ë³w£"4ÜFÉ<@:¾ÚpÑÍ‘Nhæv¦iŠ+Ú‡HÝŽ¿i¹Æ´Ps:ëõÍšõn½{xwûúmÅU½û<ù9aïCægº³»•7*?W~®w'ÞÝ†H#å�<;ÇVn?xŽÝŽÛR~®ü\ùyS~¦}ö�&oT~>ø¾@Ž�Ÿ+}g£þêƒ±àœ,<T\=ßÙùEÚ7âúœùù9òÆëüâ¤/Ò»¼Yu�g.O¾8]£×«â9òó×êƒ/žŸŸ½ü\íãG†#äö~§¬ÆÏU·:ù¹0VWúÖ»•ŸëÝzw¿ü\bçýZÅ7¼[é[Çgz`ëƒdGwéÎ ïïnåç^ûÆ3çgúâø¹rû,üLžãøL*?W~>¬AtK
VŽ…·éŽ®�ÙúÈ1òs½;žuöÄ±t_¸ÊnB¯ü\ùùyò3K·§Pšì+|!ô­þ„Ïaî½Ëi<(£�ûØ¸ŽV«øKÓéSŒÏÏs­Vˆçhß ãäçŠÉ—Íí‡­<FwdW©ºäó¿{ ÚâŽÖIåöÊÏûáç>¾Þ®*?¿lnŸŸŸi•ŸëÝ£Øß�‰\qUï>gûFåçz÷¸ø¹ÊÏON…-îV~Nù9uß²õ³âçgÚöÕà])h¿}±ü|„ÜN_,?¿ˆøu/�ŸéKägRÖ«lVïV}°Þ­w÷ÎÏPè¨ü\ï>o~Îª‡Wpw!÷EósîpÕŠ«½ó$­Ü¾­ü\ùùH8öÅq{Õ+?W~®ü\å�çÏÏ¤rTúÉîÛ›ëŽcïÒþ»;æv\7’’lü]ÒW�—ÇÏõz²+ø‡5žfie9¿x·÷¤T2êî~›?¶–àî’´ÿtýîáòÇ•L°iþéîúö‘t—7×ïn¯Þ.ˆ¸½üpEÎ?\ß^½¹¿üþñ?^}øøø™ÿñîî†|ò¿»|x¼¾»ýêýÝ»›ëÛ‹‡ÇËïn®¾’_÷�×·ï.>^_½¹zX�Vf¼»z$¿Oú2òÇõýÃÅ›»[yëñî~MøÇ»Y1*ÿÅŠ¬¿¿¾½¼QÅ¯\O<	dEZ}÷A>ÚýëÕýƒ¬ýüCˆoââþîæFÕq¨�jã�—77òlÞ§á!;Ûœ/·kY‘oóµòX|øêêöí…|ó#Ä¬x¸þ·+[fÿ~²$ÝÇË›«ÇÇ+Ë¦2¯Be³ØúpòêûË7Švo¯ï%dYõ WØ•L’ÄµÞ.€Û‘ö»›»7ÿ¬pü?—ƒæns±R�IØé‹Ð8Œ{ÇW)¿9æ‰ï$Œ£[öÔýà÷Cý@5t�_ 8Ì4éó­ûÂÿ!_³U#›xøêZÖáþâO¸œÉ¿Vþ-p8u•ø$çÍõÃû«·ßÝ\Jf{¼»½ºøîþZþ,Ö-Ãñ²¦—7�þ‰Y¼–„¸¿¸üóõ¡)�‰WüKðÆPÝIqðêŸ†BÆãõI‚÷—7ßËþt÷øx÷¡ü�#�bGuÿ6JÎøøpýöêâíÝ�·ß]?Jè¯~¼º¾{‡z;Ùí[iþ­t"Õàù¥ì-—K�ùxuq%™RB‘åägÙ·w÷�ï‰x”÷JEî~PE”ü{~¼rï¡äT—Ð�N5vŸ­ê+bZÕ<¼—EŸW³\�wÑ®"ë¿íº¹¼}¼º¿Þ½¿¼}'çLrØÄž›…Ÿ¡tÂþýÄHúÂ# …}ª™×OäùÅ,.k kE•“–•ðURÖÂ+œðÎ@ðp©®”Q˜Ò0§ýrS¿F£Ü¼Áeÿ›¸‰]kZœÍšIìÓe­‚ÅíÅü/“ÒÉ V}è‡L•ÐmçB�†2sOþµ°j”‡jY˜$€³xÓu…^.ar	Óm]CÌ…B#sí]{À&wm×0˜7.”Û¢\#ËVs©€ÑUPL­+Ècœü8õFÅÔÂ‚nøNdæ��å x6ÒºÀX)÷¯B'1b•—ð.n®¾t²ã·ƒzâÕ×�„&Ç‰«û Iþ*<­ï€—Ü_¿{ÿ˜U¬
òñvr\�Øü«2&t%çAÅ_•Q¡ð�ÅD·LL“¶¿%m¿:8iûïùùæŸ¯”"³q…>ËMÀûá'ª/Iâ6)¹ÈÞsyûvCb=¹ä9·�u‚çìZÆ(¤<{¹uS¸Ãe½òð™“Ü”¬¤e‘VA F>mÕdÁlŠi™ÅH§2¥¿™ùfLXiFGF¨±¢¢L5­ºœ$¤íÊHB&Ñ®¡<,T�„”-µð«²%ˆNÉY\ËataŸ30mB×–8�­NóíDs5ûÉ‡¨¸š¦é{¥åz S?Jam-Lýr+å		ÕÊ™M#oi)V}x˜ºf&%ÉôŸ‘…h‰ F!ÐõlU¬gÓ&m§®¢ª®©�)A
Ö!­f`$wy-—´…4ˆb'!uêê)[.¨­&9§Ž™˜•Þ	uÚ”[ú`6eêeÄ^ÿ�¤¦ŽÔÜCò0¯òp•‡‹˜»vöË´áî¾{°­M’�¼_0¡»~ÆómÛBž(è3JÐ?;rA¿Šì	ÏÕ²t&ðÃRDªÌmÐÚâGic¬½Z¼m”€ÔXã`cå>uª…wMùÝð²K#SÒ¡.¯`›ki@Û”‘¹]jRÎÐŒ Š¦Ñ”�•¼¿�I]XW°ñ—L2˜"¦”ƒiÌÐ®¶R¨od#85&óExÁ”i†R(u‚+.qbËùYc„|favê‚`àdƒ+ÊÜ]$«Ù6´Ó/c¾.müSº6æ4Þ‘„ ¿Œ5ZD&,ÐHWµÝRÌaÓÈ¯	hb�j Ö†la2K‹"EëR›	±Iì¯*sW™û9ÈÜbß2÷·£dîóyeîyæŸhï®oÞ"O!þof™ŸÅ§
šÏWÐ,>ý“ðô÷7w?*9íæò_/�øvrsý/?\¿½x{õQÖ~FðØdÖ™àëæŠ7ï%Ÿ ®.Œo.ïßê1>¼Bhöí“°×ÁªmÖþÚEaÌ’ÔX[ikR¤m—¹-	ÆhìÄ7ã?¡>ÛVK¥(×{f°œcª–ÛˆKÉ¯ÕŠ:'xjíÒÊƒ1!”ôÅ„*¨wÞ­^®í½¢…—µ—Fš€µ\³öTÙÎ…’µ	Ó0‰y‚d¡¸ÔÒYÔ(³¥\ÕÑˆï‚øzÀ$¾ží¦Â¦ª#×X•0Ï!LŠ`K9i—QÛýeíñüL8qžèfh›t}�Ü·§‘&� P	9‰9Â¬xx^bZ–×òˆjbë,ÞcÌ‘Û¤ìB¶j‹*aW	{göÇë$l¾;«öa4¬Ý·êð£T‡Õ!«‡âS_�¦Ÿ³ÓtuÓ(¬’ �v	´V³BˆLyë¥NFsq á!²ÞfìZa.§XœÒàÅÓˆÆóƒqDG”RÒC)á¥L‘ÜYxµw­$®õýÕn!\}4î9î¡� SeøcŽâ¡é:®Œ+µª™r;­‡ÉL`§%;*G·�×±r¶v­5ÖªÐvaSÌ§j{²YÜÂ4í$u"5ëË;Ëˆ#4÷0žcN åš—œ@«+ÂK™«&ã*Ð>“q·“±ÁÿŸöa2žAR«Žûñ‘8xÿß}˜ˆ«[sÿJÊõ½¬Å›÷—×·Ó;ú»÷#Îã]ˆZZ•r—Þ…èÄÄ �³Ñ%ì.D+>º¥+bû]ˆ"ìm„×Â
y.¥DZ•Zv!*ßJÏ�ˆ¯^dì›6í¶¹ù7Š81ô5Öâ5Ú…t'
[ù�¹šÛÂÃd´Ý…(™E4gÌ»·PÚ¦]€)L�Â…»LùS¸]ˆÿfTO˜r(€m�v!
ÔÃ„âüR|
±rj�Â„X; h¢ˆw!Z[³`RZa1)§6z=Á@Q÷À.D±�}:'^ÎqcÏ¢yV˜©YÇh8»“³÷Û0§@°}+_?S¢nè¬:ë†ÎjßNŠž	.ðÏöÆöÖì‰T \;m0í= öDšk­”`e&3)ýmöD*¿ÒR}[¦”»ˆ²ŸyPíÞ{"­{Ã*Þ%¨@[®a*µ2¾¶nOd;´'ÒmXLSV’¶‰6ì‰lÍ–@·×Ðû[ÒÂŽHøÚZçaÞ`÷D*$œá=‘L’«õZïõû%L¡^`÷D¶ñžHBš´žx×%ð*±{"[ƒR'«·`O¤*¾\b5mR=nÿ"10u-m5mŒ¼'²eÞF¹¤¨}¶ Šä©�RlÔÂ=‘êíU:¯Òù¡YÁçh˜�¿zwwó64‰ò~¡i7Få›6�ô9QTÿ¦ŠêUè®[2«§|U%Ž×½�ŽÞê©,ézOŸÞê©vä»­OmÉ[=U&ÞêÙd·z6ÑVÏnî\éM…ÔmL›
‹[=›æL‚4!h3"Úšˆ¶e6x«gcê�¶zžsª£ù!˜¤¦­²…cpáÞa¶zš[¬ñ[=¼Ó³É¤“­ž>lõ”5A[=Ú”.G’Æ�·zêÅ¿qÖ“Ýc‚¦?ã­žM~«§ÞxKìæN½Ñ�ƒû*�ÞêYU‰jè«J4»³‡ï·a]ö¢H|»� Žç,þ$[E§ïó�bó�ZÌª­ÞEûˆÖmÔ¤QŽS/|�›…‘vrv4MØ&k¥F)¥³�jì®‚D<mÿP“î*0¢³ó.w)N!ÉWKþ¦úÍØUÐ.5L²j|x'ós÷ój‰´(›É»
ºvI}¨Þ^Ô*nÁtÎúfWÁ¢k©�=Ã‘NÕ˜Ô	JYíAœ!*à]®žkÔö5RQÖ©vˆe+´«`i_Mšˆ*�b‘vB¸« i^e¶Éº^b6ä�	cDÕ-Óì#ð0Á®½hr0ôª�†v2MÀ·r]wCVñë™Š_ÕeýÅH•ÏR\Q0ñöL…K‰�Bfä%Dôm05PlÊÂdqJ¹©S�`:?åhƒ©”1\À“p¹¦‹¥ð›DáSÁPååíEAa7˜Zßs·Á”Ç|”òL%8.\¬Ñ%ˆ‚Ân0µ«ïnƒ©Û^
a²€#SOïÅ‰‚¡µ­¾ëR�Ky_zQØ`ê²‚(¨â(�S,N¥‹	2kq„Û`êŸcŽôS¿×ÁÎ7¾gƒiŸ™(ø»žƒ+ý)©éÙ¨_¨ïÂÙ¨³{KÌ2VÃ“Pÿ3�t1:õB}‚–¹§÷@ÐXç§;ëêúXGû?ô0Ï
3Ÿ—y&Êë;œ¿éÿ‡îøšzZ¯9U�±(—Eå(g6 ±Ü¶µPž1â6*eÊ³èíÌm±c.MÜ‰IøÇ=d›Ç|‚!X¨Ií ›rq>©ôè Ÿ¡ã½mç˜izX>ñ¬WZïû‡B"|¸¾¿¿»—Õ(ž‚þe„¦‡÷j9=°–‰m-ˆ�È—»Èÿ._ãûË%‘ü�x‡Ó+ù÷kõ�Çò×Û-Îkü9×M€åF{¡'ˆÁÿFâ¼~¸ºÉcc·Á„'Î¦ÛYvêºŸ·ÎæàFö´ç¹íèßMÛMk+ÆK‹¿&PáÊ"<¹Œ½%¾òŠÒ!!Ò4ºPZY>`:gÌÝÉ!mƒm-@YpŸº°Ñm‚n›ô
µR 
QŒ…ŽíEà
IÓ~™ãÓ®€Â”œ8£(­ö!9RoÅâQZ 4e8^¬®‹! ÒB`Ñ¥©¬#¦?NO¿"v�%zë&x‘þüÑŸ[Ò-0?�…ð•$ê?1iêi¨Ó+@bŸ\,"À<aÓ¾KÀ�þ¹4‹Ó¼? ðˆÀœczk›%Šû±(Ðß].#Ðs*ùaáÓ&�øñA§µUý)NS”6$Àã’£¯#1qiê¨&PÒ¦W�¬Â'ò"ø"¹À�ø�”égÒ|à~”.óƒ@ÜeªœÆ}CÇ‹x€—9Ë!8�ÆA,lÚB\”Ò¡‘¢Lï(­hEpWÜ�Œ#~ðóƒã ›˜žBxzæpi[”wä÷ã	v-sdÚŒþ8½‚ãÉªŸÞñ•11Ä@†þ‹©#�£/CÉpDÀ£L/2éD2!�{@Ã³ôïÆÐŸÃþï‰´À£Yè¿CiLo1�þÓìsÃÖ©”à‹H4¡îG~ÛEy? v ¿ø©>~ÈÌ]n~·€ÓÔ�÷pü'�âñx�Ò)^”òT¤ÿ@:úéÏ¡üGv@ÿDPéS,Î©ÎxÑ—^–àæéo>Åˆ4’búœ&‘cì�è¿H&Ê5™@ÒŸz'ô±ü—¦ç¡¿úøŽçÛ4]€“›XüÁ=nsi!Œ0â’˜þQ:¥¿Kw¼;×¤}îÕÊÂÓ› æ°iOÍO°ÿc}†¬×£ûÿ@žËË›Ð‘ 2ô÷êÁá�‰9@oÎ±œÀãŽ´X$éø½ãu,?*‚yúË? ¼²1÷D�þ„àêFü…é§I?dõÙ1òÿ ý_gèŸÑ �þ‰(Í�ÄÖ#oé¥l–7(-Ê SÒ÷ð#‘AË)ç�6o!ýON…¿Hâ‹5~„Õ¡g<RšùÆóÁúGÖŸáù?OÜÿÙm‘%ÆýY†þI>0ÀIZ á•a…’á´b (ÿÉäBQÌÍÿÚPè¯Òúop·°�Œæ‹8è�í?|{ù/0€ˆp*ðü<ØÇ�Ë€@Æu´¨�éÏ°òÙ¯ãÿ¹N.àü¯è¯Rg<ÐŽÿ&=ýsóS!­�zR¾\§ú�EÿËØ€}GÓºÅ°iýYÛžádýY~1¾ ÈúÒ�–ý?“^Eö>ûÍbûB–ÿEš�>{xH»ªÏ¬ÿ/"ò{f,ŸOêß…Æ/îïúÓŸ«SQ��û÷"Z_Zí"b §96‚ß‘xµ¾ìÐB&¢ ZÿÂÏ‹ˆþÒßû}p2ýÝ}ï/uûqã{¤|‰V¶ƒµÀH ;6Ó˜`>€!LqŸ%°Ü ¸õÄîý¾Ír
s¾Ü†µåßHt*½2^ÂEú³áî Q¢—n ü'žß)¦ƒT—µõ¬7Åòá	t­¶œàÓý†_ßáØ¦Y°Þ›‚°ö(m±`VG7Ù˜+é�õz±Ô/op»ÖåÖÎÌ;Uòií¢Î­„¦|ÍmÚmú>ñzD ùƒêÆ¯÷hQÁ° ðX2LëQ¤¹ÃÒ›Ø%=}ŒT@¶÷qRJ‹ˆ¾QZˆ˜Þ¼OÿGÞ„«bÀàcc½³fò$<Ï;™MsåÚÀnÖH!+1ä@œg�wÞ”Ã <ïÆ9ê­�¹q';`s ìùê€sÛÛ¸‹W7WW›¸´M<J&]ÙÆuÇK?5á~²{ú�’õ†\ÇíññÆø.f‹Š$û¯”�,ßØ‘¾½“ÜÄ¿IÝÄ?OÝÄÏ·sŸÇ±{+¿èç¦à€µù‚Á˜Ås¿f?þàûñ}?P/túÔ^è{‹7& AÙ­ûs‚Ä2Æâ4¼ôÆðiW3”n¢]rÅ´ÝQè°nÝ�Íp~»:4lHšp¬xO¡-2â½Š´ÉìMœj&ÆçÂ'‰ÓaOb6M#ü¢û7 ,	øø5»ý†kŒ£fŠÙÑ;â/—tmÙ®.²ˆñ¹Zõâ“ö¥#ü2Œ_‡²†EøÅIƒÿ5F÷z*>[„NŸ´ølçâGáOBŽÒ4J7M‹ð'szÒúl”R"ü6‹ò^2�M94Á'm#m7Fè"JÇ€R|ö§%yÚAˆ¨GE÷ÃPèùmËžá¢´ˆOêŽ³ÙŸmôhšîÇo:5M§Á[ô/xŽ«äD¿K]ŸžÚ„í>¥�òl2>éaàsX>hÆ¤ÕYZE|ºÍû.#ÌN d„ÏõVËà|¶1šŠ=|1¦?n#_eðÇ|Fiû;@Œ3!Šo`ÀŒÇ.Í![ã¯éÅrþâôdyv\·(§5>	@°Ï%¬ŒO²+|r–Ÿ¶À_‚ÏMðË9NŸ'• è}­G›¾¿3f<Úˆ¾fyi(OK­GZ�6˜öƒåÖøÜß.™Ái1>5ßI¼	€_s„µ–GóûEs{Ä8|êë³]á¥™MOçµ¹ñÙO?u˜¸ëö’õAà>Ãá¯ŒÏ8Í†ñÙ‚~~8Ù“î¶ÅŒÏ›s_`š€´Ä¼¥$kGXuÙp'wò²EÜyàOßãûD€x²–ÊŒšý£gÄ@: H„ÏƒFWJfñ¹ðÙ–ð‰õŸð™Qö£¶5-2±ÆÏ¢µé2q÷U'm þ�ßÑ€~ÎÂIáN$ÒC¥}`ðu#ç†S=->‹¤*Ýò¼Ð˜¡Ÿ´D¬ÇOH·º¬UÒuZãKkÝ�>ãVþÐZ¸ÅZ¢NsÒùfÀ6øRÇ—Z½]ß6<Ò˜Tì¥	JÌwN3ªn-S—Ö�Ÿ÷Ò:;Ö¥õµmÜÉÞy}›®Ðž¤+´ËíVhgXÐ<ºEÞ¦Üw€¨Í.éhÞå´“D'œˆÂ`n²ûl’~Ÿ]7„Ûµ—Í4\‡ñAŸ
ûÍËï8ŒiƒÓ¨þÍ†:\v£$Š„¢?EíÂÿPšð†G	lzýYZÿ3’®ÕŸoØÙ€éø·"}´¬ä¤SÅm«ü›e¥M}å¯%¬sí)Ö?Ó>Ì_„�®´ŒÃCNˆüÒêè×è›`+N2Ò­XÙú§ôQ½»™¸+ÃðO›,BX·q¡à^
êÒ-¬¹=™öù5|êRZQ•±~‰X„4¨/‹ëÏB“`Ú×—%õ§Ùî?ƒ…Pµ
¿Ì¥ó€E]2ÙÙšÖ?¡�©þ4®?L8(Tolýñ–H?�„HÍ½jô”úcÇ(�)PØÎ†ûƒ7bGá­ÙÄúó”'Úr{ô›Ù	=¹þp‘È×SƒnTÿl…'n…ƒ‚BÁõ»ñÄ3O*<P¿�4®:¼òœxdtÀ"4T6kýi$L9q«<"uÛÖŸðÇ ¾yK0•.xä$Æ³$þr,H;0²8;Rw¬ûv×8	þÆ8éãd8]ÕïÓÔÞÒÖc¢ŸÞdÒL8‹ë?ëPí¿x9UŽàÂLÀ§‘ºN™:3Êü+•
%`&ÿ¥oÔÿ¨+åË4ƒš/ÒSRãs,ÒZ…RÌYÕø°xÓ*�ÝeÛð_Ìîý¥Úô�L/™ÓÁ,¤ÞÚ«Ã­u·J˜Ð¥ÄP)JÞ¨K(¢Ž(UÆDÕ-:V•'¹ÒÆŸ-§:j÷Em¿U¨pÀèÃuLŸ;`ôZß÷ÂCãIþ€Ñ³y‡ÆC8™b†5Jãƒœt,À,ÆãõI×÷—7ßËqíîññîÃLãîN÷®�Ý¾•NÜ1s ç»nzf(yaçÖn·Ïí¹:ÓÚë€þÓ	%î°On¢„˜SÅ�’/?„ÁÕ·wM3ACÌ	•Î¬Ý=ôq[Î
¬Ê?†vië7è9A—ÐO}@¨Ù—¤ü”6J9óÞj”ok^3¾yÞ "ÆÞlƒ”èCæ•Òµg2¡]DƒŸV „Óê{íáØh'\¹O´�¸ÔÑ™‹ðks—®ij|Ï˜0pÔQ§*V€ß¾¹¶MŽ0gRh·5í·¦4uªÏ"õx&à·L0
ðÌ¬�Ûœm!´ ¦\˜;'ÔÚ¤ôC–î˜™´-Ñ­åJÐàIçÂœŒ=,jÄ„üa¡ßm�Îi„¡¡³;í€Ñè€ÑíP1p8ë”3Içc1v}ÉŠ`°0ÑŠÐ„ðm¹6n!»M�¿Þ‡x|$–ƒ*éÙöÏ\þ®‚êNÕ]Å@ð8Œ8—ŒV‹;F>£f³ª±¬‰úÏµxed,Þ‚½«Nã$0'˜iÎÿvùfiÒ¸wëe>%¸6æ€ø¢ ó­h¬+­ýZZãq¡àÐS}:=-<¡èú­´Ÿ€B _‡TþÃj»1Was‚sdsÿ›XØ¬_i˜rÖ!ÀUø<_žƒ‡V%0Hm½®` 8]„(Ð¤åôŒc8^�_‘ÞiNvõÑ¯ÖTäoN¸Ýz@„ÞxNMÔ&H]\Þ1�#(äTt„®p€w¶Žvhð¢½‘¿RGéfÏŽÖ÷Ms±EÛ]-N�ï¿%ßr@Ñ{sŸW+©ÊÁ~lçŸ‚óÝï~�Ýáòöí†Äª÷jpþzÌóÙ¤1çŒý§3³òm7hÑu„6Ê®M;)/u¢16n*où¡WHgNIÖÁÂµ[¦”¿šZ…W�º®íÔ¥U¥Î\kù»s×©†ß©ˆb51¨åy¥”uÝâ¯5œ¥-ÎÁ³jÂõp:› àgÙe=—
G§*Éeë–¡<�õ)óM;£Ø}DÁ‘÷Äkªö]–àP‡œZHÚM@)mÖ}Ñ©í¯Ài!ð[;|¹Úi8š¹í&F;S7ùñ‰,°ZQ«-Àô¿Iw~[09×.$‚¢i‡D™�ÏGõNÑ§¡n- 3šjžbS½Z€Ô£¦ªGU=ÚùúÀF^†na‹Fá8‰¶nån‡úß~4Û®ŸvO·¶óí(Ýï´®í<µú¶}l¢ÒX×vž•ªWu¢mt¢ÒLgcê§N25¿íB€£±l¶ô˜�Nc…ÜÎ+qò¼‚ÖùMB„:“¾^óPk0Zè!^X–ú
Æ	T8»Ó™…½vbœn€ðN„Àe…€5§Ók0<kZïaA’€D·câ·¶ü”G¢.Àaîáã€®¬+XçÖ`Ô*—|�jýÔ‡¹g—	¸h’[ƒÑ¡ü:­X€ ž~,¸ÄàªRU2ž‰’á–`šý	ª†ã|ô_UýºÎð’|ô_°x<÷ŠA•ú«Ô¿§-§É–ãÿNý™¤¥Â:	íÜc¼ˆúm.Zb¼µ¤¨ü
oi�åœ×ÑÚmiÐ3¨óXŠ¶4´-ÜÒà÷Pà8µÊî%Ð‹aKƒöÄêÚö|pKCNë·4´ù-*v1ÉÂt£ÚÅL/÷È²B:ÑéÞ…ó–SÞWù-ÖSíÄnihÃ–šÆoEpr+`ˆØ-ÄõÎt~Kƒüy†¶4è{ÚýËÔ›úJCüÖmipîaÂ,µxtå¤Û™ÂìQ3Èª/35ë˜Åüö°=­¶‹WªMûÖ_ê&Šº‰¢n¢¨šDÕ$ê&f½‰ïá°âž‘JívV
…a#Ör¿�BO#êîYº‡cöm´ÉI?ë×J{8–xïm“mœ‰MªöpX»Û¿‡ƒÄ¿×zGöpèÍÐ„6ìíáàÍ!l¥0ØÃ¡T¿‡ƒd÷pdáhB„=‹jý€£ú€MÞ^íp;9üno¿~Ð’S­šµpGëÖÜ.í†Û]íJE°¯#ÜïÑYUâ¯ÿ–[X¿hüÔþC;ñ¯ûû¿ÙÇ²Å¼bÿÞ6[T±¿n�¨Û#q{®ïe-Þ¼¿¼¾�ÞÁ«FÐ³mAíHVB¡0»Ð¿…Žl$ô‡ÐFátÛB«7´V# @#Pvj ©
»U\Û¡í¶&ù`×ÒWBÃYx‰7hkí£ö'€	³éÜl[Ðk nÛÂ"§ggwÁ©‰ d°ÛôñRŽ’åópZoÓõ±UR«fÛ‚Y“º¹NÓâ�ëaovÇo[`>t½Þ¶ õ¥%ÕQ‰ì9WÄ­ G&R~Û‚
h,+¢”°–1£¦(ú[ç¢Ö¬=X=E&ŠÛxÕ^òÀF2s»ŸtCºq>éçÕT^}ÒÖT.å½çeR¶U>¶€º®Q÷Tž´êíÿÚéÝ¹¾wÔî¿UYÔn–%Îé^VÕúÝkïu·K8Þ?`]ß;â“®Ã¿{÷ ¿à»½Å~ÿ Ø? }ë£=Ê'Nqÿ ðû']fû@êÓY}·—@íà™ýî7Ü?°Šà*øýçnÿ@‡¸ý$l'pgƒ·ÿwÝÞ? ŸV“5 ™Õèê[š©‚�§¥ÉÒ¯«Ò~•ö·�öE ¢-¥ý¿)HûZ6´__ÇNýgVÒu€Ný<pÉ‡Ëw.§Æáµ6e¢L¾�à°SµƒŽP5©_ÃöÒ8vXšÜ¡H€»:¢ø‰$KÞ#Y6J²ÔB‹O8ïì	¿ò—=Í	"JXrs€N	$R1JÆ
W¬ÓþfG¹Ý²©Å$Êƒ¼SHƒÁ-ô{dåï:wM§ÉhÛ*ãA†¢¬%s™£áLå6îaºjê÷‚à7£.–b„Â@9ŒÓnêE30õÁw.¥¹ÔjÝ•ñ9þ�J)Îq½�›=³ˆ0p�S²ž'ji�¶=Â7ÃPd:*!,MÎ@Î L�BIÌ†ô+ýN‘á§¸µi­£Š�Fã¨â™»ÀK�SÇÁ”Ë~dk¤âÔ:”Þ€a’,LÝyL/jLÚsïùå}z•î§&E0MÆÙçA‹¦N{Ô»¾©ÓŒíÖr£Ê™Ÿnw=IöÚÛ�àã<«€”m¤[;_n¸YïiÎnÉÎîvÞ‡ë)_ \ßK�ÞØ�
þ0„‡±Œç’¼uÿ¨&BS&ÛÞåv‡„í�¶™P;Ù=æ¹=ýSŽþÜi?OIûx÷qcº&œò�å”¤­Íöd¯~~=Jýü¤ªŸUý|rõs&›cÐbyÕ�«îü$ºó¹Õ�©Ù!ìtgx|gÎ…ÐgŒ™@Lf¡@¸P¢æ\kJ€²Ó�•žk–JÌHoLü¥áàqfÉ‰°‰HltgAœšK¡îLƒ¼]RåHAwvnh^w&Hw&›èÎ4RtÛ‹º36>�²òŒ•Ò§;¯OGêÎéz¤[Í ;÷)[¤OwVÊ­Ö�i¢;÷*pXw6!vaQMc¯õ:Ý9B§*¸Bº3†	ÊbtçDä0™U·ÊºóÉÉ	.Kòº3M18›îLi JéÝ8ÝYGTÐþyZy&)ÌådÝYG‰0?š¥‡I—À$iHç´ž¯Ã4ÓlókÆ%7QÚ…ÁÌ§uvdR9Uw®ºsÕ�_„îüÍ(ÝùuÕ�«*\Uáª
WUø™©Âgvs‹Ñ½*¼ðG¬“ 
C1*$Zs”7±Û„B´1­¶àLmÆ_èÍBÌý
§¤ãBið´‹µjƒ‘†v)‰¾õñ¦Â4ª0·0UÛ-LYYîßK‹±ÌH1ºÏÆ;óµæ´¥“sÃTg ðpÄ+X�´c.ŽªÕŠ´à¥Z†·Oy!´[‡2–ñmðRœRÃD;ýÒª0H±b ºóñ0¢ã`ÚkD®7Ð�DZãð$µEh´–ÿFÑ¨——R^Œ¼§TanyÞÆ¡@dˆÞÐ¿ýŽÛãf…‰vÒB˜$‚IFÕS‡±ª0[˜ð‡>z	I`öôöW4½äã‹ 
»ƒRŒÕ[ñÒ0+–¯«*\Uáª
¿UøÛª
¿ˆeäýh¿Ï9òÈóÐ�ŸÈ0p»çŽ]éu¾Ó‚ ßi»ú"ç·´Äò–fN¡L¢e•ï´°ë±ÌAÑ÷VnùKŸŠW}áY;ÑªhÇÌúoXªµÕªÎx£ªSj’{ò+­ÓJ¯a€¯ÏØwZ¦]WpóÎ¯‡%‹¢½ë¿£ÖÃ¸ÈùN#']pïd5fý—›É�|§IiýWt|p]/N�z˜Ë‚ï´ï;­£Œã¶3a¶vç šýmÇ¾ÓRéë¿¯Íú/‹Ñ++ÁÄ+ÙÝëu×/³²ŸÑúïzªoós)"?çE7qMY"ÃÃ"À\Àuç‰0•/ë¼ÒË}¼{ö_¼¾Ó®ž®J1GB0}©ªôV¥·*½/Béý‡QJïù!(½“uÌº`¼o¥÷€—©Le®záè…'^/NóB�Î^Q+á±Káå”†YG½lÇõ³[»A{%{Ý>M¦vÖaè…P'¹§ÖnV¡­^¨“^/ì¶¹rza×MVñÕ¤Š¡Š“9~çcf#úes´#šìnUñ+ò7�˜ "�Ž(½°Á9Ë^½0Ás¹§ šýmgÖ¡ñ¹ÎI›.X¬½³­ÉÎ˜};^Çè…�ìGœ!GçˆÓ÷^S.¸9�˜vBOåOm¬sÞËN/í€Ž’g½�@½ÐoD0Â¾("%U/¬zaÕ_˜^øÍÈ�Nçu1´ú×MÕµö…j“mGí^ÅÖ¯2Z—2wT”9˜— ?UëZëœQÊÆ­×n°6BÑ¾ÊN'æ<µŒo �fË€»®®¦sƒU«Œá9Ê�km¿o-·Î¢a—©‡	]k)ˆóOá¾˜sF®µc@faF®µ$¸mÊyv’#cÑE¸ÏµÖ®2¶“];×Úà‹»Ö«Œƒmïs­Õé¨„°®°§E7â~×Z…YŠtK!p>èZ×6ÂÅ—gY‹®µc\V[¿Ë4€ês­í‡Ù×ZêVÍ6w×®µT„UF“`öTô:ÔrÿËžHkŠAö¥žL›ìÕ8ž¥6ùÍ¶Úd^‰yfšd¯•à9j’ßÌ«I~{šä×58pÕ$«&Y·†Výõà"[ÁEIjÝv)sš³�’ä„C%	ª^ŒFŠXç¶††(I-Ž’dMw ³ßÃI
û;¡þ
¢$ém].JRëa2’Q^±ŠèSi”$“º#º�þJ L’W;	–½ñÕ�¡0Šô×±Ê%Åjœ‚Ù£¿®GCÝP%eý•NÖ_La˜n£¿j¦‹Þ)ìý3 ¿Ò!ýÃå_ý5QÉŒŽÛ�Ô_W« ?)ê¯S¶†>½þêOTQ’Dæ¢¸šøDI
[C[Ûc˜´g‹õ9Ô_Û°5Ô¿	è¯¤ê¯U­úkÕ_�þúMÕ_«þZõ×ª¿Výõ@õ×|h#-|[ÍS@YS É“eäP«¿Ú¨50u)”=Ë(Äh6´+0V×¡�&®/ÚÐF†6"ih£É+™Ô-\�m4
è´ÐFX‡·ÏÚè¤Úh+ýuæÐFÑ:Ü,¡�^ÍÚhÕ®Æ…6Šl/u# ¿¬4QßŒBo[ÊZ[b3P.t˜;!‡øÐF¦t®È:IÀ[."xÐ_ƒ]-†Ùç½a%û¡�Lø\_ªê¯U­úëÑë¯ßVýµê¯5ÆQ�qTw£œþjD@ÂEý‡�¿ëÒ‰Æ¹3"uÊGå%fSéš&sPL”"ÂÁÒ­jý¶
q¸�G)rþÃ:JQX•ª„	ªM x¬ÏÕI¢!ÿaßZFý‚OâÙœ$Œœ›(Pýú+�+Ð!î±­´Q=$�Hª¿¯T›•s¯Z®¢Æƒd€¯ÖÌ J`9âõW‚ôWWU3»þJ ýäŸh¹O¸’ ÷ý´Ÿ˜(E–#¨vZwÌÑ’¸žZõUÐQŠB…4÷¨(E*uæÀABÛŸ—b‚:Hý•„z¾ÒŒ&XÀ§y•ÔN¹g]}�A ­E·M®×ˆ*y)†9‚’tqA0b20Õ.L³sÔîÉôtO½ÎÃèÑÍUº¿°¥(À\˜4×soµÚBÿaWOä•OzÔwÏ4pýìF…<ÖXIqý¾£ê¯U­úëÑë¯ÿPw¢¾<ýu¦³bª\ãÉNS£ÂðjŠn7Y9uIjŠ`IBËÜ^«c,=oEiŠNÓš"ñšâZ­^8é<§@	È¯'²jŠ­^h NStMÈŒC‹ˆPSô;�º+lºs:qÛ.Ê>~]"YW¤`¡³µ¸Sð	žºzCjíUiŠ	Ì–OXé$d‡ˆ’Ùi*¦¬t&.›ÌåÂ�¦ýmï[é<'ÚS7iºÒc¶m'×“^Ä'-%{ê¶FS„®­9º’É0�¦ÁÐ­Ú4EÓ`§)¶#=$Hq§©Ó[¨)´A”ˆZT5Åª)VMñÈ5ÅokÌ¢z€KuÎÝÇ¡&ÕÉvªg¼˜èŽ<á<=òDraC:
|¦E.x'A¡]»Œ(@Ñ�)>ÚÖŠ…/Ug›ÄGžÈ«GžÐ¦/œ-ÁÇ"p½æ`ªÅ²Ü‘'èä†„�IÜó›‚dŽ�ñIÆ)¸G*
�$MÊÚ0Rî�ú·÷–«jüŽ‘!KÕòaþÈ“Îy2¦$št«Îj1‹‰IhÛnðÈ‚ÒÉ9,L·ý¬ÀÅÙ#OHàKÒqB×˜‰Ù–§‡¨èjNü‘'¦O"Ê"Ö"¢˜â•¸¨ã%O£óÄ0»ªà®n3'AGž„ÇðêiŸöÁˆÑÛÛ)è�™pÑ$êTˆ;q0¢¦Q¡m-L:&È/®ó9më‚u6/ELˆ«§Š¡myY• rÒÕ‡��yÁ!m¿�]5üæéUÃ�éùLÂØŽ×žjœžª=êI ucdÝy|:›;¦’ê#½Î¦¥)$)‰ÎŠ—TÉ¯^\Hra”dŽãÀxÌ�‘
¦ÚsgSZ0ì°€Ú£ÁÙ <]ÀÓ� <Fâ3x4 ’ªW)ØÞ <"±:±©IÀ6“äô,=Â€'˜@:ÜtB‚¼‰„X’ÀÔ:›MÊq«=®N:thAg#]Ï9zsœ×Ùô@
èl¤ëS±˜A¿’˜hljiažš <=GIæä‰%À'ƒ0%›y¦t|!³xL%í9ÍBùŒ=�h‡ 5½3xpÛy¬]«³Y˜Vg³÷ÖÊÌÚehDð‘¦Ú,�˜Ëp*dÇ¨E‘¥êl”s·,¼að M«³� <1L›Z€#i1t´,ÐÙP žÎºšÚS41Ltèmr	�ÇTÒÎÿZ-Ö×Ô8ÈCîÇ8Uu¶ª³½l�­Æ¦y:[fSu¶ª³”Î¦Å}¯³-‰‘Úda)š5A¼ì/¥]$1RÔÙ¬%EÙ¥C™“ŒzÀŒÈeôAg”Ji$,}v ã£¦KÔ«:£•´æ`NÂãÅ5›tñ.³>EúŽýÃrxZëÜzÅ‹\Xg3•ÖÌ¥t6‚4Ë¬ÎÆ±p."½…¢—&ël§<·t×wÞ»Ñ~ÐAîH›íÈ©˜¾˜!‰Öþ‚®m	Sà#$c}Œ�«'ÐÙô:4Z“õ˜ŠpÚ‹à°"<îlFtÆ‚Â:[‡-Ý˜F&Ag¼~uõˆ$=×ÂLN£’:ÛÒYe:£~›7àµÀÒr8ÐÙÂ%_Ç‘èer+A¼ÑQu¶ª³½l�­Æc©ûÙê~¶Ö¥†uÙ»&ç6ÕåÂ¯´í*„_ÁÛ|N‘Ü¦º|ø•ví¶‘6Þ¢…vÄ$0“ð+K~%@QáWÊ;uÐ­®?ü
|ñ²ÏÄ¡Á¡F…_!ÙJ¹‡É	–\äŽoDq*Á½Óu‚Ò(��9bS]~@ô«o¨ñc6Õ‘(üJ€¹²áWâÍL“6ÕÙ�¯�o¸	¿¢dŸ)›êÚTG(bO~…À�zSÝw…òÆ““„�Rnª£')ûÄ0áî3süE¿’=ªbÙ¿é�¼£-¿r—…m¥ñÑ1i€Ûrø•â1i»CÀW ¿�ð+3–#}ØÒ[Qø•Â¦ºªÉUMîÅir52É3]}ÛN—›¨8VÕ¯†2y–ûÉŒ{�šÚápu©ìÅšš„.Í·þ ŒéßD'6en-\pJB-­™‡uÈ‘S=¢Ð8EÌ;Lµ#u{¥JíÈ=#‹3f²¯pÕ„Ÿæ—AÐŽd¦n¼ñM4e|u4,ÿ81¯ °îdá'-ÕR�R(Åm³wb˜„€šC˜ò‡@bŽºGýv²�ï÷�€-ÀÃå£%Ñ]ë›èSŠb®š–ÖŽ˜„ƒ’ÀT²è"À¤îõ�R;‚4Z¨þ±!Aš¤N¹0)ó³ødškåï³PÍE@¨!Â-ñÁ)‰a+œÒÔÑñ½°ý óÅ¤Z§–‰A¤À”ë,@+Ô<ÆHDx¿7ÑÐŒ fÂCR�08¥Ž¶i™¬Y˜±ýn�˜hÐ¾ }+PÑkGD‘Ó¾«qýØÖÌõ#×¥QÝ-Ë/PpJbv½-ˆéRMãàÿêO¾×Ø\ûšó‚v82ð'ê·˜w!×UßÄª«vô?©qÿ¨dŽ�×Wo®¼šô§‹×÷÷w÷Wo±¶tž×–ÎVcÙ§úöixêÝÍÝ�ã:Ö=fä˜×nøaé‡¢=QFÚ§ýû‰rãî:1žc>mjŠ‚&§è)®üž‹!w1äøE±ãQÞj4‚¬=³âJN„Ü‘N0Ã#iKRÉé�ã %e7/ŠÆ1r�‡wÖè8ví¦¢ŒBîX7ñÒ{¸˜Àû—Hp¦.Èï&_6ŠOÊñá3Æy½•!C<ƒ¤Pýº#C¦ˆ‚Ü“Aõånf GØh²¾uÊÍQ»üuË¥2r†ë8fVçCAÆ[Û+(“ÈÑV!w²ìƒ¨X³èhY`È�Vn��§¡ÞÖ)KFŽ™� �©ÿ$YW‚}˜"ùq‚Ó`ÈÆ ƒ 1Ž3œcÃ<~é	^DuÒz*Å;I<¾3E“ÚB¤}ÁÏžF‘<ä°�gû‰ïˆ9Ú¨aÛÇxü:ãñÌÆxl·Žñ8M«üzC­²®ÁÕŽuKZÝ’vD§ü»;€'9ìÉØZc¶?f[ @Ù‚"G!F#÷«pzË=dè¢„“ÑéÊådp\ÎûÒì›j½b8¤ÍÃ^<š”NF´Å¾�‘s£ñœëÊnY=N|Ç†ã#æFCniKsŽŽàø |Pz»X�„Ü¶CÁèñ!ñyé²õxÜrì¸F1äµ‰:2lâûØjÅØÐ{«Ò£aëð"˜¾ yfH�ç £SÔ­¤O.—ËQŒX*Çu€µÇöã)Œ§!€ñ8�Ç@6AIZì™�Üm Y…&	Þ‘]€ E�ir@LžÂsÕ]ªuhzô@Ñ12NÖÓj þÑØ™mb@3�Å6q0jé|GLSK¿Ù«Zzp
æfzr]·¬;Ýêjé¬–ÂËlÓé(ME©X (Ùx2rÛš»,zªUë1äXÒ$XC�7™éŸvO$TÇ•lÓ2´gû�’ég<Ñ­È=r¬-P6úð/¼9k¨ÎãŽ=kÜ†3wŽš`œÃ‡íb#È¹&pv'I$¥~AÎ6>k-è'FE"Ö«7;Í­_õåk™E:ä(Èæ7�„ª/3�£#th¦"ÃÂ“ð¢Ç¤ •XØHtÄüÌDÑ`A<dµ‰íUeQOaÚæ…@Q1Y•„Hm.²V+E;Î|ã¬gÆÞÏ˜T]!éÝ¢O�ÎCvƒ¥§²&VÌž‘µ€pŒ�ºZZWKëjéËX-ývCµôHÖ9çÑ®«Zúü—1éa©àO¤Wµ«¥B‹÷ú:qâ¾ÍÂ€ˆŽàJJÙ6zJûµÅ™1äøEiµ¬•Ô'…ÜÕ™d 3.Ò¶ÖY¾k 2{”zy5„6ZãÑš,V¬V«ø=ŒÝkŠ’* °-€j¤LjµP�œ$/ÐÚe�ùõqj¼ÝržÛÕ8Î—ªý,=ß}dÆd©É	Í ª/hµŽ˜X1#®¦Qì%ŒÄÅÛqH¢Žå:‘×“�…Hj\‚E:6cQO1i
{db6 qÐ!dÄåºÖ�
¯,"(>žôöb‰6Ò9Õª5€ÌÚ…b,H²vdf‘(Ä©§Ž@�Å2íÝœ&‡Ü£dUK«ZZÕÒ¡–þÃ4µô¤ª¥U-}YjiÕÜ€æ¶BšÛj´æ¦"™`‘.Mæ47#»LR¨ÒdVs32Vœ)E.”Ìjn6´£ˆÞ4òšÛ Î4^skéÈ`ùjníš±šÛÉ<š›—tõòõœš›§/!.î*I4ÝÍ4·6@Vš›W‘Çxå²f iÄ-ï{ŽÕÜZ1ŽŸ‰j57\Á¼L{�IÒjn-ÒÜÈ›inÚI¹_s›inmÐÜÖUs«š[ÕÜªæ6^sƒÍ»øÓ(Í­†@­‡Æ¿ÔýšìžéX÷]ŽÙwyj�ÔoºS¿ïÒÙÖ…ñH#‡·!,[†2¹CÇ d TÏÚ#øÀo2¦ÆåÞøS¼d•Í�Îvß%Œ–Ã¦AVû.b�Þw û,æ¼ðÅ$È®™ñÝèä¼ÞS§BGª$:+�Øø1S_Ôe_Ï4Ç¼¯QŒ ³ïR'Ï¦AF'ÈÅvÝ‰Þw9QŸHð‘�å@C¢i!‹ˆ‚ðA}~<ó!“Ê“�ŒTm!ÇÒˆñ¦8¯Ùßp¦ £Q™½ÇA¦æ¬’p¼&é<�)§‚„S)dÛ„Í ŸÁƒáÝiˆf„‹Ž´g	FG–“Hmê¾Ë²þòÛ¢þj;“nÞí*ðjÝ::¬…þ~²�@	ý,VBÕý)6‡Éd}Ñ;HQÇûz”f]�‰¬šõ&šõvÊg�£Tã(Ÿ>F��¬beža}¾sª�©‰��Î9Ï™¥Fëæ�ƒ�sÇÉ‰qS�>/()û/l5L% ybŒZG©Ãú|‡õùëó£¯\S£,Æi·ÉE»d¨ÏwXŸïV'`5êóZ	ÕÉ%ŠÍk#
Ÿ@^Æ£Å¤¼NM¥‰vD�c(zËaü˜†­ƒ$aú‚ä+£Ï§ŒÀs�‘>ßa}¾[¯×ãzK†¥¢÷ÇÁ‚Gñ²ÖçY‡õù˜Ç§wîÎÅQê >Ÿ…¼Ø ²Zöõ�ÙŒu‹äþœC}Þ�H�ƒ€õù8w²êóUŸ¯ú|ÕçŸ§>ÿÍ‹Òç'kà5²qÕç«>_õù]èóZÓúüâAënúNþÈjÍ&.rÐ«í*ºÎï¶º˜^ùwÖ
Ì
2Éø��>Ïƒ9ƒ{K@ZÌ�¤Oðn7ú<Ö¨gÕçyD­f´>´nÐìóA]o9EŸo6f!Jm\íæ¹Xƒ	Êx¢ÏO!+Ðç›†3¨Ïwë±Ü‰>OiÃ¾#}žs§7Â<�)÷jvÓ }~3Èç(Œ_Õç«>_õùÍôùÒÑ°U‹>Zü·uU¾®Ê?Ï(T5üôÓY:¶3ÇT->£Åw¥Uy)Hzéž‰X�îIZÅWéÚÂiñ,”¼B'‚Ò‰˜¶4M‡½ìÕ+�—}©b½˜Jm<äòª<ÛhU¾a;Òâyö¨Ö¶-á{²¹áƒÊÑ€_X•£vÅÉS¨é/{ÌNbº¯“§py=UíE|nkvU~H‹—�säiÂ/Çï¹(ß±äX‹_Oçe½*ß°Î+°y]{3ßÉu,·*¿(ê¼ãµxÖuÐË~Kû \•g¬ôY®Z|Õâ«_Wå�^Ÿÿ‡z|pÕç«>_UÞ;Ð×©¼JJU^9`•WÍ÷@�O“$£ò‡p¬ò’UØ¥­êöSb@)\µð‡T^•U^ð,Ry#@)\»š64@†*ïXP¤(r³œÊÛ[+2RòÎib®[Ç F¿Gˆ©*/QJç¶H«¼v©y•— ãˆÁÕL*¯_ö�YÆb²µÊ{FJ*/Ë¨¼ý{š‡ø9´mHåàgRTyµ’‡ˆ’¥ÑDÈJ/5�»ˆ¯¶‡lU^ƒ¯ònùt•—ô%«Ê[UÞªòV•wÏ*ïßä«õ£3¤¢«dyõ3ÑöJþ}Ñ¢m3åt^Í þÍûë‡«›<¸A8`µïÝõÍÛiÏ£ëOÔS·Ó¢ í®ïïn/Þ¼¿¼¾�®[OUnÄŽ”¦fOQ§g‚ËžJyÌdÞ¼¿z€V•_€[—÷oõ¸^!~”¥wT[Ò£ê~¡Oä%áOk D�÷K¥âÊlZ_Lå©æ>±å.ªà Ÿ¦áýíNV°-üAxêÿ<U0Š¶`Á�¹LSA�ÒPô;˜©uø„Ê`{B-Xów®óCš½ü±Ì!ÝfÒ!Ó…%¢´ÿ0ÆŒì%ëŠ’æÄføÚþ´3ªƒÏð1‹MìéW6i�Pµg­2æ«Dû®–P0`ë¬¤½@i_£ þºMPq3Ò‡âº§èÃ>AMÓÅfOGÃ¨‹å[3î"ùtà!‚q	NtN(”í3eDåªLØ`õRügÐ'½"TD'¿÷7Ð£7yÚ«q8l;‹†qä˜ÆB¦à(û¦2AØ®ð�Æêaü·¸®1¾ý¡„$FÁöˆü…É�‘R¤ÊCM2?ä�ô„åèp•o#^jsÌUÀÿ¨�éÑ–¹³SJo�&Q%}üßâòtÆ¢*›
‡æ3ˆnÖºtëÒùA ›}Ì‹ÙÉ1SÈ_§Câ“kB�¹`ü/l§c6½‘¸C¢›>"ôVsRõî…î½¶Àú*¬û&Ú7¿ê=ŽøoxŒáŸ|·’‘èý`~nsÝghzö¨w<Äìßþ¸8*À1µ,„l�ÿ¨ûë
œf¤‡ àRž•QÅÎxŒ]Þ+Oˆo‹ÿAq´-ãC7)”kþñ ®ñÏÚ!üÓ­ñß3Š´mÔÿÛd<èÅ±x‹†3àf´C¤CpV`cý(h‡f½¼ U˜úèNñÆ½<¾Um\ú$~Ò°?!QÅHDYR�—¡Î…Å«T@ÐÂz§„<1“ïÒDçÅ÷–âxŸx›à¿‡ÿ˜éÜ˜ÜFcnÔ{pz¼‚–à§Ù®ñ=7Ý\ˆ+^2°Át™mlª©³É8¬‹DS¢P6(ø&GiF¶k.³Êà<
“–Ú.=Àß“+P,^R—8{UÀ*‰’(>I˜»Ål9›övFYfÑ¤ÓŒ¹Ã¿ýmõI@÷`!–C7‚FN{/¶…´…I$3_ÆÆK†ìkõc}6:e¡ýáá¢ôŽ\)Úòô§`mÕ­lÎä8ðÔ«é;hÊù�íhÿeºtþáî»‡¯>^«…êÐ(àZ!ï×Ô¸µ]Ycã:ë°>ÆÆ-lãº§u:*µío‡:î/?|¼¼‡~*`9ÞÞL†�Ÿ–�T¦¹RLpéøz”KÇÉ!¹t€:¼»»QîÞüó“9‚T/Œƒ÷Â`Oåð.ü_8w �‡%yŸ¶â!!à>øtâ¢žÿ\ÚË�@õÏ3-q¹4ðIH[Š1
Ó¡¦ÔºC@ø,W_ÒFèóiâe^BÁ+ û&÷>jfÛƒÚGã4¥ÒšF˜Ë^MÓÄé&“ö™&%iÿQðh ÅÔw”Æp2i�Qý7\C?5¶ÁïwøöõiìÅ(ÊÀ‹‰
~Œ¯$=X¿,Ì€ïWˆÙéAŠiÓ Ò ä1×Ð\�éÅ?NF1Izÿ|D¥¨{öõCZ0sºcz�¤a®%i:š|››(a&Õö–¼Æe¬pƒs´C|õ_„ß8�’�1²{ü7ƒø·cm0þ Ñ$“�Iûñ,š<š½ñpxBóT GŒÿ<½l�äTÆEÿøÄÈ®ç‡ÿc–-mšB_tcÊJƒY\©“Ü„”IÇÈãŸÆýaäü»[ü¯r]!´Ë·¡‰Óhà19ÐþÉ!“Ì3ømú(º©ü³­|D¦É+ mµ±A¶ì,þP¢H 4860éý­ã±ñønFà?f»(ÍØXžÂ=»ÉRd¨@<DãÞ˜þÁè¼ô±ÐSÂ·nÀþ§þQ;8å}—Mçñß?È4I×Wƒ:ÈoXD–ÈNÑ›šùèá{:þ{ßÄ�4((;I�ÙùWôã¿�Î[&�Ê±&7Ôm�údø�‰©Lv)ÐS£1ôð'rÛf’"ÿ·þ»¦$›õˆ<Œ±ØàÛÊux,Ûl¢ÞPG'tÇéO³â}‘dÒ.Az‡(hP²Ós¬z!º(ä4�-2š‚ þÓéåáxðmHÓ_˜lX-6T‹~ûc»lþ ÃÒ.³Üž£`èQ{Ñ4³`“ePŠ5|h”eQšPÒ`�–cfE1ç¡íÍp²ÆŒÄ ±)íN;oŒ^~DSòkXT¿†ê×�¬ŽŸX¿†îXýÄ1;m(¯q¬NªqÍ17®­)#=R~ßç‘¢�Qd�«Qþ(§‡ä�²Uh�=¹¥Ì{xÁ¾ÏH8ö��ûŽ^²y4�«Ë�Eòš×b¬¼j 	(E�W|î~|Ûh#Nµ1ú S.¼W·U¢dxÞÁ‹ôyi?™÷oéN1›•cz8TR²o,L„eÒ$,²ùûdh•¦‰lQ=il¸ÚÙ2Ï”Ð�ð=5íÛJ"‹‰éQº?èFaðK ²-Ùâ4ä1óLí}né¨ÿdèÑäè	Â¨³a:…_ì×@\¿°<O"[U_ú)èñÔýoÜøW²Úù-97ÁyXzô[ÄSÛ!é_ß~Yõi­âÃéÞù¦mÃÆë~ÃçQÚ—éºa|“úÌíFyXãQq›bÚáWDiyŽ�hÙÂëè0þ'�WÏnü�ïÿ4ƒ-:eÓÿ¯,’ˆ›�œí¦ø.-%ò\äKE°+Aâ©Þ‡5^â›¤·Ã×j;ù´�ßƒéhŽMÒÛÌOÝ|ƒŒ©óË&óFW;äíØ¿ì<~´Oiât3ç|É¢þã›�\.à+yu*¾3øßVŸÌâöÅùü•“¯¦È»Ûê¿<Â'Âw³Áø2œFÍÌ¤Ÿl¾5L‚�ç'®ö`�ô	Æ'ožÿÈ=í�ðOéöã?›0j~�üX‡Ò3tûM†ÙÙìgSô±AÏ›åücâÔñm;ïÁâ;WÑÓ(=ö`•Òzêà1zÙ2,¦æ\VÐrêiÿ)—S¨Aç;n¨ˆ_$žÁˆ–ÎØ´êc×êÛë‡pñáêæêj¢7Â™#Ê~Z´;ïƒó­Ï6ùÂñ›û»‡‡ïî~¼0ÈÆÞÔ}co¶á"ûÔN2~�“p�qtÔ±Cˆƒfò$[“èD"û¦˜04%�	Ó}K�#Âó§Ï¦‡bD3÷}= —.'PP8Ñ­Iô%"‘ž
žÁPE_ÌPõë"}..o~¼üËÃ‹ 8òÌ àð…;ÒÃ�w÷okG: úøød�÷W—?Ü_}¥{) ]©(Yî;>r®³§ª/0µ–ÛùƒÎs>ÛV~ ›5û2ë_©Ô$à¢÷¥V›,H�ùxu¡=óš‡÷w÷�ò³l‘[yïý@™‡»@™ü«ô‘`¦%§ºˆ:¤Q{zÎäÏz(‘½ÆzjnìQÙ¢”G	úÖ››˜=ŸŒigCsÌ˜9XŒFG|1Æüº$k‰ ¦›‰|Is-ä}aŠ´ÉÙaöK‡�Q)€eK2]’ø’-IêiÆ:S‘VSÐÙzru´€KÙ0ù€kU›m‘EÑg!ù·£!—M·¼LS÷ËS+ˆíÙk¥½³Y_>	€Ü@½åÒºcív²qbüÌòu�YæÛOp çf¾ q]�ë*à‚*iŠŠþq]y–‹¦8®«.Ü²¾Ó#Ã¸n:WéˆÉ0®»Ñ·\Òq3®/õ¸Þ4¢iúf*0®‡‚x\ç2®7Î³§e‚�6JÔÉjÃÉê›Q“ÕÉ¼ÓÎfS^UƒfTƒž¢eâVÂÜQÃè¡“Ì6~¾†‡àÄ{+`3~�@ÊMøi"ì•Ëí˜[êU¯T.‚ÚìƒOû­z„¹’'ð î¤$kÚÆ4Ú±g¼éTÉõpIóvá´ðž·›€¬º˜×–£’‘ÿHK)7ùÂ=ÉM³Æ¶1ívVÖ—((};JPê^¨VÿbÔi¡§î1Ú´5Ýû�Õ¦A’(mÚ”$Õ¦�æË4÷Á"emškÓªë)Ä L½^¤w¢T²±¯umK+YHYC?X+iÏ@ú’ÏßÆÓ›Ë½T>vñ:é6ÞÍ…H,ûT9žúþæîG5Ú¨f€Aèäæú_~¸~{ñöê£ú{Fœ?ŽhÆÆLæZíÚÓ±@»
o²«ã†Øžg¯Ï±¬÷ðñòÇÛ«�«„ü©&oå¿–¶îps1ž\™,yÁ	åà.šÍtúé·ZÍJmÏãáð°@óx cð ‚ËÿŒkŒPÿŸ‚„þ0X¢ù*4€VÆÉŒÙÆmÃ‹}äpÙA�ãZÈ…½DÌú;Ç!w‹‚d|BÈÈ~ÁL×àj½ªÔ/h_¿8~`´€ ±ýÂ²Àð(yàý¢€6a¾š/ŸÅ|1b˜/BCã~ò»_”Ø�ÑÓ‘ü0¦?<‹~Q@Gà�!LDù�ìºüª…w_=X<°l¿ˆã‘Gä.æ3�Gï!L”H!è“ö‹ìÛØi4ÆË!"Íg´8"ñð´ÊE¾MiœLø!ˆQ}ü`iÑgfÜo¿(èYÅñ�õÏ›ý“è�“¥¢)Í›lëy“$"ŠFŽàøÀb~ Ç(O"ªÑ"øÄë å(V?›‚üÀÃäô,¬Oa=‹=y2�‡&¯g±ž~qPü°.ä·ñPš7y~¾`Ï¾_äõn©_Œµ?Dv†çiÐÑÇÛ£Góæ^8øq’–‹MìðÐ=?< åëU1 Xi´'¯¯Þl»ØûSâÕÒƒ8(åoÒµx¿JlŽëøîîññîChÞÏ¼ää	é�S²½gÓÓ7iåšv<M:u|9S“¶#“÷ö
MÊx^$¡~4•Ž‡:?�¹Ic©ó›ž�ˆÐ0NÞÜý(›b
 v¬mç‡q‚R_ó¾»¿¼¾Í7Ï’J—È6oýÔœ·OªÛšÅ³Ø—4úÔ<Õ¤ÏvåbõË”�®ß]Hj_|ÿƒ
¦Ý«þ ÿþ¤¾÷†‚�öõ gÿ•Ô«^õªW½¶[ç¨W½êU¯zÕ«^õªW½êU¯zÕ«^õªW½êU¯zÕ«^õªW½ê5ÃE+
êU¯zÕ«^õªW½êUÈzÕ«^õªW½Ê[ƒg+Ef„UKÕRµT-UKÕRû-Å*&j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©Zª–ª¥j©C(U¯zÕ«^õªW½êU¯zÕ«^õªW½êU¯zÕ«^õªW½êU¯zÕ«^õªW½êU¯zÕ«^õªW½êU¯zÕ«^õªW½êU¯=]KÒþÓõ»‡ËW2Á¤ù§»ëÛGÒ]Þ\¿»½z» âöòÃ9ÿp}{õæþòûÇÿxõáãã_dþÇ»»òë�ÿÝåÃãõÝíW�÷W—?Ü_}%3®¤}¼¼wõH~Š¾¹º}¼º¿øx}õæjMøÇ»ùr.ÿþÊÖj±"ëï¯o/o./¯\-NˆËëûiõÝùh÷¯W÷òíôóü�|Ü5cqwssùÝÍÕP;~×ÓYÙûËÛ7°-¿`ìÝ‹ïî¯ß¾»ºxsw+o=ÞÝ£†}&ÿ~«Z¶]Ãäã¿Kñ|y}ûð•EªN­‰x¸þ·+ûrf_Î–¤ûxysõøx¥¨Ýº·¿
ow¼`+ÀÉ«ï/ß\ß¾»x{}/[%ßK4P‰XYÀ&ãjn
Wì.ß\¶#¸ÍŽàÒ¸i¿»¹{óÏŠiÿ×°Œ‰¾¤÷n~ò¦Jÿ]4H­ŠƒT:ÂèÎàúxÚ÷]wŽï ®ë¯§“~ß3&ùQJ?å?\¿-DêjŽ®5ô(ZC�Š6ôÒæñîc§}öÌh3Üšƒ§Í¯­¹pbWÂrÇC$:3‘æ�éïïî¾íøU2ãª=íùëYdà¯{�ìäàÇ»‹/on.ä õöæ
Äk[Šbvh‚k3A`[�­ ÔÍ¢)¤>|%±¸àg	”G��JR¥yiK	p5O½åãÿaãÊ�¡rí¼\+QzØ\‹&ƒƒáÚÿa×‚¹¹²ïÎØ÷ÔžxPH;NþÛ'ßßÊB_}w÷øx÷Á&/þ„YW©1¯Ô7fÝSW@º�w7×ï¯Þ^|ws)yäñîV[åÏbÅ”¿Ò½|XÙÚ+ÄN—7�þ‰Y¼–¼¿¸üóõ¡)�ø€ðï|ñHr¸â�;EïëûÐfJÎøøpýöêâíÝ�·ß]?Jè¯~¼º¾{‡-•{y+ßË[Ù^ÞJ_ª1u_Æj0ýÐÿ�Ò•ØÅ˜ý`Ôüb­üjš–«Ï¾Dýj¨~8¾˜½§qùõØr<y­!p …2]T}0ôlÓpÎ2 E6N;1@9m<¼N~, ¼ºBÐ¸±œË’üT~Êæ¡N~Ð†«ª€jx²�Êhì‡°ðE2€˜.üaP¸ûAJ[aÞ„èìÂÎ`ZËË¬:Ýn2#Úh·JÁž¸ÑPË0•œK¾'¶U|WbÝîªNmÕE¾ê[ˆ.S„¼¯G	yë#ò¾Ì5kCXE¿×¡øÍ¥Ò"o�üÑ½¿¼}''G:B:½¾¿»½xóþòúvz�eÞgÏlñ»™$§°e£…’FM­LÎïf‚sè˜K>)0-ãÈ«Ñ²ˆ„¦àÉù·i˜š¨õ_ßÕª�ÎˆŒq%/-¦/XMýLÏùZÎa¡’²–88§‘Ü£ê§Ä$']mÙx)5¬/‘T48�›Æº4Uð•²2K¤’€S�‡ÞfP/¿šg!§�=MÙ/R8ÛÝdÏû'ûC–SD¿ˆ5Ï\®Hýû¢ÈrõöÝ•\R¯®W©W›W^™{ƒÖÓÿÄèvÆh™C~•tÄ+äª·[¡-ûìöÈ­q*ö8Öìñ�ìiYÃ`ñ™±Æ_°F²Š¨øâõ³æ‹ÿiÛac
ü¯m</æ(kÂ†×{–;ÎSMød^Ùlua�w^ü�–;øa-|™£ðÅƒì_€h`õ[Âx¼~#»ÁûË›ïe‡×Suu.ßŸÞþ£Ô×Æ¤›»]
ÐÚ<4ÏKõÚ”sZ.×ªéI~µ@Aà™©ª‡Wrªgá¡q•IAÁ¥†…Õcy`yPÂ¬Zø­þ+%\«ãÀžAA}”‰‚¹÷1VHœâÎÍ‡Ztàß§zn¥ —¥ž�n´SÏÅQ©ç;õÙ­zN¶w�� (}=JP:è%ƒ½‰]uq 
Y!CÎ‡+æ\ÿaØjbeB%E˜8… œñNPÆ{•\g%n>¼í_K(ò	µj§}›I�ÿ@	TÆã�1Ê Œ±~ÇÁmcB£ÖnïkÅ
„µì¥|‚ôáÖGŠF9+ƒ�±»‡ MòÜ[Å†^»ìãÝÇiÆ•ÕÎìo‡a&y_Òº–ÿl7œ+1 ƒZ³Ÿ±„ñý“ÿ„r²ãò›/D%ÑÀÌlB§>Ñô÷“¸·è9šÅF5y·} †¤–Ä�`fá•™ã³³7×�”
¨¶0°¸B”öQ JÎX Óˆ·°0eŠ`Ê¡ü˜v´Ð²‘±S ËÔZ‚Bb@óåoãÝGoh˜Üâãt Ï×	€ìÒÏræ}¸¦õ=ûpw�í¬?�‹öbJy™bÑóØ R…±Æ(]h¨m´UÅn,ÁV	ëIµKf,«kG¡¹‰Ñœt£×P”/gì¿© °F	c]Fkò ¨vuä¹Z5bm,E+Ê®ÇäA±dGŠYkŒºŒ¬.FN‹Ö�¨Ù›“3i+—lFÆª0V…±*Œ]m&ä…/¿ÐÿŠmRŸÊ¿OT´,|u‡àðžzww£Þ+ç¨c5Es4ßÃzÌÏþ{ŠD*Ê�Å>ª3{óDXäše
—©óí=á²ðcª A¯}2%-�« y—®© W&¦lÛP@9†„‘!<ªÜ[B1�•T�`Øqí3¸õ¤Š©$vØ3Ä\tÆ¬‰1=©€ˆ�±JuÝÐÿb–èË2¶i˜ÅwŽ/ºcro< �s÷¶zœ`>Š@^Îž„W «£Ùw‚'½Qiå—0¬z#ÀÌNFã´µ&Ã˜/«bf¬¦!C\[Âï]aUVE�\ÆcuÂl…g'�%àlEA›ãfóyÑ'ÃêFl}x}–XÍŠíõš†½¬^Û÷ôCÄ*Éª<9É&[wÀ3O0[Å8ÌO`‡@÷µ³&‹¡¬|Ãs’'ÈËˆyÕ`×X-T±< š¬‡›fÑœ–ÚH¹Ú•‚7Å>!¶°mœ%FeíÃfB}l…B±9²Å†ðGâ~Fk`
0ÓSÅŒFôƒ5E‹'yƒx®•êš!ç	>}‰÷·}K¼—>^Þ?æWyíÉo^j¥·9¯‰_¦Mýp÷ÝÃW¯ßÝ¨³rýÜ}Whœ[Æîž¸qcáÛÒÝà“�Ò�¬ÈoÒÕú??^Ýª2_ÙúÀ•zš¢OŸ"îÊø )€½|sž*jÊ–ÁCßG�ï%æ”3G‰kåþ0Q–5^J3n¯ß_Ý+�ƒO�…§ WóËwWÚwxAHöìfz'ÿÿÎ3ùº-&!46ç[»þÅ…Œ˜"9qGaè_Tå™Ó<«–Š€ÁžØuNKï W w ÷Ë¾YúÌ?Á˜¼ƒz—?J\£4<ýƒù_²u_W-Õe…ËÄÇf·¯vojü¡—mnQÞÆÒf	3)áw,õ;7ïàƒï€-JQ§DùÎ5Ü�¥©rJ‚x)ã
þ4gn%FôK“w˜L^jâý4gàYH¦÷Œ16Ä%¾ÎÑ%®ó½ÃpaIÅÒ_Sû h‡ox +æîs¦eÁhÂÚßöM¸ïï~x¸Êí =I§ÛöH¦Ûgq~Êg2ö?k_½ø­?t?3yzó#c=þ­^Ömä/^ÙaºçaXBq¹gm´ˆÞgÕd…Ö>… ËÐ¥µ|÷^Î3·ì¾÷¦uý¬	\á×÷&Õ9Cû,|/KÑæÒþg3XÞ`†AìöŠò³cGN 2šhnÕ…îJ�1˜~=j0ínßÝFõ¬â$â‘ÚŽp°¡~`½.6�©9ÍŽœó¡‘ºI7#?3”Æ£ž,Â KŠ{4IyV¢Wøgm;õ=N}�•Nd3‡	ã!§gJáhkÎ!âzçùg-&U³ðHÍý"ª8~Vá’_ˆ+AË¸2ÏÚIùë˜¹�‹�1B$×Äæà÷K©ÿØ7Rß¨:[f$ý
gmÅ¶8éw—ÌÿQirRmZ­bú 3æŽ ±}ß|3£‚jEžóÃP¹œùw7vÏñüøuž›Ê�Sùño©1	Ã\±¤ú¢î[çËqò9ðßS‰ßäYp}€,ø`¸¾¹ûQ±ËÍå¿^.:¹¹þ—®ß^¼½úøøþ)@VüŸµí2�Ô�Œ2xŒ‘S¡ÍYI:®±,¬ËëomødÖ€ª¿+ëbÖý6Ïº¯vÊº{S¿vÉ²ÿÅÙ`{:Þ$î›ØûÄíO1¦Tù§¾5×2�}c	¶zkDÐÁ·1s´ñ2XûïúX[mlW5o§‹¢jÅA`Îæ&Ì­“ÿn—Bh¼(Âà/»¸ íU&^›]^K(f¦Ã[^Ì¹uèY½®3êYóxxÖV“ú5&æÛAÓ­6êÙƒY�hö6Â?|P®?y}ÍyÇC?˜2<¡‡`nÄ	×˜ö\9 ~a{•¿|�ç—n§üràšÑõœÌýe<
Ì²¬™�%Wéi�<vÚ¥nŽxé›</5/lìù+8ö0§Í˜1ˆRG Ì5ßæ¹æÕK�þs<1ãŽC´‹öú F‰Ðã’Ñ,€'�-©õ
«3bÝQô×·þ¦èFm45.Ó‘À¤Î4ýBEÅ*Hßl¸høpùîÃåŸºÂÔ½yýpu“7ç`úÞÄ…Õéf°ÅZÔ&„Ðü<Ñ¶òi—Ñu>­�CiÒÄ¬SÀ4zÞf˜t§¾BºÃ÷õÝà×u¡¸Iãúšû8�k�}(¤PWâÒºB²&¤#?:É;X[™ìpkKéUöþ0þñýNÕ¢»Œo÷xî>À§Åwð�°=¿˜<6Í)¦tnToT,~õÍ�ð>0cá÷s‚ÀñÍ¦MÕÀ}žà‡NLw:»ˆ½w6MQZ?ßu M]š#|Çé¿áñ±iž¦!~;š¦Qýx>Í»Þ47üàñmñŸOwýß…´¯Ü¾ncü’-Óãàó‘iÁï¢×�â4ïOÛ×üz|w!íz£«¯+Ó°ýÒ$—îîÁçÙû¸þ4iO’æ’^7¾üãç]ý8¢‡©…ç÷â¿£~Ôíriš¤õl4ßãèÉÇ”×ý9à³3ýÛ¥!þ¹A?ô´/™süYxüw(í*¼0øïþ_Jó§ŸôˆÒt�ï‹hüè¹?”¶õï¦¥áóòo¥éÌøüÇÃ€›3'ãi�Çî‡¯¡ôFóI„ÏÁtïüÁGøg>·Lw,ÂûàpC£dHãþÍÇ |è>}:<žD÷³ãK§»4�ØŽ7°þmTß6È«8Ýíÿp<öøï`zÿQzp<ÆøïéÍ§v|îàüYÂ7Oðßa|ÇøòŒï,þg€<SHwx¾ÄøîIw`Cü<œî¢çþA’ñ%¯Ÿ”äÍ.Iw9ù³‹ÿéá;›î—Ç#üsÜ_ÌüŒŸOðÍ§Ýäùv9,ÿcýÑé·€€øùLº<éòéúç¢GåµÉÌÄù´»4Áí%p¼".Ýa{ŒNó_×�óÔ^ñOÆž”Ø?BÚ½� ý–´Á Ò]—¹ß‹ïØ>Û‹ûQñå[ŽÒfçB”Û¿ B8¾ëó¼Ãæ;�Æôˆ”ðSlïÁøo�½©‹ìU>ÝÓ…ŽîòæúÝíÕÛL·:paWæZåPnH?-¯rL?cãïÊÊ}!o_ß^|¸¼¾MÏÙø]º´±8äóâçZ¹Øù‚y‚µ‡C9'dÂy¿?.ä´#¢'gK¥yÙR3ÂU¯,ÌÌ¦ÞQYbÓA^.u©¢£Ð•ÅV’×ƒtu´,ûIoz�åÕ9ù~CX[5-ÅSÇ‰¾RyÆÌ‘a2a„ 3âœôgÿÚ4«yF¼:Ž}gå{ºaÖ\~7	¼¬eåŸ8¬+aÌöøxõé¯g^ý½2ŒYyµ^•W+¯Ö«òj½*¯V^­WåÕÊ«õª¼Z¯#5T^­W|±M¹0C6Vyµ2Ñ\¼�cL6†7Îb•WŸ5²AYR„¥O1=¥c{£B¹5)Þ7bf—ëÄlY•Wv$dCt2ñÌÑ­.`q€çã¯ÃR".Õ³ÖïNãp�þ„?Ê$Ü„og)Š¨a ”{¾T™}Å\"Ãâéy9<²Qç–oAïìx¡b‰Ïë—;.2ý¼¹_÷­§b@€sõ>�°£n'g²©¶�>]Ä»œw§¢Ò(ïÎ®zwR‡ê’95«ºdŽG×¼.™mš5ù U“àôvwb/<Æ=wŠ2™ÓlU]2�Õ%“dÔ…�ãÔÆY)Š1YÕ%s.�ù˜]2I�W¹p$cV—ÌzíÔCª¦^Ï„W«É°^•W+¯Ö«òj½*¯V^­WåÕÊ«Ïüª.™õzâëy»dRyõ0™¨ºdÖqõ‰™°ºdV^=€‘°ºdŽaßÙ\2»çë’©B9V—Ìý¹döœ$öáú­9F,·ùZþý:uÈüìH×ž×ôçÚ»ë›·ÓÜ?AÞÝÝ¨GïÞüóÜN£¯!Þ>^ÝßúG(éÞ_Þ¾»¾}G·zºèúÇ¬¢˜þúþ€=ÿá£r¾x{÷ãíÅw×�ú«¯®ïßÞ!×Ñý¼•íå­|.[²#?^º#¸|O~Ç3á�í.q`ã›÷WpTý¸uyÿVO8áBÎ�óCýñ*…
|ºù_íêå�Ü	€î°»�¡bn{¹ËeøœtÞ¤_¹SËœ!P†–ÞàÁhÌ<âBë3—á±n!ãï¢Ó'zŽIÎ‡Aø$ j?È4.�2„³tþ˜•è4Ùc¬wAŒkÊò�¶Œ•Ž{<ëe“"ð›ü‹‚ßÆd÷'Ç…Cöà¡à6D—nA‡fZ£[vùŸnÁbÊuiÆ uÉÝ Ð@}e8Ó �ÏÍëðk“{À0¯ÃÖ3p:†=× fè2ð� Ñ‘]×á3xPFÈà�…�ˆ?Â±‰]—=—oLK2¦¾{hLN{R–3Bã\c†2Â¡á˜Ž¡ÖrœÁ»†;é=g$oIOñÔ7ô4c�'n8ï2#ÇÙŒL=bv ½üÑË0,FjšÁ¢G¢Ã`rì°)œ$ü±Þ€?âŒ¸ù'qÆ0tüÑ`ºœœtº-h–mq�´�?
4vxt0óKæJ�aK(ÅS<Ó	§£]`‡L†e(¬�dØÁt,:­Oòû$™=âŠÒŒ5IõìŸproI&-ÒŸÑÿ–×ñ[Ì!bà›�Jtª-€?Î8�érN‹âWù”×Éü‘2ÊÉ4æ�W);D¤u);D”‹�|MÎD¢ë”bbw1tÝTvˆÙ3]	F9v ]|<ùÕ©~ŽA<�œy††­¸Df~Ù °C¨|×gp×–®Ãó‰ÒÑ´fx~ã)ÌÀ³G<¿$	�º¬0‘�,¢É|Ë’Œè%Œef ŸÑ|ÒàŒO�¸Ñ!•¢Ðà”õñGçN$†„ZNá�nsþHeÜ¢Ììé¯Sùã„Žâ2ÄÝ ÃËCü1È0ýü‘dtãø#9¯Èùf$g>(7Žš_2tò"Èpgµm-`y,' u4ÍèFþ“(c� Mg±nH ITóÜ[BÆYƒßÒuŸåæ—.�_m1°˜?XÌ ë�.,&%Iý„aúyŒåùƒ¥üAbþ ±|:™?º¡%(’^£ÊzÄ'ƒl	3šø-#å�¨Î&úm4¿`þðCú BÓ§ÎökÀÙ?2úm7`ÿÈ©¢¤7cMã	gŒý#–­“ÆÅZu’qÒÈ0£´è�±^wŠa¢ùå$ž_ºœÉ¤Ì0‘9�‘ì•â˜a†ÍPvØ„?èHþàqF7ÙþÁcé³ë†2ÆðGÌQ,a‡­ùƒ%BYÌ`mÄpnˆ3ÀüW5Xt†¶~óGÞàžŸ=fÍè3
ªè m<¶|og_/e06â-C
oÎ.¿%¬Œ vˆìë€ÎsÂ¦ÉÉ1EËq,,P�Å¤(ƒŠ«I†ˆI2²ËqåŒ‘„Ú}F¼‚'f€1’?ÒÓÎ“ãµ)ò[Ï+.ùSý¶ÇŸÊ9+—ª1§#+×€CqûeÚÜwß=|õñúÝ�ò•sýi('ïwjÛÖHã¦ÑòïZ^(§4í£=Óò�>sÏ²Ñc<&Qs_)úî��šuýö]L“-Ý:_YJXwçÀyH:åÅ‡«›««‰]S-üB>:\.íiâ— ‰êN¡‘'¶‘í³lä(:º&.vÛÄÝÌ–¿ÎÌ–�w{¦Ê_íl¤é�œ†‡}m«y#>£ñ<ÍÕy6od²_æ�zèÒcô�¦Õóú¹y^ÏæqÌž·çõ®àò§ñ¼>Ò¿ø[/·sf¯×Îäâ/ga™žás>‰3XoÈ I�2ìs=[7Ž©M…Êèð[˜Ê}LmÉË¼–óé5¥CÎäÂ>0Œ[u”:–[Ë–Qk™+±Ž˜Í‰è’d1‹ù#Êèòa­Ò#(‚ÙuC1Ö½íÔ´eCër¥!v1#O}ßâ2âš3LW`‡ÐPc9øm`v0¼ªn™e8v H4ì ±Š[bv˜xTÄ5µÁ“¡C+¦þ˜LýrXEÄì�É`‚*œv$æ²8ƒc|ð.~$É`™Œ˜û3ÎáÙ³C(ñ*¡>ìêc2J¯í&RfÐlD!¥1¡º¨D™ÈDv8KØátˆ?Rº²Ã¢~.ƒL`‡³³¾¥Åì�Ÿ, ±Yš±Œ36è³“ þ˜ËRëDvXê»QÍ¦šÁ£Œ³xLî’É"É8�ñ18®Ÿuƒ@»¡Öõ¿¶a¨-º±,šÔ}Õ3ì`ŸanNœÁ@Æ–ì0Ž?z2h”Q`6Äg1;tvˆ2!/É8�Iyl˜è õ!~¿¥ƒ]Ì¡±ý¢ÔwP8ÃÏ@sÔv«h~Y!�„ëtÇ�wj×-áì�xí7%Hñd��•]ž,à[òÂãq	>y ï|Ð¡g ·4ñ¼÷¹¦.¨iäÌÞÇ�©ØvèFRŸGÒ…c&°åYvˆ2Rv8ccØ�²ÃP¤~††„Ívø,f‡n;€¹³ƒÉ Se�zæ—RF˜pÜPGzA–.8�2ìd�4ž1²C×õfd'–JQÆY”q:f²š£’Ùã,Ê8Å@ü3Yt…É‚�a,9Ro‰ ”ëõi&#ÇZ†ÛBv <63hv ˆ’]Œ¡nˆ.ÝPÌ8Ý€Îú€~K]½%/;œõO©æ9�1jnÐ¤$Cšg1£dwÈ(š¤¨W–ÍÄeœÆSAÂ3]Œ±�d²ðÔŸdwè™pNO»0JåícÍPÄ>Ø�€Œfx«dÄ˜%vð&²–¢ƒ%vàqF�‰ Àœ“ê§cÊìÐ+–Ìýì@cù£C’4d+‰$ž=eˆ�“…‰þÀ;è€Œ9Ôr•BÌü¢• `0µÂ…~–6pFQYí3‚N¨òú”÷�Åƒ¢E€D0HŒäžJ‡€ŽzKb_ïR#µØ¡¯¦])ƒÂOý°â¨�2ÂºG(Ñ9ê[vp†ÄùX3R"jP†^p1¡×’X¼¨ƒKd2"’!êÇ¯M3ânJÈö@1Œ.†Éžµ×ðÓz'Îê2|bÆŸØeo¼³ïŽ¥‡äð=¯{°q�; ÷à'aÖsåx©ºãñú—*?áŸ¶ÿ\M\oOm»—á%üW»ôþë²—°ª•	¢;’:¯9\ñÿÚÍÜŠŸ4FðKòú|I>Äs{¸ŸÆWÿO«É¯Ë;q†@n×¨Ë°ñÿAuzhãñ�-2Ô©*º€ú²º„|XÀQù$Q?…ÍÐPIr5qº1ç¦û[MC&\�=äÂ‰1ÖäQ(ŠH6M³1(\F•° È0HE(Ôçk ê“€Œdèšè�ÃCã®|Fã3<vq'�rm€á¬éÙ —‘4¿€�Þ¹Gô± „P§‘ kPMqóQF3˜‘Â ñ#84-ÉpkbŒ5QóK�3‚Ü1-q‰Æõ}PÞðX¸¿D5MÚ23Æb>ëÇ�2BšF´˜¡ûÏ
`Ì¼uå3\=RŒ	
ëaza`J�ª.|Æ|#EŒuCJ1†3ŸÑ€q0ò
ñØTŒÑc4ŒÁcÂe�M˜n�…³ˆÇ–¨W6ªC 1;fË¨{P\1ÇÈQ@aÔ‘]%”�z¥Ø®WNÅX<D`L7•èÇ�†œŒ5c1&9h¹9>6Äœi£^¹(tBØñ8×”2@¶V¸g»^‰ê±*Žü0ƒ8Æ÷â…Åü4¶å8f%âÅ yDMí¶h¦H¥y¬»q¬‰GþM1Ö÷NAÂÐ?Æ¬XÛ¤%]ð’0Ñ”2€<V’.âŒ¡^©�[n.~m„±€°Hkñã0F
Í/wÜáGÈˆGH„1ßµ·ÁÙPºwL‚ÒH’.¨<}Í`Æ0–Ï€ZRš¡t*—!sÌŠ*h>‹šŸà£0Ð\ ¹2è•”ŽÕÄGgÄø•Á’$s2eŸáÈH¢7d‡HUÏëàhõ’í³ƒ–Õ-×ºvÜ(o‹½»ûþá«7WÊFx¡Øû…ÅõØËvf‹ÝÄ.E{ìRâ¿ÔÃŽëU>û´^õÚí5rnñ¹Àh<ÛÔ²¶3ËúÉ§–»ûÛìÔÒ¸iü�§Ö3µ�o¦�*fè‹Ö87x‡®]qÃ¯Ü ]ƒÊ¼pÐbFßòùï¬k¹aøyÚÏÆÆd×òòï66k²_ÝÜý(û¤I`Vf–�£aMBl²gW‰ÁF7äÜüP^×Ô~+ý•[>|¢®÷àAˆ=ÇÂH·�nÿùV"|"{êË¿2¾°f°ýS:Ò¾JGÚ×Ç:oŸ­¢ïMôØÙ�
àòï%›«®¨üTA=¹¹þ—®ß^¼½úøø¾Ï¹d;—#À1×÷w·oÞ_^ßN§ØÔ±F<³˜hì™�ÊýD§g“'?çºƒv¥VŸ#ÐêC“ì¹×­¼ô‡hÁ¥ó9G9Á*©¡ðC¿ˆƒ·õËlúr¢¤�ªÕkš‡'Kdà�”áñs*ªŸ¼–ò_»¢+U¿•Ê0#$ËVÍßIÕ†Ú+
šX×2ÊÚÂ¥!Ñv=L¸;ù×Ò¦gxB$­4ð:õë…GxBˆ6§u2¯~NÄŸ¢Ï8G`P¡6’ÈTdè+Ävê…ãE›ÊÖO´Ìu¾ÖW·õŠXëÏ«h=¼¶Ø^•@áQO@x”çxïd¸wûâ¨½RlåE«ÓM/�/†X˜)¼ SËÏ
R�_D®¿eëÇtÇe´Ô=Ì+hÊÏ´ÍÖOŸH“ƒ·tTC‹È�/¹ú©ªéþÖõö_‘©Ï5õVìo4ƒ¿6OY€¤¾,Oß@X?žç?ÇÄ:6c±~B$õã¶[åá±WfDeÉ `ÇÓž$Ç:F˜¬UQZïÓöÊÎšŸ±b1}ÛÈz6U5êî)ÅÅî÷0ˆþÛë>\è}g[¸ë§O¼Uæg‰1y.“‹p;ddóÏßõPÑXN�:ø³Ä¸š§žk/yAm-pê<ªù¡œsált…M³Ó¬ûÞTéó“|cFš!6hÅŸzØñêÏ�W·
øÃW7ª-ê‰Ð0€ _²Ð<a÷�²ãnÞ§Û5o¤1ðëQÆÀ³y��óšõ63-Î`|ö¼ç|6ÅÔu…íŽÕRxÐ–B>Öö5ìïíoRp>ÊY/vÑD ˜ý�°ûfÌ–
ôáQ.±-ka[É¦ðX¨Ÿ%Î�@ékÙÓÞLýú5¹~9ZhµRÖ¯m
—…·Žë—±Ì¨,­ö6
f?¼¤½‚ÕÛß$<1ð�Û&ÔgaÄŸˆÉm‹´n1D_Ñd –lµÂ²·r?V&Œ†±|E$ð°±A(¯{Ý‡ôGŒ†^à5Û›}ÂC¾#šç¼=NØ,ïhÍRxœ7|Õ: Eý^4Ì½¢±Ö0ÇN_Á
�/ž"¦C(¦z‡B€ÇýÖ"Tø­¶€ðÎüÛËÐ¶Qœ÷ós“ö7Õàuðš�þy2[?ÛèÆÂ£CðD²ñ&» N&<‘ÂË�M0rOf�·¥S
Íó¡Ä;‰ç8‡fÚËŠÖbË��‚Šày–ÊÕ�ÒÂ²Ë©ãgž#ˆ éš³½­ªí­ÚÞªímg¶·Ý™8fµCÑ~£Ú3Ž±çZvg?�Yj]nò1XÝTó^·Q±­6Óím¦ßŒ²™¾:Ê¬œók÷c=$K-yAÑÎXµÔ¾@KíÁX€•ÑâTiÜÖ·ÅXÏ’%ôÆ	þ*ê	ÞøV9vbxîC�ZFðÜeý]pÞ�Æðì%WI}7ç�·ÂÞG¾½ö	ZÈ�ƒêHo¦¶è]Ö¤Þ;±·UZ?÷DÉ@Èé9ŸOùFñ>xLâc<õD�…Æú÷Žõëã?�^;þöÑ×=ÑKàv’'fßŒû»¹�ÈEºÅO7Oò	ðÀÑuâ_I§¶—k×ªÓÌ&ô7÷D	žDÇ+NÇó‹~"ßµá]ùÔñÑüžÈÁÓÕW§+�†ž(ÕOÞÕ¾’ãëgž(’‹3fù=ÂUÑû’�ÃóqIn[Ð!ú2;úåÇ=·hgÊŽ×Ü\ý\çYgÎ Î¼m,¼ðÄ:ðLÝåKJÆ?å§«Ÿ@à®Z€«ø‰-À»3bìÑÔ{FÒöh[öÙÑµÌ¹ŽžÏß²‘ÆµoG×Î«Câs5®Í¸Áø9Ú®žGÄ…j1Û¯Ål”eKØ]ù}˜m´ïNoìR{»Ú¢£Z+á‰Œæ©ò¨6Á }Wú	½û,·/QO‘×dØ·&ÌF@*hyË23Z¸´¯½xŸ­6£‰<ð¼T?³Ž�hÖ&W°DékE=Öí#d‰.Uh¯‰‰Ïú,e¼¥Á¹è…Ç%{àµñ>G¡y¦èÚÈ”aŽÑ"þX¬iã²;‹Ô³;:–ŸõBôZ¶ôŽ`³\ÙZhÉiÝ¾h¿Ï¶í«nG°y·@ûs½¯'€ž(ûn‰xÉ¾gž/,[ÀPÖ&62X¿vÈ’Ç•j/²–äeØ;�-Óæ‰<.¨Ë–ùÀübŸÈò³µòRIýÜ­(X¶¸6«‹K”ˆ,[ÜšîõSFáñ–7ÿDÙ+´¥'?žŠ¬eK>QXyÐËâœŠ<}-<Xcn=Åó¾ˆf ”V2„íŽÀräž(�3okÍÔâ5Ud…à…'ò–-q¦�âE€f¯ôP‡÷+è'�e«­–­jÙª–­mÙrVq´-ûìIí?rà¸¿¼}s•9ú‘Ùã#ûOspG?N´‚Šâ,Æ+¢üO5tóÎâŽ³†qåK°¡t¨„Ò€JÔkü5Â		Ž¬<´ÿíp(œM–¢{’KsÖïú¼‚-òg¦{üÈJ½}wÕ#+Ñcµµ?ð¯S–>»=?]ý.=©˜Ï;]íx®™2'ü÷íF½pU8N…sˆp8‚3ö¼‚Oá*™+g•ýË�Yä`ÿ›Â`ÿáúm1töyªœ|zpÊÉ—¹ŠlÆýÞ"y?eîg¿^}„ûJÈÆ /«�†ŽbÑ¼ÙÓ6“Ý‡_t]wÞ©‹vî2¿(uá²Êåf«ß*®+ÏÆ0Œ®ì$…®k,)-Èk·+×ŠáZu0{²Œ[µ:½qŽ@±´�]åv:þo÷zR­,¨†2ÇHG*%úqÕa\)P¡œEQÓ$ÍQ«•®ˆÇ;³ay‡@xÚUƒ ™"ž�ˆï–Ë!P:}âã>Q­tµôk$¼~\5þ#\�)lð®ñÍ8gŸ0Æ»”…´kÿë®÷¢Ž„�²Š½Öv/•¿Ž	¯ß~P²€*Ä:m<cò_‹™4_/aåîN5ÐÖ*aGÖé�^€¦�‚´¯V´wŽð¬ÐãMgV@„
!£ °¨î&6³Kºs'�³÷b˜E(•Œ;�^¬í$46‚ÛÁxbp¬+O£×½C_}òÕtaÁÙrBÃ«Ýz™.îÎ6 økØw5(	k`èË�WÊQäéW¬¡›Èì4MœêÊ2JÇNÌu]Õ™t­>¥¾}:¨;ëºWÅZQG}\+†¨z!kÔpµËÓ'Xjÿ«ðÀÁ®±o³x+U×·7W›-Þ’C]‘Þ¶Q;«³[oˆžÓ:çPðIñä³'ÛTAË6ûYÕ÷Ú°Ó—,¨Ý~ýe2Ýv^MGŸ?#ú=|PÓØVô[pˆõ™šw~ÜÍûôxƒ=�•ÃÊKó^ïôÜ�1«_�Zíx½¯ÕŽ?ïpÁä9&®SÄi›·XBi±¤®ÙŠÀÜ‡Ÿ¢5�¶mO‹{µBT!·{¤5‘8Šå²{ntä˜ö•
h…ó8Ey ðUé2aMžäÑ†ÃjQ—wÞömn£¼‰À·úP@ž¯j8GÛš¢UíW<Þ{cü £<·
³É¨–6Ür9œWÚýCã½A ÏßÐy*™Á•>²pa*¢zƒ¶:QÁ!1¸/×_+]…e Åa+ ªéå+·9¯mU­,_iRy<æ:ˆ�àn;dºùÏ‡Í9Oöþ©ÍPVK…“×¡ÔØynßš‹tñ¨ˆ¨ódÅy¶qawdëöc\	--àJ‡îç”¯lÒ
P1AGaïA»PÛ–¸Õ¬P^\®qyffÿ�òÚLžb†2‹Ú]�âä‰Lž¼Î‹ÕâjSV6�£<uV/ŽÂîÐ4[éZ!,ä)PWÂnG¢o�Q�É2¡ã„<õ0™×3ô	Ÿ)tg³G´}�xMX®ééÎ!ØÎ‹7nš<N{¶fú¨QªR´ñyº_ƒ©ÃäÉr'm{Vª•	çµm›æ)’Ö5�º&P×ö½&p€VR¶’<£”�œeˆ¬¶¼EòÉÇÅç¹î±¿M «C	oöt&ó³ã^ð8;àãfjÞùq7oûÓ-MþßŒ2ù²�™ü`¶EÌk¯Ÿaåbç[ó�b/e!bÞ=uày®°}j¡Ö Œ%Dzcá3
.¥÷ç)#
‹òÌ÷Tüá.®ËØ
—cq]ÀÛx…Ï?×¶¯JµÊ^g*•µ®>l-Ñ*Do­,®˜×J‡¥OAñ¶Wªf/úYn21Š=¤2®t…XLAí‚ž6S5ŠÅUåªnÈÍ¨\å@i”·ønkÅZ»ÏTÀõAŠ�ôÈY8åAšKoX0S_Æ;UxmŸàÜÐÀV3‚gA©ÿ”½RÎÿ
T'¸òÈW�ÕkUTÅÛÐí2Œ¡ie€pjÃu¦;š½
@ËÝ/Æ‚[:»Æø¾jëú=ªVÚ{^�¯Ð	c"·U[ÆN„zê¤�1äRBÁR(L@Ð­ÿÐÙ‚Æ]Ù�BHã@‹Y ¶à¡ÈŠXfà1¸J:rªõ!5u]Û×PýÝYDÝ™§C‰Ã´µ›�ªø…úøÍ¸Š ¦Ã	øxÂ2�&!�Sº$£­·¡üÉâ¡¯å¹á(íƒñ×B82´ž±ÚÂBhIXzQ{\¢…\;=àƒh&±þŽf·ön†1îÛlJœºEœü€}jG¤SšþDk ‹ºP× êÀ¯ÌnJ^•�f}æFrÕ²ŸTÏòêz™êþØ7OÙ˜œfTKCP©3l³;³¾íÕZÁ÷dZ:ù÷áHpqz¶D7ÊIO¼Üw ”lÞ`tîb".ýíÉâêí;�îW	¢®úƒÑ‘Z
Ýu{i¿ˆ¸}˜¦ÿÐ;4&³óÃc´¤Áö;<6Bî‘y³^ÇwÂtÖ‚n\ýƒ­*G»¿éí®ßf¥@·rËªø4Ã2ù¿²9» ßV¦œ*ûíFZ0™1ÒybéèI%w!ë2Û8z²ß05ÅNÛ;8ö	}çÏYècÿ{•
êuLbÜX!n`!æÐ¥·¿ï¯U[ú­×ÏZS­†Ô«^ì¼cEç(BŽE¯}¼ûX\Ýx]W7žpuãÿ>‡&:‡n<|`ÑX]vÏgu‘#Rü~�±tÜ+ã»óå�¿-Œ²^_}¸¼¾½pg �Ú«q6ïÈx�‘ì@Œ‰›6ösD=±àˆ÷&ÐÁ%½{¨rÉ§…82þ„<8CG <WîL(w´úƒ2X¶ç?T
9œx¯ƒpÓœƒ|R+û¡DŠ“L9ÆQL!GÊœ¨žðÝ]R;K÷ÔŠedud·h³ÕTù†¡Gþ°7.�ˆz,Äì0Î´ÚcŸ¤§ëLÕho` ÿä$‹SuH¶‰n£^@;û>Â!öÂ>€¸p ¸4×ÈamGE¹V4=bÝ<oO¸4 8ƒQÍõ9ßæÃV2ÜM¢Gq�Gåº-™ir›wŸŠ“\ðª6X‡ºOúãD�Rð¸àætqšÒ\èH/9P-V,Wmcº�ŒúZeCjeA)45öÔã›N]ØCt‘Å¢lIAäHšyÒàJuq½1AÓ4p„áY§ñ>æŽ¡Ølc\ÿe—Ê£�fº˜p±¢hº¡G÷µBcH E‘«¼Ž{#F×ªõž2c�‹£ÂŽqÝý‘„ç/‹0ÛE..”êW0èTÔICÊFÒ!æò¤¥ÎYª•£,+ó  1y\Q
Çn²¤;k\ifË¶]ÇËhT+{CÂh†­4·‹mãýzhoBÐR†Š=è�	¡—I-çíJØy‹v¶%áVz¢M	3…0žN­iááGÇJ;>µàÚwòüÚ72¼¿cØõËÚ„!ö¦èi›w~ÜÍûôÀF×§kýò¸7-gŒÔ4öä”>!ôþîîû‡lk´ì©n÷ g»´•ÿ¦ÇV>áÄæŸÌk$Ÿ×Ü½™Éýð°Òë9f+ñÆöC2�ót63}šˆFlÄ"|�m)TÂx¼~#‰÷—7ß“Öì4zÖ'=ÏÌê9…A'"|ÆÀ]uÕê W­ÈPä«O3ì¼¡qÅS#«*ßü${ƒY�:õF@ÎY²Ð4T-¡4»:Ïð þpxJ™›æóô �ââ7ËÔ©=ý‚5Í¬*e×›,(ý‘ž’¡OsmGÃý	™E=X§- =©ì©]‡bY$6Š€ŸgÚ!Ëwæ=”3G»
½ äCM¼_AúœúC7´e¿£neN~`P§mX›€¢!4oìÏÖJ¯Axt’zN¤í‰]«àöL[+».e"ü›
Yêg?×«9]æF (î—ºxãAµ£Aq°ßÀkýª®ø0öù•Á˜Fþ+×@}
‹=/$B§1Ú9·ÖyêCÅA^ä›¾ª(oRP™¾¿0ÌÀÂBpŠ+ý<¿Ú¼XÅë³ÈÀ&á¦cò\ÌvpÁhŽß¸>u§4^­Ë‹ã™ZIP)ïŽ 1é†WuXÏçS{lJŒ{öt“ú(u~Ò©
 xf!¥¸ÒgÆ4ŠAó 8Ëvæ6 £ž8�«Õ'mûF£\âvÓè4_+ÅUMÃÑêÒê	"_�ñ-®«Lu•é°_íŽ?Îøþî‡è`
”a}«Çþ§¾»íLÇl¬gÔ3êwÆ€7ÉïåXá#ñ[ßß;5ææ�+nÀ¿„2¨âäÇ¿|¼º�<ö(¡Èòò³l‰Û»{Ù´Þ"RÈõEòïùñÊ½‡’ÓM_{xÔÎçÞªæá½6ºYÇv¾C5?c3±T’Ïp	!šèÒz´7B%wÕõZ¨Èê*>´G­)!?ê×‚ºŒ»Jr®Õtý§-gTÃÂ{õ“µ¢ªf4©Ucnøk¥>ÖÊ—Z*âvP¯ø(‡U)êZàÙZ‰<(*2�4([N™ªr”5Ty°¿6(YøaŽSm}ž©AS ²‰W˜ŠŠ"C¨¦tCm¿píàTžæÂpŠE¶ÿÐ 46øûd™®ñ üîYf‰{vÆš½BÈ™�d†ãë¹œÀ²Û9ô×NâL¨}
Ó>ùº"¡!êzÅÜ;ÕŽÜ½ÑÑð%€V™êd’™Ý­ÞÅÓ˜ƒƒõAÄªœü8á¶F²¢¦ZÖŒ
*ZÍÂ§úhüƒJ_ÅÐ6Škó=+ì$nÒ´eÑ&tXJ(Ðj;m|½·Ô`ø¹Áý\DýCŸ >_5]Lt};ËVÒÕôMFK£KFïŸÔJ——­(€Ê²h�r”i<EÌ_ª_K»	Aønï?¨¨Bê”³WæUúvõÙekezhÜ@µÇŠÑ®TNfä³Õi­ô Í2cÓÃVW¥�•�Ælgënq¥Ù_½›NÄ��6Í9w=$\)=ºŠ&Á›ÂÖ÷e5W3q5ïÕL<û‰¬Ú2|tG ¸–5/Ïûþeº§—ó6ü)AþÄÕô‡ÛëÇ‡¯$BTŠ(°ÇPÆQõ�¤|C›à¡˜;È@ æÿ·ÿLŽê¢Ï~½êU¯zÕ«^õªW½êU¯zÕ«^õªW½êU¯z/:¡¥ã¡RUœŽz�*çWÏÇ”]vXPzl'Ç5zŠnœÐTœL(À�.»CùÄjÏÐ{vÚ–C^J<bÎÒqf±´Šè„ÒÓêMv×ÊZº–ÞºtÝñà¸Ðró^&-iåïZúpÇ2q<Ù±J“ÖeZ+wXúpÆ“Á'ÊJÚ(>9rÖÒ/e<![·òp¨Ce$¤ŽÈµôKO6aØÆ÷øÒtR¿¤»c÷:BÐÚ‹Ÿ÷xBLßÙõxr8ÄÙf„˜»*t‡¥k¿|Qã	=°ñ„ìvrÅkYû„=±4}¥k/®ãÉs2Çîr<ÙéèSGˆZºÚO6p4!;OGû#DO^Ðx'‰;u[#ÛXDöŒïÃéÅu„¨¥÷9žÐ	ý~çÒ	¥‡2žÐç*ÍÔ¢–Þëx2aÃSj;ó¯ïÐ—@ù:žÔÒO=žPä89rÙ™±%ßá+-ŸûAFf«\µÓñÄ[+&�'$cáØ�ïI¥åÙh•Âžéx†ø©ãÉNWxŒ‡]¥e!ž—uë¥�'¨² @£;³ÉÔ´Ò²ŽUš9 ñdE…F™n¡“~OŽh<©cUO6OÐ 2mUg·zŽ>o§�YöÐJ×m•O¢ñdìÐS�'Ù‹?ëñ»ŽUz|9òÉäá„ìÍ%ÿpFä:žðÌ]ûü~õ�ÉqcŸÅxr¼}þpÜí�A¨#ÄLöØ‘ØÃ	Øv8ãI«^¬­ –’OF¹‚<‹á¤ZDjégÌ'Ïß>8J¡‡²­Žµt•O³7Œ0^ÄxRÇªjÑ®#Ä6¥á6œAwü:žÔ>_{q•OÊ¥Éèñ„Ôñ¤Ž>u„¨<ØWš˜ãärüãÉË°øÕ>_Kï€«ð�a}¦Ê'Û”¦/Cš©ýò…–ÎE&ÛÄKí)O/¦un¨#D-}°ò	ýÔ�'y9…zù¬*#[I_-«÷D-½wÊç×o¦­ãÐÃOHOjéZú	ýcé´ñ„Öñ¤–ž¿4©89Òøl¤Ž'U>Ù¾ôÀ#Ó(Z{ñsO(²Ÿl`—ÝáxBwÿà`í)´•Æg�Ð1¯îëe ×�Àé˜˜§úÛGQ¼ŠQ0'�Ñ>âq8HõD:Ät�=~“Ð¨ïÍ¶ªJÓuÛù{ñ¸ÒÃòÉ¡ØOfu¦ÉEGÑŽCXÀ«×ÓñÆä²tÎ�™’±¶^•ëµ'ôÒƒ®ù´©®2[½êU§‹¹@Õñ¤^õªW•‡ëU™°^G†È%iÿéúÝÃå�+™àÒüÓÝõí#YÜßÝÜ\~wsµ âöòÃ9ÿp}{õæþòûÇÿxõáãã_dþÇ»»òû�ÿÝåÃãõÝíW?Ü^?>|õæêöñêþâãõÕ›«‡i/ïß]=’_‡ò°ÄÅ›»[™ÿxw¿&üãÝ1±ô¿´µüéŠ¬¿¿¾½¼¹xx¼|ô•:À.¯ïW¤Õwä£Ý¿^Ý?ÈÊÐÏÿñ7„”›õhÖÛwW?^ÞÜ\\ýùúÑµïoKíS%Íç…¼›øixÄßF­r\3W«ºË›ëw·Wo‡hõ7¥¶„¦ëthÊ_çÐ£ÊèåZöE¾e¿à>ÞÝ\?¼¿z{ñÝÍå›~x¼»½ºøîþZþ,7˜¬Èïz˜Ì¥þ´&âáúß®l�Öò¯SßKÒ}¼¼¹z|¼R¼þÊUê3€_—b%æiŠƒò÷ 7ÜË‡%�- ý<@{w}óÁ|ø *ï¯®nòu„3’{]ñ/sØ¿x��Ô?º ?…$ŒÇë7’±Þ_Þ|OÚïîï>”€ƒžøp÷ƒdÆËÛ·ƒ5ú<<t{õø^2Ô�—÷�þ1Nøå;Ã[“š¤ºhÒ—º+Ù^$!\<þåãÕÅ•än	E–_�ŸeKÜÞÝ?¾'ÍÃ{ù]*#Û*ËX0ùýxå^DÉ©.!;ñãÅw×�t�Íê-²›V½
­ró 'ô«ï/ß\ß¾»x{}oÞ"©Þp5‰È&Iæ‚+v—í.	÷+’<€W~n]Þ¿ÕÓEx…ÐLAíd�¿¹“¼'ß÷jÛ–É‹òV_gòß—Ô2J©úÝèÿúg'ï€Bá'ï�E™ú“çmÓ´´¡¬a‚²[5Š	&äÏ…º^éìå²]nÐLÆNT%Ø©l Ö¶'æÖrXö[5SÁê$0ÖmP/L×¬kU½|Í6F35“Tm†’µRAi¹äÛLÞð®i:MFÀëñÀgéš5­å6ÄY“jÆµ@Ûö\þÛüBšÀª,2JðNÜLÅP'VÆw�ÙX9RìH:þy¡þj¾pUÿuIRþxùøþÇË¿ ÈMïîï~¸•R“)s!�ß_ÞZÈY1ù4±ÝQ’oâ­ûÍ`ëPÃ„Sù�µa«ckX{¬k�•b«��{oXs¬Ûr¸iáù:µð,RÏyµð½…g;àüñîãaÙŽª9â@Í�1?õØ#þ_‘eàTþ›çR¦	©3*«‚UYÚ�/ÊªQ<••Bf43UW,´ý„.ååªºZnW^kfìtãGþê,>ƒ2îò·ÄC€{ÒvÝ43A/Ü–.|�éÖõ„§¾ÎÊÎ1*d5…µ­IŸohíÈá‚z°Û™ÿJ†�®Bª!¤B^’!¤©†�ª}öhŸßLÓ>çÑ#·Òü6S€çU]«ÁA­£;‚ÙÒ_œê»ùÊ¶Æî3W¨SÍ—R.fY‰�5Îù¸e÷‰šŽ¼„009mè|P)W:	×Kõ¦¾«U»2+Ú›"�Ò5UÐOdmÜ¶]¬Ú-ášKi¾f©Ô½Õv$×‰^Ë_ÈZ{ÍwóúRF¹«ï)•ˆh|íWÂ’ÍÜ¦i…�	Yo:ƒzª9Öâ7¼nûú
íöbußó—¥ùnéFZâªW…¸*ÄU!¡ÿ,Ý)`÷Dnö'ÖÍ~�Õàå¼jðfšì+„èË(«¯?^ËÉíþâòÏ×„þe&µó�#Z«hr}ÚLÉù®ß^]¼½ûñV)[ú«¯®ïßÞ!Ùœ¾ÔU¾]©|Ã¥=Êûw9ÎaC¡II±S¿å‡ÌÂd©Jú·Ìj„½¸üm.Sÿj4 “æaÒLê ªp<![×½QÉ½á= ¦ÜÃ–Â· ñðT)óá~h8F�Ö$¼ü”°dÁ¥®#åŒ¹Ò¾¶žþ·’Í…U¬(h5�0w–\7Hl[‚	ðÀ 2®fêýL0_O’‡)É?‚`ðÛàêaÕ…§‘Ã¬ ÓqˆHý˜ØgÀ›Ç‰†‰„øvúŽ¾â.±ïå@);ÅW²'©á?LN`�)sñ'=-ý%+÷5»ÚË÷Y´YMWaKM„Ï,!<I¥»®ôS³ÎÚ6ˆ?uƒ6$€°²åÎêŒ­¥�w]­ÿP"ƒ.ë¨ðu–
öÞ…1|'Òr[ÖM·²§þ²—cª¬YeÍ(kÆr¡èäwçsƒÇÝ¯ÍdM )ÜÎÀ#kÆïÁoÜLÖlGÈš¢pq#öÉš‹²&Æ 5%{dM1BÖcdMQ€Y�5q5�¬	É pß/kFÏ IeÍnT˜<Ío9Ñ’]»®xóÜ*ÞìZ¶ÜUÅ—ÏµâëçÊãë™×2K
æ[›V�7úº±Ð«þ˜úÃB¯ØN\�(k>C÷r¢öns5¡ÊiÍX¬h¹¶·oÙ9èŽ—~Yn ˆn4®� }ý§i—KÕu×6ºuùý@Ñ®IµŸì¯ŸŒ\ >ŠÎò×}¡ÀâÈYŸEAÀvÙK~ƒ«sùáãå=ø÷�Wòï×ê÷�“ãÛ/9ƒ1f+ÇÛgaÉÙé[Ù\#°›ìÊ³›@F»²¢�ñü×àì*f6äEixqy±i—˜’Vž–0í7ôÙŠ¡´ókeA’Fi¼A0Ün$Š¢7Q*’PM}¡z’ˆ8úGiNQšË4�œC9ØûfÒ;�r�Ý(Auµ]§Ei�[Ó¢ôúÏpaúËª±ˆàEúðCDï"?ˆˆDÄ:Ýa‚NqMé?"Íã4ëOÃhc�5R±ÙHúÇ×¨ß&i�é+úéOqš¢4¦¿KûP”¦cé/6¥¿�TÑ+Ÿf÷ÛþñÕwS7ì]p ßz¥[Q¦wJÑó‹ø!J¶®¦¿¦»ìøÏ’xw�ºo¢ÎmCœFÃU×ÎEÿÙû{›¦{éŸÊm»Móš?"zl@Lo©0ËíPºíãÇ§¡ÿd&xøÑðÍÒtÂ.ôÛñPhH7leG€dþoÇ�ÿ)µ¬�2õŽýôgPþkŸ’þ‘87E~E¿!ú‹iüÑöðC*¸ýcz2¶˜Bÿ¶?-
ôNè/bù/MÏH1„ß©ýsšþ8‘Êõk˜�¾kŸ[·E|Çÿ~’^,6¢t$o ùïIé¿Ý|¬Ÿan¤TŸÆÍÀ6½.�¿]O¿ïãç„Ú>~ÐgËòÿ ýO¶¢ÿÜôNèÏwKÿè}œ»ñ%ÿë=Ähþ£çÀ¿íðx%t°îçƒ)ô�´ÿòüsƒJO›ÏÙ@:¡? ?ß–?Dô~‘§¿¸KwÎ6§í'žþ<ÚXÎy‘þ›©Ê�D4_Äið^lÿÙ\ÿOé?UžÛ¾ƒÎLÞËo<ðc£-µx¼é¤;DÿT�ÛÉü„ÓÚ¨'åËE*®Ò-ô¿ ?›y>àØÚŒäA˜V
`[”ÿÆÌÿ|Ôx8Í¾Q”W]ÕçÕÿsé9é7^¶¼Wû›>>ôáWD gJ‘ŒEBâQó¿ZÚ‰(ˆÖ¿X¯E@úÛa?£
ýEbùÖ˜Q¤1t…Yèqaò|–À|¡ð¤os=dZúsÖÚ´6 Øž+œ�#‚6–þ|ùZü@³V<¿‹¶˜R]ÖÔ³ÞË‡ËÍõ?ßáÍû ÍƒqÅ„µGi‹7ŠjF†§­P¤Ö…Ì„+ ½]Câ`ÝFQ5�Mû³û´à(ía6¸ž›œ¦Ëýºf†…¥Óziî°ôní’žhƒ¼'bû3kKiÑ7JÓ›õéÿ…� ±÷Óëàƒ`=†fpnW×yÞ³iš{Ñ»ö~›:j}¸ûîá«�×ïn®o/>\Ý\]e]e©ÂÁ†�u›Zî6"Íè&þ²·‰·°�kŽ±qKÛ¸“iÜ˜sõ4Ì9ö˜Ñ¢S±].>Þ¿©L#Q‘dPù•bÖÝ»K~�ºKþ$u—<­î’{v—üy>¶ç†žŠ|/þ‘ü°¼2ŸÇ^âãò>›?Ù»Ï&ÌÐ!øJi~„øˆN§…»SÑµŒ’:xŽ0 |–`øuA£3Õ©Ài¶Åií{ô‰$qZ5¥'M#|£ûW ,	ôø6Û�†"+Œ#AæÀïøFYÄø].{ñKûÒ¾Æ·C¡`¾qÒÐc…Ñ¿Ú	~ç÷éc	>ã4�ÒB�Ÿ¢'�ÇÄ¿ mKQÈ›ÉˆAæâà]¢w1°"�â·?-ÉE™t[º†RÏï~ˆÝbŒÝ”·Eò�3ƒ�‰>(BƒÓà-.ÝzüŠ°ô@uš´­Á¯C·IÏ…ùCÂï°ü‘õéi¢ôi~qG²ã‰Å°ð#ü®¿‹ÅDŸ¹ÉòÜ|Ã…hœ6èkNû©4Ì�»zXÚÛÛáwÑméÃ2‡�ÊÉ„…ßÜøar	+ã—<~gÆ'ßÚçcÔÂz‹Þ'ÓÄ%‰ð|L¤ödÆ(6Ì)Ÿ¥kîÝ–øÜ¿³øÜ ü“ÆøÕ|©–o ¾µÍ�kùÎãŠóy#Ò1>§¬in0¾šOñÃ„Ž?ÆõgðYÆoœfƒø�¶fœÁç®}ˆæõÚ³�[ç–„·ÃïvÆùdääµè>�=
‰ˆœ2,B…™ß�”ð,ô‹!Ÿ?[›^
ÒØú€|Ô"8ÇïÐçA ,Âà{žCå„Ï¯»ÓÝÔµÞ›O�i™ö�ñB½ÁŸžU„›uÌDÎÌ€pBì )€�&Ü&Î§ Øi8J”ØÝ52îJ]³^u‘b;Y³�øMº�x’.$¶{8{ˆk‘;¸DÆ‡a=I&•Æv6Ý4Dþ'�Ò)ä¥“��tš4�ÁÔåîú4ÁÌý ³á´âPZŒ0væGjÐ}™º„´ÍÐí1ã¡þ°„I+’ß‡ißUÿi–Úì<C£úûÚâtÜ>Ø—×äê[¨¿œjýÂ\3­þ¬\Z¨?�êÒ$jék�l€OoŽÜ<gøúsŽëËx\ß¡4ÈJÛC|øõGµUõçQýqšÄiÕŸ`
…þ¢DrÑVõç¯´þCé†	ƒr5È�ìÃÓêŸ¶‡ÅõgÓz€‡IÉv7ø'Óé·�¥õ�Ó„Î¨Eè§“ë;X÷ÍwJ1ÂƒgAs™›ÿÛÇ†úƒª*çØËŠ-˜»îã?ÔÚB|ý15è|õ'óãŸ€7ä27ÿ°©ãéíÏÀØiÄ£ØºmýÙ¬õ§ûý6?{õïDÖ!÷á%†ø�±©øNØðdÀuäwÄ³J9}®Jyz¼=vÈýJÅÁBÕùi�Òz¤~¬%¸ý±ùïJB£ñGã?HC{?Ô´ >¤Ð§~qa”ýa$'üÑ¸÷“êõ6´ñ�Õ·üàîBAàI o?,ÐF!PJ/ñê1ûÑd>tðAyƒ+® (ÔIÈhã>È(ð!ÉLí‹«%n,Ö²ïsà£Ú§ïÈ�×¬"„ù ö£qü?b” �à ±£&­áDÏŽ"‚¬)£Gï°un"F·uÖÚ1¿¢Š·C ãi¦UQ?Rå„‡ïŒÉÈäÈ€§yn²yÂ|`.É $Ð£oPhâ_¼ à	‚ƒs’ŠgÀcd÷1fPŸ×”ÁÇôß@ðŽiÌ˜rBõŒI )ÂÄ±RdÓ; �RJ›mf8òî“Y¶çüuö$9í¹;Fž×Í+¡Ì SÌ+ä¤„BáÔ4eÀÓbee•½ENïT¸Q¨1#
¥Ñ�:pKmXà‚9>°þ«{CÑ³6Aq>,ÂÌ'SßL¢­nJRü@ãoà�	Ño#¹²ÔCÉ4Ž:p-¢‘�kµ�¢E‚P—¯NC0Ðo¡ÚZ¥ÿQK„ôõÂ‘°¡hj��Ww:©‡IÍn�Ó·]µÞ‹7374|ý°lwdiÔ9qæØ³5Šçm9T;cþÄ;b?OÛ3ù¸¦ozŽkúfƒCK·_þ«¾ƒsFNUëzpéán6¤;ÚløÜD¥{:¢è	Œ%ê Õ`gÕžT‹Å¢qç¶ë,R>h•Œ>huÙØãç8h•ô´Ê7;h•„ƒVÅf­Šä Õ.To=â U1`î´ÉA«dÞƒV[�O
ê°áA«Ä @­®†Z%ÙƒVIö Õåä�™ˆ†÷¿zwwó6Ì©_Â™CÞï‰†¢íúÇrÀ<ÙõQ•¥sóý4ÖÕ»}¦õîúëýÔ‘Vf”ÓõŸGwQ€L_¿zûî*Òžº“éÝÛ²î@Z¶«Qì¸Ü-´jµeX}ûz”úvvp–ÆÍ4À½:ðîGü	èX7w?*âæò_/�jqrsý/?\¿½x{õññ}Ÿ~p~}w{ñæýåõítÒ»¹/uïÐÔÞ‰mo+&ÚhºZ­Z³$,Çá'6·}Ög­|s±±‘Rû�úqjŸ/™ŸS˜�‰äí¯>}ž™çÂ›�µ½ÞÛs™5i[«íšhcAÖOxO|c²ÎY~¡=×,@„éÏô¬óÐ¶æe“Â"g¶£¤*Ãâò¦Î[å£Bª¢€ð1âío†ùE—)Ó0€¡Pš8õ{MÕ
;å×
§ú­½œI€jÿ®ð˜6È@H=]ž^Ç�K¯Û™Äºuï¹5§ÛSsö®)Í# ±Æ´Å]¸å÷½žw Úà?“~Íï›Qšß«Íõ™+€Àgûæòöñêþ ïÞ_Þ¾“‚;­:ßKÖùøSë’Ç—mnïîßïKEõ‡V
”ŠJ�““T(¸Ñ,ÍFa§—´#—`íÅ¹¹�³­Wãˆ×'ÏxBƒ®K Úù>yÕ’c¯+«O5ÄkÃŒÆúµ/O¨ÓK]�nÄm…™úÍCå­:ÕÐ Â6}Ú+‡pqpÊ •KåÜy]�|â/_A"¯²¬
Ÿõ[ë�ƒÞ
Ë8réuÏ¦1zò9vÝµ…ˆ«/,�®Óí÷V�¥Ü7J3˜v¬¥a+8Õ[}�ŠºªK�UE}Á*êÞ¹ªªìâÞFæaNs=Âeô'ÒÆ÷èùÃËž?ÕÖ°‰­áÛ}8	×ý,ÕI÷ÅhðãVWÕþ­ªt]§Wµã«Ý#b\1›&ÞŽ9°_\êÔ°J”%á¶£�h‡KÐ8Ñ«y”[ÕBday‘úÕUêÕ+‡1Ÿ ÔoÝA¡À€6‡c†¿‰&Z]µ
3ÓïòŠ\{^Eø±5gé’2w+¶êöZí¥!”ÂMùåX°=‹™�®ÙMX¶ e“º¹¥�Zª·$·«ü“Y¬¤ª[K¼½ËîäÖÊ.Vò¹¥�eÊ‚óoU]«êú„ªë^¤évON©ûQË·ñe¬ºø‹\6ÞF™#U™{
7â±–UÁÛ^ÁÛÓŠ,9nïÓ(ž‚R�¬îÐû€ )š4¹Ÿ™x
Ëã)�íã)´@’×¿uà&O¡‹ã)“€x
¤O�â)�ñH£6Þ-šÈ‹±ñLô{ãx
d§ñˆ§@@<ÅSåyg‹€
ù)kaö¼å³9B%ƒïIÉxy1¾¹£ª;¢“	fÜbô,Oçæû²érjö,¨ÙÅ„¥�¢[Ãàïzóƒ�hc¿'¡±=LŠ`Ò>˜0(8á×Í¶
S?ÃÂÆÕLÂý¢À¿””‡"Ü†‚àþQsÇàþÕF`'¦<9-|R>©#ŒLêÄyç~ùšÙ{ŒFå¼Ž•a’P¡4&².Oòõ$™èó4�iqès Ÿe¨d¶Îè1›hˆ€d¾o™E¥Ì²pöÞ:¯ï|^ç/¿šw^ßÊ=ýéã]mçk~\“ÿaº8ïÇ¾bBép?ÄExâ6x“	Säƒ:¹Em¤O€2¿Û4ð51£|$(Â‰„@Q¾t›†’R±—0ÈéˆúH ´”wÈU¿q”+1I…ˆroÒ³{ˆ¯¤þ–&)`+�eãVX$
êdÂYQA“zÑR`)]‘LPrâ=YtÂ¸[$ÁåB[%e<† ‚uwÂ3Y�—U„'~
“àø` Vpª�JYÅÝ×µ°ÉÃ’UÄ¾²·Èeÿnåã’WšýÊ+ßÖ%™cØùDö~d2¥pFá"QI7í’Ì{~
¢šŽ@0Fk‘³
ŠÏ¨=ãR¹C—0#¹&@¹ð–ÑÄ4	„©Á´ºµ‡J
ô2<^©>Y|2cEù”W—F�´¯Ñ0K€Ìµ„•Ç@sJ20ŽÓYŒ×é*¨½Ì@ËÌÔkt,G¨8)²' ˜ŠR‰ 4}É M•Aæ_iú�Zê¦Œº)ãÅÛ¹¾éÊ³¨®<G{öÈ®à²ñ>8dgštu¦	>Ó„”Î4!½gš4é™&dË3M´ïL‚œ¢ògš�ä|“Œy‚3MHÖGŸïÆÈØ3M¦Øˆ¾Ù\>ûúEÉgìP¼T¾™>ËÛ3Ë»�uh‡ÞºßŒtQ©S|�âw8Å7»™â›æÀ�-ópÒ`2ã�-ÃS|3Ç±eýSüÇ–5ÏûØ²:Å×)þ¹Nñßæ´xžNñâŽe{–k.}‡mÑ_S|Þ”›l·fzí™=]Kªï¸2î2¾u$ô;’QóBÁ2&B
‹0B’BÆ›�”/ó&T$×—¨]õ¨tá$ßá�öÛÍÚo’�¶é÷œ)úÉ·9q;ÓÛCè‹/á|CòTË°ù!Áv}b�öiÿ2Ùõ˜¸Œ”Vuž;)e­(cå®¬mŒÒ�ÞÎB_¦ù2d qNÂpÃåFûD®nì‰å”§ðøŽ¼oFxÍ!xóšfÜ	³ÓÁ
VÉ"fæÖÒ+”ÑÓ»ñ]Ô“;‹D=†4FÂqý?SÆï#�9ÊëË¸ß^Rêa¼lU,dä%j“:,ùd×ÝõÛç#Ÿlw=Ç1<H„©Û_MIÜ =e èS4Ð—Œ6ÄEÇú0™�@dåf#J™2œÆUEË*A`aEÊI)ùŠ›ó×x�øñòææá++InVi<P¼’_¨ïBÇCèâ J¼ÐìýkãAü¹âpÉõÃÕMÇAò™{£ö-…|AÒØ‰ó\à Q¢ûdð©.NG8­Jº“(Ýu�ÉèlÊ¥]©4Ms÷™ÏÈè¸�ÐK1žF§=˜Î“…eÉžMo~å ltÛ2m®–á»,NÓþû˜îPO³i÷Ù…bÝÑõ˜t<êúÒÌ¦¹CsÄ÷¬ËÓ£ÄÝ¤ÃkGøß#»íð_¢G^Ë»Ûý4 =LD;à¹ntÌ„;˜Ž::ˆ61�yrq	ÿ»âÌÛ–¶Ë8+û#¸Y@¼é‚å	Ê7lR:Ô3âŸA0CøÏÒ£‡=¦÷ïúÃ(‰Æ8&Úæ0�SQ(^ªñ§³øÎà9„ÿXÒdl¾“tÿ¥q ›ŒÿAþb ‡™è10ÍJ«ªHz°)ø'~§I}†Æ÷.N'øï"|w¾ã4@l¢7lÉÿiû’öòñóUŠ=„oŠñßußqZÁNè1q~eÑ@Â¢ùR§;„oÜåMÖ)Jöâ»›�ÿéòBþZá÷é†°Ô²èÝ]þÍˆŽ.;€wî ¾ãjÆ5ê"œŒÿ»ˆ!”Ñ.9Ã÷•ºù´õn ·OeAÌŽÔ‘–Ôa‘%WRÅ9ÁÏp­bz„ŒÑÂ(‹:cÑÀ%£q;'v¡kå¤Õ.ÂC—ÓR¡$³µR“Á7 ­YaÐâ…[.í�V7’4�’CÛH�Ÿe­z…×ÄµPÓG„(Bw•dñ~„Ó¾ñSÿp™ŠW6Ÿð>7n•™A¹¿¡Dï:s/B¯ù:‰¤bžfvu¬¥GÞ}²å>Í¼ìk*â_j}~lwfK°èí”ÀÅûÐÙ©Kw�u™¡Ò"ÒûºU.¼R_\ý¢ÈGˆ–î/&àûC}ÚTC÷�qTEg$j¨Aì"aÉ�ŠÀöÿÎ�ú‚‚Ô1“GT±<	7)Åù9%ÐõØÝþÀ¯I-À]àõ¸-ë ý‡uL[¿›ç|ŽÍ]Ë–ÚchÌ‰mÐjO�ÙÑ‘˜§å#T6mRì x†;ŒkÐ¯K.þ÷—>^Þ?‚Ó`@C{µã\þýT}ÏÒÙõ~™[w×�]Þ¸µä~zHKî[Å¬Ül½~®•òy\›&®·ÿ<G³�}…Ø¡à¸¯;.ç4o}-‘#«ü°°oí::ƒ„´#¡lŽ„]–¨ ý†™¬úÂb%gØ? J1ZN3¼VÊr´±'P~2«Ð˜ƒJtqFVƒ*e°¼™>}1E>,k)O}¦+äÌa æß.27úÊu]/6]<*ãŸn‹²%þ‡Þ2F‡ðßÑhÕz:þ×‰ý‡÷àŸaaà­‘Y…E††ü“Ö‘sU?¾ûé…ù³CÃhîÉR�u¶õM0Qºp¼~žc²!üÓÑøÏ8ŒõÚãSñOÐMŸö¾EH¥d×O¬mñ�ª ¯““Iþ“;ý€ÿX¸I‡‰Å"Æ?‰ŠÄD“x'½ÎÙú²^÷�_,%ØÃk™‹Z×‰Ö™YTz±Èãà;J÷�X]X¯||:þ7˜Q‡‰tþFNW˜Ÿ£å¶°LÔ¥Ã¹í1Gã=•ŸØvMŠ,3Œ»N]@6 ºBk%CSCÎ/©æñ;¼2·%3Ò�–uçbLÌ¸ì6D Ú#Ñ²¢è	5)ˆ<hÅ¾‹"þ»¸âh-–v]n­vpLñ�×9Í}áñmÒ‘f¸nÊèeÑûY4V°a•¢�¬(éc~,àð«ˆÒºM‹POëÔ­ÅâAŒ… �:ï&üÏBdfÑ/@õYâÀ”ê‹©þXÀ?ióúÜ8§‘ÑÃEc(¼ ˜¹¼|•‘Wƒ>QÆÒØ=¢áÆF`çÊ8ŒuñŸŒÝ�ïÆkû=ø8-Xƒ¦±¦c]ª�/J½ÔÑ@é/‰ˆ¶µ‚	€¯ÄN•Lbm�?†\®÷w�ŽÝ0Dùü ¹  ©“ÆCq÷µŽ�5È©Šô{n÷ÃL�ÜØÖ¶Öuô"‚]Àæxà,Bpˆ¡âC�G¨ó6 ‘‚hðÆ€«íéFßø~ðÈ Ï{o€ÂóÞC ¾/»šOâû`ß¹P0 	É5Ù}øf÷x'@7 ·›9ø…�Lû(õ/Âí·ùôy¬ÊË¹ŸFÞjÑ­€þMiôêí»+íMp¡ÓÙ(g¡ÌÕŸ¯³QõwíD Ö×?^>¾/xBüm©•Ñ²*hf¿‹E²ö{4.X?Òãš²ÿÄ<Þû& ‹/¸xâ EOFÁ®ì.ò¼§ü`Ö{8íI§ö‰ü;;ÆÆ�ØÆ�<í Yðj*.½QMºØ¿oÓïÓIøHºÐÍ‡#_ÂŠOü¡§? ÷Yld‡©ä™»JõÅ²eÿ?Ýü^f"¢+Û‹�€Þ–â�çÂ¬/ì/ùÐ
Á¤y˜´ “{VüôF}ôàž 6æy³V°© ²íYûž‰©®ÿ«gÜn-Æ«><;V‰AÞiRUu0®³n¥­†�ÙóðL<}j«”‡ã‡¦(r5Sï·¦›ÞzRÑÜä«^jÂSˆñÈnZ0ªÂÐøˆï4LoHîAÌr|C!Gä`æxÅrxÞ;?ž•ÿ:§ «é-[zè/­%àPDü¢á{9[IÐ6¦àŸ²FSFNúW·�÷ù�t»(fÓô×}ö‚Ø òeÖ ’4¦í·ì®1»¢ÎzfêÄý(.¥kÕ¢óË™ÌzÜ¡ÿë¾ýlëày¿*
ºz<ØË¡z›‰¸U(ÝÎ¿üP„R>AÈû÷ô�ápª.ÈÒ§­ØCpý‘Å<œ�Kàï¦žlc˜4“`Â³uãƒ~Ñ‘ÌÊý¼>¡ïÊïV×]Š
/P
{¶°û¡á8¾ø4`pq_–¶"‰ —ŒªÊÕ¡4Öu0=È>¦Þ-ÿ$PSÕ¶3Æ"{Ø±Ð.ŠÂnï­'åþZ¨f(ÀñÉØ÷‹ÎÂŸÄh§a"�LÌbÚ&<üÊ…º}!bß•$S–¨¶œW¶æÝ¾úÓôƒ†&‹1b61æÓ«Û·o®ÿò•Z$»øþæNq>]Vö}+,ºt™˜Ú¸—ÿ£-|£…ˆŸ&€ÌsÓ!9H5o¤ôô+`–˜¿}¼Ô2ˆ[/ÞÝ\>Ì¾0mï¦"O1Rî—�›îxóøÃýUBžÏ¢[�ùxEÄÛËÇË¾páÿ[3ö
:8ãúbê—œäþÊqó ÏÿÊ•Þ>~mïiß+ñ"s6Ò_MkÍeèêôÿ’CõÛOpß¾¿»û>ÕJÔ‚ðIÁð¾Ã®=Èê4Ïêd÷½‰ì¾|÷¯`»GÛãÈ6¢èî[Agš1^ãyíþîí�
Û{ÞkÚÖþoVàNg‹ÕÚå®W`Í–/å#8#Ê?)Ð.!ºÎþGð“K÷×Jz•i<¢zûÒÁ	KÿvpRÛ›g„sWY)¼�ÖÈç¯ßHQúýåÍ÷„K!~~¨­‘å{z%ùG9åsnLÊ!E Þðp…­‡ÜX	äGË[³Ï"áØ_Æ«äÚl"åÝ>å|,7[/Ø=ïÒ½ÉôÉØï°E¨±s*œÚ¾àÙKöŒ‚ëN–^BÍÚ¶e�L-rEôóÅ»{@‡ $Äçñ ññúêÍU~R…I­‡10Mÿ%V§·[VÎöU×“[ù»›ñnÒ?~÷�‡Ç««�½ó§¨óç!¨ñ¼öÄzâÿš�H…9™öîÉõNÁìÝì$U¾KÍäV~6é�?õ½óûËÇ‹Ç»¯î31ôN­ûwÌ³¶`O†ôçƒSIL�:
MÖ@Åó2èM5KNÆG³ÇÁwreç3Dô�¯ÊþoCë$Ëž”�PBQH–½ƒ<)s}	ä*é ±Lâ4$zžY ØÒvÝÃ/j°P*MàgÀ�Žû?É<ÓÆÏ´½ïÆÁ:EÈh¼©ë¬a›Òg#>è½VCŽ°jŠý23Å~¸~ûö&3É.ÒIö¼N²u’ÝÕèúÂ'Yöœ&Y±{Ùb‡ÞÍÿqãÉ{ì3b«	ô3hSE6y¨ÚáIÈNÄùbçyÁnÉÌ‰ëÌl9ôŒO¬7xÆÌÄCÏœ–¸ß¥,K`aÄË½BÍ'¡çÜþ>O„žò3@PâãÞÓ×ž6÷LÛÛžÑô9�ÂgÓxgQxé$ÞYx!N»æ8ŽoÇqôŸÝŽÛ‹³˜?É˜�w±tùÚ.A¼ÆÒå§Õ	ñ%™t«gÖ3ðÌz*É>]¾Ý¹eãÓ2ÄÕåÃãŸÁ±C‰¿ýoÍW»\­–êÏ~-ËE|�stû|Ì[âÀ¾CET¸lc„ãˆ]Ìˆƒž9Gdå+g&´ÜyB_5î·DuÌÐY<€°Yá-ÀóÙXo¢·4Þ±+S¹ŒÆºµ¼Æ‚©î4÷–¡Œ‘tÙò-%º4cÀì0Æb1ÿ;Ê˜ðó¦³a¬Cúý[XLlÔê(cD[Z³`WtÉ¼åuôÓÙ0v2®¿ˆLÆø·à¾ÿI\õOçÁØˆqÌí*I2F¼e±X¯ÕŸþXœîhLžár]„ªé[aÍüáêÍ�ü­÷|]|�nY¤[C^Í®pm¦&Í§¦=µÂõL\’«~XõÃê~µK5èâ¿L÷×'ëõ‰ú[¯_…ìW³Í+pz´NÓŒ©‰Lè¸òñïžZ…Ýïp”!=Oïf~çfX0×vÑ/UÿÎIˆŽ B�(G*‹C[•ð“ÀŸPÿ®oe(Àß?Íá#þyJøè;?ô)ð³eÿÊZÞW§ò¿ú[��ÆJ‰ßŒ’_W³üáÇxÛˆžØôy˜òU§Þ¥ç	Œý;´tO—>×g«³³•ú;;Û…ô¹F³/0î0cŽñï‰×	ØlP¼)þž^g=Gp¶•Nºøcë�"».ºxÁþ$Àß?0X	…R€²\.ü×“ðßMÇÿÀ<	¿—ë™è»ô=	¿7�ÿü¹ÓþuzÚÊÿêïô|#éó³XúLc×œÙØ5g5vM�²«ì…Å®ù_tìÿ·Zâ¼ H/Q›(:Ìª÷^sô½gú>„3ÇýEïK†îÓÿ{O²ÝHŽ¶Ld&©ªºfz/c¿çëÌ«nO{¾Áßì›«¥©’ŸJ¬‘Ôîn�Àß1þ€9ÌÁGü3¾[�-2I‘"!‘	@ ‘Xœèþxû9Þ#ò¥øü…/³zÀ;¤_;@ë÷¢¿Ü®î÷ørê¼†ðE&9Â³¹�¾ˆ¥~Óm‡Šß­®¯o6Ýsu`«‹Çõwû['˜`¢÷7ßKâ	h÷×€‰¾ô"‘ÖOW+ÕíÒqï§¥ã¾Uù´ÈõÃêÃúþêãÍj4£ -ëoÿ…ïÕý”ŸÝ@‡a`¹€7gº=€ã$Õ™n›‘Ê¸Ü]ß>dê�A1ò€;AN…Å²¼ãÄ¼"0ÂE‘�Ú'Âb�:äþ…#ÈÛ·oÓÔñè¶Ÿ‹Ó`1‚³¼ó–gHfÕŠ	4â«!§Áb�n� P�u ÝêûâÅ”öLLÆ’
~@8Én¸e¸KÃb—/SúçèæýÖ€t[Ð/Týò¤?ÅtKºaH·åqÐm.\=’n•¨fmý’nµñkzÔ!u*ˆŠRg«Ú~šÒûÇ½áJ|êíø�A~c�ßd±L@­õüÆ ¿1ˆÁ³Þ0H7éÆ©û�ý´ÙN¾½†t{1xöÀ›×�n¯·å·€n$¦[O?%GÒO¡“ûòf“Ê7Ë·1üÆOu<%ÛõÓ3Ý”°kr¤²�åÒ\žJŒóvÈ?=ºé”sM*A�yD*b>AžJ“Væýž4Ä%=Zº��ošn•í�2Š™ú«±;˜ÍD`Aœ»ø¢iGÙ…·á· ç…I]ðŽ@H·VC:nºáqtóùÍ#Uie@·
Ð­¶Æ‘gºm¯ÿ©iymå^KOÁˆû)ƒ]ó'CúéTQ’n>Á¸`¶'è¡ (õá~LD·EÓ4'@7R²œÉ551vÈ·ÝI¿ŸJÍe©>¤Í|viv�-5­Å£>Î*t‚ùôñö!s&ËÒì¶]žÏd9”3Yz¶ÂÌ>ÕóŽ”óŽ”ó™,c¶˜”½ñÙ)ú`óñ6½M¶C6s–„wš„=O‚õ…6t5,ÏœŽQ_DÇ„Ç¡]”Ç¼cêXú8ý­è¹óúu–·ƒú¥èI)¬Q&t$ü’¢çb±_~ÉÔ¨»~î£ÎpÝî>.ýÓi&©‰C©öÛ¨~*ÏÌA9ú³.¯*ñï)ñ¯ÏJüÁ™ƒ^ÄF·³V¼­V|T2Õ~¶]íÇÚÑö±·^Åo®¦ >É\ƒÛƒååçbì!ÅãËó�4ÞùØ\º•�_ÉC¥/[mãrRÝ£t÷óÄºÜtG}dú|®Ê[ì^·âðö¯ÅîëWÂò=]HÖ¨#4Qy{¡§>Â]Ñ«óŽp[#ÜF§êç]Ð1×Áñí×[?ìÙj(á\cêöKÌ5Ê©æR*Ïìyíê°öáò:šk¼	æñI—æ€¤ËóIûU¼�ìø‡³9ú�DaeÉ˜ù/Ãc{‚‚#¢ƒ’¢ã‚šÞøþÈø—tùAû�‚su¢üçáX‰ƒ™Àˆ¡îX|¿z®ðæí'3bLgz¾s¡÷p…÷Ø#VÈù.ðÓ}‡Ž[ðð»F)gôÖÒºÚy»3ñ"a¦2”É ¢Å.¹øÈ÷Èdà&ƒLRÑ²ätæ2:x¸J¯®3`�<6¨C@¥,©™Ê ËLËÌê€OÀ¶ÓÝÈÙ®¾¥Ö:¹ˆbý7-c\§Æ€W˜øAN�©ÿÐ×
rJh%>òß=\œ
‰GEx2Î=$Õ‡y™,�¥”$Î•ÎÛÛ‡?ßÞ|{3¨7Ü‡Qï9«+gu%)Wb+˜½2š{èÍÝô:„õ‡ekðsyÄ<nC\^„IW€8÷PPºÊ‹¬n¸×�\‰®¬¯�NSûRå‹óÝ8ç+ëÏöÆáµ8‚{ŠÎëjÇ_Yÿýêîîêýêþ¾×>Â&…F&…F'…V<Ãí?ø?åÝ+úÓñ^µjù_IŸø³W"_t¼´_\jßâ�¾¥Ù^Æ=Ü¾ìãx6®§ö<&V¯'ý{¹Á…:_À›±"›L¼tpb�…<ç²ž‹¿z5ômØ¯»×ÊÖó¹ü_ÄòÝß<}”6ˆõããíÝã¯õ3Ü"ÄÌ§ð•šáß¯ïC¢¨¹¾ù,øQ®‘Z¨8^sØî[ªPÝü'L)6=©œ�Ë¸0.jŠ/3Mñ¿-Ú©%>Ý¶x«©ÿÅ~0Aæ.Ì‡Ÿ.Aÿ(™_‚É	ï¦�€
 nêwi9FO·©ãË1’”cð÷Á´þ*¦µìVåéÒúÜ2q+ÿ	!ã¸ßÏÜŸPw“ÁmöuúUíé¶þ;=NcïA“J6–lãå`ˆ’÷ [ã‚Ûž+*I7T‘|�µxwŠä;b°ŸP(üÌôQù…[Æã¡4ý*V¥°%§+h¿´Š èx6ý:MÒÖ±çXÊ%^È`2þ½OFÒöøÓ%c#h¨þ(JÄßÆcrqÚsï?RMÂÄ<Õ0:\3ëÒFJ�ob�¹<í–þa¸Ö›Q’G*Õ­V>´Éþ!(N\Â]ô™a2”ü]š’'Ìüÿ-Z1ßwDÑ(*j…_>|§Þ�~^?<­î~ýávuÿd¡µw&>¯â-—	’é¦}X’Xœûø¤(=ì=ð&W¥þ*YÖÕõú»÷w7S½èû+¸+ôöñæNPwTÍ ?¬ï®Ö›èµ6 ûïïÖêê¿»Õ¿­ ‡ÏïnÿðÝíõ•`ô§�]ï–Çƒªr ~žn ‰Zæ—èS¿lŸ¸²§r²ïQß¸rnÖÄê¡Ÿ‹·lÊ�\ÛÉ¾©;Díì¿Œ¶uZÛÎU¬ñ¿Ë… Ïæ398ÈíüDP‚Ëšvnû†L©W;¶HPŠ<tP„£ðƒ#QÎTy	ü"¢Zð�zm<ØyÂM¨’>yôø|Ñ–C\	Ú^!yQÒ›¾y ¦mK3%Ï-òqkCÔ9h Þíq1×I/‰#dïåTË¤·èK ª¹jÁ©AëAÔ±=G] ^}$mC[MÒ,f›%½¼Ã»L¶EÄ©êzG“B½™ÔQ€zŒoŠïS}·³–°n qÀHXXŒ:Î¡ŽP/ê9aÓÝ»“TG¡—„(dF ^U¤1“%êÔ“µÉ@C^ð½(/l¶yžêŒ¢ºÁ<¦z/ê<…e'ê<ÛyMZ€zža$¢
õ…¡…A½_‚'Ù¾§’1êyA�ï¦†a­5ê<’0=ƒOêýÞ¸w¢Þ4åQïm¨'A?’iÌû„#jFÿòÅ0(7q&‡$ BiÔ¥’ <D×­iXÃ¡"ÀG�þé!¿#[À¡Ö*œCÅQk_­f—Ò‡Ft¸�úJhb±òÊ-Æû‘û‰�úÊ­žj¡ZÔ¡7®JyµJhÊ]y]ª®u£ìïb{ÆelÏxu¶gœ¶=ãl„8D#ÄÄåÐI�›"7v§ÈAUnVÝFttFÏ’BÈ¡[RòæZÏæ	óI¨nôj]ºé3Ÿ�Ð|’´™ÈlµPPç3gÎêe¯ÛPA�l&á\ ª*‰/ø¶LáOë÷Ãô¹ÌwCCI8clñ­ÕýOóš+Î !Ôé­#<kWð¬#Y|‡¯¡ïnM"áä!ß{Aú"Ÿ¾Ú‚ã»;0Ž*»c2°¢/QørCßzS|Ç?h‡ñµ—‹ÅøJ÷Jãëî¬v²‰,Ë€Ô:1‹û[Õ
6®©s‹o½O3Ç2/ SøÖœ£<¾ÛÛ6Ê3(RòÌ4ôÁQµÆW'�Ê^±Ñx‘~�Z1êšÕZ1Æ¾úð
†ZÐñ™.ZkÅQØ+F)¾ŠÏÕL)Û)6›’ob¢ØÔr¦�cZtA^¶½c£yûÄ'¸ƒYïe(W‘?)Œ±žÜ
æ©¡„­t¼›Ã)'â.Èy;Y¥~8mèò!ëZ
P§©|Žâ†½Ñšgt†Q8éE!yJqD~Ò€ÜkÉtõµ*ÐNÍå¹WÜ,o�ç\�}ïÈüY0º² ÉŽp=FÌþSƒ€£yÄQýÁën¦ì)¼aLnUthË+ƒBç=á±é·pCé‘­¿{eŽ™Yh!8AP‚Î›Ü»P:Ÿ=ß*Šò/#õ·ª;S_¢ÞrÉƒlUýn5ñ.x0ý‡ôÿ]R"˜gfë¯iëÏŒ8œEõGcë�¶£Ïæí�RÆ€Dý%ÿPê?�–1„ò¾¿ÿmG�av¬äËáT8¬¿5mUz-–+8Bñ=¢þ¨§þq˜oPÿ>yJ²ôHó¿¬?Õæt[ªG9[¡º&d`ó¢­ªŸ&Í×¥Â…+*Áÿµ¬>©Z(µMÿŸ’·£âG4WÔ¼Gß`N´’.¬¿ì¹÷½ã_GuÇ£¾ñt�KÖ?­Çã¿! Æ&yS}×úÏé#~÷õÕ@ÿá‘úo~Q‚ß(‹MS4>ýFè·yþÛ,¼ Œõú/Þ"BÓZ8ïBÞÜ¤�ÉËÔ#ÀPÖîÝÕ»óúÏÌ7°�yóŸxUQhªó'Lð�-òÀ»ù*´Ùy³bä…¹?kŽðŒÂ>y�#$OÎzù «ßO|«_z“U©’FGml¹Éê"0ÂôføPÌúá“ÀSo¦µ¿mjüK`›‘õk¶ÛÄ�ùLË ~‘iˆ‰Z`¤óH¡ç^T´Ãu;àþ�VÜtŸHIQõ°	zH³³MVø£jÊä©Å—r»˜ÿ7â«aê6®¾…Éoí[Õjµ(5³R¥Ì}kMã�·‘°y‰›¤Sy—•¹ùÆ@€åº¼8™·Ñya^y=j:oBâÜÒJŽ¤Ã!­’¹#®Pñ¯ëÛû'ÄWw·Ä@”hé›OŸŸ~¿^¯ï¿—O«‡7OQÌÑÏëGpº‡ºû¢A³ß+ÞP|³Ù ¦6Š§ÇÍàüÃ¹¹éºÉ\'´ßÕ¼û{Á¶ïwX/M4OøNn¿¯ úä,ýs+/Îðzž±~2]»õƒ	N©/Ï)Ê*S{T¤¯Ù1÷î(nÕñüƒËòácÂêZÚ#U+·w>hs	_. ÁåDÎ›a;øt7ð+_üM‰?, ,grŒNÁ/pòÎ$Ï_ôµ¯(€ þ!€W	d$\nÂTð%_Û)„n@ü¾ÖËæ
P¢¯ŸA¾‹o‚˜Ç7A¼Ùn ÂÇOëõÓÇƒšIùiýøøãî§ƒ›ð[Öû}ŸoaË/2Œ6‡ív±Ë¶ÈwMôv8‡Üëž—g¶Ðã™H÷l¬¡ÿô/l5½º¦fúWÓZ?…§VÏš·õò=^.ák=jH	^g5¸F:£†ç9û‹�ëô844<W–žm‹éz=[Xè³™-¢Hyê8Jù.ŒgiÊªå]
®¬Šêy4®çK[‹ù|X%B4Ú_–e[ÁKSÖkPV™-…�còÆeÙ|¯`½kë¥iH‘¡a¶ö—ÂyLÔE[–e�†ò¶+Õ^H·}ƒ"–È”•B2RKw‡1V«§pË?4ÁØ-€NÈi†”O×¿j÷&£¶½øL/©[ÎêfäŠ@Ïk¬a¦t6•±&­¢‹š+#SêÌ5¦‡¿óú¥îN>:k2ùÛG™
bjú$&óL‹'3÷K› ínm^aà1BˆÎG˜Ë>CxáºFÐID–´d}ÎÂ‹à”ê.a¦áP6Ð!ÿ !‡@8Ý™©ÇseeÝO§�š0²ÚU‚Î,MoŸÎÒ°.ñaeœ>ÛnA{¡v&ßš¹w0ðtÍóÏópÚ-ÀÊ©¼äþz�ä®�GrŸO£x)òÁCk€&U˜››
ä?åW¯Ï/p©áêºv‘]xÄÇÜ‘fŸ¨}B�È¢AH	4[S®óÓà†9já9p8Z0¨àpÇ)Ï_â–€Sypu£Ü 8E�áHÃQéÂzytJÃ¡È€Aú¢\´	>¥ZP'ˆÌY[q”É¯ž–'PBÞÛè$dóE~}Ï¡Èû©öàÆ†Æ|‚»Ð–Çôruô¨íŒÊÏ±Fiä(µãµEGµ¯pï‡n°íÝÅHþämZqo»ªF�›)çìU¥×�˜µÝÎç¼­GUÂE"óÀ	W«iSQÊÃ*¸>ôÁÂ¹b_ëK•!&rÎ€)®NäŠÞT£�ÃEÝ»•!o{×[$tQ•zÛ2àhª7“ZÍeH”¡ÒÈÜæå6w—/E+wÈ+ªfµšVhP{T°Ý,Cxk£µ§eù&nUÔÙæðÕóšÔènGèßÆ7¨ñx„ÞÒ¤v°¬£[žýÕÖÎfcèÿÚCÒ¤aDqˆµ—sÕ§¶:$2é˜t%³IpÞe“Èp…1Þ&©ˆ²¹É$úË&i­O*eÅØ†”82Æß…·´ÿ›xaŸÜv²¿.º©º~>És;)±;[Ê7`}`�1˜m;Od Ñ–õ´cx!—SáŽÉ<œXÃ=1¯òJ¢Œ0Ê$€u4iÓ[OåÃ±Ùõb1Zâd¹6;m³óTö²/»u6»µ‘67)†:hº.K¹zH.ÛN{Ò�LïÉîLWV÷
·+Sk(ÿ‚KÑ–iÛñâ¼Bû8Wh� Î@ÿ~uw ÿL†¯¾]ßßëª_=ýøùæJ­Áa÷"W…~žLq/˜ýcw=Í6IÒå(Q­R`´P)ÄŒâI�|àÕ*?Š¤ÇV/!ŸîöÛ\;ÆÙ}r%²^ÁÇ¡l¤µrûëÿ3ûÜšnÖjWqÏrk«3§»
WË¼Õ ®-%*®ô–-Ìºð—ù3¯@æ•Î”.u iƒ©*F+lvD@ÇÙØíÝuÇQé,�ï·tnJ¯ö_:2«ûU»ë†öWï“LK�lw–Ü¡Sê×ˆm>âí ð‘™{¡Â+}îõE’Ý9¡§©í‚ãj‚ºû{­ý=>eG;Ôþ~¿Bªbí¯™lcû§Õ‡O«ãµh½´}uûºb·ªãîæÞÿ‹Œ¥IÏÁšô¤Ûó¡ä”Ì½˜v/´7unBX€ó;ÔtµÚ ó9k�€G«vÑ ÏùÀê¯¢(¼‚ �--¯Ï•^;€ÄÂKäèöñ¨i ý8¯ŠnsË�¦F>Ã $×¥8	[Ph@ÿê’O+š <?php

declare(strict_types=1);

namespace redstone\inventory;

use pocketmine\event\EventPriority;
use pocketmine\event\player\PlayerJoinEvent;
use pocketmine\inventory\Inventory;
use pocketmine\network\mcpe\protocol\ContainerOpenPacket;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\plugin\PluginBase;
use RuntimeException;

final class RedstoneInventoryManager{

	public function __construct(PluginBase $loader){
		$hacky_block_inventory_handler = static function(int $id, Inventory $inventory) : ?array{
			return $inventory instanceof HackyBlockInventory ? [
				ContainerOpenPacket::blockInv($id, $inventory->getWindowType(), BlockPosition::fromVector3($inventory->getHolder()))
			] : null;
		};
		$loader->getServer()->getPluginManager()->registerEvent(PlayerJoinEvent::class, static function(PlayerJoinEvent $event) use($hacky_block_inventory_handler) : void{
			$manager = $event->getPlayer()->getNetworkSession()->getInvManager() ?? throw new RuntimeException("Expected non-null inventory manager");

			// shift $hacky_block_inventory_handler to the first position in the callbacks set
			// so the default callback does not override this one
			$callbacks = $manager->getContainerOpenCallbacks();
			$callbacks_list = $callbacks->toArray();
			$callbacks->clear();
			$callbacks->add($hacky_block_inventory_handler, ...$callbacks_list);
		}, EventPriority::MONITOR, $loader);
		$loader->getServer()->getPluginManager()->registerEvents(new TrappedChestListener(), $loader);
	}
}<?php

declare(strict_types=1);

namespace redstone\inventory;

use Generator;
use pocketmine\block\inventory\ChestInventory;
use pocketmine\block\inventory\DoubleChestInventory;
use pocketmine\event\inventory\InventoryCloseEvent;
use pocketmine\event\inventory\InventoryOpenEvent;
use pocketmine\event\Listener;
use pocketmine\inventory\Inventory;
use pocketmine\world\Position;
use redstone\block\TrappedChest;

final class TrappedChestListener implements Listener{

	/**
	 * @param InventoryOpenEvent $event
	 * @priority MONITOR
	 */
	public function onInventoryOpen(InventoryOpenEvent $event) : void{
		$this->notify($event->getInventory());
	}

	/**
	 * @param InventoryCloseEvent $event
	 * @priority MONITOR
	 */
	public function onInventoryClose(InventoryCloseEvent $event) : void{
		$this->notify($event->getInventory());
	}

	private function notify(Inventory $inventory) : void{
		foreach($this->getHolders($inventory) as $holder){
			$block = $holder->world->getBlockAt($holder->x, $holder->y, $holder->z);
			if($block instanceof TrappedChest){
				$block->onViewersChange();
			}
		}
	}

	/**
	 * @return Generator<Position>
	 */
	private function getHolders(Inventory $inventory) : Generator{
		if($inventory instanceof DoubleChestInventory){
			yield $inventory->getLeftSide()->getHolder();
			yield $inventory->getRightSide()->getHolder();
		}elseif($inventory instanceof ChestInventory){
			yield $inventory->getHolder();
		}
	}
}
<?php

declare(strict_types=1);

namespace redstone\inventory;

use pocketmine\inventory\SimpleInventory;
use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes;
use pocketmine\world\Position;

final class DispenserInventory extends SimpleInventory implements HackyBlockInventory{

	protected Position $holder;

	public function __construct(
		Position $holder,
		readonly private int $window_type = WindowTypes::DISPENSER
	){
		parent::__construct(9);
		$this->holder = $holder;
	}

	public function getWindowType() : int{
		return $this->window_type;
	}

	public function getHolder() : Position{
		return $this->holder;
	}
}<?php

declare(strict_types=1);

namespace redstone\inventory;

use pocketmine\block\inventory\BlockInventory;
use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes;

interface HackyBlockInventory extends BlockInventory{

	/**
	 * @return int WindowTypes::*
	 */
	public function getWindowType() : int;
}<?php

declare(strict_types=1);

namespace redstone\world\sound;

use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
use pocketmine\world\sound\Sound;

final class PistonInSound implements Sound{

	public function encode(Vector3 $pos) : array{
		return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::PISTON_IN, $pos, false)];
	}
}<?php

declare(strict_types=1);

namespace redstone\world\sound;

use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
use pocketmine\world\sound\Sound;

final class PistonOutSound implements Sound{

	public function encode(Vector3 $pos) : array{
		return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::PISTON_OUT, $pos, false)];
	}
}<?php

declare(strict_types=1);

namespace redstone\world;

use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\world\ChunkListener;
use pocketmine\world\ChunkListenerNoOpTrait;
use pocketmine\world\World;
use redstone\block\Observer;
use redstone\block\utils\HelperUtils;

final class ObserverChunkListener implements ChunkListener{
	use ChunkListenerNoOpTrait;

	public function __construct(
		readonly private World $world
	){}

	public function onBlockChanged(Vector3 $block) : void{
		foreach(HelperUtils::$side_offsets as $side => $offset){
			$observer = $this->world->getBlockAt((int) $block->x + $offset->x, (int) $block->y + $offset->y, (int) $block->z + $offset->z);
			if($observer instanceof Observer && $observer->getFacing() === Facing::opposite($side)){
				$observer->onObservedBlockChange();
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace redstone\world;

use Generator;
use InvalidArgumentException;
use pocketmine\block\Block;
use pocketmine\block\tile\Hopper as HopperTile;
use pocketmine\block\Opaque;
use pocketmine\math\Facing;
use pocketmine\world\format\Chunk;
use pocketmine\world\World;
use RangeException;
use redstone\block\power\PowerSource;
use redstone\block\power\Waitable;
use redstone\block\RedstoneWire;
use redstone\block\utils\HelperUtils;
use function array_key_exists;

final class RedstoneWorld{

	public const WAITABLE_UPDATE_NORMAL_PRIORITY = 0;
	public const WAITABLE_UPDATE_LOW_PRIORITY = 1;

	public static function redstoneTicks(int $redstone_ticks) : int{
		return $redstone_ticks * 2;
	}

	readonly private Vector3TickMap $waitable_low_priority;
	readonly private Vector3TickMap $waitable_normal_priority;
	readonly private ObserverChunkListener $observer_listener;

	private int $tick = 0;

	/** @var array<int, RedstoneChunk|null> */
	private array $chunks = [];

	public function __construct(
		readonly public World $world
	){
		$this->waitable_low_priority = new Vector3TickMap();
		$this->waitable_normal_priority = new Vector3TickMap();
		$this->observer_listener = new ObserverChunkListener($world);
	}

	public function getTick() : int{
		return $this->tick;
	}

	public function tick() : void{
		$tick = ++$this->tick;
		foreach($this->getWaitableUpdateAt($tick) as [$x, $y, $z]){
			$block = $this->world->getBlockAt($x, $y, $z);
			if($block instanceof Waitable){
				$block->onRedstoneTickReceive();
			}
		}
	}

	public function scheduleWaitableUpdate(Block&Waitable $waitable, int $game_ticks, int $priority = self::WAITABLE_UPDATE_NORMAL_PRIORITY, bool $override = false) : void{
		$game_ticks >= 1 || throw new RangeException("game_ticks must be >= 1, got " . $game_ticks);
		$pos = $waitable->getPosition();
		$this->scheduleWaitableUpdateAt($pos->x, $pos->y, $pos->z, $this->tick + $game_ticks, $priority, $override);
	}

	public function scheduleWaitableUpdateAt(int $x, int $y, int $z, int $full_tick, int $priority, bool $override) : void{
		if($priority === self::WAITABLE_UPDATE_NORMAL_PRIORITY){
			$this->waitable_normal_priority->push($x, $y, $z, $full_tick, $override);
		}elseif($priority === self::WAITABLE_UPDATE_LOW_PRIORITY){
			$this->waitable_low_priority->push($x, $y, $z, $full_tick, $override);
		}else{
			throw new InvalidArgumentException("Unexpected priority: {$priority}");
		}
	}

	/**
	 * @param int $full_tick
	 * @return Generator<array{int, int, int}>
	 */
	private function getWaitableUpdateAt(int $full_tick) : Generator{
		yield from $this->waitable_normal_priority->pop($full_tick);
		yield from $this->waitable_low_priority->pop($full_tick);
	}

	public function loadChunk(int $chunkX, int $chunkZ) : void{
		$this->chunks[World::chunkHash($chunkX, $chunkZ)] = null;
		$this->world->registerChunkListener($this->observer_listener, $chunkX, $chunkZ);
		foreach($this->world->getChunk($chunkX, $chunkZ)?->getTiles() ?? [] as $tile){
			if($tile instanceof HopperTile){
				$this->world->scheduleDelayedBlockUpdate($tile->getPosition(), 1);
			}
		}
	}

	public function unloadChunk(int $chunkX, int $chunkZ) : void{
		unset($this->chunks[World::chunkHash($chunkX, $chunkZ)]);
		$this->world->unregisterChunkListener($this->observer_listener, $chunkX, $chunkZ);
	}

	public function getChunk(int $chunkX, int $chunkZ) : ?RedstoneChunk{
		$index = World::chunkHash($chunkX, $chunkZ);
		return array_key_exists($index, $this->chunks) ? ($this->chunks[$index] ??= new RedstoneChunk()) : null;
	}

	public function setExtraDataAt(int $x, int $y, int $z, BlockData $data) : void{
		$this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)?->setBlockData($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK, $data);
	}

	public function removeExtraDataAt(int $x, int $y, int $z) : void{
		$this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)?->removeBlockData($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK);
	}

	public function getExtraDataAt(int $x, int $y, int $z) : ?BlockData{
		return $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)?->getBlockData($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK);
	}

	/**
	 * Returns all power sources that have the potential to strongly power
	 * the opaque block.
	 *
	 * @param Block $block
	 * @param int $facing
	 * @param int|null $ignored_side
	 * @return Generator<PowerSource>
	 */
	public function getPotentiallyStronglyPoweringSources(Block $block, int $facing, ?int $ignored_side = null) : Generator{
		if($block instanceof PowerSource && !($block instanceof RedstoneWire) && $block->canPower($facing)){
			yield -1 => $block;
		}
		if($block instanceof Opaque){
			$block_pos = $block->getPosition();
			foreach(Facing::ALL as $side){
				if($side === $ignored_side){
					continue;
				}
				$source = HelperUtils::getBlockAtSide($block_pos, $side);
				if($source instanceof PowerSource && $source->canStronglyPower(Facing::opposite($side))){
					yield $side => $source;
				}
			}
		}
	}


	/**
	 * Returns all power sources that are strongly powering the opaque
	 * block.
	 *
	 * @param Block $block
	 * @param int $facing
	 * @param int|null $ignored_side
	 * @return Generator<PowerSource>
	 */
	public function getStronglyPoweringSources(Block $block, int $facing, ?int $ignored_side = null) : Generator{
		foreach($this->getPotentiallyStronglyPoweringSources($block, $facing, $ignored_side) as $side => $source){
			if($source->getPowerLevel() > 0){
				yield $side => $source;
			}
		}
	}

	/**
	 * Returns whether the opaque block is strongly powered.
	 *
	 * @param Block $block
	 * @param int $facing
	 * @param int|null $ignored_side
	 * @return bool
	 */
	public function isStronglyPowered(Block $block, int $facing, ?int $ignored_side = null) : bool{
		return $this->getStronglyPoweringSources($block, $facing, $ignored_side)->current() !== null;
	}
}<?php

declare(strict_types=1);

namespace redstone\world;

use pocketmine\world\World;

final class RedstoneChunk{

	/** @var array<int, BlockData> */
	private array $extra_data = [];

	public function __construct(){
	}

	public function setBlockData(int $x, int $y, int $z, BlockData $data) : void{
		$this->extra_data[World::chunkBlockHash($x, $y, $z)] = $data;
	}

	public function getBlockData(int $x, int $y, int $z) : ?BlockData{
		return $this->extra_data[World::chunkBlockHash($x, $y, $z)] ?? null;
	}

	public function removeBlockData(int $x, int $y, int $z) : void{
		unset($this->extra_data[World::chunkBlockHash($x, $y, $z)]);
	}
}<?php

declare(strict_types=1);

namespace redstone\world;

interface BlockData{
}<?php

declare(strict_types=1);

namespace redstone\world;

use pocketmine\event\Listener;
use pocketmine\event\world\ChunkLoadEvent;
use pocketmine\event\world\ChunkUnloadEvent;
use pocketmine\event\world\WorldLoadEvent;
use pocketmine\event\world\WorldUnloadEvent;

final class RedstoneWorldListener implements Listener{

	public function __construct(
		readonly private RedstoneWorldManager $manager
	){}

	/**
	 * @param WorldLoadEvent $event
	 * @priority LOWEST
	 */
	public function onWorldLoad(WorldLoadEvent $event) : void{
		$this->manager->load($event->getWorld());
	}

	/**
	 * @param WorldUnloadEvent $event
	 * @priority MONITOR
	 */
	public function onWorldUnload(WorldUnloadEvent $event) : void{
		$this->manager->unload($event->getWorld());
	}

	/**
	 * @param ChunkLoadEvent $event
	 * @priority LOWEST
	 */
	public function onChunkLoad(ChunkLoadEvent $event) : void{
		$this->manager->get($event->getWorld())->loadChunk($event->getChunkX(), $event->getChunkZ());
	}

	/**
	 * @param ChunkUnloadEvent $event
	 * @priority MONITOR
	 */
	public function onChunkUnload(ChunkUnloadEvent $event) : void{
		$this->manager->getNullable($event->getWorld())?->unloadChunk($event->getChunkX(), $event->getChunkZ());
	}
}<?php

declare(strict_types=1);

namespace redstone\world;

use pocketmine\scheduler\ClosureTask;
use pocketmine\plugin\PluginBase;
use pocketmine\world\World;

final class RedstoneWorldManager{

	public static self $any;
	
	/** @var array<int, RedstoneWorld> */
	private array $worlds = [];

	public function __construct(PluginBase $plugin){
		self::$any = $this;
		$plugin->getServer()->getPluginManager()->registerEvents(new RedstoneWorldListener($this), $plugin);
		foreach($plugin->getServer()->getWorldManager()->getWorlds() as $world){
			$this->load($world);
		}
		$plugin->getScheduler()->scheduleRepeatingTask(new ClosureTask(function() : void{
			foreach($this->worlds as $world){
				$world->tick();
			}
		}), 1);
	}

	public function load(World $world) : void{
		if(isset($this->worlds[$world->getId()])){
			return;
		}
		$redstone_world = $this->worlds[$world->getId()] = new RedstoneWorld($world);
		foreach($world->getLoadedChunks() as $chunk_hash => $_){
			World::getXZ($chunk_hash, $chunkX, $chunkZ);
			$redstone_world->loadChunk($chunkX, $chunkZ);
		}
	}

	public function unload(World $world) : void{
		unset($this->worlds[$world->getId()]);
	}

	public function get(World $world) : RedstoneWorld{
		return $this->worlds[$world->getId()];
	}

	public function getNullable(World $world) : ?RedstoneWorld{
		return $this->worlds[$world->getId()] ?? null;
	}
}
<?php

declare(strict_types=1);

namespace redstone\world;

use Generator;
use pocketmine\world\World;

final class Vector3TickMap{

	/** @var array<int, array{int, int, int, int}> */
	public array $vector_tick_map = [];

	/** @var array<int, array<int, array{int, int, int}>> */
	private array $tick_vector_map = [];

	public function __construct(){
	}

	public function push(int $x, int $y, int $z, int $tick, bool $override) : void{
		if(isset($this->vector_tick_map[$hash = World::blockHash($x, $y, $z)])){
			if(!$override){
				return;
			}
			unset($this->tick_vector_map[$this->vector_tick_map[$hash][3]][$hash], $this->vector_tick_map[$hash]);
		}
		$this->vector_tick_map[$hash] = [$x, $y, $z, $tick];
		$this->tick_vector_map[$tick][$hash] = [$x, $y, $z];
	}

	/**
	 * @param int $tick
	 * @return Generator<array{int, int, int}>
	 */
	public function pop(int $tick) : Generator{
		if(isset($this->tick_vector_map[$tick])){
			$map = $this->tick_vector_map[$tick];
			unset($this->tick_vector_map[$tick]);
			foreach($map as $hash => $entry){
				unset($this->vector_tick_map[$hash]);
				yield $entry;
			}
		}
	}
}<?php

declare(strict_types=1);

namespace redstone\event;

use pocketmine\block\Block;
use pocketmine\event\block\BlockEvent;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use redstone\block\Piston;
use redstone\block\tile\piston\PistonMoveInfo;

final class PistonPullBlockEvent extends BlockEvent implements Cancellable{
	use CancellableTrait;

	/**
	 * @param Piston $piston
	 * @param Block $block
	 * @param list<PistonMoveInfo> $movements
	 */
	public function __construct(
		readonly public Piston $piston,
		Block $block,
		readonly public array $movements = []
	){
		parent::__construct($block);
	}
}<?php

declare(strict_types=1);

namespace redstone\event;

use pocketmine\block\Block;
use pocketmine\event\block\BlockEvent;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\math\Vector3;
use redstone\block\Piston;
use redstone\block\tile\piston\PistonMoveInfo;

final class PistonPushBlockEvent extends BlockEvent implements Cancellable{
	use CancellableTrait;

	/**
	 * @param Piston $piston
	 * @param Vector3 $arm_pos
	 * @param list<PistonMoveInfo> $movements
	 */
	public function __construct(
		readonly public Piston $piston,
		readonly public Vector3 $arm_pos,
		readonly public array $movements
	){
		parent::__construct($this->piston);
	}
}<?php

declare(strict_types=1);

namespace redstone;

use pocketmine\crafting\ShapedRecipe;
use pocketmine\crafting\ShapelessRecipe;
use pocketmine\event\block\BlockBreakEvent;
use pocketmine\event\entity\ItemSpawnEvent;
use pocketmine\event\Listener;
use pocketmine\item\ItemTypeIds;
use pocketmine\plugin\PluginBase;
use redstone\block\RedstoneBlockFactory;
use redstone\block\tile\RedstoneTileFactory;
use redstone\inventory\RedstoneInventoryManager;
use redstone\vanilla\ExtraVanillaData;
use redstone\vanilla\ExtraVanillaItems;
use redstone\world\RedstoneWorldManager;
use ReflectionProperty;

final class RedstoneModule implements Listener{

	public function __construct(
		private readonly PluginBase $plugin
	){
	}

	public function load() : void{
		ExtraVanillaData::registerOnAllThreads($this->plugin->getServer()->getAsyncPool());
		RedstoneBlockFactory::init();
		RedstoneTileFactory::init();
	}

	public function enable() : void{
		new RedstoneInventoryManager($this->plugin);
		new RedstoneWorldManager($this->plugin);

		$this->plugin->getServer()->getPluginManager()->registerEvents($this, $this->plugin);

		$crafting_manager = $this->plugin->getServer()->getCraftingManager();
		foreach($crafting_manager->getCraftingRecipeIndex() as $recipe){
			if($recipe instanceof ShapedRecipe || $recipe instanceof ShapelessRecipe){
				$results = $recipe->getResults();
				$changed = false;
				foreach($results as $index => $result){
					if($result->getTypeId() === ItemTypeIds::REDSTONE_DUST){
						$results[$index] = ExtraVanillaItems::REDSTONE_DUST()->setCount($result->getCount());
						$changed = true;
					}
				}
				if($changed){
					(new ReflectionProperty($recipe, "results"))->setValue($recipe, $results);
				}
			}
		}
	}

	/**
	 * @param BlockBreakEvent $event
	 * @priority LOWEST
	 */
	public function handleBlockBreak(BlockBreakEvent $event) : void{
		$drops = $event->getDrops();
		$changed = false;
		foreach($drops as $index => $drop){
			if($drop->getTypeId() === ItemTypeIds::REDSTONE_DUST){
				$drops[$index] = ExtraVanillaItems::REDSTONE_DUST()->setCount($drop->getCount());
				$changed = true;
			}
		}
		if($changed){
			$event->setDrops($drops);
		}
	}

	/**
	 * @param ItemSpawnEvent $event
	 * @priority LOWEST
	 */
	public function handleItemSpawn(ItemSpawnEvent $event) : void{
		$entity = $event->getEntity();
		$item = $entity->getItem();
		if($item->getTypeId() === ItemTypeIds::REDSTONE_DUST){
			$pos = $entity->getLocation();
			$mot = $entity->getMotion();
			$entity->flagForDespawn();
			$pos->world->dropItem($pos, ExtraVanillaItems::REDSTONE_DUST()->setCount($item->getCount()), $mot);
		}
	}
}
<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\Block;
use pocketmine\block\Opaque;
use pocketmine\block\utils\PoweredByRedstoneTrait;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use redstone\block\power\Powerable;
use redstone\block\power\PowerableTrait;
use redstone\block\tile\dispenser\Dispenser as DispenserTile;
use function abs;

class Dispenser extends Opaque implements Powerable{
	use OptimizedBlockTrait;
	use PowerableTrait;
	use PoweredByRedstoneTrait;

	protected int $facing = Facing::DOWN;
	protected int $activation_delay = 0;
	protected int $deactivation_delay = 1;
	protected bool $requires_strong_power = false;

	protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{
		$w->facing($this->facing);
		$w->bool($this->powered);
	}

	public function setFacing(int $facing) : self{
		$this->facing = $facing;
		return $this;
	}

	public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
		if($player !== null){
			if(abs($player->getPosition()->x - $this->position->x) < 2 && abs($player->getPosition()->z - $this->position->z) < 2){
				$y = $player->getEyePos()->y;

				if($y - $this->position->y > 2){
					$this->facing = Facing::UP;
				}elseif($this->position->y - $y > 0){
					$this->facing = Facing::DOWN;
				}else{
					$this->facing = Facing::opposite($player->getHorizontalFacing());
				}
			}else{
				$this->facing = Facing::opposite($player->getHorizontalFacing());
			}
		}

		return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
	}

	public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
		if($player instanceof Player){
			$tile = $this->position->world->getTileAt($this->position->x, $this->position->y, $this->position->z);
			if($tile instanceof DispenserTile){
				$player->setCurrentWindow($tile->getInventory());
			}
		}
		return true;
	}

	public function acceptsPowerFromSide(int $side) : bool{
		return $side !== $this->facing;
	}

	public function getFacing() : int{
		return $this->facing;
	}

	protected function onReceivePower(int $power) : void{
		$powered = $power > 0;
		if($powered !== $this->powered){
			$this->powered = $powered;
			$this->position->world->setBlockAt($this->position->x, $this->position->y, $this->position->z, $this, false);
			if($powered){
				$tile = $this->position->world->getTileAt($this->position->x, $this->position->y, $this->position->z);
				if($tile instanceof DispenserTile){
					$tile->onPower($this->facing);
				}
			}
		}
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\WeightedPressurePlateLight as VanillaWeightedPressurePlateLight;
use redstone\block\power\PowerSource;

class WeightedPressurePlateLight extends VanillaWeightedPressurePlateLight implements PowerSource{
	use OptimizedBlockTrait;
	use PressurePlateTrait;

	public function isPressed() : bool{
		return $this->signalStrength > 0;
	}

	public function setPressed(bool $value) : self{
		$this->signalStrength = $value ? 15 : 0;
		return $this;
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use redstone\block\power\PowerableDoorTrait;
use redstone\block\power\Powerable;

final class WoodenDoor extends \pocketmine\block\WoodenDoor implements Powerable{
	use PowerableDoorTrait;
}<?php

declare(strict_types=1);

namespace redstone\block;

use Generator;
use pocketmine\block\Block;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use redstone\block\power\Transmittable;
use redstone\block\utils\HelperUtils;

trait ButtonTrait{

	public function getPowerLevel() : int{
		return $this->pressed ? 15 : 0;
	}

	public function getOutputPowerLevel() : int{
		return $this->getPowerLevel();
	}

	public function canPower(int $side) : bool{
		return true;
	}

	/**
	 * @return Generator<Transmittable>
	 */
	protected function getRelyingOnSupportBlocks() : Generator{
		$supporting_side = $this->getSupportingSide();
		$supporting = HelperUtils::getBlockAtSide($this->position, $supporting_side);
		$skip_side = Facing::opposite($supporting_side);
		foreach(Facing::ALL as $side){
			if($side !== $skip_side){
				$block = HelperUtils::getBlockAtSide($supporting->position, $side);
				if($block instanceof Transmittable){
					yield $block;
				}
			}
		}
	}

	/**
	 * @return Generator<Transmittable>
	 */
	protected function getRelyingBlocks() : Generator{
		yield from $this->getRelyingOnSupportBlocks();
		$skip_side = $this->getSupportingSide();
		foreach(Facing::HORIZONTAL as $side){
			if($side !== $skip_side){
				$block = HelperUtils::getBlockAtSide($this->position, $side);
				if($block instanceof Transmittable){
					yield $block;
				}
			}
		}
	}

	public function getSupportingSide() : int{
		return Facing::opposite($this->facing);
	}

	public function canStronglyPower(int $side) : bool{
		return $side === $this->getSupportingSide();
	}

	public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
		return $blockClicked->isSolid() && parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
	}

	public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{
		if($this->pressed){
			$this->pressed = false;
			foreach($this->getRelyingOnSupportBlocks() as $block){
				$block->power($this);
			}
		}

		return parent::onBreak($item, $player, $returnedItems);
	}

	public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
		$previous_state = $this->pressed;
		if(parent::onInteract($item, $face, $clickVector, $player, $returnedItems)){
			if($this->pressed !== $previous_state && $this->pressed){
				foreach($this->getRelyingBlocks() as $side){
					$side->power($this);
				}
			}
			return true;
		}
		return false;
	}

	public function onNearbyBlockChange() : void{
		if(!HelperUtils::getBlockAtSide($this->position, Facing::opposite($this->facing))->isSolid()){
			$this->position->world->useBreakOn($this->position);
		}else{
			parent::onNearbyBlockChange();
		}
	}

	public function onScheduledUpdate() : void{
		$previous_state = $this->pressed;
		parent::onScheduledUpdate();
		if($this->pressed !== $previous_state){
			foreach($this->getRelyingBlocks() as $side){
				$side->power($this);
			}
		}
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\StoneButton as VanillaStoneButton;
use redstone\block\power\PowerSource;

class StoneButton extends VanillaStoneButton implements PowerSource{
	use ButtonTrait;
	use OptimizedBlockTrait;
}<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\item\Item;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use redstone\block\power\Powerable;
use redstone\block\power\PowerableOpenableTrait;

final class Trapdoor extends \pocketmine\block\Trapdoor implements Powerable{
	use OptimizedBlockTrait;
	use PowerableOpenableTrait;

	public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
		return false;
	}
}
<?php

declare(strict_types=1);

namespace redstone\block\utils;

use redstone\world\BlockData;
use function array_shift;

final class RedstoneTorchBlockData implements BlockData{

	/** @var list<int> */
	public array $counters = [];

	public function __construct(){
	}

	public function count(int $tick) : void{
		$this->counters[] = $tick;
		if(count($this->counters) > 9){
			array_shift($this->counters);
		}
	}

	public function isBurntOut(int $tick) : bool{
		return count($this->counters) >= 8 && $tick - $this->counters[0] <= 60;
	}
}<?php

declare(strict_types=1);

namespace redstone\block\utils;

use Generator;
use pocketmine\block\Block;
use pocketmine\block\tile\Tile;
use pocketmine\block\tile\TileFactory;
use pocketmine\block\VanillaBlocks;
use pocketmine\inventory\BaseInventory;
use pocketmine\inventory\Inventory;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\world\Position;
use pocketmine\world\World;
use function count;

final class HelperUtils{

	/** @var array<Facing::DOWN|Facing::UP|Facing::NORTH|Facing::SOUTH|Facing::WEST|Facing::EAST, Vector3> */
	public static array $side_offsets;

	public static function init() : void{
		self::$side_offsets = [];
		foreach(Facing::ALL as $side){
			self::$side_offsets[$side] = Vector3::zero()->getSide($side);
		}
	}

	public static function getBlockAtSide(Position $position, int $side, int $step = 1) : Block{
		$offset = self::$side_offsets[$side];
		return $position->world->getBlockAt(
			$position->x + ($offset->x * $step),
			$position->y + ($offset->y * $step),
			$position->z + ($offset->z * $step)
		);
	}

	/**
	 * @template T
	 * @param list<T> $array
	 * @return Generator<T>
	 */
	public static function reverse(array $array) : Generator{
		for($i = count($array) - 1; $i >= 0; $i--){
			yield $array[$i];
		}
	}

	/**
	 * @param Inventory $inventory
	 * @param bool $include_empty
	 * @return array<int, Item>
	 */
	public static function fastReadOnlyInventoryContents(Inventory $inventory, bool $include_empty = false) : array{
		static $_inventory_slots = null;
		if($_inventory_slots === null){
			$air = VanillaItems::AIR();
			$_inventory_slots = (static function($inventory, bool $include_empty) use($air){
				$contents = [];
				foreach($inventory->slots as $index => $slot){
					if($slot !== null){
						$contents[$index] = $slot;
					}elseif($include_empty){
						$contents[$index] = $air;
					}
				}
				return $contents;
			})->bindTo(null, BaseInventory::class);
		}
		return $_inventory_slots($inventory, $include_empty);
	}

	public static function moveBlockAndTile(World $world, Vector3 $source, Vector3 $destination, bool $update = true) : void{
		$block = $world->getBlockAt($source->x, $source->y, $source->z);
		$tile_nbt = $world->getTileAt($source->x, $source->y, $source->z)?->saveNBT();
		$world->setBlockAt($source->x, $source->y, $source->z, VanillaBlocks::AIR(), $update);
		if($tile_nbt !== null){
			$world->setBlockAt($destination->x, $destination->y, $destination->z, $block, $update);
			$existing_tile = $world->getTileAt($destination->x, $destination->y, $destination->z);
			if($existing_tile !== null){
				$world->removeTile($existing_tile);
			}
			$tile_nbt->setInt(Tile::TAG_X, $destination->x);
			$tile_nbt->setInt(Tile::TAG_Y, $destination->y);
			$tile_nbt->setInt(Tile::TAG_Z, $destination->z);
			$world->addTile(TileFactory::getInstance()->createFromData($world, $tile_nbt));
		}else{
			$world->useBreakOn($destination);
			$world->setBlockAt($destination->x, $destination->y, $destination->z, $block, $update);
		}
	}
}<?php

declare(strict_types=1);

namespace redstone\block\utils;

use redstone\world\BlockData;

final class RedstoneRepeaterBlockData implements BlockData{

	public const OPERATION_SWITCH_ON = 0;
	public const OPERATION_DISTRIBUTE = 1;
	public const OPERATION_SWITCH_RECALCULATE = 2;

	/**
	 * @param self::OPERATION_* $operation
	 */
	public function __construct(
		public int $operation
	){}
}<?php

declare(strict_types=1);

namespace redstone\block\utils;

use Closure;
use pocketmine\block\Block;
use pocketmine\block\GlazedTerracotta;
use pocketmine\block\HoneyBlock;
use pocketmine\block\Slime;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\world\World;
use function array_merge;
use function array_search;
use function array_slice;
use function count;

final class PistonStructureResolver{

	public const PUSH_LIMIT = 12;

	/** @var list<Block> */
	private array $to_push = [];

	/** @var list<Block> */
	private array $to_destroy = [];

	/** @var list<int> */
	private array $to_push_hashes = [];

	private int $push_direction;
	private Vector3 $start_pos;

	/**
	 * @param Closure(Block, Vector3) : bool $can_move
	 */
	public function __construct(
		readonly private World $world,
		readonly private Vector3 $piston_pos,
		int $piston_direction,
		readonly private bool $extending,
		readonly private Closure $can_move
	){
		if($extending){
			$this->push_direction = $piston_direction;
			$this->start_pos = $piston_pos->getSide($piston_direction);
		}else{
			$this->push_direction = Facing::opposite($piston_direction);
			$this->start_pos = $piston_pos->getSide($piston_direction, 2);
		}
	}

	public static function isSticky(Block $block) : bool{
		return $block instanceof Slime || $block instanceof HoneyBlock;
	}

	private static function sticksToPiston(Block $block) : bool{
		return !($block instanceof GlazedTerracotta);
	}

	private static function canStickTogether(Block $sticky, Block $adjacent) : bool{
		if(!self::isSticky($sticky) || !self::sticksToPiston($adjacent)){
			return false;
		}
		if(self::isSticky($adjacent)){
			return $sticky->getTypeId() === $adjacent->getTypeId();
		}
		return true;
	}

	private static function isEmpty(Block $block) : bool{
		return $block->canBeReplaced();
	}

	private static function breaksWhenMoved(Block $block) : bool{
		return $block->canBeFlowedInto();
	}

	private function canPush(Block $block) : bool{
		if(!$this->extending && $block instanceof GlazedTerracotta){
			return false;
		}
		return ($this->can_move)($block, $block->getPosition()->getSide($this->push_direction));
	}

	private function getBlock(Vector3 $pos) : Block{
		return $this->world->getBlockAt((int) $pos->x, (int) $pos->y, (int) $pos->z);
	}

	private function isPiston(Vector3 $pos) : bool{
		return $pos->equals($this->piston_pos);
	}

	private function indexOf(Vector3 $pos) : int{
		$index = array_search(World::blockHash((int) $pos->x, (int) $pos->y, (int) $pos->z), $this->to_push_hashes, true);
		return $index === false ? -1 : $index;
	}

	private function push(Block $block) : void{
		$pos = $block->getPosition();
		$this->to_push[] = $block;
		$this->to_push_hashes[] = World::blockHash((int) $pos->x, (int) $pos->y, (int) $pos->z);
	}

	public function resolve() : bool{
		$this->to_push = [];
		$this->to_push_hashes = [];
		$this->to_destroy = [];

		$start = $this->getBlock($this->start_pos);
		if(self::isEmpty($start)){
			return true;
		}
		if(self::breaksWhenMoved($start)){
			if($this->extending){
				$this->to_destroy[] = $start;
			}
			return true;
		}
		if(!$this->canPush($start)){
			return false;
		}
		if(!$this->addBlockLine($this->start_pos)){
			return false;
		}
		for($i = 0; $i < count($this->to_push); $i++){
			$block = $this->to_push[$i];
			if(self::isSticky($block) && !$this->addBranchingBlocks($block)){
				return false;
			}
		}
		return true;
	}

	private function addBlockLine(Vector3 $origin) : bool{
		$block = $this->getBlock($origin);
		if(self::isEmpty($block) || self::breaksWhenMoved($block)){
			return true;
		}
		if(!$this->canPush($block) || $this->isPiston($origin) || $this->indexOf($origin) > -1){
			return true;
		}

		$backward = Facing::opposite($this->push_direction);
		$count = 1;
		if($count + count($this->to_push) > self::PUSH_LIMIT){
			return false;
		}
		while(self::isSticky($block)){
			$pos = $origin->getSide($backward, $count);
			$previous = $block;
			$block = $this->getBlock($pos);
			if(self::isEmpty($block) || self::breaksWhenMoved($block) || !self::canStickTogether($previous, $block) || !$this->canPush($block) || $this->isPiston($pos)){
				break;
			}
			$count++;
			if($count + count($this->to_push) > self::PUSH_LIMIT){
				return false;
			}
		}

		$added = 0;
		for($k = $count - 1; $k >= 0; $k--){
			$this->push($this->getBlock($origin->getSide($backward, $k)));
			$added++;
		}

		$step = 1;
		while(true){
			$pos = $origin->getSide($this->push_direction, $step);
			$index = $this->indexOf($pos);
			if($index > -1){
				$this->reorderListAtCollision($added, $index);
				for($i = 0; $i <= $index + $added; $i++){
					$block = $this->to_push[$i];
					if(self::isSticky($block) && !$this->addBranchingBlocks($block)){
						return false;
					}
				}
				return true;
			}

			$block = $this->getBlock($pos);
			if(self::isEmpty($block)){
				return true;
			}
			if($this->isPiston($pos)){
				return false;
			}
			if(self::breaksWhenMoved($block)){
				$this->to_destroy[] = $block;
				return true;
			}
			if(!$this->canPush($block)){
				return false;
			}
			if(count($this->to_push) >= self::PUSH_LIMIT){
				return false;
			}
			$this->push($block);
			$added++;
			$step++;
		}
	}

	private function reorderListAtCollision(int $count, int $index) : void{
		$size = count($this->to_push);
		$this->to_push = array_merge(
			array_slice($this->to_push, 0, $index),
			array_slice($this->to_push, $size - $count, $count),
			array_slice($this->to_push, $index, $size - $count - $index)
		);
		$this->to_push_hashes = array_merge(
			array_slice($this->to_push_hashes, 0, $index),
			array_slice($this->to_push_hashes, $size - $count, $count),
			array_slice($this->to_push_hashes, $index, $size - $count - $index)
		);
	}

	private function addBranchingBlocks(Block $sticky) : bool{
		$push_axis = Facing::axis($this->push_direction);
		$pos = $sticky->getPosition();
		foreach(Facing::ALL as $side){
			if(Facing::axis($side) === $push_axis){
				continue;
			}
			$neighbour_pos = $pos->getSide($side);
			$neighbour = $this->getBlock($neighbour_pos);
			if(self::canStickTogether($sticky, $neighbour) && !$this->addBlockLine($neighbour_pos)){
				return false;
			}
		}
		return true;
	}

	/**
	 * @return list<Block>
	 */
	public function getToPush() : array{
		return $this->to_push;
	}

	/**
	 * @return list<Block>
	 */
	public function getToDestroy() : array{
		return $this->to_destroy;
	}

	public function getPushDirection() : int{
		return $this->push_direction;
	}
}
<?php

declare(strict_types=1);

namespace redstone\block\utils;

use redstone\world\BlockData;

final class PoweredBlockData implements BlockData{

	public function __construct(
		public bool $powered
	){}
}
<?php

declare(strict_types=1);

namespace redstone\block\tile;

use pocketmine\block\tile\Tile;
use pocketmine\block\tile\TileFactory as VanillaTileFactory;
use redstone\block\tile\comparator\ComparatorWeightRegistry;
use redstone\block\tile\dispenser\DispensableItemManager;
use redstone\block\tile\dispenser\Dispenser;
use redstone\block\tile\dispenser\Dropper;
use redstone\block\tile\piston\PistonArm;
use ReflectionProperty;
use function array_keys;

final class RedstoneTileFactory{

	public static function init() : void{
		self::register(Dispenser::class, ["Dispenser", "minecraft:dispenser"]);
		self::register(Dropper::class, ["Dropper", "minecraft:dropper"]);
		self::register(PistonArm::class, ["PistonArm", "minecraft:piston_arm"]);
		DispensableItemManager::init();
		ComparatorWeightRegistry::init();
	}

	/**
	 * @param class-string<Tile> $className
	 * @param string[] $saveNames
	 */
	private static function register(string $className, array $saveNames) : void{
		VanillaTileFactory::getInstance()->register($className, $saveNames);
	}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\piston;

use pocketmine\block\Block;
use pocketmine\block\tile\Spawnable;
use pocketmine\data\bedrock\block\BlockStateNames;
use pocketmine\math\Facing;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\IntTag;
use redstone\block\Piston;
use function assert;

class PistonArm extends Spawnable{

	private const TAG_STATE_INTERNAL = "_state";
	private const TAG_STATE = "State";
	private const TAG_PROGRESS = "Progress";
	private const TAG_STICKY = "Sticky";

	public const BE_STATE_UNEXTENDED = 0;
	public const BE_STATE_PUSHING = 1;
	public const BE_STATE_EXTENDED = 2;
	public const BE_STATE_PULLING = 3;

	public bool $sticky = false;

	/** @var Piston::STATE_* */
	public int $state = Piston::STATE_RETRACT_IDLE;

	protected function addAdditionalSpawnData(CompoundTag $nbt) : void{
		$nbt->setByte(self::TAG_STICKY, $this->sticky ? 1 : 0);
		$nbt->setFloat(self::TAG_PROGRESS, match($this->state){
			Piston::STATE_RETRACT_IDLE => 0.0,
			Piston::STATE_CONTRACT_BEGIN, Piston::STATE_RETRACT_BEGIN, Piston::STATE_RETRACT_WAITING => 0.5,
			Piston::STATE_CONTRACT_IDLE => 1.0
		});
		$nbt->setByte(self::TAG_STATE, match($this->state){
			Piston::STATE_RETRACT_IDLE => self::BE_STATE_UNEXTENDED,
			Piston::STATE_CONTRACT_IDLE => self::BE_STATE_EXTENDED,
			Piston::STATE_CONTRACT_BEGIN, Piston::STATE_RETRACT_BEGIN, Piston::STATE_RETRACT_WAITING => self::BE_STATE_PUSHING
		});
	}

	public function readSaveData(CompoundTag $nbt) : void{
		$this->sticky = (bool) $nbt->getByte(self::TAG_STICKY, $this->sticky ? 1 : 0);
		$progress_tag = $nbt->getTag(self::TAG_PROGRESS);
		if($progress_tag !== null){
			$this->state = match($progress_tag->getValue()){
				0.0 => Piston::STATE_RETRACT_IDLE,
				0.5 => Piston::STATE_CONTRACT_BEGIN,
				1.0 => Piston::STATE_CONTRACT_IDLE,
			};
		}else{
			$this->state = $nbt->getByte(self::TAG_STATE_INTERNAL, $this->state);
		}
	}

	protected function writeSaveData(CompoundTag $nbt) : void{
		$nbt->setByte(self::TAG_STATE_INTERNAL, $this->state);
		$nbt->setByte(self::TAG_STICKY, $this->sticky ? 1 : 0);
	}

	public function getRenderUpdateBugWorkaroundStateProperties(Block $block) : array{
		assert($block instanceof Piston);
		return [BlockStateNames::FACING_DIRECTION => new IntTag(Facing::opposite($block->getFacing()))];
	}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\piston;

use pocketmine\block\Block;
use pocketmine\block\tile\Tile;
use pocketmine\math\Vector3;

final class PistonMoveInfo{

	public function __construct(
		readonly public Block $block,
		readonly public ?Tile $tile,
		readonly public Vector3 $from,
		readonly public Vector3 $to
	){}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\comparator;

use Closure;
use pocketmine\block\Block;
use redstone\block\Redstone;
use redstone\block\RedstoneComparator;
use redstone\block\RedstoneRepeater;
use redstone\block\RedstoneWire;
use redstone\vanilla\ExtraVanillaBlocks;

final class ComparatorWeightRegistry{

	/** @var Closure[] */
	private static array $computers = [];

	public static function init() : void{
		$block = ExtraVanillaBlocks::REDSTONE_WIRE();
		self::register($block, static function(RedstoneWire $block) : int{ return $block->getPowerLevel(); });

		$block = ExtraVanillaBlocks::REDSTONE();
		self::register($block, static function(Redstone $block) : int{ return $block->getPowerLevel(); });

		$block = ExtraVanillaBlocks::REDSTONE_REPEATER();
		self::register($block, static function(RedstoneRepeater $block) : int{ return $block->getPowerLevel(); });

		$block = ExtraVanillaBlocks::REDSTONE_COMPARATOR();
		self::register($block, static function(RedstoneComparator $block) : int{ return $block->getPowerLevel(); });
	}

	/**
	 * @template TBlock of Block
	 * @param TBlock $block
	 * @param Closure(TBlock) : int $computer
	 */
	public static function register(Block $block, Closure $computer) : void{
		self::$computers[$block->getTypeId()] = $computer;
	}

	/**
	 * @param Block $block
	 * @return (Closure(Block) : int)|null $computer
	 */
	public static function get(Block $block) : ?Closure{
		return self::$computers[$block->getTypeId()] ?? null;
	}

	public static function getValue(Block $block) : int{
		return isset(self::$computers[$id = $block->getTypeId()]) ? (self::$computers[$id])($block) : 0;
	}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\comparator;

use pocketmine\block\inventory\BlockInventory;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\InventoryListener;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use redstone\block\RedstoneComparator;

final class ComparatorInventoryListener implements InventoryListener{

	public static function instance(int $facing) : self{
		static $instances = [];;
		return $instances[$facing] ??= new self(Vector3::zero()->getSide(Facing::opposite($facing)));
	}

	private function __construct(
		readonly public Vector3 $facing
	){}

	public function update(Inventory $inventory) : void{
		if(!($inventory instanceof BlockInventory)){
			$inventory->getListeners()->remove($this);
			return;
		}
		$holder = $inventory->getHolder();
		if($holder->world === null){
			$inventory->getListeners()->remove($this);
			return;
		}
		$comparator = $holder->world->getBlockAt($holder->x + $this->facing->x, $holder->y + $this->facing->y, $holder->z + $this->facing->z);
		if(!($comparator instanceof RedstoneComparator)){
			$inventory->getListeners()->remove($this);
			return;
		}
		$comparator->onContainerInputChange();
	}

	public function onSlotChange(Inventory $inventory, int $slot, Item $oldItem) : void{
		$this->update($inventory);
	}

	public function onContentChange(Inventory $inventory, array $oldContents) : void{
		$this->update($inventory);
	}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\dispenser;

use pocketmine\inventory\Inventory;
use pocketmine\item\LiquidBucket;
use pocketmine\item\VanillaItems;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\Position;

class LiquidBucketDispensableItem extends DropDispensableItem{

	public function dispense(Position $pos, Inventory $inventory, int $slot, Vector3 $side_pos, int $facing, ?Player $player = null) : bool{
		$world = $pos->getWorld();
		if($world->getBlockAt($side_pos->x, $side_pos->y, $side_pos->z)->canBeReplaced()){
			/** @var LiquidBucket $item */
			$item = $inventory->getItem($slot);

			$world->setBlockAt($side_pos->x, $side_pos->y, $side_pos->z, $item->getLiquid());
			$inventory->setItem($slot, VanillaItems::BUCKET()->setCount($item->getCount()));
			return true;
		}

		return parent::dispense($pos, $inventory, $slot, $side_pos, $facing, $player);
	}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\dispenser;

use pocketmine\block\tile\Container;
use pocketmine\block\tile\ContainerTrait;
use pocketmine\block\tile\Nameable;
use pocketmine\block\tile\NameableTrait;
use pocketmine\block\tile\Spawnable;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
use pocketmine\network\mcpe\protocol\types\LevelEvent;
use pocketmine\player\Player;
use pocketmine\world\World;
use redstone\inventory\DispenserInventory;

class Dispenser extends Spawnable implements Container, Nameable{
	use ContainerTrait;
	use NameableTrait {
		addAdditionalSpawnData as addNameSpawnData;
	}

	private ?DispenserInventory $inventory;

	public function __construct(World $world, Vector3 $pos){
		parent::__construct($world, $pos);
		$this->inventory = $this->createInventory();
	}

	protected function createInventory() : DispenserInventory{
		return new DispenserInventory($this->position);
	}

	public function readSaveData(CompoundTag $nbt) : void{
		$this->loadName($nbt);
		$this->loadItems($nbt);
	}

	protected function writeSaveData(CompoundTag $nbt) : void{
		$this->saveName($nbt);
		$this->saveItems($nbt);
	}

	public function close() : void{
		if(!$this->closed){
			$this->inventory->removeAllViewers();
			$this->inventory = null;
			parent::close();
		}
	}

	public function getInventory() : DispenserInventory{
		return $this->inventory;
	}

	public function getRealInventory() : DispenserInventory{
		return $this->inventory;
	}

	public function getDefaultName() : string{
		return "Dispenser";
	}

	public function onPower(int $facing, ?Player $player = null) : void{
		static $faces = [
			0 => 40,
			1 => 13,
			2 => 19,
			3 => 7,
			4 => 21,
			5 => 5,
		];

		$contents = $this->inventory->getContents();
		if(count($contents) === 0){
			$this->position->world->broadcastPacketToViewers($this->position, LevelEventPacket::create(LevelEvent::SOUND_CLICK_FAIL, 0, $this->position));
			return;
		}

		/** @var int $slot */
		$slot = array_rand($contents);
		if(!$this->dispenseSlot($slot, $facing, $player)){
			$this->position->world->broadcastPacketToViewers($this->position, LevelEventPacket::create(LevelEvent::SOUND_CLICK_FAIL, 0, $this->position));
			return;
		}

		$pos = $this->position->getSide($facing)->add(0.5, $facing === 0 ? -1 : ($facing === 1 ? 1 : 0.5), 0.5);
		$this->position->world->broadcastPacketToViewers($this->position, LevelEventPacket::create(LevelEvent::SOUND_CLICK, 0, $this->position));
		$this->position->world->broadcastPacketToViewers($pos, LevelEventPacket::create(LevelEvent::PARTICLE_SHOOT, $faces[$facing], $pos));
	}

	protected function dispenseSlot(int $slot, int $facing, ?Player $player) : bool{
		return DispensableItemManager::get($this->inventory->getItem($slot))->dispense($this->position, $this->inventory, $slot, $this->position->getSide($facing), $facing, $player);
	}

	protected function addAdditionalSpawnData(CompoundTag $nbt) : void{
		$this->addNameSpawnData($nbt);
	}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\dispenser;

use pocketmine\block\Liquid;
use pocketmine\block\VanillaBlocks;
use pocketmine\inventory\Inventory;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\Position;

class BucketDispensableItem extends DropDispensableItem{

	public function dispense(Position $pos, Inventory $inventory, int $slot, Vector3 $side_pos, int $facing, ?Player $player = null) : bool{
		$world = $pos->getWorld();
		$block = $world->getBlockAt($side_pos->x, $side_pos->y, $side_pos->z);
		if($block instanceof Liquid && $block->isSource()){
			$world->setBlockAt($side_pos->x, $side_pos->y, $side_pos->z, VanillaBlocks::AIR());
			$item = $inventory->getItem($slot);
			$item->pop();
			$inventory->setItem($slot, $item);
			foreach($inventory->addItem($block->getFlowingForm()->asItem()) as $drop){
				$world->dropItem($side_pos->add(0.3, 0.3, 0.3), $drop);
			}
			return true;
		}

		return parent::dispense($pos, $inventory, $slot, $side_pos, $facing, $player);
	}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\dispenser;

use pocketmine\inventory\Inventory;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\Position;

interface DispensableItem{

	/**
	 * Returns whether the item was dispensed in some way (used to determine which sound
	 * and particle to play).
	 *
	 * @param Position $pos
	 * @param Inventory $inventory
	 * @param int $slot
	 * @param Vector3 $side_pos
	 * @param int $facing
	 * @param Player|null $player
	 * @return bool
	 */
	public function dispense(Position $pos, Inventory $inventory, int $slot, Vector3 $side_pos, int $facing, ?Player $player = null) : bool;
}<?php

declare(strict_types=1);

namespace redstone\block\tile\dispenser;

use InvalidArgumentException;
use pocketmine\entity\Living;
use pocketmine\inventory\ArmorInventory;
use pocketmine\inventory\Inventory;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\Position;

class ArmorDispensableItem extends DropDispensableItem{

	protected int $slot;

	public function __construct(int $slot){
		if(
			$slot !== ArmorInventory::SLOT_HEAD &&
			$slot !== ArmorInventory::SLOT_CHEST &&
			$slot !== ArmorInventory::SLOT_LEGS &&
			$slot !== ArmorInventory::SLOT_FEET
		){
			throw new InvalidArgumentException("Invalid armor inventory slot: {$slot}");
		}

		$this->slot = $slot;
	}

	public function dispense(Position $pos, Inventory $inventory, int $slot, Vector3 $side_pos, int $facing, ?Player $player = null) : bool{
		$item = $inventory->getItem($slot);
		$world = $pos->getWorld();
		foreach($world->getNearbyEntities(AxisAlignedBB::one()->offset($side_pos->x, $side_pos->y, $side_pos->z)) as $entity){
			if($entity instanceof Living){
				$armor_inventory = $entity->getArmorInventory();
				if($armor_inventory->isSlotEmpty($this->slot)){
					$armor_inventory->setItem($this->slot, $item->pop());
					$inventory->setItem($slot, $item);
					return true;
				}
			}
		}

		return parent::dispense($pos, $inventory, $slot, $side_pos, $facing, $player);
	}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\dispenser;

use pocketmine\entity\Entity;
use pocketmine\item\Item;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\World;

class ProjectileDispensableItem extends EntityDispensableItem{

	protected function onEntityCreate(Entity $entity, Vector3 $side_pos, World $world, int $facing, Item $item, ?Player $player = null) : void{
		$entity->setMotion((new Vector3(0.0, 0.0, 0.0))->getSide($facing));
	}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\dispenser;

use pocketmine\inventory\Inventory;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\Position;

class DropDispensableItem implements DispensableItem{

	public function dispense(Position $pos, Inventory $inventory, int $slot, Vector3 $side_pos, int $facing, ?Player $player = null) : bool{
		$item = $inventory->getItem($slot);
		$pos->getWorld()->dropItem($side_pos->add(0.3, 0.3, 0.3), $item->pop(), (new Vector3(0.0, 0.0, 0.0))->getSide($facing)->multiply(0.4));
		$inventory->setItem($slot, $item);
		return true;
	}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\dispenser;

use pocketmine\block\VanillaBlocks;
use pocketmine\entity\Location;
use pocketmine\entity\object\PrimedTNT;
use pocketmine\entity\projectile\Arrow;
use pocketmine\entity\projectile\Egg;
use pocketmine\entity\projectile\Snowball;
use pocketmine\inventory\ArmorInventory;
use pocketmine\item\Armor;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use pocketmine\player\Player;

final class DispensableItemManager{

	private static DispensableItem $default;

	/** @var DispensableItem[] */
	private static array $items = [];

	public static function init() : void{
		self::register(VanillaItems::ARROW(), new ProjectileDispensableItem(static function(Location $location, Item $item, ?Player $player) : Arrow{ return new Arrow($location, $player, false); }));
		self::register(VanillaItems::EGG(), new ProjectileDispensableItem(static function(Location $location, Item $item, ?Player $player) : Egg{ return new Egg($location, $player); }));
		self::register(VanillaItems::SNOWBALL(), new ProjectileDispensableItem(static function(Location $location, Item $item, ?Player $player) : Snowball{ return new Snowball($location, $player); }));
		self::register(VanillaItems::LAVA_BUCKET(), new LiquidBucketDispensableItem());
		self::register(VanillaItems::WATER_BUCKET(), new LiquidBucketDispensableItem());
		self::register(VanillaItems::BUCKET(), new BucketDispensableItem());
		self::register(VanillaBlocks::TNT()->asItem(), new EntityDispensableItem(static function(Location $location, Item $item, ?Player $player) : PrimedTNT{ return new PrimedTNT($location); }));

		self::register(VanillaBlocks::MOB_HEAD()->asItem(), new ArmorDispensableItem(ArmorInventory::SLOT_HEAD));
		self::register(VanillaBlocks::CARVED_PUMPKIN()->asItem(), new ArmorDispensableItem(ArmorInventory::SLOT_HEAD));
		foreach(VanillaItems::getAll() as $item){
			if($item instanceof Armor){
				self::register($item, new ArmorDispensableItem($item->getArmorSlot()));
			}
		}

		self::registerFallback(new DropDispensableItem());
	}

	public static function register(Item $item, DispensableItem $dispensable) : void{
		self::$items[$item->getStateId()] = $dispensable;
	}

	public static function registerFallback(DispensableItem $item) : void{
		self::$default = $item;
	}

	public static function get(Item $item) : DispensableItem{
		return self::$items[$item->getStateId()] ?? self::$default;
	}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\dispenser;

use Closure;
use pocketmine\entity\Entity;
use pocketmine\entity\Location;
use pocketmine\inventory\Inventory;
use pocketmine\item\Item;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\Position;
use pocketmine\world\World;

class EntityDispensableItem implements DispensableItem{

	/**
	 * @param Closure(Location, Item, ?Player) : Entity $entity_creator
	 */
	public function __construct(
		private Closure $entity_creator
	){}

	public function dispense(Position $pos, Inventory $inventory, int $slot, Vector3 $side_pos, int $facing, ?Player $player = null) : bool{
		$world = $pos->getWorld();
		$item = $inventory->getItem($slot);
		$item_removed = $item->pop();
		$inventory->setItem($slot, $item);

		$entity = ($this->entity_creator)(Location::fromObject($side_pos->add(0.5, 0.5, 0.5), $world), $item_removed, $player);
		$this->onEntityCreate($entity, $side_pos, $world, $facing, $item_removed, $player);
		$entity->spawnToAll();
		return true;
	}

	protected function onEntityCreate(Entity $entity, Vector3 $side_pos, World $world, int $facing, Item $item, ?Player $player = null) : void{
	}
}<?php

declare(strict_types=1);

namespace redstone\block\tile\dispenser;

use pocketmine\block\tile\Container;
use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes;
use pocketmine\player\Player;
use redstone\inventory\DispenserInventory;

class Dropper extends Dispenser{

	protected function createInventory() : DispenserInventory{
		return new DispenserInventory($this->position, WindowTypes::DROPPER);
	}

	public function getDefaultName() : string{
		return "Dropper";
	}

	protected function dispenseSlot(int $slot, int $facing, ?Player $player) : bool{
		$side_pos = $this->position->getSide($facing);
		$destination = $this->position->world->getTileAt($side_pos->x, $side_pos->y, $side_pos->z);
		if($destination instanceof Container){
			return $this->insertInto($slot, $destination);
		}
		return (new DropDispensableItem())->dispense($this->position, $this->getInventory(), $slot, $side_pos, $facing, $player);
	}

	private function insertInto(int $slot, Container $destination) : bool{
		$inventory = $this->getInventory();
		$destination_inventory = $destination->getInventory();
		$item = $inventory->getItem($slot);
		$item_to_move = (clone $item)->setCount(1);
		if(!$destination_inventory->canAddItem($item_to_move)){
			return false;
		}

		$item->pop();
		$inventory->setItem($slot, $item);
		$destination_inventory->addItem($item_to_move);
		return true;
	}
}
<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\Hopper as VanillaHopper;
use pocketmine\block\inventory\BrewingStandInventory;
use pocketmine\block\inventory\FurnaceInventory;
use pocketmine\block\Jukebox;
use pocketmine\block\tile\BrewingStand as BrewingStandTile;
use pocketmine\block\tile\Container;
use pocketmine\block\tile\Furnace as FurnaceTile;
use pocketmine\block\tile\Hopper as HopperTile;
use pocketmine\block\tile\Jukebox as JukeboxTile;
use pocketmine\block\tile\Tile;
use pocketmine\entity\object\ItemEntity;
use pocketmine\event\block\BlockItemPickupEvent;
use pocketmine\inventory\Inventory;
use pocketmine\item\Bucket;
use pocketmine\item\GlassBottle;
use pocketmine\item\Item;
use pocketmine\item\Potion;
use pocketmine\item\Record;
use pocketmine\item\SplashPotion;
use pocketmine\item\VanillaItems;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use redstone\block\power\Powerable;
use redstone\block\power\PowerableTrait;
use ReflectionProperty;
use function min;

class Hopper extends VanillaHopper implements Powerable{
	use OptimizedBlockTrait;
	use PowerableTrait;

	public const TRANSFER_COOLDOWN = 8;

	private const BOWL_DEPTH = 6 / 16;

	protected int $activation_delay = 0;
	protected int $deactivation_delay = 0;
	protected bool $requires_strong_power = false;

	protected function onReceivePower(int $power) : void{
		$powered = $power > 0;
		if($powered !== $this->powered){
			$this->powered = $powered;
			$this->position->world->setBlockAt($this->position->x, $this->position->y, $this->position->z, $this, false);
		}
	}

	public function onPostPlace() : void{
		$this->recalculatePowerState();
		$this->position->world->scheduleDelayedBlockUpdate($this->position, 1);
	}

	public function onScheduledUpdate() : void{
		$world = $this->position->world;
		$tile = $world->getTile($this->position);
		if(!($tile instanceof HopperTile)){
			return;
		}

		$world->scheduleDelayedBlockUpdate($this->position, 1);
		if($this->powered){
			return;
		}

		$cooldown = self::getTransferCooldown($tile);
		if($cooldown > 0){
			self::setTransferCooldown($tile, --$cooldown);
			if($cooldown > 0){
				return;
			}
		}

		$inventory = $tile->getInventory();
		$success = $this->push($inventory);
		$origin = $this->getLoadedTile($this->position->getSide(Facing::UP));
		if($origin instanceof Container){
			$success = $this->pull($inventory, $origin->getRealInventory()) || $success;
		}elseif(!($origin instanceof JukeboxTile)){
			$success = $this->pickup($inventory) || $success;
		}
		if($success){
			self::setTransferCooldown($tile, self::TRANSFER_COOLDOWN);
		}
	}

	private function push(Inventory $inventory) : bool{
		$destination = null;
		for($slot = 0, $size = $inventory->getSize(); $slot < $size; $slot++){
			if($inventory->isSlotEmpty($slot)){
				continue;
			}
			if($destination === null && ($destination = $this->getLoadedTile($this->position->getSide($this->getFacing()))) === null){
				return false;
			}

			$item = $inventory->getItem($slot);
			if($destination instanceof FurnaceTile){
				if($this->getFacing() === Facing::DOWN){
					$furnace_slot = FurnaceInventory::SLOT_INPUT;
				}elseif($item->getFuelTime() > 0){
					$furnace_slot = FurnaceInventory::SLOT_FUEL;
				}else{
					continue;
				}
				if($this->transferToSlot($inventory, $slot, $destination->getInventory(), $furnace_slot)){
					return true;
				}
			}elseif($destination instanceof BrewingStandTile){
				$brewing_inventory = $destination->getInventory();
				$brewing_slot = $this->getBrewingStandSlot($brewing_inventory, $item);
				if($brewing_slot !== null && $this->transferToSlot($inventory, $slot, $brewing_inventory, $brewing_slot)){
					return true;
				}
			}elseif($destination instanceof JukeboxTile){
				return $this->pushIntoJukebox($inventory, $slot, $destination);
			}elseif($destination instanceof Container){
				if($this->transferToInventory($inventory, $slot, $destination->getInventory())){
					if($destination instanceof HopperTile && self::getTransferCooldown($destination) === 0){
						self::setTransferCooldown($destination, self::TRANSFER_COOLDOWN);
					}
					return true;
				}
			}else{
				return false;
			}
		}
		return false;
	}

	private function pushIntoJukebox(Inventory $inventory, int $slot, JukeboxTile $destination) : bool{
		$item = $inventory->getItem($slot);
		$jukebox = $destination->getBlock();
		if(!($item instanceof Record) || !($jukebox instanceof Jukebox) || $jukebox->getRecord() !== null){
			return false;
		}

		$record = $item->pop();
		$inventory->setItem($slot, $item);
		$jukebox->insertRecord($record);
		$this->position->world->setBlock($jukebox->getPosition(), $jukebox);
		return true;
	}

	private function pull(Inventory $inventory, Inventory $origin) : bool{
		if($origin instanceof FurnaceInventory){
			if($origin->getFuel() instanceof Bucket){
				return $this->transferToInventory($origin, FurnaceInventory::SLOT_FUEL, $inventory);
			}
			return !$origin->getResult()->isNull() && $this->transferToInventory($origin, FurnaceInventory::SLOT_RESULT, $inventory);
		}

		if($origin instanceof BrewingStandInventory){
			$slots = [BrewingStandInventory::SLOT_BOTTLE_LEFT, BrewingStandInventory::SLOT_BOTTLE_MIDDLE, BrewingStandInventory::SLOT_BOTTLE_RIGHT];
		}else{
			$slots = [];
			for($slot = 0, $size = $origin->getSize(); $slot < $size; $slot++){
				$slots[] = $slot;
			}
		}
		foreach($slots as $slot){
			if(!$origin->isSlotEmpty($slot) && $this->transferToInventory($origin, $slot, $inventory)){
				return true;
			}
		}
		return false;
	}

	private function pickup(Inventory $inventory) : bool{
		$pickup_box = new AxisAlignedBB(
			$this->position->x,
			$this->position->y + 1 - self::BOWL_DEPTH,
			$this->position->z,
			$this->position->x + 1,
			$this->position->y + 1.75,
			$this->position->z + 1
		);

		foreach($this->position->world->getNearbyEntities($pickup_box) as $entity){
			if(!($entity instanceof ItemEntity) || $entity->isClosed() || $entity->isFlaggedForDespawn() || $entity->getPickupDelay() === ItemEntity::NEVER_DESPAWN){
				continue;
			}

			$item = $entity->getItem();
			$ev = new BlockItemPickupEvent($this, $entity, $item, $inventory);
			$ev->call();
			$destination = $ev->getInventory();
			if($ev->isCancelled() || $destination === null){
				continue;
			}

			$picked_up = $ev->getItem();
			$quantity = min($destination->getAddableItemQuantity($picked_up), $item->getCount());
			if($quantity <= 0){
				continue;
			}

			$destination->addItem((clone $picked_up)->setCount($quantity));
			$remaining = $item->getCount() - $quantity;
			if($remaining > 0){
				$entity->setStackSize($remaining);
			}else{
				$entity->flagForDespawn();
			}
			return true;
		}
		return false;
	}

	private function transferToSlot(Inventory $source, int $source_slot, Inventory $destination, int $destination_slot) : bool{
		$item = $source->getItem($source_slot);
		$existing = $destination->getItem($destination_slot);
		$max_stack_size = min($destination->getMaxStackSize(), $item->getMaxStackSize());
		if(!$existing->isNull() && (!$existing->canStackWith($item) || $existing->getCount() >= $max_stack_size)){
			return false;
		}

		$moved = $item->pop();
		$source->setItem($source_slot, $item);
		$destination->setItem($destination_slot, $existing->isNull() ? $moved : $existing->setCount($existing->getCount() + 1));
		return true;
	}

	private function transferToInventory(Inventory $source, int $source_slot, Inventory $destination) : bool{
		$item = $source->getItem($source_slot);
		$moved = (clone $item)->setCount(1);
		if(!$destination->canAddItem($moved)){
			return false;
		}

		$item->pop();
		$source->setItem($source_slot, $item);
		$destination->addItem($moved);
		return true;
	}

	private function getBrewingStandSlot(BrewingStandInventory $inventory, Item $item) : ?int{
		if($this->getFacing() === Facing::DOWN){
			return BrewingStandInventory::SLOT_INGREDIENT;
		}
		if($item->equals(VanillaItems::BLAZE_POWDER(), true, false)){
			return BrewingStandInventory::SLOT_FUEL;
		}
		if(!($item instanceof Potion) && !($item instanceof SplashPotion) && !($item instanceof GlassBottle)){
			return null;
		}
		foreach([BrewingStandInventory::SLOT_BOTTLE_LEFT, BrewingStandInventory::SLOT_BOTTLE_MIDDLE, BrewingStandInventory::SLOT_BOTTLE_RIGHT] as $slot){
			if($inventory->isSlotEmpty($slot)){
				return $slot;
			}
		}
		return null;
	}

	private function getLoadedTile(Vector3 $pos) : ?Tile{
		$world = $this->position->world;
		return $world->isInLoadedTerrain($pos) ? $world->getTile($pos) : null;
	}

	private static function getTransferCooldown(HopperTile $tile) : int{
		return (new ReflectionProperty(HopperTile::class, "transferCooldown"))->getValue($tile);
	}

	private static function setTransferCooldown(HopperTile $tile, int $cooldown) : void{
		(new ReflectionProperty(HopperTile::class, "transferCooldown"))->setValue($tile, $cooldown);
	}
}
<?php

declare(strict_types=1);

namespace redstone\block;

use Generator;
use pocketmine\block\Block;
use pocketmine\block\Flowable;
use pocketmine\block\Glowstone;
use pocketmine\block\RedstoneRepeater as VanillaRedstoneRepeater;
use pocketmine\block\Slab;
use pocketmine\block\utils\SlabType;
use pocketmine\item\Item;
use pocketmine\math\Axis;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use redstone\block\power\PowerSource;
use redstone\block\power\ToggleablePowerSource;
use redstone\block\power\Transmittable;
use redstone\block\power\Waitable;
use redstone\block\utils\HelperUtils;
use redstone\block\utils\RedstoneRepeaterBlockData;
use redstone\world\RedstoneWorld;
use redstone\world\RedstoneWorldManager;
use function assert;

class RedstoneRepeater extends VanillaRedstoneRepeater implements ToggleablePowerSource, Transmittable, Waitable{
	use OptimizedBlockTrait;

	public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
		if($this->canBePlacedUpon(HelperUtils::getBlockAtSide($blockReplace->position, Facing::DOWN))){
			if($player !== null){
				$this->facing = Facing::opposite($player->getHorizontalFacing());
			}

			return Flowable::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
		}

		return false;
	}

	private function canBePlacedUpon(Block $block) : bool{
		return !$block->isTransparent() || ($block instanceof Slab && $block->getSlabType() === SlabType::TOP) || $block instanceof Glowstone;
	}

	public function getPowerLevel() : int{
		return $this->powered ? 15 : 0;
	}

	public function getOutputPowerLevel() : int{
		return $this->getPowerLevel();
	}

	public function canPower(int $side) : bool{
		return $side === Facing::opposite($this->facing);
	}

	public function canStronglyPower(int $side) : bool{
		return $this->canPower($side);
	}

	public function switch(bool $state) : void{
		if($state !== $this->powered){
			$this->position->world->setBlockAt($this->position->x, $this->position->y, $this->position->z, $this->setPowered($state), false);
		}
	}

	public function onNearbyBlockChange() : void{
		if(!$this->canBePlacedUpon(HelperUtils::getBlockAtSide($this->position, Facing::DOWN))){
			$this->position->world->useBreakOn($this->position);
			return;
		}

		if(!$this->isLocked()){
			$world = RedstoneWorldManager::$any->get($this->position->world);
			if($world->getExtraDataAt($this->position->x, $this->position->y, $this->position->z) instanceof RedstoneRepeaterBlockData){
				return;
			}
			$world->setExtraDataAt($this->position->x, $this->position->y, $this->position->z, new RedstoneRepeaterBlockData(RedstoneRepeaterBlockData::OPERATION_SWITCH_RECALCULATE));
			$world->scheduleWaitableUpdate($this, RedstoneWorld::redstoneTicks($this->delay));
		}
	}

	public function isLocked() : bool{
		foreach(Facing::axis($this->facing) === Axis::Z ? [Facing::EAST, Facing::WEST] : [Facing::NORTH, Facing::SOUTH] as $side){
			$repeater = HelperUtils::getBlockAtSide($this->position, $side);
			if($repeater instanceof self && $repeater->isPowered() && $repeater->canPower(Facing::opposite($side))){
				return true;
			}
		}
		return false;
	}

	public function power(PowerSource $source) : void{
		assert($source instanceof Block);
		$world = RedstoneWorldManager::$any->get($this->position->world);
		if($world->getExtraDataAt($this->position->x, $this->position->y, $this->position->z) instanceof RedstoneRepeaterBlockData){
			$world->scheduleWaitableUpdate($this, RedstoneWorld::redstoneTicks($this->delay)); // corner case. override=false is important
			return;
		}
		$powered = $this->recalculatePower($world) > 0;
		if($powered === $this->powered){
			return;
		}
		$world->setExtraDataAt($this->position->x, $this->position->y, $this->position->z, new RedstoneRepeaterBlockData($powered ? RedstoneRepeaterBlockData::OPERATION_SWITCH_ON : RedstoneRepeaterBlockData::OPERATION_SWITCH_RECALCULATE));
		$world->scheduleWaitableUpdate($this, RedstoneWorld::redstoneTicks($this->delay));
	}

	private function recalculatePower(RedstoneWorld $world) : int{
		$behind = HelperUtils::getBlockAtSide($this->position, $this->facing); // uh facing = the block behind repeater apparently?
		return (($behind instanceof PowerSource && $behind->getPowerLevel() > 0) || $world->isStronglyPowered($behind, $opposite_side = Facing::opposite($this->facing), $opposite_side)) ? 15 : 0;
	}

	public function onRedstoneTickReceive() : void{
		$world = RedstoneWorldManager::$any->get($this->position->world);
		$extra_data = $world->getExtraDataAt($this->position->x, $this->position->y, $this->position->z);
		if(!($extra_data instanceof RedstoneRepeaterBlockData)){
			return;
		}
		$this->runRepeaterState($world, $extra_data->operation);
	}

	/**
	 * @param RedstoneWorld $world
	 * @param RedstoneRepeaterBlockData::OPERATION_*|null $state
	 */
	public function runRepeaterState(RedstoneWorld $world, ?int $state) : void{
		if($this->isLocked()){
			return;
		}
		$extra_data = $world->getExtraDataAt($this->position->x, $this->position->y, $this->position->z);
		if(!($extra_data instanceof RedstoneRepeaterBlockData)){
			return;
		}
		while(true){
			if($state === RedstoneRepeaterBlockData::OPERATION_SWITCH_ON){
				if(!$this->powered){
					$this->switch(true);
					$state = RedstoneRepeaterBlockData::OPERATION_DISTRIBUTE;
				}else{
					$state = null;
				}
			}elseif($state === RedstoneRepeaterBlockData::OPERATION_DISTRIBUTE){
				$extra_data->operation = RedstoneRepeaterBlockData::OPERATION_SWITCH_RECALCULATE;
				$world->scheduleWaitableUpdate($this, RedstoneWorld::redstoneTicks($this->delay));
				$facing = HelperUtils::getBlockAtSide($this->position, Facing::opposite($this->facing));
				if($facing instanceof Transmittable){
					$facing->power($this);
				}
				foreach($this->getSupportingBlocks() as $block){
					$block->power($this);
				}
				break;
			}elseif($state === RedstoneRepeaterBlockData::OPERATION_SWITCH_RECALCULATE){
				$powered = $this->recalculatePower($world) > 0;
				if($powered === $this->powered){
					$state = null;
				}else{
					$this->switch($powered);
					$state = RedstoneRepeaterBlockData::OPERATION_DISTRIBUTE;
				}
			}elseif($state === null){
				$world->removeExtraDataAt($this->position->x, $this->position->y, $this->position->z);
				break;
			}
		}
	}

	/**
	 * @return Generator<Transmittable>
	 */
	public function getSupportingBlocks() : Generator{
		$facing = HelperUtils::getBlockAtSide($this->position, Facing::opposite($this->facing));
		if($facing instanceof self){
			return; // TODO: Generify this - blocks such as torches stop further flow of current as well ([repeater][torch][lamp] won't make [lamp] block light)
		}
		foreach(Facing::ALL as $face){
			if($face !== $this->facing){
				$block = HelperUtils::getBlockAtSide($facing->position, $face);
				if($block instanceof Transmittable){
					yield $block;
				}
			}
		}
	}

	public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{
		if($this->powered){
			$this->switch(false);
			RedstoneWorldManager::$any->get($this->position->world)->removeExtraDataAt($this->position->x, $this->position->y, $this->position->z);
			$facing = HelperUtils::getBlockAtSide($this->position, Facing::opposite($this->facing));
			if($facing instanceof Transmittable){
				$facing->power($this);
			}
			foreach($this->getSupportingBlocks() as $block){
				$block->power($this);
			}
		}
		return parent::onBreak($item, $player, $returnedItems);
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\RedstoneLamp as VanillaRedstoneLamp;
use redstone\block\power\Powerable;
use redstone\block\power\PowerableTrait;

class RedstoneLamp extends VanillaRedstoneLamp implements Powerable{
	use OptimizedBlockTrait;
	use PowerableTrait;

	protected int $activation_delay = 0;
	protected int $deactivation_delay = 2;
	protected bool $requires_strong_power = true;

	protected function onReceivePower(int $power) : void{
		$powered = $power > 0;
		if($powered !== $this->powered){
			$this->position->world->setBlockAt($this->position->x, $this->position->y, $this->position->z, $this->setPowered($powered), false);
		}
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use redstone\block\power\Powerable;
use redstone\block\power\PowerableOpenableTrait;

final class CopperTrapdoor extends \pocketmine\block\CopperTrapdoor implements Powerable{
	use OptimizedBlockTrait;
	use PowerableOpenableTrait;
}
<?php

declare(strict_types=1);

namespace redstone\block;

use Generator;
use pocketmine\block\Block;
use pocketmine\block\Flowable;
use pocketmine\block\Glowstone;
use pocketmine\block\RedstoneComparator as VanillaRedstoneComparator;
use pocketmine\block\Slab;
use pocketmine\block\tile\Container;
use pocketmine\block\utils\SlabType;
use pocketmine\item\Item;
use pocketmine\math\Axis;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use pocketmine\world\sound\RedstonePowerOffSound;
use pocketmine\world\sound\RedstonePowerOnSound;
use redstone\block\power\PowerSource;
use redstone\block\power\Transmittable;
use redstone\block\power\VariablePowerSource;
use redstone\block\power\Waitable;
use redstone\block\tile\comparator\ComparatorInventoryListener;
use redstone\block\tile\comparator\ComparatorWeightRegistry;
use redstone\block\utils\HelperUtils;
use redstone\world\RedstoneWorld;
use redstone\world\RedstoneWorldManager;
use function floor;
use function min;

class RedstoneComparator extends VanillaRedstoneComparator implements VariablePowerSource, Transmittable, Waitable{
	use OptimizedBlockTrait;

	public function getPowerLevel() : int{
		return $this->signalStrength;
	}

	public function getOutputPowerLevel() : int{
		return $this->getPowerLevel();
	}

	public function canPower(int $side) : bool{
		return $side === Facing::opposite($this->facing);
	}

	public function canStronglyPower(int $side) : bool{
		return $this->canPower($side);
	}

	public function setPowerLevel(int $level) : void{
		if($level !== $this->signalStrength){
			$this->signalStrength = $level;
			$this->position->world->setBlockAt($this->position->x, $this->position->y, $this->position->z, $this->setPowered($this->signalStrength > 0), false);
			$facing = HelperUtils::getBlockAtSide($this->position, Facing::opposite($this->facing));
			if($facing instanceof Transmittable){
				$facing->power($this);
			}
			foreach($this->getSupportingBlocks() as $block){
				$block->power($this);
			}
		}
	}

	public function onNearbyBlockChange() : void{
		if(!$this->canBePlacedUpon(HelperUtils::getBlockAtSide($this->position, Facing::DOWN))){
			$this->position->world->useBreakOn($this->position);
			return;
		}
		$this->setPowerLevel($this->recalculatePower());
		$this->notifyContainer();
	}

	public function power(PowerSource $source) : void{
		if($source->getPowerLevel() !== $this->signalStrength){
			RedstoneWorldManager::$any->get($this->position->world)->scheduleWaitableUpdate($this, RedstoneWorld::redstoneTicks(1), RedstoneWorld::WAITABLE_UPDATE_LOW_PRIORITY);
		}
	}

	private function recalculatePower() : int{
		$behind = HelperUtils::getBlockAtSide($this->position, $this->facing);
		$world = RedstoneWorldManager::$any->get($this->position->world);
		if($behind instanceof PowerSource){
			$rear = $behind->getPowerLevel();
		}else{
			$tile = $behind->position->world->getTileAt($behind->position->x, $behind->position->y, $behind->position->z);
			if($tile instanceof Container){
				$inventory = $tile->getInventory();
				$fullness = 0;
				foreach(HelperUtils::fastReadOnlyInventoryContents($inventory) as $item){
					$fullness += min(1, $item->getCount() / $item->getMaxStackSize());
				}
				$rear = $fullness > 0 ? (int) floor(1 + ($fullness / $inventory->getSize()) * 14) : 0;
			}else{
				$rear = 0;
				/** @var PowerSource $source */
				foreach($world->getStronglyPoweringSources($behind, $opposite_side = Facing::opposite($this->facing), $opposite_side) as $source){
					$rear = $source->getPowerLevel();
					break;
				}
			}
		}

		if($rear === 0){
			return 0;
		}

		$side_strengths = [];
		foreach(Facing::axis($this->facing) === Axis::Z ? [Facing::EAST, Facing::WEST] : [Facing::NORTH, Facing::SOUTH] as $side){
			$side_strengths[] = ComparatorWeightRegistry::getValue(HelperUtils::getBlockAtSide($this->position, $side));
		}

		return $this->isSubtractMode ? max($rear - max($side_strengths), 0) :
			($side_strengths[0] <= $rear && $side_strengths[1] <= $rear ? $rear : 0);
	}

	public function onRedstoneTickReceive() : void{
		$this->setPowerLevel($this->recalculatePower());
	}

	public function onContainerInputChange() : void{
		RedstoneWorldManager::$any->get($this->position->world)->scheduleWaitableUpdate($this, RedstoneWorld::redstoneTicks(1), RedstoneWorld::WAITABLE_UPDATE_LOW_PRIORITY);
	}

	/**
	 * @return Generator<Transmittable>
	 */
	public function getSupportingBlocks() : Generator{
		$facing = HelperUtils::getBlockAtSide($this->position, Facing::opposite($this->facing));
		if(!($facing instanceof self)){
			foreach(Facing::ALL as $face){
				if($face !== $this->facing){
					$block = HelperUtils::getBlockAtSide($facing->position, $face);
					if($block instanceof Transmittable){
						yield $block;
					}
				}
			}
		}
	}

	private function canBePlacedUpon(Block $block) : bool{
		return !$block->isTransparent() || ($block instanceof Slab && $block->getSlabType() === SlabType::TOP) || $block instanceof Glowstone;
	}

	public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
		if($this->canBePlacedUpon(HelperUtils::getBlockAtSide($blockReplace->position, Facing::DOWN))){
			if($player !== null){
				$this->facing = Facing::opposite($player->getHorizontalFacing());
			}
			return Flowable::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
		}
		return false;
	}

	public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
		$old_state = $this->isSubtractMode();
		if(parent::onInteract($item, $face, $clickVector, $player, $returnedItems)){
			if($old_state !== $this->isSubtractMode()){
				$this->position->world->addSound($this->position->add(0.5, 0.5, 0.5), $this->isSubtractMode() ? new RedstonePowerOnSound() : new RedstonePowerOffSound());
			}
			return true;
		}
		return false;
	}

	public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{
		$powered = $this->powered;
		$this->powered = false;
		if(parent::onBreak($item, $player, $returnedItems)){
			if($powered){
				foreach($this->getSupportingBlocks() as $block){
					$block->power($this);
				}
			}
			return true;
		}
		return false;
	}

	private function notifyContainer() : void{
		$behind_pos = $this->position->getSide($this->facing);
		$behind = $this->position->world->getTileAt($behind_pos->x, $behind_pos->y, $behind_pos->z);
		if($behind instanceof Container){
			$inventory = $behind->getRealInventory();
			$listener = ComparatorInventoryListener::instance($this->facing);
			if(!$inventory->getListeners()->contains($listener)){
				$inventory->getListeners()->add($listener);
				$listener->update($inventory);
			}
		}
	}

	public function onScheduledUpdate() : void{
		$this->notifyContainer();
	}
}
<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\Block;
use pocketmine\block\BlockIdentifier;
use pocketmine\block\BlockTypeInfo;
use pocketmine\block\Slime;
use pocketmine\block\tile\Tile;
use pocketmine\block\tile\TileFactory;
use pocketmine\block\Transparent;
use pocketmine\block\VanillaBlocks;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\event\block\BlockTeleportEvent;
use pocketmine\item\Item;
use pocketmine\math\Axis;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use redstone\block\power\Movable;
use redstone\block\power\Powerable;
use redstone\block\power\PowerableTrait;
use redstone\block\tile\piston\PistonArm;
use redstone\block\tile\piston\PistonMoveInfo;
use redstone\block\utils\PistonStructureResolver;
use redstone\event\PistonPullBlockEvent;
use redstone\event\PistonPushBlockEvent;
use redstone\vanilla\ExtraVanillaBlocks;
use redstone\world\RedstoneWorld;
use redstone\world\sound\PistonInSound;
use redstone\world\sound\PistonOutSound;
use ReflectionMethod;
use RuntimeException;
use function abs;
use function assert;
use function count;
use function max;
use function min;

class Piston extends Transparent implements Powerable, Movable{
	use OptimizedBlockTrait;
	use PowerableTrait;

	public const SLIME_LAUNCH_MOTION = 0.75;

	public const STATE_CONTRACT_IDLE = 0;
	public const STATE_CONTRACT_BEGIN = 1;
	public const STATE_RETRACT_BEGIN = 2;
	public const STATE_RETRACT_WAITING = 3;
	public const STATE_RETRACT_IDLE = 4;

	protected int $facing = Facing::NORTH;

	/** @var self::STATE_* */
	protected int $state = self::STATE_RETRACT_IDLE;

	protected int $activation_delay = 0;
	protected int $deactivation_delay = 0;
	protected bool $requires_strong_power = false;

	public function __construct(
		BlockIdentifier $idInfo,
		string $name,
		BlockTypeInfo $typeInfo,
		readonly protected bool $sticky
	){
		parent::__construct($idInfo, $name, $typeInfo);
	}

	protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{
		$w->facing($this->facing);
	}

	public function readStateFromWorld() : Block{
		$result = parent::readStateFromWorld();
		if($result !== $this){
			return $result;
		}

		$tile = $this->position->getWorld()->getTileAt($this->position->x, $this->position->y, $this->position->z);
		if($tile instanceof PistonArm){
			$this->state = $tile->state;
		}
		return $this;
	}

	public function writeStateToWorld() : void{
		parent::writeStateToWorld();
		$tile = $this->position->getWorld()->getTileAt($this->position->x, $this->position->y, $this->position->z);
		assert($tile instanceof PistonArm);
		$tile->state = $this->state;
		$tile->sticky = $this->sticky;
		$tile->clearSpawnCompoundCache();
		$this->executeState();
	}

	public function isSticky() : bool{
		return $this->sticky;
	}

	public function getFacing() : int{
		return $this->facing;
	}

	public function setFacing(int $facing) : self{
		$this->facing = $facing;
		return $this;
	}

	/**
	 * @return self::STATE_*
	 */
	public function getState() : int{
		return $this->state;
	}

	/**
	 * @param self::STATE_* $state
	 * @return self
	 */
	public function setState(int $state) : self{
		$this->state = $state;
		return $this;
	}

	private function executeState() : void{
		$initial_state = $this->state;
		do{
			$old_state = $this->state;
			switch($this->state){
				case self::STATE_CONTRACT_BEGIN:
					$block = $this->getSide($this->getSideFacing());
					if($block instanceof PistonArmBlock){
						if($block->getFacing() !== $this->facing){
							$this->setState(self::STATE_RETRACT_BEGIN);
						}else{
							$this->setState(self::STATE_CONTRACT_IDLE);
						}
					}elseif($this->pushBlocks()){
						$this->setState(self::STATE_CONTRACT_IDLE);
						$this->position->world->addSound($this->position->add(0.5, 0.5, 0.5), new PistonOutSound());
					}else{
						$this->setState(self::STATE_RETRACT_IDLE);
					}
					break;
				case self::STATE_CONTRACT_IDLE:
					$block = $this->getSide($this->getSideFacing());
					if(!($block instanceof PistonArmBlock) || $block->getFacing() !== $this->facing){
						$block->position->world->setBlockAt($block->position->x, $block->position->y, $block->position->z, ExtraVanillaBlocks::PISTON_ARM_BLOCK()->setFacing($this->facing), false);
					}
					break;
				case self::STATE_RETRACT_BEGIN:
					$block = $this->getSide($this->getSideFacing());
					if($block instanceof PistonArmBlock && $block->getFacing() === $this->facing){
						$this->setState(self::STATE_RETRACT_WAITING);
					}else{
						$this->setState(self::STATE_RETRACT_IDLE);
					}
					break;
				case self::STATE_RETRACT_WAITING:
					$this->position->world->scheduleDelayedBlockUpdate($this->position, RedstoneWorld::redstoneTicks(1));
					break;
				case self::STATE_RETRACT_IDLE:
					$block = $this->getSide($this->getSideFacing());
					if($block instanceof PistonArmBlock && $block->getFacing() === $this->facing){
						$block->position->world->setBlockAt($block->position->x, $block->position->y, $block->position->z, VanillaBlocks::AIR());
					}
					break;
				default:
					throw new RuntimeException("Unexpected state: {$this->state}");
			}
		}while($old_state !== $this->state);
		if($this->state !== $initial_state){
			$this->position->world->setBlockAt($this->position->x, $this->position->y, $this->position->z, $this, false);
		}
	}

	public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
		if($player !== null){
			if(abs($player->getPosition()->x - $this->position->x) < 2 && abs($player->getPosition()->z - $this->position->z) < 2){
				$y = $player->getEyePos()->y;

				if($y - $this->position->y > 2){
					$this->facing = Facing::UP;
				}elseif($this->position->y - $y > 0){
					$this->facing = Facing::DOWN;
				}else{
					$this->facing = $player->getHorizontalFacing();
				}
			}else{
				$this->facing = $player->getHorizontalFacing();
			}
		}
		return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
	}

	public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{
		if($this->state !== self::STATE_RETRACT_IDLE){
			$this->setState(self::STATE_RETRACT_IDLE);
			$this->executeState();
		}
		return parent::onBreak($item, $player, $returnedItems);
	}

	private function canBlockBeBroken(Block $block) : bool{
		return $block->isTransparent() && !$block->isSolid();
	}

	private function canBlockBeMoved(Block $block, Vector3 $future_pos) : bool{
		if($this->canBlockBeBroken($block) || $block->canBeFlowedInto() || !$block->getBreakInfo()->isBreakable()){
			return false;
		}
		if($block instanceof Movable ? !$block->canBeMoved() : ($block->position->world->getTileAt($block->position->x, $block->position->y, $block->position->z) !== null)){
			return false;
		}
		($ev = new BlockTeleportEvent($block, $future_pos))->call();
		return !$ev->isCancelled();
	}

	private function createStructureResolver(bool $extending) : PistonStructureResolver{
		return new PistonStructureResolver(
			$this->position->world,
			$this->position->asVector3(),
			$this->getSideFacing(),
			$extending,
			fn(Block $block, Vector3 $future_pos) : bool => !($block instanceof PistonArmBlock) && $this->canBlockBeMoved($block, $future_pos)
		);
	}

	/**
	 * @return list<PistonMoveInfo>
	 */
	private function createMovements(PistonStructureResolver $resolver) : array{
		$direction = $resolver->getPushDirection();
		$movements = [];
		foreach($resolver->getToPush() as $block){
			$pos = $block->getPosition();
			$movements[] = new PistonMoveInfo($block, $pos->world->getTile($pos), $pos->asVector3(), $pos->getSide($direction));
		}
		return $movements;
	}

	/**
	 * @param list<PistonMoveInfo> $movements
	 * @param list<Block> $to_destroy
	 */
	private function moveStructure(array $movements, array $to_destroy) : void{
		$world = $this->position->world;
		foreach($to_destroy as $block){
			$world->useBreakOn($block->getPosition());
		}

		$tile_nbts = [];
		foreach($movements as $index => $movement){
			$tile_nbts[$index] = $world->getTileAt($movement->from->x, $movement->from->y, $movement->from->z)?->saveNBT();
		}
		foreach($movements as $movement){
			$world->setBlockAt($movement->from->x, $movement->from->y, $movement->from->z, VanillaBlocks::AIR(), false);
		}

		foreach($movements as $index => $movement){
			$to = $movement->to;
			$world->setBlockAt($to->x, $to->y, $to->z, $movement->block, false);
			$tile_nbt = $tile_nbts[$index];
			if($tile_nbt !== null){
				$existing_tile = $world->getTileAt($to->x, $to->y, $to->z);
				if($existing_tile !== null){
					$world->removeTile($existing_tile);
				}
				$tile_nbt->setInt(Tile::TAG_X, $to->x);
				$tile_nbt->setInt(Tile::TAG_Y, $to->y);
				$tile_nbt->setInt(Tile::TAG_Z, $to->z);
				$world->addTile(TileFactory::getInstance()->createFromData($world, $tile_nbt));
			}
		}

		foreach($movements as $movement){
			foreach([$movement->from, $movement->to] as $pos){
				$world->updateAllLight($pos->x, $pos->y, $pos->z);
				$world->notifyNeighbourBlockUpdate($pos);
			}
		}
	}

	private function pullBlocks() : bool{
		$arm_pos = $this->position->getSide($this->getSideFacing());
		$arm = $this->position->world->getBlockAt($arm_pos->x, $arm_pos->y, $arm_pos->z);
		if(!($arm instanceof PistonArmBlock) && !$arm->canBeReplaced() && !$arm->canBeFlowedInto()){
			return false;
		}
		$this->position->world->setBlockAt($arm_pos->x, $arm_pos->y, $arm_pos->z, VanillaBlocks::AIR(), false);

		$resolver = $this->createStructureResolver(false);
		$resolved = $resolver->resolve() && count($resolver->getToPush()) > 0;
		$ev = new PistonPullBlockEvent($this, $this->getSide($this->getSideFacing(), 2), $resolved ? $this->createMovements($resolver) : []);
		if(!$resolved){
			$ev->cancel();
		}
		$ev->call();
		if($ev->isCancelled()){
			return false;
		}
		$this->moveStructure($ev->movements, $resolver->getToDestroy());
		return true;
	}

	private function pushBlocks() : bool{
		$resolver = $this->createStructureResolver(true);
		if(!$resolver->resolve()){
			return false;
		}
		$movements = $this->createMovements($resolver);
		$arm_pos = $this->position->getSide($this->getSideFacing());
		$ev = new PistonPushBlockEvent($this, $arm_pos, $movements);
		$ev->call();
		if($ev->isCancelled()){
			return false;
		}
		$this->launchEntitiesOnSlime($movements);
		$this->pushEntities($movements);
		$this->moveStructure($movements, $resolver->getToDestroy());
		$this->position->world->useBreakOn($arm_pos);
		return true;
	}

	/**
	 * @param list<PistonMoveInfo> $movements
	 */
	private function launchEntitiesOnSlime(array $movements) : void{
		$facing = $this->getSideFacing();
		$direction = Vector3::zero()->getSide($facing);
		$lift = Facing::axis($facing) === Axis::Y ? 0 : 0.25;

		$launched = [];
		foreach($movements as $movement){
			if(!($movement->block instanceof Slime)){
				continue;
			}
			$from = $movement->from;
			$bb = AxisAlignedBB::one()
				->offset($from->x, $from->y, $from->z)
				->addCoord($direction->x, $direction->y, $direction->z)
				->addCoord(0, $lift, 0);
			foreach($this->position->world->getNearbyEntities($bb) as $entity){
				if($entity->isFlaggedForDespawn() || $entity->isClosed() || isset($launched[$entity->getId()])){
					continue;
				}
				$launched[$entity->getId()] = true;
				$motion = $entity->getMotion();
				$entity->setMotion(new Vector3(
					self::getSlimeLaunchMotion($motion->x, $direction->x),
					self::getSlimeLaunchMotion($motion->y, $direction->y),
					self::getSlimeLaunchMotion($motion->z, $direction->z)
				));
			}
		}
	}

	private static function getSlimeLaunchMotion(float $current, int $offset) : float{
		$launch = $offset * self::SLIME_LAUNCH_MOTION;
		if($offset > 0){
			return max($current, $launch);
		}
		if($offset < 0){
			return min($current, $launch);
		}
		return $current;
	}

	/**
	 * @param list<PistonMoveInfo> $movements
	 */
	private function pushEntities(array $movements) : void{
		$facing = $this->getSideFacing();
		$arm_pos = $this->position->getSide($facing);
		$targets = [$arm_pos];
		foreach($movements as $movement){
			$targets[] = $movement->to;
		}

		$pushed = [];
		foreach($targets as $target){
			$target_pos = $target->getSide($facing)->add(0.5, 0.5, 0.5);
			foreach($this->position->world->getNearbyEntities(AxisAlignedBB::one()
				->contract(0.0625, 0.0625, 0.0625)
				->offset($target->x, $target->y, $target->z)) as $entity){
				if($entity->isFlaggedForDespawn() || $entity->isClosed() || isset($pushed[$entity->getId()])){
					continue;
				}
				$pushed[$entity->getId()] = true;
				$pos = $entity->getPosition();
				$diff = Vector3::zero()->getSide($facing);
				$diff->x *= abs($target_pos->x - $pos->x);
				$diff->y *= abs($target_pos->y - $pos->y);
				$diff->z *= abs($target_pos->z - $pos->z);
				$_move = new ReflectionMethod($entity, "move");
				$_updateMovement = new ReflectionMethod($entity, "updateMovement");
				$_move->invoke($entity, $diff->x, $diff->y, $diff->z);
				$_updateMovement->invoke($entity);
			}
		}
	}

	public function getSideFacing() : int{
		return $this->facing >= 2 ? Facing::opposite($this->facing) : $this->facing;
	}

	public function isPowered() : bool{
		return $this->state !== self::STATE_RETRACT_IDLE;
	}

	public function acceptsPowerFromSide(int $side) : bool{
		return $side !== $this->getSideFacing();
	}

	protected function onReceivePower(int $power) : void{
		if($power > 0){
			if($this->state !== self::STATE_CONTRACT_BEGIN && $this->state !== self::STATE_CONTRACT_IDLE && $this->state !== self::STATE_RETRACT_BEGIN){
				$next_state = self::STATE_CONTRACT_BEGIN;
			}else{
				$next_state = null;
			}
		}else{
			if($this->state !== self::STATE_RETRACT_BEGIN && /*$this->state !== self::STATE_RETRACT_WAITING && */$this->state !== self::STATE_RETRACT_IDLE && $this->state !== self::STATE_CONTRACT_BEGIN){
				$next_state = self::STATE_RETRACT_BEGIN;
			}else{
				$next_state = null;
			}
		}
		if($next_state !== null){
			$this->position->world->setBlockAt($this->position->x, $this->position->y, $this->position->z, $this->setState($next_state), false);
		}
	}

	public function onScheduledUpdate() : void{
		if($this->state === self::STATE_RETRACT_WAITING){
			if($this->sticky){
				$this->pullBlocks();
			}
			$this->position->world->addSound($this->position->add(0.5, 0.5, 0.5), new PistonInSound());
			$this->position->world->setBlockAt($this->position->x, $this->position->y, $this->position->z, $this->setState(self::STATE_RETRACT_IDLE), false);
		}
	}

	public function canBeMoved() : bool{
		return $this->state === self::STATE_RETRACT_IDLE;
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use redstone\block\power\Powerable;
use redstone\block\power\PowerableOpenableTrait;

final class WoodenTrapdoor extends \pocketmine\block\WoodenTrapdoor implements Powerable{
	use OptimizedBlockTrait;
	use PowerableOpenableTrait;
}
<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\WoodenPressurePlate as VanillaWoodenPressurePlate;
use redstone\block\power\PowerSource;

class WoodenPressurePlate extends VanillaWoodenPressurePlate implements PowerSource{
	use OptimizedBlockTrait;
	use PressurePlateTrait;
}<?php

declare(strict_types=1);

namespace redstone\block;

use Generator;
use pocketmine\block\RedstoneTorch as VanillaRedstoneTorch;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\player\Player;
use redstone\block\power\PowerSource;
use redstone\block\power\ToggleablePowerSource;
use redstone\block\power\Transmittable;
use redstone\block\power\Waitable;
use redstone\block\utils\HelperUtils;
use redstone\block\utils\RedstoneTorchBlockData;
use redstone\world\RedstoneWorld;
use redstone\world\RedstoneWorldManager;

class RedstoneTorch extends VanillaRedstoneTorch implements ToggleablePowerSource, Transmittable, Waitable{
	use OptimizedBlockTrait;

	public function getPowerLevel() : int{
		return $this->lit ? 15 : 0;
	}

	public function getOutputPowerLevel() : int{
		return $this->getPowerLevel();
	}

	public function canPower(int $side) : bool{
		return $side !== Facing::opposite($this->facing);
	}

	public function canStronglyPower(int $side) : bool{
		return $side === Facing::UP;
	}

	public function switch(bool $state) : void{
		if($state === $this->lit){
			return;
		}

		$this->position->world->setBlockAt($this->position->x, $this->position->y, $this->position->z, $this->setLit($state), false);
		foreach($this->getRelyingBlocks() as $block){
			$block->power($this);
		}
	}

	public function power(?PowerSource $source = null) : void{
		RedstoneWorldManager::$any->get($this->position->world)->scheduleWaitableUpdate($this, RedstoneWorld::redstoneTicks(1));
	}

	private function updateState(bool $manual) : void{
		$state = !RedstoneWorldManager::$any->get($this->position->world)->isStronglyPowered(HelperUtils::getBlockAtSide($this->position, Facing::opposite($this->facing)), $this->facing);
		if($state === $this->lit){
			return;
		}

		if($manual){
			$world = RedstoneWorldManager::$any->get($this->position->world);
			$data = $world->getExtraDataAt($this->position->x, $this->position->y, $this->position->z);
			if(!($data instanceof RedstoneTorchBlockData)){
				$data = new RedstoneTorchBlockData();
				$world->setExtraDataAt($this->position->x, $this->position->y, $this->position->z, $data);
			}
			$tick = $world->getTick();
			if($state){
				$data->count($tick);
			}
			if($data->isBurntOut($tick)){
				$state = false;
			}
		}
		$this->switch($state);
	}

	public function onRedstoneTickReceive() : void{
		$this->updateState(true);
	}

	/**
	 * @return Generator<Transmittable>
	 */
	protected function getRelyingBlocks() : Generator{
		$sides = Facing::HORIZONTAL;
		$sides[] = Facing::DOWN;
		foreach($sides as $side){
			$block = HelperUtils::getBlockAtSide($this->position, $side);
			if($block instanceof Transmittable){
				yield $block;
			}
		}

		yield from $this->getRelyingOnSupportBlocks();
	}

	/**
	 * @return Generator<Transmittable>
	 */
	protected function getRelyingOnSupportBlocks() : Generator{
		$up = HelperUtils::getBlockAtSide($this->position, Facing::UP);
		if($up instanceof Transmittable){
			yield $up;
		}
		foreach(Facing::ALL as $side){
			if($side !== Facing::DOWN){
				$block = HelperUtils::getBlockAtSide($up->position, $side);
				if($block instanceof Transmittable){
					yield $block;
				}
			}
		}
	}

	public function onPostPlace() : void{
		$this->power();
		foreach($this->getRelyingBlocks() as $block){
			$block->power($this);
		}
	}

	public function onScheduledUpdate() : void{
		RedstoneWorldManager::$any->get($this->position->world)->removeExtraDataAt($this->position->x, $this->position->y, $this->position->z);
		$this->updateState(false);
	}

	public function onNearbyBlockChange() : void{
		RedstoneWorldManager::$any->get($this->position->world)->removeExtraDataAt($this->position->x, $this->position->y, $this->position->z);
		$this->updateState(false);
		parent::onNearbyBlockChange();
	}

	public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{
		if($this->lit){
			$this->lit = false;
			foreach($this->getRelyingOnSupportBlocks() as $block){
				$block->power($this);
			}
		}

		RedstoneWorldManager::$any->get($this->position->world)->removeExtraDataAt($this->position->x, $this->position->y, $this->position->z);
		return parent::onBreak($item, $player, $returnedItems);
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use Generator;
use pocketmine\block\Block;
use pocketmine\block\Opaque;
use pocketmine\block\utils\PoweredByRedstoneTrait;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use redstone\block\power\PowerSource;
use redstone\block\power\Transmittable;
use redstone\block\power\Waitable;
use redstone\block\utils\HelperUtils;
use redstone\world\RedstoneWorld;
use redstone\world\RedstoneWorldManager;
use function abs;

class Observer extends Opaque implements PowerSource, Waitable{
	use OptimizedBlockTrait;
	use PoweredByRedstoneTrait;

	protected int $facing = Facing::DOWN;

	protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{
		$w->facing($this->facing);
		$w->bool($this->powered);
	}

	public function getFacing() : int{
		return $this->facing;
	}

	public function setFacing(int $facing) : self{
		$this->facing = $facing;
		return $this;
	}

	public function getOutputSide() : int{
		return Facing::opposite($this->facing);
	}

	public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
		if($player !== null){
			if(abs($player->getPosition()->x - $this->position->x) < 2 && abs($player->getPosition()->z - $this->position->z) < 2){
				$y = $player->getEyePos()->y;

				if($y - $this->position->y > 2){
					$this->facing = Facing::DOWN;
				}elseif($this->position->y - $y > 0){
					$this->facing = Facing::UP;
				}else{
					$this->facing = $player->getHorizontalFacing();
				}
			}else{
				$this->facing = $player->getHorizontalFacing();
			}
		}

		return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
	}

	public function getPowerLevel() : int{
		return $this->powered ? 15 : 0;
	}

	public function getOutputPowerLevel() : int{
		return $this->getPowerLevel();
	}

	public function canPower(int $side) : bool{
		return $side === $this->getOutputSide();
	}

	public function canStronglyPower(int $side) : bool{
		return $side === $this->getOutputSide();
	}

	public function onObservedBlockChange() : void{
		RedstoneWorldManager::$any->get($this->position->world)->scheduleWaitableUpdate($this, RedstoneWorld::redstoneTicks(1));
	}

	public function onRedstoneTickReceive() : void{
		$this->powered = !$this->powered;
		$this->position->world->setBlockAt($this->position->x, $this->position->y, $this->position->z, $this, false);
		foreach($this->getRelyingBlocks() as $block){
			$block->power($this);
		}
		if($this->powered){
			RedstoneWorldManager::$any->get($this->position->world)->scheduleWaitableUpdate($this, RedstoneWorld::redstoneTicks(1));
		}
	}

	public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{
		if($this->powered){
			$this->powered = false;
			foreach($this->getRelyingBlocks() as $block){
				$block->power($this);
			}
		}

		return parent::onBreak($item, $player, $returnedItems);
	}

	/**
	 * @return Generator<Transmittable>
	 */
	protected function getRelyingBlocks() : Generator{
		$output_side = $this->getOutputSide();
		$output = HelperUtils::getBlockAtSide($this->position, $output_side);
		if($output instanceof Transmittable){
			yield $output;
		}
		$skip_side = Facing::opposite($output_side);
		foreach(Facing::ALL as $side){
			if($side !== $skip_side){
				$block = HelperUtils::getBlockAtSide($output->position, $side);
				if($block instanceof Transmittable){
					yield $block;
				}
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace redstone\block;

use Generator;
use pocketmine\block\Block;
use pocketmine\block\SimplePressurePlate;
use pocketmine\item\Item;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use redstone\block\power\Transmittable;
use redstone\block\utils\HelperUtils;

trait PressurePlateTrait{

	public function writeStateToWorld() : void{
		parent::writeStateToWorld();
		if($this->hasOutputSignal()){
			foreach($this->getRelyingBlocks() as $block){
				$block->power($this);
			}
		}
	}

	protected function recalculateCollisionBoxes() : array{
		return [new AxisAlignedBB(0.0625, 0, 0.0625, 0.9375, $this->hasOutputSignal() ? 0.03125 : 0.0625, 0.9375)];
	}

	public function canBePlacedAt(Block $blockReplace, Vector3 $clickVector, int $face, bool $isClickedBlock) : bool{
		if(parent::canBePlacedAt($blockReplace, $clickVector, $face, $isClickedBlock)){
			$pos = $blockReplace->getPosition();
			$below = $pos->down();
			return $pos->getWorld()->getBlockAt($below->x, $below->y, $below->z)->isSolid();
		}
		return false;
	}

	public function getPowerLevel() : int{
		return $this->hasOutputSignal() ? 15 : 0;
	}

	public function getOutputPowerLevel() : int{
		return $this->getPowerLevel();
	}

	public function canPower(int $side) : bool{
		return true;
	}

	public function canStronglyPower(int $side) : bool{
		return $side === Facing::DOWN;
	}

	/**
	 * @return Generator<Transmittable>
	 */
	protected function getRelyingOnSupportBlocks() : Generator{
		$supporting_side = $this->getSupportingSide();
		$supporting = HelperUtils::getBlockAtSide($this->position, $supporting_side);
		$skip_side = Facing::opposite($supporting_side);
		foreach(Facing::ALL as $side){
			if($side !== $skip_side){
				$block = HelperUtils::getBlockAtSide($supporting->position, $side);
				if($block instanceof Transmittable){
					yield $block;
				}
			}
		}
	}

	/**
	 * @return Generator<Transmittable>
	 */
	protected function getRelyingBlocks() : Generator{
		yield from $this->getRelyingOnSupportBlocks();
		$skip_side = $this->getSupportingSide();
		foreach(Facing::HORIZONTAL as $side){
			if($side !== $skip_side){
				$block = HelperUtils::getBlockAtSide($this->position, $side);
				if($block instanceof Transmittable){
					yield $block;
				}
			}
		}
	}

	public function getSupportingSide() : int{
		return Facing::DOWN;
	}

	public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{
		if($this->hasOutputSignal()){
			$this->setPressed(false);
			foreach($this->getRelyingOnSupportBlocks() as $block){
				$block->power($this);
			}
		}
		return parent::onBreak($item, $player, $returnedItems);
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\TNT as VanillaTNT;
use redstone\block\power\Powerable;
use redstone\block\power\PowerableTrait;

class TNT extends VanillaTNT implements Powerable{
	use OptimizedBlockTrait;
	use PowerableTrait;

	protected int $activation_delay = 0;
	protected int $deactivation_delay = 0;
	protected bool $requires_strong_power = false;

	public function isPowered() : bool{
		return false;
	}

	protected function onReceivePower(int $power) : void{
		if($power > 0){
			$this->ignite();
		}
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use Generator;
use pocketmine\block\tile\Chest as ChestTile;
use pocketmine\block\TrappedChest as VanillaTrappedChest;
use pocketmine\math\Facing;
use redstone\block\power\PowerSource;
use redstone\block\power\Transmittable;
use redstone\block\power\Waitable;
use redstone\block\utils\HelperUtils;
use redstone\world\RedstoneWorldManager;
use function count;
use function min;

class TrappedChest extends VanillaTrappedChest implements PowerSource, Waitable{
	use OptimizedBlockTrait;

	public function getPowerLevel() : int{
		$tile = $this->position->world->getTileAt($this->position->x, $this->position->y, $this->position->z);
		if(!($tile instanceof ChestTile)){
			return 0;
		}
		return min(15, count($tile->getInventory()->getViewers()));
	}

	public function getOutputPowerLevel() : int{
		return $this->getPowerLevel();
	}

	public function canPower(int $side) : bool{
		return true;
	}

	public function canStronglyPower(int $side) : bool{
		return $side === Facing::DOWN;
	}

	public function onViewersChange() : void{
		RedstoneWorldManager::$any->get($this->position->world)->scheduleWaitableUpdate($this, 1);
	}

	public function onRedstoneTickReceive() : void{
		foreach($this->getRelyingBlocks() as $block){
			$block->power($this);
		}
	}

	/**
	 * @return Generator<Transmittable>
	 */
	protected function getRelyingBlocks() : Generator{
		foreach(Facing::ALL as $side){
			$block = HelperUtils::getBlockAtSide($this->position, $side);
			if($block instanceof Transmittable){
				yield $block;
			}
		}

		$below = HelperUtils::getBlockAtSide($this->position, Facing::DOWN);
		foreach(Facing::ALL as $side){
			if($side !== Facing::UP){
				$block = HelperUtils::getBlockAtSide($below->position, $side);
				if($block instanceof Transmittable){
					yield $block;
				}
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace redstone\block;

use redstone\block\utils\HelperUtils;

trait OptimizedBlockTrait{

	public function getSide(int $side, int $step = 1){
		return HelperUtils::getBlockAtSide($this->position, $side, $step);
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use redstone\block\power\Powerable;
use redstone\block\power\PowerableOpenableTrait;

final class FenceGate extends \pocketmine\block\FenceGate implements Powerable{
	use OptimizedBlockTrait;
	use PowerableOpenableTrait;
}
<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\item\Item;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use redstone\block\power\PowerableDoorTrait;
use redstone\block\power\Powerable;

final class Door extends \pocketmine\block\Door implements Powerable{
	use PowerableDoorTrait;

	public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
		return false;
	}
}
<?php

declare(strict_types=1);

namespace redstone\block\power;

use pocketmine\math\Facing;
use redstone\block\utils\HelperUtils;
use redstone\world\RedstoneWorld;
use redstone\world\RedstoneWorldManager;

trait PowerableTrait{

	public function getActivationDelay() : int{
		return $this->activation_delay;
	}

	public function getDeactivationDelay() : int{
		return $this->deactivation_delay;
	}

	public function requiresStrongPower() : bool{
		return $this->requires_strong_power;
	}

	public function onNearbyBlockChange() : void{
		parent::onNearbyBlockChange();
		$this->recalculatePowerState();
	}

	public function onPostPlace() : void{
		$this->recalculatePowerState();
	}

	public function recalculatePowerState() : void{
		$power = 0;
		$blocks = [];

		if(!$this->requires_strong_power){
			foreach(Facing::ALL as $side){
				if($this->acceptsPowerFromSide($side)){
					$block = HelperUtils::getBlockAtSide($this->position, $side);
					$blocks[$side] = $block;
					if($block instanceof PowerSource && $block->canPower(Facing::opposite($side))){
						$block_power = $block->getOutputPowerLevel();
						if($block_power > $power){
							$power = $block_power;
							if($power === 15){
								break;
							}
						}
					}
				}
			}
		}

		if($power !== 15){
			$world = RedstoneWorldManager::$any->get($this->position->world);
			if($world->isStronglyPowered($this, -1)){
				$power = 15;
			}else{
				foreach(Facing::ALL as $side){
					if($this->acceptsPowerFromSide($side)){
						$block = $blocks[$side] ?? HelperUtils::getBlockAtSide($this->position, $side);
						if($world->isStronglyPowered($block, $opposite_side = Facing::opposite($side), $opposite_side)){
							$power = 15;
							break;
						}
					}
				}
			}
		}

		$this->onReceivePower($power);
	}

	public function power(PowerSource $source) : void{
		$delay = $this->isPowered() ? $this->deactivation_delay : $this->activation_delay;
		if($delay > 0){
			RedstoneWorldManager::$any->get($this->position->world)->scheduleWaitableUpdate($this, RedstoneWorld::redstoneTicks($delay), override: true);
		}else{
			$this->onRedstoneTickReceive();
		}
	}

	public function onRedstoneTickReceive() : void{
		$this->recalculatePowerState();
	}

	public function acceptsPowerFromSide(int $side) : bool{
		return true;
	}

	protected function onReceivePower(int $power) : void{
	}
}<?php

declare(strict_types=1);

namespace redstone\block\power;

/**
 * Blocks that change their state when powered.
 * RULE: Powerable blocks should NEVER recalculate power without receiving a
 * Powerable::power() call!
 */
interface Powerable extends Transmittable, Waitable{

	/**
	 * Delay in redstone ticks after which this block
	 * may activate or 0 to try activating instantly.
	 */
	public function getActivationDelay() : int;

	/**
	 * Delay in redstone ticks after which this block
	 * may deactivate or 0 to try deactivating instantly.
	 */
	public function getDeactivationDelay() : int;

	/**
	 * Returns whether this block ONLY accepts strong power.
	 */
	public function requiresStrongPower() : bool;

	/**
	 * Returns whether this block accepts power
	 * from a side.
	 *
	 * @param int $side
	 * @return bool
	 */
	public function acceptsPowerFromSide(int $side) : bool;

	/**
	 * Returns whether this block is powered/activated.
	 *
	 * @return bool
	 */
	public function isPowered() : bool;

	/**
	 * Calculates new power state and does the visual changes if
	 * there needs to be any.
	 */
	public function recalculatePowerState() : void;
}<?php

declare(strict_types=1);

namespace redstone\block\power;

use pocketmine\block\Door;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\sound\DoorSound;

trait PowerableDoorTrait{
	use PowerableOpenableTrait;

	public function onNearbyBlockChange() : void{
		Door::onNearbyBlockChange();
	}

	public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
		$this->open = !$this->open;

		$other = $this->getSide($this->top ? Facing::DOWN : Facing::UP);
		$world = $this->position->getWorld();
		if($other instanceof Door && $other->hasSameTypeId($this)){
			$other->open = $this->open;
			$world->setBlock($other->position, $other, false);
		}

		$world->setBlock($this->position, $this, false);
		$world->addSound($this->position, new DoorSound());
		return true;
	}
}
<?php

declare(strict_types=1);

namespace redstone\block\power;

interface VariablePowerSource extends PowerSource{

	public function setPowerLevel(int $level) : void;
}<?php

declare(strict_types=1);

namespace redstone\block\power;

interface ToggleablePowerSource extends PowerSource{

	public function switch(bool $state) : void;
}<?php

declare(strict_types=1);

namespace redstone\block\power;

/**
 * Blocks that need to receive power to give out power or
 * fuck with input power to change the value of output
 * power.
 * These are almost always PowerSource as well.
 */
interface Transmittable{

	/**
	 * Called by a PowerSource when it's power level changes so
	 * this block can recalculate it's power state.
	 *
	 * @param PowerSource $source
	 */
	public function power(PowerSource $source) : void;
}<?php

declare(strict_types=1);

namespace redstone\block\power;

interface Waitable{

	/**
	 * Used as a callback method when {@see RedstoneWorld::scheduleWaitableUpdate()}
	 * gets called.
	 */
	public function onRedstoneTickReceive() : void;
}<?php

declare(strict_types=1);

namespace redstone\block\power;

use pocketmine\item\Item;
use pocketmine\player\Player;
use pocketmine\world\sound\DoorSound;
use redstone\block\utils\PoweredBlockData;
use redstone\world\RedstoneWorldManager;

/**
 * Openable blocks whose Bedrock state has no powered bit. The last received
 * power state is kept in the redstone world so that only power changes open
 * or close the block, leaving the player free to toggle it while it stays
 * powered or unpowered.
 */
trait PowerableOpenableTrait{
	use PowerableTrait;

	protected int $activation_delay = 0;
	protected int $deactivation_delay = 0;
	protected bool $requires_strong_power = false;

	public function isPowered() : bool{
		$data = RedstoneWorldManager::$any->get($this->position->world)->getExtraDataAt($this->position->x, $this->position->y, $this->position->z);
		return $data instanceof PoweredBlockData ? $data->powered : $this->open;
	}

	protected function onReceivePower(int $power) : void{
		$powered = $power > 0;
		if($powered === $this->isPowered()){
			return;
		}

		RedstoneWorldManager::$any->get($this->position->world)->setExtraDataAt($this->position->x, $this->position->y, $this->position->z, new PoweredBlockData($powered));
		if($powered !== $this->open){
			$this->position->world->setBlockAt($this->position->x, $this->position->y, $this->position->z, $this->setOpen($powered), false);
			$this->position->world->addSound($this->position, new DoorSound());
		}
	}

	public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{
		RedstoneWorldManager::$any->get($this->position->world)->removeExtraDataAt($this->position->x, $this->position->y, $this->position->z);
		return parent::onBreak($item, $player, $returnedItems);
	}
}
<?php

declare(strict_types=1);

namespace redstone\block\power;

interface PowerSource{

	/**
	 * Returns the power level of this block.
	 *
	 * @return int
	 */
	public function getPowerLevel() : int;

	/**
	 * Returns the power this block can output to it's
	 * surroundings. It's usually the same as getPowerLevel()
	 * except for wires which "transmit" power, thereby losing
	 * 1 level per block.
	 *
	 * @return int
	 */
	public function getOutputPowerLevel() : int;

	/**
	 * Returns whether this block can power a Powerable
	 * block located at it's side.
	 *
	 * @param int $side
	 * @return bool
	 */
	public function canPower(int $side) : bool;

	/**
	 * Returns whether this block can strongly power an opaque
	 * block located at it's side.
	 *
	 * @param int $side
	 * @return bool
	 */
	public function canStronglyPower(int $side) : bool;
}<?php

declare(strict_types=1);

namespace redstone\block\power;

interface Movable{

	public function canBeMoved() : bool;
}<?php

declare(strict_types=1);

namespace redstone\block;

use redstone\block\utils\HelperUtils;

final class RedstoneBlockFactory{

	public static function init() : void{
		HelperUtils::init();
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use Generator;
use pocketmine\block\Block;
use pocketmine\block\RedstoneWire as VanillaRedstoneWire;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use pocketmine\world\World;
use redstone\block\power\PowerSource;
use redstone\block\power\Transmittable;
use redstone\block\utils\HelperUtils;
use redstone\vanilla\ExtraVanillaItems;
use redstone\world\RedstoneWorldManager;
use function array_values;
use function assert;
use function max;

class RedstoneWire extends VanillaRedstoneWire implements PowerSource, Transmittable{
	use OptimizedBlockTrait;

	public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
		return HelperUtils::getBlockAtSide($this->position, Facing::DOWN)->isSolid() && parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
	}

	public function onNearbyBlockChange() : void{
		if(!HelperUtils::getBlockAtSide($this->position, Facing::DOWN)->isSolid()){
			$this->position->world->useBreakOn($this->position);
			return;
		}
		$diff = $this->recalculateBestPower() - $this->signalStrength;
		if($diff !== 0){
			$this->updatePowerLevels([$this], $diff);
		}else{
			foreach($this->getRelyingHorizontalBlocks() as $block){
				if(!($block instanceof self)){
					$block->power($this);
				}
			}
		}
		parent::onNearbyBlockChange();
	}

	public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{
		$this->signalStrength = 0;
		if(parent::onBreak($item, $player, $returnedItems)){
			foreach($this->getRelyingBlocks() as $block){
				$block->power($this);
			}
			return true;
		}

		return false;
	}

	public function getPowerLevel() : int{
		return $this->signalStrength;
	}

	public function getOutputPowerLevel() : int{
		return max($this->signalStrength - 1, 0);
	}

	public function canStronglyPower(int $side) : bool{
		if($side === Facing::DOWN){
			return true;
		}

		if($side === Facing::UP){
			return false;
		}

		$above_transparent = HelperUtils::getBlockAtSide($this->position, Facing::UP)->isTransparent();
		foreach($side === Facing::NORTH || $side === Facing::SOUTH ? [Facing::WEST, Facing::EAST] : [Facing::NORTH, Facing::SOUTH] as $side2){
			$block = HelperUtils::getBlockAtSide($this->position, $side2);
			if($block instanceof PowerSource){
				return false;
			}

			if($above_transparent){
				$up = HelperUtils::getBlockAtSide($block->position, Facing::UP);
				if($up instanceof self){
					return false;
				}
			}

			if($block->isTransparent()){
				$down = HelperUtils::getBlockAtSide($block->position, Facing::DOWN);
				if($down instanceof self){
					return false;
				}
			}
		}

		return true;
	}

	private function collectBreadthFirst(array $blocks) : array{
		$queue = [];
		foreach($blocks as $block){
			if($block instanceof self){
				$queue[] = [$block, 0];
			}
		}
		$queue_index = 0;
		$wires = [];
		$others = [];
		while(isset($queue[$queue_index])){
			[$wire, $distance] = $queue[$queue_index++];
			assert($wire instanceof self);
			if(isset($wires[$hash = World::blockHash($wire->position->x, $wire->position->y, $wire->position->z)])){
				continue;
			}
			$wires[$hash] = [$wire, $distance];
			foreach($wire->getRelyingBlocks() as $neighbour){
				$hash2 = World::blockHash($neighbour->position->x, $neighbour->position->y, $neighbour->position->z);
				if($neighbour instanceof self){
					if($distance <= 15 && !isset($wires[$hash2])){
						$queue[] = [$neighbour, $distance + 1];
					}
				}elseif(isset($others[$hash2])){
					$others[$hash2][1][$hash] = $wire;
				}else{
					$others[$hash2] = [$neighbour, [$hash => $wire]];
				}
			}
		}
		return [array_values($wires), $others];
	}

	private function updatePowerLevels(array $blocks, int $diff) : void{
		[$wires, $others] = $this->collectBreadthFirst($blocks);
		foreach($wires as [$wire, $distance]){
			assert($wire instanceof self);
			if($wire->signalStrength !== 0){
				$wire->signalStrength = 0;
				$wire->position->world->setBlockAt($wire->position->x, $wire->position->y, $wire->position->z, $wire, false);
			}
		}
		$reverse = $diff < 0; // power reduced
		do{
			$changed = 0;
			foreach($reverse ? HelperUtils::reverse($wires) : $wires as [$wire, $distance]){
				assert($wire instanceof self);
				$new_power = $wire->recalculateBestPower();
				if($new_power !== $wire->signalStrength){
					$wire->signalStrength = $new_power;
					$wire->position->world->setBlockAt($wire->position->x, $wire->position->y, $wire->position->z, $wire, false);
					$changed++;
				}
			}
			$reverse = !$reverse;
		}while($changed > 0);
		foreach($others as [$block, $power_list]){
			foreach($power_list as $entry){
				$block->power($entry);
			}
		}
	}

	public function power(PowerSource $source) : void{
		if($source->getOutputPowerLevel() !== $this->signalStrength){
			$blocks = [$this];
			foreach(Facing::ALL as $side){
				$block = $source->getSide($side);
				if($block->position->equals($this->position)){
					continue;
				}
				$blocks[] = $block;
			}
			$this->updatePowerLevels($blocks, $this->recalculateBestPower() - $this->signalStrength);
		}
	}

	public function canPower(int $side) : bool{
		return $side !== Facing::UP && ($side === Facing::DOWN || $this->canPowerHorizontalSide($side));
	}

	protected function canPowerHorizontalSide(int $horizontal_side) : bool{
		$above_transparent = HelperUtils::getBlockAtSide($this->position, Facing::UP)->isTransparent();
		$side_opposite = Facing::opposite($horizontal_side);
		foreach(Facing::HORIZONTAL as $side){
			if($side === $side_opposite){
				continue;
			}

			$block = HelperUtils::getBlockAtSide($this->position, $side);
			if($block instanceof self){
				return false;
			}

			if($above_transparent){
				$up = HelperUtils::getBlockAtSide($block->position, Facing::UP);
				if($up instanceof self){
					return false;
				}
			}

			if($block->isTransparent()){
				$down = HelperUtils::getBlockAtSide($block->position, Facing::DOWN);
				if($down instanceof self){
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * @return Generator<Transmittable>
	 */
	protected function getRelyingBlocks() : Generator{
		$down = HelperUtils::getBlockAtSide($this->position, Facing::DOWN);
		if($down instanceof Transmittable){
			yield $down;
		}

		foreach(Facing::ALL as $side){
			if($side !== Facing::UP){
				$block = HelperUtils::getBlockAtSide($down->position, $side);
				if($block instanceof Transmittable){
					yield $block;
				}
			}
		}

		yield from $this->getRelyingHorizontalBlocks();
	}

	/**
	 * @return Generator<Transmittable>
	 */
	protected function getRelyingHorizontalBlocks() : Generator{
		$blocks = [];
		foreach(Facing::HORIZONTAL as $side){
			$block = HelperUtils::getBlockAtSide($this->position, $side);
			if($block instanceof Transmittable){
				yield $block;
			}
			$blocks[$side] = $block;
		}
		$above_transparent = HelperUtils::getBlockAtSide($this->position, Facing::UP)->isTransparent();
		foreach($blocks as $side => $block){
			if(!($block instanceof self) && $this->canStronglyPower($side)){
				$side_opposite = Facing::opposite($side);
				foreach(Facing::ALL as $side2){
					if($side2 === $side_opposite){
						continue;
					}

					$block2 = HelperUtils::getBlockAtSide($block->position, $side2);
					if($block2 instanceof Transmittable){
						yield $block2;
					}
				}
			}

			if($above_transparent){
				$up = HelperUtils::getBlockAtSide($block->position, Facing::UP);
				if($up instanceof self){
					yield $up;
				}
			}

			if($block->isTransparent()){
				$down = HelperUtils::getBlockAtSide($block->position, Facing::DOWN);
				if($down instanceof self){
					yield $down;
				}
			}
		}
	}

	protected function recalculateBestPower() : int{
		$world = RedstoneWorldManager::$any->get($this->position->world);
		$above = HelperUtils::getBlockAtSide($this->position, Facing::UP);
		$below = HelperUtils::getBlockAtSide($this->position, Facing::DOWN);
		foreach([Facing::DOWN => $above, Facing::UP => $below] as $opposite_side => $block){
			foreach($world->getStronglyPoweringSources($block, $opposite_side) as $power_source){
				if(!($power_source instanceof self)){
					return 15;
				}
			}
		}

		$best_power = 0;
		$above_transparent = $above->isTransparent();
		foreach(Facing::HORIZONTAL as $side){
			$block = HelperUtils::getBlockAtSide($this->position, $side);
			if($block instanceof PowerSource){
				if($block instanceof self || $block->canPower(Facing::opposite($side))){
					$power = $block->getOutputPowerLevel();
					if($power > $best_power){
						$best_power = $power;
						if($best_power === 15){
							break;
						}
					}
				}
			}else{
				foreach($world->getStronglyPoweringSources($block, Facing::opposite($side)) as $power_source){
					if(!($power_source instanceof self)){
						return 15;
					}
				}
				if($above_transparent){
					$up = HelperUtils::getBlockAtSide($block->position, Facing::UP);
					if($up instanceof self){
						$power = $up->getOutputPowerLevel();
						if($power > $best_power){
							$best_power = $power;
							if($best_power === 15){
								break;
							}
						}
					}
				}
				if($block->isTransparent()){
					$down = HelperUtils::getBlockAtSide($block->position, Facing::DOWN);
					if($down instanceof self){
						$power = $down->getOutputPowerLevel();
						if($power > $best_power){
							$best_power = $power;
							if($best_power === 15){
								break;
							}
						}
					}
				}
			}
		}
		return $best_power;
	}

	public function asItem() : Item{
		return ExtraVanillaItems::REDSTONE_DUST();
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\Redstone as VanillaRedstone;
use redstone\block\power\PowerSource;

class Redstone extends VanillaRedstone implements PowerSource{
	use OptimizedBlockTrait;

	public function getPowerLevel() : int{
		return 15;
	}

	public function getOutputPowerLevel() : int{
		return $this->getPowerLevel();
	}

	public function canPower(int $side) : bool{
		return true;
	}

	public function canStronglyPower(int $side) : bool{
		return false;
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use Generator;
use pocketmine\block\Lever as VanillaLever;
use pocketmine\block\utils\LeverFacing;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\sound\RedstonePowerOffSound;
use pocketmine\world\sound\RedstonePowerOnSound;
use redstone\block\power\PowerSource;
use redstone\block\power\Transmittable;
use redstone\block\utils\HelperUtils;

class Lever extends VanillaLever implements PowerSource{
	use OptimizedBlockTrait;

	private ?int $supporting_side = null;

	public function getPowerLevel() : int{
		return $this->activated ? 15 : 0;
	}

	public function getOutputPowerLevel() : int{
		return $this->getPowerLevel();
	}

	public function canPower(int $side) : bool{
		return true;
	}

	/**
	 * @return Generator<Transmittable>
	 */
	protected function getRelyingOnSupportBlocks() : Generator{
		$supporting_side = $this->getSupportingSide();
		$supporting = HelperUtils::getBlockAtSide($this->position, $supporting_side);
		$skip_side = Facing::opposite($supporting_side);
		foreach(Facing::ALL as $side){
			if($side !== $skip_side){
				$block = HelperUtils::getBlockAtSide($supporting->position, $side);
				if($block instanceof Transmittable){
					yield $block;
				}
			}
		}
	}

	/**
	 * @return Generator<Transmittable>
	 */
	protected function getRelyingBlocks() : Generator{
		yield from $this->getRelyingOnSupportBlocks();
		$skip_side = $this->getSupportingSide();
		foreach(Facing::HORIZONTAL as $side){
			if($side !== $skip_side){
				$block = HelperUtils::getBlockAtSide($this->position, $side);
				if($block instanceof Transmittable){
					yield $block;
				}
			}
		}
	}

	public function setFacing(LeverFacing $facing) : VanillaLever{
		$this->supporting_side = null;
		return parent::setFacing($facing);
	}

	private function recalculateSupportingSide() : int{
		if($this->facing === LeverFacing::DOWN_AXIS_X || $this->facing === LeverFacing::DOWN_AXIS_Z){
			return Facing::UP;
		}

		if($this->facing === LeverFacing::UP_AXIS_X || $this->facing === LeverFacing::UP_AXIS_Z){
			return Facing::DOWN;
		}

		return Facing::opposite($this->facing->getFacing());
	}

	public function getSupportingSide() : int{
		return $this->supporting_side ??= $this->recalculateSupportingSide();
	}

	public function canStronglyPower(int $side) : bool{
		return $side === $this->getSupportingSide();
	}

	public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{
		if($this->activated){
			$this->activated = false;
			foreach($this->getRelyingOnSupportBlocks() as $block){
				$block->power($this);
			}
		}

		return parent::onBreak($item, $player, $returnedItems);
	}

	private function onInteractParent() : bool{
		$this->activated = !$this->activated;
		$world = $this->position->getWorld();
		$world->setBlock($this->position, $this, false);
		$world->addSound(
			$this->position->add(0.5, 0.5, 0.5),
			$this->activated ? new RedstonePowerOnSound() : new RedstonePowerOffSound()
		);
		return true;
	}

	public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
		if($this->onInteractParent()){
			foreach($this->getRelyingBlocks() as $side){
				$side->power($this);
			}
			return true;
		}
		return false;
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\item\Item;
use pocketmine\item\ItemTypeIds;
use redstone\vanilla\ExtraVanillaItems;

class RedstoneOre extends \pocketmine\block\RedstoneOre{

	public function getDropsForCompatibleTool(Item $item) : array{
		$items = parent::getDropsForCompatibleTool($item);
		foreach($items as $index => $i){
			if($i->getTypeId() === ItemTypeIds::REDSTONE_DUST){
				$items[$index] = ExtraVanillaItems::REDSTONE_DUST()->setCount($i->getCount());
			}
		}
		return $items;
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\Transparent;
use pocketmine\block\utils\AnyFacing;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\player\Player;
use redstone\block\utils\HelperUtils;

class PistonArmBlock extends Transparent implements AnyFacing{
	use OptimizedBlockTrait;

	protected int $facing = Facing::NORTH;

	protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{
		$w->facing($this->facing);
	}

	public function getFacing() : int{
		return $this->facing;
	}

	public function setFacing(int $facing) : self{
		$this->facing = $facing;
		return $this;
	}

	public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{
		$side = HelperUtils::getBlockAtSide($this->position, $this->getPistonSide());
		if($side instanceof Piston && !$this->position->world->useBreakOn($side->getPosition(), $item, $player, true, $returnedItems)){
			return false;
		}
		return parent::onBreak($item, $player, $returnedItems);
	}

	public function getPistonSide() : int{
		return $this->facing >= 2 ? $this->facing : Facing::opposite($this->facing);
	}

	public function getDropsForCompatibleTool(Item $item) : array{
		return [];
	}

	public function getSilkTouchDrops(Item $item) : array{
		return [];
	}
}<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\StonePressurePlate as VanillaStonePressurePlate;
use redstone\block\power\PowerSource;

class StonePressurePlate extends VanillaStonePressurePlate implements PowerSource{
	use PressurePlateTrait;
	use OptimizedBlockTrait;
}<?php

declare(strict_types=1);

namespace redstone\block;

use redstone\block\power\PowerableDoorTrait;
use redstone\block\power\Powerable;

final class CopperDoor extends \pocketmine\block\CopperDoor implements Powerable{
	use PowerableDoorTrait;
}<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\WoodenButton as VanillaWoodenButton;
use redstone\block\power\PowerSource;

class WoodenButton extends VanillaWoodenButton implements PowerSource{
	use ButtonTrait;
	use OptimizedBlockTrait;
}<?php

declare(strict_types=1);

namespace redstone\block;

use pocketmine\block\WeightedPressurePlateHeavy as VanillaWeightedPressurePlateHeavy;
use redstone\block\power\PowerSource;

class WeightedPressurePlateHeavy extends VanillaWeightedPressurePlateHeavy implements PowerSource{
	use OptimizedBlockTrait;
	use PressurePlateTrait;

	public function isPressed() : bool{
		return $this->signalStrength > 0;
	}

	public function setPressed(bool $value) : self{
		$this->signalStrength = $value ? 15 : 0;
		return $this;
	}
}<?php

declare(strict_types=1);

namespace redstone\vanilla;

use pocketmine\block\Block;

class Redstone extends \pocketmine\item\Redstone{

	public function getBlock(?int $clickedFace = null) : Block{
		return ExtraVanillaBlocks::REDSTONE_WIRE();
	}
}<?php

declare(strict_types=1);

namespace redstone\vanilla;

use InvalidArgumentException;
use pocketmine\block\Block;
use pocketmine\block\Door;
use pocketmine\block\RedstoneLamp;
use pocketmine\block\RuntimeBlockStateRegistry;
use pocketmine\block\utils\LeverFacing;
use pocketmine\block\VanillaBlocks;
use pocketmine\block\WoodenButton;
use pocketmine\data\bedrock\block\BlockStateNames as StateNames;
use pocketmine\data\bedrock\block\BlockStateSerializeException;
use pocketmine\data\bedrock\block\BlockStateStringValues as StringValues;
use pocketmine\data\bedrock\block\BlockTypeNames as Ids;
use pocketmine\data\bedrock\block\convert\BlockStateDeserializerHelper as Helper;
use pocketmine\data\bedrock\block\convert\BlockStateReader as Reader;
use pocketmine\data\bedrock\block\convert\BlockStateSerializerHelper;
use pocketmine\data\bedrock\block\convert\BlockStateWriter as Writer;
use pocketmine\data\bedrock\block\convert\BlockSerializerDeserializerRegistrar;
use pocketmine\data\bedrock\block\convert\FlattenedIdModel;
use pocketmine\data\bedrock\block\convert\Model;
use pocketmine\data\bedrock\block\convert\property\CommonProperties;
use pocketmine\data\bedrock\block\convert\property\IntFromRawStateMap;
use pocketmine\data\bedrock\item\ItemTypeNames;
use pocketmine\data\bedrock\item\SavedItemData;
use pocketmine\item\Item;
use pocketmine\item\StringToItemParser;
use pocketmine\item\VanillaItems;
use pocketmine\math\Facing;
use pocketmine\scheduler\AsyncPool;
use pocketmine\scheduler\AsyncTask;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use pocketmine\world\format\io\GlobalItemDataHandlers;
use redstone\block\CopperTrapdoor;
use redstone\block\Dispenser;
use redstone\block\FenceGate;
use redstone\block\Hopper;
use redstone\block\Lever;
use redstone\block\Observer;
use redstone\block\Piston;
use redstone\block\PistonArmBlock;
use redstone\block\RedstoneComparator;
use redstone\block\RedstoneRepeater;
use redstone\block\RedstoneTorch;
use redstone\block\RedstoneWire;
use redstone\block\StoneButton;
use redstone\block\StonePressurePlate;
use redstone\block\TNT;
use redstone\block\Trapdoor;
use redstone\block\TrappedChest;
use redstone\block\WeightedPressurePlateHeavy;
use redstone\block\WeightedPressurePlateLight;
use redstone\block\WoodenPressurePlate;
use redstone\block\WoodenTrapdoor;
use ReflectionProperty;
use function mb_strtoupper;

final class ExtraVanillaData{

	public static function registerOnAllThreads(AsyncPool $pool) : void{
		self::registerOnCurrentThread();
		$pool->addWorkerStartHook(function(int $worker) use($pool) : void{
			$pool->submitTaskToWorker(new class extends AsyncTask{
				public function onRun() : void{
					ExtraVanillaData::registerOnCurrentThread();
				}
			}, $worker);
		});
	}

	public static function registerOnCurrentThread() : void{
		self::registerBlocks();
		self::registerItems();
	}

	private static function registerBlocks() : void{
		$serializer = GlobalBlockStateHandlers::getSerializer();
		$deserializer = GlobalBlockStateHandlers::getDeserializer();

		self::registerSimpleBlock(Ids::ACACIA_BUTTON, ExtraVanillaBlocks::ACACIA_BUTTON(), ["acacia_button"], false);
		self::registerSimpleBlock(Ids::ACACIA_PRESSURE_PLATE, ExtraVanillaBlocks::ACACIA_PRESSURE_PLATE(), ["acacia_pressure_plate"], false);
		self::registerSimpleBlock(Ids::BIRCH_BUTTON, ExtraVanillaBlocks::BIRCH_BUTTON(), ["birch_button"], false);
		self::registerSimpleBlock(Ids::BIRCH_PRESSURE_PLATE, ExtraVanillaBlocks::BIRCH_PRESSURE_PLATE(), ["birch_pressure_plate"], false);
		self::registerSimpleBlock(Ids::CHERRY_BUTTON, ExtraVanillaBlocks::CHERRY_BUTTON(), ["cherry_button"], false);
		self::registerSimpleBlock(Ids::CHERRY_PRESSURE_PLATE, ExtraVanillaBlocks::CHERRY_PRESSURE_PLATE(), ["cherry_pressure_plate"], false);
		self::registerSimpleBlock(Ids::CRIMSON_BUTTON, ExtraVanillaBlocks::CRIMSON_BUTTON(), ["crimson_button"], false);
		self::registerSimpleBlock(Ids::CRIMSON_PRESSURE_PLATE, ExtraVanillaBlocks::CRIMSON_PRESSURE_PLATE(), ["crimson_pressure_plate"], false);
		self::registerSimpleBlock(Ids::DARK_OAK_BUTTON, ExtraVanillaBlocks::DARK_OAK_BUTTON(), ["dark_oak_button"], false);
		self::registerSimpleBlock(Ids::DARK_OAK_PRESSURE_PLATE, ExtraVanillaBlocks::DARK_OAK_PRESSURE_PLATE(), ["dark_oak_pressure_plate"], false);
		self::registerSimpleBlock(Ids::DISPENSER, ExtraVanillaBlocks::DISPENSER(), ["dispenser"], false);
		self::registerSimpleBlock(Ids::DROPPER, ExtraVanillaBlocks::DROPPER(), ["dropper"], false);
		self::registerSimpleBlock(Ids::HOPPER, ExtraVanillaBlocks::HOPPER(), [], false);
		self::registerSimpleBlock(Ids::TRAPPED_CHEST, ExtraVanillaBlocks::TRAPPED_CHEST(), [], false);
		self::registerSimpleBlock(Ids::HEAVY_WEIGHTED_PRESSURE_PLATE, ExtraVanillaBlocks::WEIGHTED_PRESSURE_PLATE_HEAVY(), ["heavy_weighted_pressure_plate"], false);
		self::registerSimpleBlock(Ids::JUNGLE_BUTTON, ExtraVanillaBlocks::JUNGLE_BUTTON(), ["jungle_button"], false);
		self::registerSimpleBlock(Ids::JUNGLE_PRESSURE_PLATE, ExtraVanillaBlocks::JUNGLE_PRESSURE_PLATE(), ["jungle_pressure_plate"], false);
		self::registerSimpleBlock(Ids::LEVER, ExtraVanillaBlocks::LEVER(), ["lever"], false);
		self::registerSimpleBlock(Ids::LIGHT_WEIGHTED_PRESSURE_PLATE, ExtraVanillaBlocks::WEIGHTED_PRESSURE_PLATE_LIGHT(), ["light_weighted_pressure_plate"], false);
		self::registerSimpleBlock(Ids::MANGROVE_BUTTON, ExtraVanillaBlocks::MANGROVE_BUTTON(), ["mangrove_button"], false);
		self::registerSimpleBlock(Ids::MANGROVE_PRESSURE_PLATE, ExtraVanillaBlocks::MANGROVE_PRESSURE_PLATE(), ["mangrove_pressure_plate"], false);
		self::registerSimpleBlock(Ids::OBSERVER, ExtraVanillaBlocks::OBSERVER(), ["observer"], false);
		self::registerSimpleBlock(Ids::WOODEN_BUTTON, ExtraVanillaBlocks::OAK_BUTTON(), ["wooden_button"], false);
		self::registerSimpleBlock(Ids::WOODEN_PRESSURE_PLATE, ExtraVanillaBlocks::OAK_PRESSURE_PLATE(), ["wooden_pressure_plate"], false);
		self::registerSimpleBlock(Ids::PISTON, ExtraVanillaBlocks::PISTON(), ["piston"], false);
		self::registerSimpleBlock(Ids::PISTON_ARM_COLLISION, ExtraVanillaBlocks::PISTON_ARM_BLOCK(), ["piston_arm_collision"], false);
		self::registerSimpleBlock(Ids::POLISHED_BLACKSTONE_BUTTON, ExtraVanillaBlocks::POLISHED_BLACKSTONE_BUTTON(), ["polished_blackstone_button"], false);
		self::registerSimpleBlock(Ids::POLISHED_BLACKSTONE_PRESSURE_PLATE, ExtraVanillaBlocks::POLISHED_BLACKSTONE_PRESSURE_PLATE(), ["polished_blackstone_pressure_plate"], false);
		self::registerSimpleBlock(Ids::REDSTONE_BLOCK, ExtraVanillaBlocks::REDSTONE(), ["redstone_block"]);
		self::registerSimpleBlock(Ids::REDSTONE_ORE, ExtraVanillaBlocks::REDSTONE_ORE(), ["redstone_ore"]);
		self::registerSimpleBlock(Ids::UNPOWERED_COMPARATOR, ExtraVanillaBlocks::REDSTONE_COMPARATOR(), ["powered_comparator", "unpowered_comparator"]);
		self::registerSimpleBlock(Ids::REDSTONE_LAMP, ExtraVanillaBlocks::REDSTONE_LAMP(), ["redstone_lamp", "lit_redstone_lamp"], false);
		self::registerSimpleBlock(Ids::POWERED_REPEATER, ExtraVanillaBlocks::REDSTONE_REPEATER(), ["powered_repeater", "unpowered_repeater"], false);
		self::registerSimpleBlock(Ids::REDSTONE_TORCH, ExtraVanillaBlocks::REDSTONE_TORCH(), ["redstone_torch"], false);
		self::registerSimpleBlock(Ids::REDSTONE_WIRE, ExtraVanillaBlocks::REDSTONE_WIRE(), ["redstone_wire"], false);
		self::registerSimpleBlock(Ids::SPRUCE_BUTTON, ExtraVanillaBlocks::SPRUCE_BUTTON(), ["spruce_button"], false);
		self::registerSimpleBlock(Ids::SPRUCE_PRESSURE_PLATE, ExtraVanillaBlocks::SPRUCE_PRESSURE_PLATE(), ["spruce_pressure_plate"], false);
		self::registerSimpleBlock(Ids::STICKY_PISTON, ExtraVanillaBlocks::STICKY_PISTON(), ["sticky_piston"], false);
		self::registerSimpleBlock(Ids::STONE_BUTTON, ExtraVanillaBlocks::STONE_BUTTON(), ["stone_button"], false);
		self::registerSimpleBlock(Ids::STONE_PRESSURE_PLATE, ExtraVanillaBlocks::STONE_PRESSURE_PLATE(), ["stone_pressure_plate"], false);
		self::registerSimpleBlock(Ids::TNT, ExtraVanillaBlocks::TNT(), ["tnt"], false);
		self::registerSimpleBlock(Ids::WARPED_BUTTON, ExtraVanillaBlocks::WARPED_BUTTON(), ["warped_button"], false);
		self::registerSimpleBlock(Ids::WARPED_PRESSURE_PLATE, ExtraVanillaBlocks::WARPED_PRESSURE_PLATE(), ["warped_pressure_plate"], false);

		self::registerSimpleBlock(Ids::IRON_DOOR, ExtraVanillaBlocks::IRON_DOOR(), [], false);
		self::registerSimpleBlock(Ids::ACACIA_DOOR, ExtraVanillaBlocks::ACACIA_DOOR(), [], false);
		self::registerSimpleBlock(Ids::BIRCH_DOOR, ExtraVanillaBlocks::BIRCH_DOOR(), [], false);
		self::registerSimpleBlock(Ids::CHERRY_DOOR, ExtraVanillaBlocks::CHERRY_DOOR(), [], false);
		self::registerSimpleBlock(Ids::CRIMSON_DOOR, ExtraVanillaBlocks::CRIMSON_DOOR(), [], false);
		self::registerSimpleBlock(Ids::DARK_OAK_DOOR, ExtraVanillaBlocks::DARK_OAK_DOOR(), [], false);
		self::registerSimpleBlock(Ids::JUNGLE_DOOR, ExtraVanillaBlocks::JUNGLE_DOOR(), [], false);
		self::registerSimpleBlock(Ids::MANGROVE_DOOR, ExtraVanillaBlocks::MANGROVE_DOOR(), [], false);
		self::registerSimpleBlock(Ids::WOODEN_DOOR, ExtraVanillaBlocks::OAK_DOOR(), [], false);
		self::registerSimpleBlock(Ids::PALE_OAK_DOOR, ExtraVanillaBlocks::PALE_OAK_DOOR(), [], false);
		self::registerSimpleBlock(Ids::SPRUCE_DOOR, ExtraVanillaBlocks::SPRUCE_DOOR(), [], false);
		self::registerSimpleBlock(Ids::WARPED_DOOR, ExtraVanillaBlocks::WARPED_DOOR(), [], false);
		self::registerSimpleBlock(Ids::COPPER_DOOR, ExtraVanillaBlocks::COPPER_DOOR(), [], false);

		$deserializer->map(Ids::IRON_DOOR, fn(Reader $in) => Helper::decodeDoor(ExtraVanillaBlocks::IRON_DOOR(), $in));
		$deserializer->map(Ids::ACACIA_DOOR, fn(Reader $in) => Helper::decodeDoor(ExtraVanillaBlocks::ACACIA_DOOR(), $in));
		$deserializer->map(Ids::BIRCH_DOOR, fn(Reader $in) => Helper::decodeDoor(ExtraVanillaBlocks::BIRCH_DOOR(), $in));
		$deserializer->map(Ids::CHERRY_DOOR, fn(Reader $in) => Helper::decodeDoor(ExtraVanillaBlocks::CHERRY_DOOR(), $in));
		$deserializer->map(Ids::CRIMSON_DOOR, fn(Reader $in) => Helper::decodeDoor(ExtraVanillaBlocks::CRIMSON_DOOR(), $in));
		$deserializer->map(Ids::DARK_OAK_DOOR, fn(Reader $in) => Helper::decodeDoor(ExtraVanillaBlocks::DARK_OAK_DOOR(), $in));
		$deserializer->map(Ids::JUNGLE_DOOR, fn(Reader $in) => Helper::decodeDoor(ExtraVanillaBlocks::JUNGLE_DOOR(), $in));
		$deserializer->map(Ids::MANGROVE_DOOR, fn(Reader $in) => Helper::decodeDoor(ExtraVanillaBlocks::MANGROVE_DOOR(), $in));
		$deserializer->map(Ids::WOODEN_DOOR, fn(Reader $in) => Helper::decodeDoor(ExtraVanillaBlocks::OAK_DOOR(), $in));
		$deserializer->map(Ids::PALE_OAK_DOOR, fn(Reader $in) => Helper::decodeDoor(ExtraVanillaBlocks::PALE_OAK_DOOR(), $in));
		$deserializer->map(Ids::SPRUCE_DOOR, fn(Reader $in) => Helper::decodeDoor(ExtraVanillaBlocks::SPRUCE_DOOR(), $in));
		$deserializer->map(Ids::WARPED_DOOR, fn(Reader $in) => Helper::decodeDoor(ExtraVanillaBlocks::WARPED_DOOR(), $in));

		$serializer->map(ExtraVanillaBlocks::IRON_DOOR(), fn(Door $block) => BlockStateSerializerHelper::encodeDoor($block, new Writer(Ids::IRON_DOOR)));
		$serializer->map(ExtraVanillaBlocks::ACACIA_DOOR(), fn(Door $block) => BlockStateSerializerHelper::encodeDoor($block, new Writer(Ids::ACACIA_DOOR)));
		$serializer->map(ExtraVanillaBlocks::BIRCH_DOOR(), fn(Door $block) => BlockStateSerializerHelper::encodeDoor($block, new Writer(Ids::BIRCH_DOOR)));
		$serializer->map(ExtraVanillaBlocks::CHERRY_DOOR(), fn(Door $block) => BlockStateSerializerHelper::encodeDoor($block, new Writer(Ids::CHERRY_DOOR)));
		$serializer->map(ExtraVanillaBlocks::CRIMSON_DOOR(), fn(Door $block) => BlockStateSerializerHelper::encodeDoor($block, new Writer(Ids::CRIMSON_DOOR)));
		$serializer->map(ExtraVanillaBlocks::DARK_OAK_DOOR(), fn(Door $block) => BlockStateSerializerHelper::encodeDoor($block, new Writer(Ids::DARK_OAK_DOOR)));
		$serializer->map(ExtraVanillaBlocks::JUNGLE_DOOR(), fn(Door $block) => BlockStateSerializerHelper::encodeDoor($block, new Writer(Ids::JUNGLE_DOOR)));
		$serializer->map(ExtraVanillaBlocks::MANGROVE_DOOR(), fn(Door $block) => BlockStateSerializerHelper::encodeDoor($block, new Writer(Ids::MANGROVE_DOOR)));
		$serializer->map(ExtraVanillaBlocks::OAK_DOOR(), fn(Door $block) => BlockStateSerializerHelper::encodeDoor($block, new Writer(Ids::WOODEN_DOOR)));
		$serializer->map(ExtraVanillaBlocks::PALE_OAK_DOOR(), fn(Door $block) => BlockStateSerializerHelper::encodeDoor($block, new Writer(Ids::PALE_OAK_DOOR)));
		$serializer->map(ExtraVanillaBlocks::SPRUCE_DOOR(), fn(Door $block) => BlockStateSerializerHelper::encodeDoor($block, new Writer(Ids::SPRUCE_DOOR)));
		$serializer->map(ExtraVanillaBlocks::WARPED_DOOR(), fn(Door $block) => BlockStateSerializerHelper::encodeDoor($block, new Writer(Ids::WARPED_DOOR)));

		$deserializer->map(Ids::DISPENSER, function(Reader $in) : Block{
			return ExtraVanillaBlocks::DISPENSER()
				->setFacing($in->readFacingDirection())
				->setPowered($in->readBool(StateNames::TRIGGERED_BIT));
		});
		$serializer->map(ExtraVanillaBlocks::DISPENSER(), function(Dispenser $block) : Writer{
			return Writer::create(Ids::DISPENSER)
				->writeBool(StateNames::TRIGGERED_BIT, $block->isPowered())
				->writeFacingDirection($block->getFacing());
		});

		$deserializer->map(Ids::DROPPER, function(Reader $in) : Block{
			return ExtraVanillaBlocks::DROPPER()
				->setFacing($in->readFacingDirection())
				->setPowered($in->readBool(StateNames::TRIGGERED_BIT));
		});
		$serializer->map(ExtraVanillaBlocks::DROPPER(), function(Dispenser $block) : Writer{
			return Writer::create(Ids::DROPPER)
				->writeBool(StateNames::TRIGGERED_BIT, $block->isPowered())
				->writeFacingDirection($block->getFacing());
		});

		$deserializer->map(Ids::HOPPER, function(Reader $in) : Block{
			return ExtraVanillaBlocks::HOPPER()
				->setFacing($in->readFacingWithoutUp())
				->setPowered($in->readBool(StateNames::TOGGLE_BIT));
		});
		$serializer->map(ExtraVanillaBlocks::HOPPER(), function(Hopper $block) : Writer{
			return Writer::create(Ids::HOPPER)
				->writeBool(StateNames::TOGGLE_BIT, $block->isPowered())
				->writeFacingWithoutUp($block->getFacing());
		});
		self::unregisterSimpleItem(ItemTypeNames::HOPPER, VanillaBlocks::HOPPER()->asItem());

		$deserializer->map(Ids::TRAPPED_CHEST, function(Reader $in) : Block{
			return ExtraVanillaBlocks::TRAPPED_CHEST()
				->setFacing($in->readCardinalHorizontalFacing());
		});
		$serializer->map(ExtraVanillaBlocks::TRAPPED_CHEST(), function(TrappedChest $block) : Writer{
			return Writer::create(Ids::TRAPPED_CHEST)
				->writeCardinalHorizontalFacing($block->getFacing());
		});

		self::registerOpenableBlocks();

		$deserializer->map(Ids::OBSERVER, function(Reader $in) : Block{
			return ExtraVanillaBlocks::OBSERVER()
				->setFacing($in->mapIntFromString(StateNames::MC_FACING_DIRECTION, self::observerFacingMap()))
				->setPowered($in->readBool(StateNames::POWERED_BIT));
		});
		$serializer->map(ExtraVanillaBlocks::OBSERVER(), function(Observer $block) : Writer{
			return Writer::create(Ids::OBSERVER)
				->mapIntToString(StateNames::MC_FACING_DIRECTION, self::observerFacingMap(), $block->getFacing())
				->writeBool(StateNames::POWERED_BIT, $block->isPowered());
		});

		self::unmapBlockSerializers(Ids::PISTON, ExtraVanillaBlocks::PISTON());
		self::unmapBlockSerializers(Ids::STICKY_PISTON, ExtraVanillaBlocks::STICKY_PISTON());
		$deserializer->map(Ids::PISTON, function(Reader $in) : Block{
			return ExtraVanillaBlocks::PISTON()
				->setFacing($in->readFacingDirection());
		});
		$deserializer->map(Ids::STICKY_PISTON, function(Reader $in) : Block{
			return ExtraVanillaBlocks::STICKY_PISTON()
				->setFacing($in->readFacingDirection());
		});
		$serializer->map(ExtraVanillaBlocks::PISTON(), function(Piston $block) : Writer{
			return Writer::create(Ids::PISTON)
				->writeFacingDirection($block->getFacing());
		});
		$serializer->map(ExtraVanillaBlocks::STICKY_PISTON(), function(Piston $block) : Writer{
			return Writer::create(Ids::STICKY_PISTON)
				->writeFacingDirection($block->getFacing());
		});

		$deserializer->map(Ids::PISTON_ARM_COLLISION, function(Reader $in) : Block{
			return ExtraVanillaBlocks::PISTON_ARM_BLOCK()
				->setFacing($in->readFacingDirection());
		});
		$serializer->map(ExtraVanillaBlocks::PISTON_ARM_BLOCK(), function(PistonArmBlock $block) : Writer{
			return Writer::create(Ids::PISTON_ARM_COLLISION)
				->writeFacingDirection($block->getFacing());
		});

		self::unmapBlockSerializers(Ids::REDSTONE_TORCH, ExtraVanillaBlocks::REDSTONE_TORCH());
		self::unmapBlockSerializers(Ids::UNLIT_REDSTONE_TORCH, ExtraVanillaBlocks::REDSTONE_TORCH());
		$deserializer->map(Ids::REDSTONE_TORCH, function(Reader $in) : Block{
			return ExtraVanillaBlocks::REDSTONE_TORCH()
				->setFacing($in->readTorchFacing())
				->setLit();
		});
		$deserializer->map(Ids::UNLIT_REDSTONE_TORCH, function(Reader $in) : Block{
			return ExtraVanillaBlocks::REDSTONE_TORCH()
				->setFacing($in->readTorchFacing())
				->setLit(false);
		});
		$serializer->map(ExtraVanillaBlocks::REDSTONE_TORCH(), function(RedstoneTorch $block) : Writer{
			return Writer::create($block->isLit() ? Ids::REDSTONE_TORCH : Ids::UNLIT_REDSTONE_TORCH)
				->writeTorchFacing($block->getFacing());
		});

		self::unmapBlockSerializers(Ids::REDSTONE_WIRE, ExtraVanillaBlocks::REDSTONE_WIRE());
		$deserializer->map(Ids::REDSTONE_WIRE, function(Reader $in) : Block{
			return ExtraVanillaBlocks::REDSTONE_WIRE()
				->setOutputSignalStrength($in->readBoundedInt(StateNames::REDSTONE_SIGNAL, 0, 15));
		});
		$serializer->map(ExtraVanillaBlocks::REDSTONE_WIRE(), function(RedstoneWire $block) : Writer{
			return Writer::create(Ids::REDSTONE_WIRE)
				->writeInt(StateNames::REDSTONE_SIGNAL, $block->getOutputSignalStrength());
		});

		self::unmapBlockSerializers(Ids::LIT_REDSTONE_LAMP, ExtraVanillaBlocks::REDSTONE_LAMP());
		self::unmapBlockSerializers(Ids::REDSTONE_LAMP, ExtraVanillaBlocks::REDSTONE_LAMP());
		$deserializer->map(Ids::REDSTONE_LAMP, fn() => ExtraVanillaBlocks::REDSTONE_LAMP()->setPowered(false));
		$deserializer->map(Ids::LIT_REDSTONE_LAMP, fn() => ExtraVanillaBlocks::REDSTONE_LAMP()->setPowered(true));
		$serializer->map(ExtraVanillaBlocks::REDSTONE_LAMP(), fn(RedstoneLamp $block) => new Writer($block->isPowered() ? Ids::LIT_REDSTONE_LAMP : Ids::REDSTONE_LAMP));

		self::unmapBlockSerializers(Ids::POWERED_COMPARATOR, ExtraVanillaBlocks::REDSTONE_COMPARATOR());
		self::unmapBlockSerializers(Ids::UNPOWERED_COMPARATOR, ExtraVanillaBlocks::REDSTONE_COMPARATOR());
		$deserializer->map(Ids::POWERED_COMPARATOR, fn(Reader $in) => Helper::decodeComparator(ExtraVanillaBlocks::REDSTONE_COMPARATOR(), $in));
		$deserializer->map(Ids::UNPOWERED_COMPARATOR, fn(Reader $in) => Helper::decodeComparator(ExtraVanillaBlocks::REDSTONE_COMPARATOR(), $in));
		$serializer->map(ExtraVanillaBlocks::REDSTONE_COMPARATOR(), function(RedstoneComparator $block) : Writer{
			return Writer::create($block->isPowered() ? Ids::POWERED_COMPARATOR : Ids::UNPOWERED_COMPARATOR)
				->writeBool(StateNames::OUTPUT_LIT_BIT, $block->isPowered())
				->writeBool(StateNames::OUTPUT_SUBTRACT_BIT, $block->isSubtractMode())
				->writeCardinalHorizontalFacing($block->getFacing());
		});
		self::unregisterSimpleItem(ItemTypeNames::COMPARATOR, VanillaBlocks::REDSTONE_COMPARATOR()->asItem());

		self::unmapBlockSerializers(Ids::POWERED_REPEATER, ExtraVanillaBlocks::REDSTONE_REPEATER());
		self::unmapBlockSerializers(Ids::UNPOWERED_REPEATER, ExtraVanillaBlocks::REDSTONE_REPEATER());
		$deserializer->map(Ids::POWERED_REPEATER, fn(Reader $in) => Helper::decodeRepeater(ExtraVanillaBlocks::REDSTONE_REPEATER(), $in)->setPowered(true));
		$deserializer->map(Ids::UNPOWERED_REPEATER, fn(Reader $in) => Helper::decodeRepeater(ExtraVanillaBlocks::REDSTONE_REPEATER(), $in)->setPowered(false));
		$serializer->map(ExtraVanillaBlocks::REDSTONE_REPEATER(), function(RedstoneRepeater $block) : Writer{
			return Writer::create($block->isPowered() ? Ids::POWERED_REPEATER : Ids::UNPOWERED_REPEATER)
				->writeCardinalHorizontalFacing($block->getFacing())
				->writeInt(StateNames::REPEATER_DELAY, $block->getDelay() - 1);
		});
		self::unregisterSimpleItem(ItemTypeNames::REPEATER, VanillaBlocks::REDSTONE_REPEATER()->asItem());

		self::unmapBlockSerializers(Ids::LEVER, ExtraVanillaBlocks::LEVER());
		$deserializer->map(Ids::LEVER, function(Reader $in) : Block{
			return ExtraVanillaBlocks::LEVER()
				->setActivated($in->readBool(StateNames::OPEN_BIT))
				->setFacing(match($value = $in->readString(StateNames::LEVER_DIRECTION)){
					StringValues::LEVER_DIRECTION_DOWN_NORTH_SOUTH => LeverFacing::DOWN_AXIS_Z,
					StringValues::LEVER_DIRECTION_DOWN_EAST_WEST => LeverFacing::DOWN_AXIS_X,
					StringValues::LEVER_DIRECTION_UP_NORTH_SOUTH => LeverFacing::UP_AXIS_Z,
					StringValues::LEVER_DIRECTION_UP_EAST_WEST => LeverFacing::UP_AXIS_X,
					StringValues::LEVER_DIRECTION_NORTH => LeverFacing::NORTH,
					StringValues::LEVER_DIRECTION_SOUTH => LeverFacing::SOUTH,
					StringValues::LEVER_DIRECTION_WEST => LeverFacing::WEST,
					StringValues::LEVER_DIRECTION_EAST => LeverFacing::EAST,
					default => throw $in->badValueException(StateNames::LEVER_DIRECTION, $value),
				});
		});

		$serializer->map(ExtraVanillaBlocks::LEVER(), function(Lever $block) : Writer{
			return Writer::create(Ids::LEVER)
				->writeBool(StateNames::OPEN_BIT, $block->isActivated())
				->writeString(StateNames::LEVER_DIRECTION, match($block->getFacing()){
					LeverFacing::DOWN_AXIS_Z => StringValues::LEVER_DIRECTION_DOWN_NORTH_SOUTH,
					LeverFacing::DOWN_AXIS_X => StringValues::LEVER_DIRECTION_DOWN_EAST_WEST,
					LeverFacing::UP_AXIS_Z => StringValues::LEVER_DIRECTION_UP_NORTH_SOUTH,
					LeverFacing::UP_AXIS_X => StringValues::LEVER_DIRECTION_UP_EAST_WEST,
					LeverFacing::NORTH => StringValues::LEVER_DIRECTION_NORTH,
					LeverFacing::SOUTH => StringValues::LEVER_DIRECTION_SOUTH,
					LeverFacing::WEST => StringValues::LEVER_DIRECTION_WEST,
					LeverFacing::EAST => StringValues::LEVER_DIRECTION_EAST,
					default => throw new BlockStateSerializeException("Invalid Lever facing " . $block->getFacing()->name()),
				});
		});

		$deserializer->map(Ids::TNT, function(Reader $in) : Block{
			return ExtraVanillaBlocks::TNT()
				->setUnstable($in->readBool(StateNames::EXPLODE_BIT))
				->setWorksUnderwater(false);
		});
		$serializer->map(ExtraVanillaBlocks::TNT(), fn(TNT $block) => Writer::create($block->worksUnderwater() ? Ids::UNDERWATER_TNT : Ids::TNT)
			->writeBool(StateNames::EXPLODE_BIT, $block->isUnstable())
		);

		$serializer->map(ExtraVanillaBlocks::ACACIA_BUTTON(), fn(WoodenButton $block) => BlockStateSerializerHelper::encodeButton($block, new Writer(Ids::ACACIA_BUTTON)));
		$serializer->map(ExtraVanillaBlocks::BIRCH_BUTTON(), fn(WoodenButton $block) => BlockStateSerializerHelper::encodeButton($block, new Writer(Ids::BIRCH_BUTTON)));
		$serializer->map(ExtraVanillaBlocks::CRIMSON_BUTTON(), fn(WoodenButton $block) => BlockStateSerializerHelper::encodeButton($block, new Writer(Ids::CRIMSON_BUTTON)));
		$serializer->map(ExtraVanillaBlocks::DARK_OAK_BUTTON(), fn(WoodenButton $block) => BlockStateSerializerHelper::encodeButton($block, new Writer(Ids::DARK_OAK_BUTTON)));
		$serializer->map(ExtraVanillaBlocks::JUNGLE_BUTTON(), fn(WoodenButton $block) => BlockStateSerializerHelper::encodeButton($block, new Writer(Ids::JUNGLE_BUTTON)));
		$serializer->map(ExtraVanillaBlocks::MANGROVE_BUTTON(), fn(WoodenButton $block) => BlockStateSerializerHelper::encodeButton($block, new Writer(Ids::MANGROVE_BUTTON)));
		$serializer->map(ExtraVanillaBlocks::OAK_BUTTON(), fn(WoodenButton $block) => BlockStateSerializerHelper::encodeButton($block, new Writer(Ids::WOODEN_BUTTON)));
		$serializer->map(ExtraVanillaBlocks::POLISHED_BLACKSTONE_BUTTON(), fn(StoneButton $block) => BlockStateSerializerHelper::encodeButton($block, new Writer(Ids::POLISHED_BLACKSTONE_BUTTON)));
		$serializer->map(ExtraVanillaBlocks::SPRUCE_BUTTON(), fn(WoodenButton $block) => BlockStateSerializerHelper::encodeButton($block, new Writer(Ids::SPRUCE_BUTTON)));
		$serializer->map(ExtraVanillaBlocks::STONE_BUTTON(), fn(StoneButton $block) => BlockStateSerializerHelper::encodeButton($block, new Writer(Ids::STONE_BUTTON)));
		$serializer->map(ExtraVanillaBlocks::WARPED_BUTTON(), fn(WoodenButton $block) => BlockStateSerializerHelper::encodeButton($block, new Writer(Ids::WARPED_BUTTON)));

		$deserializer->map(Ids::ACACIA_BUTTON, fn(Reader $in) => Helper::decodeButton(ExtraVanillaBlocks::ACACIA_BUTTON(), $in));
		$deserializer->map(Ids::BIRCH_BUTTON, fn(Reader $in) => Helper::decodeButton(ExtraVanillaBlocks::BIRCH_BUTTON(), $in));
		$deserializer->map(Ids::CRIMSON_BUTTON, fn(Reader $in) => Helper::decodeButton(ExtraVanillaBlocks::CRIMSON_BUTTON(), $in));
		$deserializer->map(Ids::DARK_OAK_BUTTON, fn(Reader $in) => Helper::decodeButton(ExtraVanillaBlocks::DARK_OAK_BUTTON(), $in));
		$deserializer->map(Ids::JUNGLE_BUTTON, fn(Reader $in) => Helper::decodeButton(ExtraVanillaBlocks::JUNGLE_BUTTON(), $in));
		$deserializer->map(Ids::MANGROVE_BUTTON, fn(Reader $in) => Helper::decodeButton(ExtraVanillaBlocks::MANGROVE_BUTTON(), $in));
		$deserializer->map(Ids::WOODEN_BUTTON, fn(Reader $in) => Helper::decodeButton(ExtraVanillaBlocks::OAK_BUTTON(), $in));
		$deserializer->map(Ids::POLISHED_BLACKSTONE_BUTTON, fn(Reader $in) => Helper::decodeButton(ExtraVanillaBlocks::POLISHED_BLACKSTONE_BUTTON(), $in));
		$deserializer->map(Ids::SPRUCE_BUTTON, fn(Reader $in) => Helper::decodeButton(ExtraVanillaBlocks::SPRUCE_BUTTON(), $in));
		$deserializer->map(Ids::STONE_BUTTON, fn(Reader $in) => Helper::decodeButton(ExtraVanillaBlocks::STONE_BUTTON(), $in));
		$deserializer->map(Ids::WARPED_BUTTON, fn(Reader $in) => Helper::decodeButton(ExtraVanillaBlocks::WARPED_BUTTON(), $in));

		$serializer->map(ExtraVanillaBlocks::ACACIA_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => BlockStateSerializerHelper::encodeSimplePressurePlate($block, new Writer(Ids::ACACIA_PRESSURE_PLATE)));
		$serializer->map(ExtraVanillaBlocks::BIRCH_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => BlockStateSerializerHelper::encodeSimplePressurePlate($block, new Writer(Ids::BIRCH_PRESSURE_PLATE)));
		$serializer->map(ExtraVanillaBlocks::CRIMSON_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => BlockStateSerializerHelper::encodeSimplePressurePlate($block, new Writer(Ids::CRIMSON_PRESSURE_PLATE)));
		$serializer->map(ExtraVanillaBlocks::DARK_OAK_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => BlockStateSerializerHelper::encodeSimplePressurePlate($block, new Writer(Ids::DARK_OAK_PRESSURE_PLATE)));
		$serializer->map(ExtraVanillaBlocks::WEIGHTED_PRESSURE_PLATE_HEAVY(), fn(WeightedPressurePlateHeavy $block) => Writer::create(Ids::HEAVY_WEIGHTED_PRESSURE_PLATE)->writeInt(StateNames::REDSTONE_SIGNAL, $block->getOutputSignalStrength()));
		$serializer->map(ExtraVanillaBlocks::JUNGLE_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => BlockStateSerializerHelper::encodeSimplePressurePlate($block, new Writer(Ids::JUNGLE_PRESSURE_PLATE)));
		$serializer->map(ExtraVanillaBlocks::WEIGHTED_PRESSURE_PLATE_LIGHT(), fn(WeightedPressurePlateLight $block) => Writer::create(Ids::LIGHT_WEIGHTED_PRESSURE_PLATE)->writeInt(StateNames::REDSTONE_SIGNAL, $block->getOutputSignalStrength()));
		$serializer->map(ExtraVanillaBlocks::MANGROVE_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => BlockStateSerializerHelper::encodeSimplePressurePlate($block, new Writer(Ids::MANGROVE_PRESSURE_PLATE)));
		$serializer->map(ExtraVanillaBlocks::OAK_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => BlockStateSerializerHelper::encodeSimplePressurePlate($block, new Writer(Ids::WOODEN_PRESSURE_PLATE)));
		$serializer->map(ExtraVanillaBlocks::POLISHED_BLACKSTONE_PRESSURE_PLATE(), fn(StonePressurePlate $block) => BlockStateSerializerHelper::encodeSimplePressurePlate($block, new Writer(Ids::POLISHED_BLACKSTONE_PRESSURE_PLATE)));
		$serializer->map(ExtraVanillaBlocks::SPRUCE_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => BlockStateSerializerHelper::encodeSimplePressurePlate($block, new Writer(Ids::SPRUCE_PRESSURE_PLATE)));
		$serializer->map(ExtraVanillaBlocks::STONE_PRESSURE_PLATE(), fn(StonePressurePlate $block) => BlockStateSerializerHelper::encodeSimplePressurePlate($block, new Writer(Ids::STONE_PRESSURE_PLATE)));
		$serializer->map(ExtraVanillaBlocks::WARPED_PRESSURE_PLATE(), fn(WoodenPressurePlate $block) => BlockStateSerializerHelper::encodeSimplePressurePlate($block, new Writer(Ids::WARPED_PRESSURE_PLATE)));

		$deserializer->map(Ids::ACACIA_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(ExtraVanillaBlocks::ACACIA_PRESSURE_PLATE(), $in));
		$deserializer->map(Ids::BIRCH_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(ExtraVanillaBlocks::BIRCH_PRESSURE_PLATE(), $in));
		$deserializer->map(Ids::CRIMSON_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(ExtraVanillaBlocks::CRIMSON_PRESSURE_PLATE(), $in));
		$deserializer->map(Ids::DARK_OAK_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(ExtraVanillaBlocks::DARK_OAK_PRESSURE_PLATE(), $in));
		$deserializer->map(Ids::HEAVY_WEIGHTED_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeWeightedPressurePlate(ExtraVanillaBlocks::WEIGHTED_PRESSURE_PLATE_HEAVY(), $in));
		$deserializer->map(Ids::JUNGLE_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(ExtraVanillaBlocks::JUNGLE_PRESSURE_PLATE(), $in));
		$deserializer->map(Ids::LIGHT_WEIGHTED_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeWeightedPressurePlate(ExtraVanillaBlocks::WEIGHTED_PRESSURE_PLATE_LIGHT(), $in));
		$deserializer->map(Ids::MANGROVE_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(ExtraVanillaBlocks::MANGROVE_PRESSURE_PLATE(), $in));
		$deserializer->map(Ids::WOODEN_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(ExtraVanillaBlocks::OAK_PRESSURE_PLATE(), $in));
		$deserializer->map(Ids::POLISHED_BLACKSTONE_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(ExtraVanillaBlocks::POLISHED_BLACKSTONE_PRESSURE_PLATE(), $in));
		$deserializer->map(Ids::SPRUCE_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(ExtraVanillaBlocks::SPRUCE_PRESSURE_PLATE(), $in));
		$deserializer->map(Ids::STONE_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(ExtraVanillaBlocks::STONE_PRESSURE_PLATE(), $in));
		$deserializer->map(Ids::WARPED_PRESSURE_PLATE, fn(Reader $in) => Helper::decodeSimplePressurePlate(ExtraVanillaBlocks::WARPED_PRESSURE_PLATE(), $in));
	}

	private static function registerOpenableBlocks() : void{
		$serializer = GlobalBlockStateHandlers::getSerializer();
		$registrar = new BlockSerializerDeserializerRegistrar(GlobalBlockStateHandlers::getDeserializer(), $serializer);
		$common_properties = CommonProperties::getInstance();
		$extra_blocks = ExtraVanillaBlocks::getAll();

		foreach(VanillaBlocks::getAll() as $name => $vanilla){
			$block = $extra_blocks[mb_strtoupper($name)] ?? null;
			if($block instanceof CopperTrapdoor){
				RuntimeBlockStateRegistry::getInstance()->register($block);
				$registrar->mapFlattenedId(FlattenedIdModel::create($block)
					->idComponents([...$common_properties->copperIdPrefixes, "copper_trapdoor"])
					->properties($common_properties->trapdoorProperties)
				);
			}elseif($block instanceof Trapdoor || $block instanceof WoodenTrapdoor){
				RuntimeBlockStateRegistry::getInstance()->register($block);
				$registrar->mapModel(Model::create($block, $serializer->serializeBlock($vanilla)->getName())->properties($common_properties->trapdoorProperties));
			}elseif($block instanceof FenceGate){
				RuntimeBlockStateRegistry::getInstance()->register($block);
				$registrar->mapModel(Model::create($block, $serializer->serializeBlock($vanilla)->getName())->properties($common_properties->fenceGateProperties));
			}else{
				continue;
			}
			self::mapStringToItem($block, StringToItemParser::getInstance()->lookupBlockAliases($vanilla));
		}
	}

	private static function observerFacingMap() : IntFromRawStateMap{
		static $map = null;
		return $map ??= IntFromRawStateMap::string([
			Facing::DOWN => StringValues::MC_FACING_DIRECTION_DOWN,
			Facing::UP => StringValues::MC_FACING_DIRECTION_UP,
			Facing::NORTH => StringValues::MC_FACING_DIRECTION_NORTH,
			Facing::SOUTH => StringValues::MC_FACING_DIRECTION_SOUTH,
			Facing::WEST => StringValues::MC_FACING_DIRECTION_WEST,
			Facing::EAST => StringValues::MC_FACING_DIRECTION_EAST,
		]);
	}

	private static function registerItems() : void{
		$parser = StringToItemParser::getInstance();
		self::registerSimpleItem(ItemTypeNames::REDSTONE, ExtraVanillaItems::REDSTONE_DUST(), $parser->lookupAliases(VanillaItems::REDSTONE_DUST()));
		self::map1to1Block(ItemTypeNames::COMPARATOR, ExtraVanillaBlocks::REDSTONE_COMPARATOR(), $parser->lookupBlockAliases(VanillaBlocks::REDSTONE_COMPARATOR()));
		self::map1to1Block(ItemTypeNames::REPEATER, ExtraVanillaBlocks::REDSTONE_REPEATER(), $parser->lookupBlockAliases(VanillaBlocks::REDSTONE_REPEATER()));
		self::map1to1Block(ItemTypeNames::HOPPER, ExtraVanillaBlocks::HOPPER(), $parser->lookupBlockAliases(VanillaBlocks::HOPPER()));
		self::mapStringToItem(ExtraVanillaBlocks::TRAPPED_CHEST(), $parser->lookupBlockAliases(VanillaBlocks::TRAPPED_CHEST()));
		self::map1to1Block(Ids::PISTON, ExtraVanillaBlocks::PISTON(), []);
		self::map1to1Block(Ids::STICKY_PISTON, ExtraVanillaBlocks::STICKY_PISTON(), []);

		foreach([
			[ItemTypeNames::IRON_DOOR, VanillaBlocks::IRON_DOOR(), ExtraVanillaBlocks::IRON_DOOR()],
			[ItemTypeNames::JUNGLE_DOOR, VanillaBlocks::JUNGLE_DOOR(), ExtraVanillaBlocks::JUNGLE_DOOR()],
			[ItemTypeNames::MANGROVE_DOOR, VanillaBlocks::MANGROVE_DOOR(), ExtraVanillaBlocks::MANGROVE_DOOR()],
			[ItemTypeNames::PALE_OAK_DOOR, VanillaBlocks::PALE_OAK_DOOR(), ExtraVanillaBlocks::PALE_OAK_DOOR()],
			[ItemTypeNames::SPRUCE_DOOR, VanillaBlocks::SPRUCE_DOOR(), ExtraVanillaBlocks::SPRUCE_DOOR()],
			[ItemTypeNames::WARPED_DOOR, VanillaBlocks::WARPED_DOOR(), ExtraVanillaBlocks::WARPED_DOOR()],
			[ItemTypeNames::WOODEN_DOOR, VanillaBlocks::OAK_DOOR(), ExtraVanillaBlocks::OAK_DOOR()],
		] as [$id, $vanilla, $new]){
			self::unregisterSimpleItem($id, $vanilla->asItem());
			self::map1to1Block($id, $new, $parser->lookupBlockAliases($vanilla));
		}
	}

	/**
	 * @param string $id
	 * @param Block $block
	 * @param string[] $stringToItemParserNames
	 */
	private static function map1to1Block(string $id, Block $block, array $stringToItemParserNames) : void{
		GlobalItemDataHandlers::getDeserializer()->mapBlock($id, fn() => $block);
		GlobalItemDataHandlers::getSerializer()->mapBlock($block, fn() => new SavedItemData($id));
		self::mapStringToItem($block, $stringToItemParserNames);
	}

	/**
	 * @param string[] $stringToItemParserNames
	 */
	private static function registerSimpleBlock(string $id, Block $block, array $stringToItemParserNames, bool $map_serializer = true) : void{
		RuntimeBlockStateRegistry::getInstance()->register($block);

		self::unmapBlockSerializers($id, $block);
		if($map_serializer){
			self::mapBlockSerializers($id, $block);
		}

		self::mapStringToItem($block, $stringToItemParserNames);
	}

	/**
	 * @param string $name
	 * @param Block $block
	 * @param string[] $stringToItemParserNames
	 */
	private static function mapStringToItem(Block $block, array $stringToItemParserNames) : void{
		$parser = StringToItemParser::getInstance();
		foreach($stringToItemParserNames as $name){
			try{
				$parser->registerBlock($name, fn() => clone $block);
			}catch(InvalidArgumentException){
				$existing = $parser->parse($name);
				$parser->override($name, fn() => (clone $block)->asItem());
				$_reverseMap = new ReflectionProperty($parser, "reverseMap");
				$reverseMap = $_reverseMap->getValue($parser);
				$reverseMap[$existing->getStateId()][$name] = true;
				$_reverseMap->setValue($parser, $reverseMap);
			}
		}
	}

	private static function unmapBlockSerializers(string $id, Block $block) : void{
		$deserializer = GlobalBlockStateHandlers::getDeserializer();

		$_property = new ReflectionProperty($deserializer, "deserializeFuncs");
		$value = $_property->getValue($deserializer);
		unset($value[$id]);
		$_property->setValue($deserializer, $value);

		$_property = new ReflectionProperty($deserializer, "simpleCache");
		$value = $_property->getValue($deserializer);
		unset($value[$id]);
		$_property->setValue($deserializer, $value);

		$serializer = GlobalBlockStateHandlers::getSerializer();

		$_property = new ReflectionProperty($serializer, "serializers");
		$value = $_property->getValue($serializer);
		unset($value[$block->getTypeId()]);
		$_property->setValue($serializer, $value);

		$_property = new ReflectionProperty($serializer, "cache");
		$value = $_property->getValue($serializer);
		unset($value[$block->getTypeId()]);
		$_property->setValue($serializer, $value);
	}

	private static function mapBlockSerializers(string $id, Block $block) : void{
		GlobalBlockStateHandlers::getDeserializer()->mapSimple($id, fn() => clone $block);
		GlobalBlockStateHandlers::getSerializer()->mapSimple($block, $id);
	}

	private static function unregisterSimpleItem(string $id, Item $item) : void{
		$deserializer = GlobalItemDataHandlers::getDeserializer();

		$_property = new ReflectionProperty($deserializer, "deserializers");
		$value = $_property->getValue($deserializer);
		unset($value[$id]);
		$_property->setValue($deserializer, $value);

		$serializer = GlobalItemDataHandlers::getSerializer();

		$_property = new ReflectionProperty($serializer, "itemSerializers");
		$value = $_property->getValue($serializer);
		unset($value[$item->getTypeId()]);
		$_property->setValue($serializer, $value);

		$_property = new ReflectionProperty($serializer, "blockItemSerializers");
		$value = $_property->getValue($serializer);
		unset($value[$item->getTypeId()]);
		$_property->setValue($serializer, $value);
	}

	/**
	 * @param string[] $stringToItemParserNames
	 */
	private static function registerSimpleItem(string $id, Item $item, array $stringToItemParserNames) : void{
		self::unregisterSimpleItem($id, $item);
		GlobalItemDataHandlers::getDeserializer()->map($id, fn() => clone $item);
		GlobalItemDataHandlers::getSerializer()->map($item, fn() => new SavedItemData($id));

		foreach($stringToItemParserNames as $name){
			try{
				StringToItemParser::getInstance()->register($name, fn() => clone $item);
			}catch(InvalidArgumentException){
				StringToItemParser::getInstance()->override($name, fn() => clone $item);
			}
		}
	}
}<?php

declare(strict_types=1);

namespace redstone\vanilla;

use pocketmine\item\Item;
use pocketmine\item\ItemIdentifier;
use pocketmine\item\ItemTypeIds;
use pocketmine\item\VanillaItems;
use pocketmine\utils\CloningRegistryTrait;
use ReflectionProperty;

/**
 * @method static Redstone REDSTONE_DUST()
 */
final class ExtraVanillaItems{
	use CloningRegistryTrait;

	private function __construct(){
	}

	protected static function register(string $name, Item $item) : void{
		self::_registryRegister($name, $item);
	}

	/**
	 * @return Item[]
	 * @phpstan-return array<string, Item>
	 */
	public static function getAll() : array{
		//phpstan doesn't support generic traits yet :(
		/** @var Item[] $result */
		$result = self::_registryGetAll();
		return $result;
	}

	protected static function setup() : void{
		self::register("redstone_dust", new Redstone(new ItemIdentifier(ItemTypeIds::newId()), "Redstone Dust"));
	}
}<?php

declare(strict_types=1);

namespace redstone\vanilla;

use pocketmine\block\Block;
use pocketmine\block\BlockBreakInfo as BreakInfo;
use pocketmine\block\BlockIdentifier as BID;
use pocketmine\block\BlockToolType;
use pocketmine\block\BlockTypeIds as Ids;
use pocketmine\block\BlockTypeInfo as Info;
use pocketmine\block\PressurePlate;
use pocketmine\block\VanillaBlocks;
use pocketmine\block\WeightedPressurePlate;
use pocketmine\item\ToolTier;
use pocketmine\utils\CloningRegistryTrait;
use redstone\block\CopperDoor;
use redstone\block\CopperTrapdoor;
use redstone\block\Dispenser;
use redstone\block\Door;
use redstone\block\FenceGate;
use redstone\block\Hopper;
use redstone\block\Lever;
use redstone\block\Observer;
use redstone\block\Piston;
use redstone\block\PistonArmBlock;
use redstone\block\Redstone;
use redstone\block\RedstoneComparator;
use redstone\block\RedstoneLamp;
use redstone\block\RedstoneOre;
use redstone\block\RedstoneRepeater;
use redstone\block\RedstoneTorch;
use redstone\block\RedstoneWire;
use redstone\block\StoneButton;
use redstone\block\StonePressurePlate;
use redstone\block\tile\dispenser\Dispenser as DispenserTile;
use redstone\block\tile\dispenser\Dropper as DropperTile;
use redstone\block\tile\piston\PistonArm;
use redstone\block\TNT;
use redstone\block\Trapdoor;
use redstone\block\TrappedChest;
use redstone\block\WeightedPressurePlateHeavy;
use redstone\block\WeightedPressurePlateLight;
use redstone\block\WoodenButton;
use redstone\block\WoodenDoor;
use redstone\block\WoodenPressurePlate;
use redstone\block\WoodenTrapdoor;
use ReflectionProperty;

/**
 * * @method static Dispenser DISPENSER()
 * * @method static Dispenser DROPPER()
 * * @method static Hopper HOPPER()
 * * @method static TrappedChest TRAPPED_CHEST()
 * * @method static Lever LEVER()
 * * @method static Observer OBSERVER()
 * * @method static Redstone REDSTONE()
 * * @method static RedstoneOre REDSTONE_ORE()
 * * @method static RedstoneWire REDSTONE_WIRE()
 * * @method static PistonArmBlock PISTON_ARM_BLOCK()
 * * @method static Piston PISTON()
 * * @method static Piston STICKY_PISTON()
 * * @method static Door IRON_DOOR()
 * * @method static RedstoneComparator REDSTONE_COMPARATOR()
 * * @method static RedstoneLamp REDSTONE_LAMP()
 * * @method static RedstoneRepeater REDSTONE_REPEATER()
 * * @method static RedstoneTorch REDSTONE_TORCH()
 * * @method static StoneButton STONE_BUTTON()
 * * @method static StonePressurePlate STONE_PRESSURE_PLATE()
 * * @method static TNT TNT()
 * * @method static WeightedPressurePlateHeavy WEIGHTED_PRESSURE_PLATE_HEAVY()
 * * @method static WeightedPressurePlateLight WEIGHTED_PRESSURE_PLATE_LIGHT()
 * * @method static StoneButton POLISHED_BLACKSTONE_BUTTON()
 * * @method static StonePressurePlate POLISHED_BLACKSTONE_PRESSURE_PLATE()
 * * @method static CopperDoor COPPER_DOOR()
 * * @method static WoodenDoor OAK_DOOR()
 * * @method static WoodenButton OAK_BUTTON()
 * * @method static WoodenPressurePlate OAK_PRESSURE_PLATE()
 * * @method static WoodenDoor SPRUCE_DOOR()
 * * @method static WoodenButton SPRUCE_BUTTON()
 * * @method static WoodenPressurePlate SPRUCE_PRESSURE_PLATE()
 * * @method static WoodenDoor BIRCH_DOOR()
 * * @method static WoodenButton BIRCH_BUTTON()
 * * @method static WoodenPressurePlate BIRCH_PRESSURE_PLATE()
 * * @method static WoodenDoor JUNGLE_DOOR()
 * * @method static WoodenButton JUNGLE_BUTTON()
 * * @method static WoodenPressurePlate JUNGLE_PRESSURE_PLATE()
 * * @method static WoodenDoor ACACIA_DOOR()
 * * @method static WoodenButton ACACIA_BUTTON()
 * * @method static WoodenPressurePlate ACACIA_PRESSURE_PLATE()
 * * @method static WoodenDoor DARK_OAK_DOOR()
 * * @method static WoodenButton DARK_OAK_BUTTON()
 * * @method static WoodenPressurePlate DARK_OAK_PRESSURE_PLATE()
 * * @method static WoodenDoor MANGROVE_DOOR()
 * * @method static WoodenButton MANGROVE_BUTTON()
 * * @method static WoodenPressurePlate MANGROVE_PRESSURE_PLATE()
 * * @method static WoodenDoor CRIMSON_DOOR()
 * * @method static WoodenButton CRIMSON_BUTTON()
 * * @method static WoodenPressurePlate CRIMSON_PRESSURE_PLATE()
 * * @method static WoodenDoor WARPED_DOOR()
 * * @method static WoodenButton WARPED_BUTTON()
 * * @method static WoodenPressurePlate WARPED_PRESSURE_PLATE()
 * * @method static WoodenDoor CHERRY_DOOR()
 * * @method static WoodenButton CHERRY_BUTTON()
 * * @method static WoodenPressurePlate CHERRY_PRESSURE_PLATE()
 * * @method static WoodenDoor PALE_OAK_DOOR()
 * * @method static WoodenButton PALE_OAK_BUTTON()
 * * @method static WoodenPressurePlate PALE_OAK_PRESSURE_PLATE()
 */
final class ExtraVanillaBlocks{
	use CloningRegistryTrait;

	private function __construct(){
		//NOOP
	}

	protected static function register(string $name, Block $block) : void{
		self::_registryRegister($name, $block);
	}

	/**
	 * @return Block[]
	 * @phpstan-return array<string, Block>
	 */
	public static function getAll() : array{
		//phpstan doesn't support generic traits yet :(
		/** @var Block[] $result */
		$result = self::_registryGetAll();
		return $result;
	}

	protected static function setup() : void{
		self::register("dispenser", new Dispenser(new BID(Ids::newId(), DispenserTile::class), "Dispenser", new Info(new BreakInfo(3.5, BlockToolType::PICKAXE))));
		self::register("dropper", new Dispenser(new BID(Ids::newId(), DropperTile::class), "Dropper", new Info(new BreakInfo(3.5, BlockToolType::PICKAXE))));

		self::register("observer", new Observer(new BID(Ids::newId()), "Observer", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD))));

		$block = VanillaBlocks::LEVER();
		self::register("lever", new Lever(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));

		$block = VanillaBlocks::REDSTONE();
		self::register("redstone", new Redstone(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));

		$block = VanillaBlocks::REDSTONE_ORE();
		self::register("redstone_ore", new RedstoneOre(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));

		$block = VanillaBlocks::REDSTONE_WIRE();
		self::register("redstone_wire", new RedstoneWire(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));

		self::register("piston_arm_block", new PistonArmBlock(new BID(Ids::newId()), "Piston Arm", new Info(new BreakInfo(0.5))));
		self::register("piston", new Piston(new BID(Ids::newId(), PistonArm::class), "Piston", new Info(new BreakInfo(0.5)), false));
		self::register("sticky_piston", new Piston(new BID(Ids::newId(), PistonArm::class), "Sticky Piston", new Info(new BreakInfo(0.5)), true));

		foreach(VanillaBlocks::getAll() as $identifier => $block){
			if($block instanceof \pocketmine\block\RedstoneComparator){
				self::register($identifier, new RedstoneComparator(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));
			}elseif($block instanceof \pocketmine\block\RedstoneLamp){
				self::register($identifier, new RedstoneLamp(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));
			}elseif($block instanceof \pocketmine\block\RedstoneRepeater){
				self::register($identifier, new RedstoneRepeater(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));
			}elseif($block instanceof \pocketmine\block\RedstoneTorch){
				self::register($identifier, new RedstoneTorch(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));
			}elseif($block instanceof \pocketmine\block\StoneButton){
				self::register($identifier, new StoneButton(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));
			}elseif($block instanceof \pocketmine\block\StonePressurePlate){
				$deactivationDelayTicks = (new ReflectionProperty(PressurePlate::class, "deactivationDelayTicks"))->getValue($block);
				self::register($identifier, new StonePressurePlate(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags()), $deactivationDelayTicks));
			}elseif($block instanceof \pocketmine\block\TNT){
				self::register($identifier, new TNT(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));
			}elseif($block instanceof \pocketmine\block\WeightedPressurePlateHeavy){
				$deactivationDelayTicks = (new ReflectionProperty(PressurePlate::class, "deactivationDelayTicks"))->getValue($block);
				$signalStrengthFactor = (new ReflectionProperty(WeightedPressurePlate::class, "signalStrengthFactor"))->getValue($block);
				self::register($identifier, new WeightedPressurePlateHeavy(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags()), $deactivationDelayTicks, $signalStrengthFactor));
			}elseif($block instanceof \pocketmine\block\WeightedPressurePlateLight){
				$deactivationDelayTicks = (new ReflectionProperty(PressurePlate::class, "deactivationDelayTicks"))->getValue($block);
				$signalStrengthFactor = (new ReflectionProperty(WeightedPressurePlate::class, "signalStrengthFactor"))->getValue($block);
				self::register($identifier, new WeightedPressurePlateLight(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags()), $deactivationDelayTicks, $signalStrengthFactor));
			}elseif($block instanceof \pocketmine\block\WoodenButton){
				self::register($identifier, new WoodenButton(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags()), $block->getWoodType()));
			}elseif($block instanceof \pocketmine\block\WoodenPressurePlate){
				self::register($identifier, new WoodenPressurePlate(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags()), $block->getWoodType()));
			}elseif($block instanceof \pocketmine\block\Hopper){
				self::register($identifier, new Hopper(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));
			}elseif($block instanceof \pocketmine\block\TrappedChest){
				self::register($identifier, new TrappedChest(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));
			}elseif($block instanceof \pocketmine\block\CopperTrapdoor){
				self::register($identifier, new CopperTrapdoor(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));
			}elseif($block instanceof \pocketmine\block\WoodenTrapdoor){
				self::register($identifier, new WoodenTrapdoor(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags()), $block->getWoodType()));
			}elseif($block instanceof \pocketmine\block\Trapdoor){
				self::register($identifier, new Trapdoor(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));
			}elseif($block instanceof \pocketmine\block\FenceGate){
				self::register($identifier, new FenceGate(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags()), $block->getWoodType()));
			}elseif($block instanceof \pocketmine\block\CopperDoor){
				self::register($identifier, new CopperDoor(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));
			}elseif($block instanceof \pocketmine\block\WoodenDoor){
				self::register($identifier, new WoodenDoor(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags()), $block->getWoodType()));
			}elseif($block instanceof \pocketmine\block\Door){
				self::register($identifier, new Door(new BID(Ids::newId(), $block->getIdInfo()->getTileClass()), $block->getName(), new Info($block->getBreakInfo(), $block->getTypeTags())));
			}
		}
	}
}<?php

declare(strict_types=1);

namespace behaviorpack\recipe;

use behaviorpack\BehaviorPack;
use behaviorpack\BehaviorPackException;
use behaviorpack\ContentLoader;
use pocketmine\crafting\CraftingManager;
use pocketmine\crafting\FurnaceRecipe;
use pocketmine\crafting\FurnaceType;
use pocketmine\crafting\PotionContainerChangeRecipe;
use pocketmine\crafting\PotionTypeRecipe;
use pocketmine\crafting\ShapedRecipe;
use pocketmine\crafting\ShapelessRecipe;
use pocketmine\crafting\ShapelessRecipeType;
use pocketmine\crafting\ExactRecipeIngredient;
use pocketmine\plugin\PluginBase;
use function count;
use function implode;
use function in_array;
use function is_array;
use function is_string;
use function max;
use function str_contains;
use function str_pad;
use function strlen;
use function strtolower;

/**
 * Registers the recipes of the "recipes" folder of every behavior pack into
 * the server crafting manager. The crafting data sent to players is rebuilt
 * by the server when a recipe is registered.
 */
final class RecipeLoader implements ContentLoader{

	private const POTION_CONTAINERS = ["minecraft:potion", "minecraft:splash_potion", "minecraft:lingering_potion"];

	private int $registered = 0;

	/** @var array<string, true> */
	private array $warnedItems = [];

	public function __construct(
		private PluginBase $plugin
	){}

	public function getName() : string{
		return "recipes";
	}

	public function requiresCustomies() : bool{
		return false;
	}

	public function load(array $packs) : void{
		$manager = $this->plugin->getServer()->getCraftingManager();
		foreach($packs as $pack){
			foreach($pack->listFiles("recipes", "json") as $file){
				$this->loadFile($manager, $pack, $file);
			}
		}
		if($this->registered > 0){
			$this->plugin->getLogger()->info("Behavior packs: " . $this->registered . " recipes");
		}
	}

	public function close() : void{
		$this->warnedItems = [];
	}

	private function loadFile(CraftingManager $manager, BehaviorPack $pack, string $file) : void{
		$location = $pack->getName() . "/" . $pack->relativePath($file);
		try{
			$json = BehaviorPack::readJson($file);
		}catch(BehaviorPackException $e){
			$this->plugin->getLogger()->warning("Skipped recipe $location: " . $e->getMessage());
			return;
		}

		foreach($json as $key => $body){
			if(!is_string($key) || !is_array($body) || $key === "format_version"){
				continue;
			}
			try{
				$this->registered += $this->loadRecipe($manager, strtolower($key), $body);
			}catch(UnknownItemException $e){
				$warnKey = $location . "|" . $e->getIdentifier();
				if(!isset($this->warnedItems[$warnKey])){
					$this->warnedItems[$warnKey] = true;
					$this->plugin->getLogger()->warning("Skipped recipe $location: unknown item \"" . $e->getIdentifier() . "\"");
				}
			}catch(RecipeException | \InvalidArgumentException $e){
				$this->plugin->getLogger()->warning("Skipped recipe $location: " . $e->getMessage());
			}
		}
	}

	/**
	 * Registers one recipe and returns how many server recipes it produced.
	 *
	 * @param array<mixed> $body
	 * @throws RecipeException
	 * @throws UnknownItemException
	 */
	private function loadRecipe(CraftingManager $manager, string $type, array $body) : int{
		return match($type){
			"minecraft:recipe_shaped" => $this->loadShaped($manager, $body),
			"minecraft:recipe_shapeless" => $this->loadShapeless($manager, $body),
			"minecraft:recipe_furnace" => $this->loadFurnace($manager, $body),
			"minecraft:recipe_brewing_mix" => $this->loadBrewingMix($manager, $body),
			"minecraft:recipe_brewing_container" => $this->loadBrewingContainer($manager, $body),
			"minecraft:recipe_smithing_transform", "minecraft:recipe_smithing_trim" => throw new RecipeException("Smithing recipes are not supported by the server crafting system"),
			default => throw new RecipeException("Unsupported recipe type \"$type\"")
		};
	}

	/**
	 * @param array<mixed> $body
	 * @return list<string>
	 */
	private function tags(array $body) : array{
		$tags = [];
		foreach(is_array($body["tags"] ?? null) ? $body["tags"] : [] as $tag){
			if(is_string($tag)){
				$tags[] = strtolower($tag);
			}
		}
		return $tags;
	}

	/**
	 * @param array<mixed> $body
	 * @throws RecipeException
	 * @throws UnknownItemException
	 */
	private function loadShaped(CraftingManager $manager, array $body) : int{
		$tags = $this->tags($body);
		if(count($tags) > 0 && !in_array("crafting_table", $tags, true)){
			throw new RecipeException("Shaped recipes are only supported on the crafting table (tags: " . implode(", ", $tags) . ")");
		}

		$pattern = $body["pattern"] ?? null;
		if(!is_array($pattern) || count($pattern) === 0){
			throw new RecipeException("Shaped recipe must have a pattern");
		}
		$shape = [];
		$width = 0;
		foreach($pattern as $row){
			if(!is_string($row)){
				throw new RecipeException("Shaped recipe pattern rows must be strings");
			}
			$shape[] = $row;
			$width = max($width, strlen($row));
		}
		foreach($shape as $i => $row){
			$shape[$i] = str_pad($row, $width);
		}

		$key = $body["key"] ?? [];
		if(!is_array($key)){
			throw new RecipeException("Shaped recipe key must be an object");
		}
		$ingredients = [];
		foreach($key as $symbol => $descriptor){
			$symbol = (string) $symbol;
			if(strlen($symbol) !== 1){
				throw new RecipeException("Shaped recipe key symbols must be one character, got \"$symbol\"");
			}
			if(!str_contains(implode("", $shape), $symbol)){
				continue;
			}
			[$ingredients[$symbol],] = RecipeItemResolver::ingredient($descriptor);
		}

		$results = RecipeItemResolver::results($body["result"] ?? null);
		$manager->registerShapedRecipe(new ShapedRecipe($shape, $ingredients, $results));
		return 1;
	}

	/**
	 * @param array<mixed> $body
	 * @throws RecipeException
	 * @throws UnknownItemException
	 */
	private function loadShapeless(CraftingManager $manager, array $body) : int{
		$types = [];
		$tags = $this->tags($body);
		foreach(count($tags) === 0 ? ["crafting_table"] : $tags as $tag){
			$recipeType = match($tag){
				"crafting_table" => ShapelessRecipeType::CRAFTING,
				"stonecutter" => ShapelessRecipeType::STONECUTTER,
				"cartography_table" => ShapelessRecipeType::CARTOGRAPHY,
				"smithing_table" => ShapelessRecipeType::SMITHING,
				default => null
			};
			if($recipeType !== null){
				$types[$recipeType->name] = $recipeType;
			}
		}
		if(count($types) === 0){
			throw new RecipeException("Shapeless recipe has no supported tag (tags: " . implode(", ", $tags) . ")");
		}

		$descriptors = $body["ingredients"] ?? null;
		if(!is_array($descriptors) || count($descriptors) === 0){
			throw new RecipeException("Shapeless recipe must have ingredients");
		}
		if(!isset($descriptors[0])){
			$descriptors = [$descriptors];
		}
		$ingredients = [];
		foreach($descriptors as $descriptor){
			[$ingredient, $count] = RecipeItemResolver::ingredient($descriptor);
			for($i = 0; $i < $count; ++$i){
				$ingredients[] = $ingredient;
			}
		}
		if(count($ingredients) > 9){
			throw new RecipeException("Shapeless recipe has more than 9 ingredients");
		}

		$results = RecipeItemResolver::results($body["result"] ?? null);
		foreach($types as $recipeType){
			$manager->registerShapelessRecipe(new ShapelessRecipe($ingredients, $results, $recipeType));
		}
		return count($types);
	}

	/**
	 * @param array<mixed> $body
	 * @throws RecipeException
	 * @throws UnknownItemException
	 */
	private function loadFurnace(CraftingManager $manager, array $body) : int{
		$types = [];
		$tags = $this->tags($body);
		foreach($tags as $tag){
			$furnaceType = match($tag){
				"furnace" => FurnaceType::FURNACE,
				"blast_furnace" => FurnaceType::BLAST_FURNACE,
				"smoker" => FurnaceType::SMOKER,
				"campfire" => FurnaceType::CAMPFIRE,
				"soul_campfire" => FurnaceType::SOUL_CAMPFIRE,
				default => null
			};
			if($furnaceType !== null){
				$types[$furnaceType->name] = $furnaceType;
			}
		}
		if(count($types) === 0){
			throw new RecipeException("Furnace recipe has no supported tag (tags: " . implode(", ", $tags) . ")");
		}

		[$input,] = RecipeItemResolver::ingredient($body["input"] ?? null);
		$output = RecipeItemResolver::result($body["output"] ?? null);
		foreach($types as $furnaceType){
			$manager->getFurnaceRecipeManager($furnaceType)->register(new FurnaceRecipe($output, $input));
		}
		return count($types);
	}

	/**
	 * @param array<mixed> $body
	 * @throws RecipeException
	 * @throws UnknownItemException
	 */
	private function loadBrewingMix(CraftingManager $manager, array $body) : int{
		$input = $body["input"] ?? null;
		$output = $body["output"] ?? null;
		if(!is_string($input) || !is_string($output)){
			throw new RecipeException("Brewing mix must have string input and output");
		}
		[$reagent,] = RecipeItemResolver::ingredient($body["reagent"] ?? null);

		if(RecipeItemResolver::isPotionType($input) || RecipeItemResolver::isPotionType($output)){
			$inputType = RecipeItemResolver::potionType($input);
			$outputType = RecipeItemResolver::potionType($output);
			foreach(self::POTION_CONTAINERS as $container){
				$manager->registerPotionTypeRecipe(new PotionTypeRecipe(
					new ExactRecipeIngredient(RecipeItemResolver::potion($container, $inputType)),
					$reagent,
					RecipeItemResolver::potion($container, $outputType)
				));
			}
			return count(self::POTION_CONTAINERS);
		}

		$manager->registerPotionTypeRecipe(new PotionTypeRecipe(
			new ExactRecipeIngredient(RecipeItemResolver::result($input)),
			$reagent,
			RecipeItemResolver::result($output)
		));
		return 1;
	}

	/**
	 * @param array<mixed> $body
	 * @throws RecipeException
	 * @throws UnknownItemException
	 */
	private function loadBrewingContainer(CraftingManager $manager, array $body) : int{
		$input = $body["input"] ?? null;
		$output = $body["output"] ?? null;
		if(!is_string($input) || !is_string($output)){
			throw new RecipeException("Brewing container recipe must have string input and output");
		}
		[$reagent,] = RecipeItemResolver::ingredient($body["reagent"] ?? null);
		$manager->registerPotionContainerChangeRecipe(new PotionContainerChangeRecipe(
			RecipeItemResolver::knownIdentifier($input),
			$reagent,
			RecipeItemResolver::knownIdentifier($output)
		));
		return 1;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\recipe;

/**
 * Thrown when a recipe file is malformed or references something the server
 * cannot provide. The loader logs it and skips the recipe.
 */
final class RecipeException extends \RuntimeException{

}
<?php

declare(strict_types=1);

namespace behaviorpack\recipe;

/**
 * Thrown when a recipe references an item identifier that no registered item
 * matches, vanilla or custom.
 */
final class UnknownItemException extends \RuntimeException{

	public function __construct(
		private string $identifier
	){
		parent::__construct("Unknown item \"$identifier\"");
	}

	public function getIdentifier() : string{
		return $this->identifier;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\recipe;

use pocketmine\crafting\CraftingManagerFromDataHelper;
use pocketmine\crafting\ExactRecipeIngredient;
use pocketmine\crafting\MetaWildcardRecipeIngredient;
use pocketmine\crafting\RecipeIngredient;
use pocketmine\crafting\TagWildcardRecipeIngredient;
use pocketmine\data\bedrock\PotionTypeIdMap;
use pocketmine\item\Item;
use pocketmine\item\PotionType;
use function count;
use function ctype_digit;
use function explode;
use function implode;
use function is_array;
use function is_int;
use function is_string;
use function max;
use function str_contains;
use function str_starts_with;
use function strlen;
use function strtolower;
use function substr;
use function strtoupper;

/**
 * Turns the item descriptors of Bedrock recipe files into items and recipe
 * ingredients, using the same lookup the server uses for its own recipes, so
 * vanilla and custom identifiers resolve alike.
 */
final class RecipeItemResolver{

	private const WILDCARD_META = 32767;
	private const POTION_TYPE_PREFIX = "minecraft:potion_type:";

	/**
	 * Resolves an ingredient descriptor: a string ("minecraft:stick",
	 * "minecraft:wool:3") or an object with item/data or tag. Returns the
	 * ingredient and how many times it is required.
	 *
	 * @return array{RecipeIngredient, int}
	 * @throws RecipeException
	 * @throws UnknownItemException
	 */
	public static function ingredient(mixed $descriptor) : array{
		if(is_string($descriptor)){
			return [self::itemIngredient($descriptor, null), 1];
		}
		if(!is_array($descriptor)){
			throw new RecipeException("Ingredient must be a string or an object");
		}

		$count = self::count($descriptor);
		$tag = $descriptor["tag"] ?? null;
		if($tag !== null){
			if(!is_string($tag) || $tag === ""){
				throw new RecipeException("Ingredient tag must be a string");
			}
			return [new TagWildcardRecipeIngredient(self::namespaced($tag)), $count];
		}

		$item = $descriptor["item"] ?? null;
		if(!is_string($item) || $item === ""){
			throw new RecipeException("Ingredient must have an item or a tag");
		}
		return [self::itemIngredient($item, self::data($descriptor)), $count];
	}

	/**
	 * Resolves a result descriptor: a string or an object with item, data
	 * and count.
	 *
	 * @throws RecipeException
	 * @throws UnknownItemException
	 */
	public static function result(mixed $descriptor) : Item{
		if(is_string($descriptor)){
			[$identifier, $meta] = self::splitIdentifier($descriptor);
			return self::item($identifier, $meta ?? 0, 1);
		}
		if(!is_array($descriptor)){
			throw new RecipeException("Result must be a string or an object");
		}

		$item = $descriptor["item"] ?? null;
		if(!is_string($item) || $item === ""){
			throw new RecipeException("Result must have an item");
		}
		[$identifier, $meta] = self::splitIdentifier($item);
		$data = self::data($descriptor);
		if($data !== null){
			$meta = $data;
		}
		if($meta === self::WILDCARD_META){
			$meta = 0;
		}
		return self::item($identifier, $meta ?? 0, self::count($descriptor));
	}

	/**
	 * Resolves a list of results, accepting a single descriptor too.
	 *
	 * @return list<Item>
	 * @throws RecipeException
	 * @throws UnknownItemException
	 */
	public static function results(mixed $descriptor) : array{
		if(is_array($descriptor) && isset($descriptor[0])){
			$results = [];
			foreach($descriptor as $entry){
				$results[] = self::result($entry);
			}
			return $results;
		}
		return [self::result($descriptor)];
	}

	/**
	 * Resolves an identifier of the form "minecraft:potion_type:<name>" to
	 * the matching potion type.
	 *
	 * @throws RecipeException
	 */
	public static function potionType(string $identifier) : PotionType{
		if(!self::isPotionType($identifier)){
			throw new RecipeException("Expected a potion type, got \"$identifier\"");
		}
		$name = strtoupper(substr($identifier, strlen(self::POTION_TYPE_PREFIX)));
		foreach(PotionType::cases() as $case){
			if($case->name === $name){
				return $case;
			}
		}
		throw new RecipeException("Unknown potion type \"$identifier\"");
	}

	public static function isPotionType(string $identifier) : bool{
		return str_starts_with(strtolower($identifier), self::POTION_TYPE_PREFIX);
	}

	/**
	 * Builds a potion container item ("minecraft:potion", ...) of the given
	 * potion type.
	 *
	 * @throws UnknownItemException
	 */
	public static function potion(string $containerId, PotionType $type) : Item{
		return self::item($containerId, PotionTypeIdMap::getInstance()->toId($type), 1);
	}

	/**
	 * Checks that an identifier resolves to a known item and returns it in
	 * its namespaced form.
	 *
	 * @throws UnknownItemException
	 */
	public static function knownIdentifier(string $identifier) : string{
		[$identifier,] = self::splitIdentifier($identifier);
		self::item($identifier, 0, 1);
		return $identifier;
	}

	/**
	 * @throws UnknownItemException
	 */
	public static function item(string $identifier, int $meta, int $count) : Item{
		$item = CraftingManagerFromDataHelper::deserializeItemStackFromFields($identifier, $meta, $count, null, null);
		if($item === null){
			throw new UnknownItemException($meta === 0 ? $identifier : "$identifier:$meta");
		}
		return $item;
	}

	/**
	 * @throws UnknownItemException
	 */
	private static function itemIngredient(string $descriptor, ?int $data) : RecipeIngredient{
		[$identifier, $meta] = self::splitIdentifier($descriptor);
		if($data !== null){
			$meta = $data;
		}
		if($meta === null || $meta === -1 || $meta === self::WILDCARD_META){
			self::item($identifier, 0, 1);
			return new MetaWildcardRecipeIngredient($identifier);
		}
		return new ExactRecipeIngredient(self::item($identifier, $meta, 1));
	}

	/**
	 * Splits "namespace:id" or "namespace:id:data" into the namespaced
	 * identifier and the optional data value.
	 *
	 * @return array{string, ?int}
	 */
	private static function splitIdentifier(string $descriptor) : array{
		$parts = explode(":", $descriptor);
		$meta = null;
		$last = count($parts) - 1;
		if($last >= 1 && ctype_digit($parts[$last])){
			$meta = (int) $parts[$last];
			unset($parts[$last]);
		}
		return [self::namespaced(implode(":", $parts)), $meta];
	}

	private static function namespaced(string $identifier) : string{
		return str_contains($identifier, ":") ? $identifier : "minecraft:" . $identifier;
	}

	/**
	 * @param array<mixed> $descriptor
	 * @throws RecipeException
	 */
	private static function data(array $descriptor) : ?int{
		$data = $descriptor["data"] ?? null;
		if($data === null){
			return null;
		}
		if(!is_int($data)){
			throw new RecipeException("Item data must be an integer");
		}
		return $data;
	}

	/**
	 * @param array<mixed> $descriptor
	 * @throws RecipeException
	 */
	private static function count(array $descriptor) : int{
		$count = $descriptor["count"] ?? 1;
		if(!is_int($count)){
			throw new RecipeException("Item count must be an integer");
		}
		return max(1, $count);
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack;

use behaviorpack\custom\CustomContentLoader;
use behaviorpack\entity\EntityLoader;
use behaviorpack\loot\LootTableLoader;
use behaviorpack\recipe\RecipeLoader;
use behaviorpack\script\ScriptLoader;
use pocketmine\event\EventPriority;
use pocketmine\event\plugin\PluginEnableEvent;
use pocketmine\plugin\PluginBase;
use Symfony\Component\Filesystem\Path;
use Throwable;
use function count;

/**
 * Loads the behavior packs dropped in the configured folder, behavior_packs
 * next to resource_packs by default.
 *
 * When Customies is installed, every loader runs as soon as Customies enables,
 * so custom items, blocks and entities are registered before Customies freezes
 * them and before the recipes and loot tables that reference them. Without
 * Customies, the loaders that need it are skipped and the others run when this
 * plugin enables.
 */
final class BehaviorPackModule{

	public const CUSTOMIES = "Customies";

	/** @var list<ContentLoader> */
	private array $loaders;

	/** @var list<BehaviorPack> */
	private array $packs = [];

	private bool $loaded = false;

	public function __construct(
		private PluginBase $plugin,
		private BehaviorPackSettings $settings
	){
		$this->loaders = [];
		if($settings->itemsAndBlocks){
			$this->loaders[] = new CustomContentLoader($plugin);
		}
		if($settings->entities){
			$this->loaders[] = new EntityLoader($plugin);
		}
		if($settings->recipes){
			$this->loaders[] = new RecipeLoader($plugin);
		}
		if($settings->lootTables){
			$this->loaders[] = new LootTableLoader($plugin);
		}
		if($settings->scripts){
			$this->loaders[] = new ScriptLoader($plugin, $settings->scriptTimeoutMs);
		}
	}

	public function getPacksDirectory() : string{
		return Path::isAbsolute($this->settings->folder) ? $this->settings->folder : Path::join($this->plugin->getServer()->getDataPath(), $this->settings->folder);
	}

	/**
	 * @return list<BehaviorPack>
	 */
	public function getPacks() : array{
		return $this->packs;
	}

	public function enable() : void{
		$discovery = new PackDiscovery($this->plugin->getDataFolder() . "cache/behavior_packs", $this->plugin->getLogger());
		$this->packs = $discovery->discover($this->getPacksDirectory());
		if(count($this->packs) === 0){
			return;
		}
		$this->plugin->getLogger()->info("Behavior packs found: " . count($this->packs));

		$customies = $this->plugin->getServer()->getPluginManager()->getPlugin(self::CUSTOMIES);
		if($customies === null){
			$this->plugin->getLogger()->warning("Customies is not installed: custom items, blocks and entities are skipped");
			$this->runLoaders(false);
			return;
		}
		if($customies->isEnabled()){
			$this->runLoaders(true);
			return;
		}
		$this->plugin->getServer()->getPluginManager()->registerEvent(PluginEnableEvent::class, function(PluginEnableEvent $event) : void{
			if($event->getPlugin()->getName() === self::CUSTOMIES){
				$this->runLoaders(true);
			}
		}, EventPriority::MONITOR, $this->plugin);
	}

	public function disable() : void{
		foreach($this->loaders as $loader){
			try{
				$loader->close();
			}catch(Throwable $e){
				$this->plugin->getLogger()->logException($e);
			}
		}
	}

	private function runLoaders(bool $customies) : void{
		if($this->loaded){
			return;
		}
		$this->loaded = true;

		foreach($this->loaders as $loader){
			if($loader->requiresCustomies() && !$customies){
				continue;
			}
			try{
				$loader->load($this->packs);
			}catch(Throwable $e){
				$this->plugin->getLogger()->error("Behavior packs: " . $loader->getName() . " failed");
				$this->plugin->getLogger()->logException($e);
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack;

use Logger;
use ZipArchive;
use function basename;
use function explode;
use function in_array;
use function is_array;
use function is_dir;
use function is_file;
use function mkdir;
use function pathinfo;
use function scandir;
use function sha1_file;
use function str_replace;
use function strtolower;
use function touch;
use function usort;
use const PATHINFO_EXTENSION;
use const PATHINFO_FILENAME;

/**
 * Finds the behavior packs inside a folder: extracted packs, .mcpack, .mcaddon
 * and .zip archives, including archives nested inside other archives. Archives
 * are extracted into a cache folder keyed by their SHA-1, so an unchanged
 * archive is not extracted again. Resource packs are skipped.
 */
final class PackDiscovery{

	private const ARCHIVE_EXTENSIONS = ["mcpack", "mcaddon", "zip"];

	/** @var array<string, BehaviorPack> */
	private array $packs = [];

	public function __construct(
		private string $cacheDirectory,
		private Logger $logger
	){}

	/**
	 * @return list<BehaviorPack>
	 */
	public function discover(string $directory) : array{
		$this->packs = [];
		if(!is_dir($directory)){
			@mkdir($directory, 0777, true);
			return [];
		}
		$this->scan($directory);

		$packs = [];
		foreach($this->packs as $pack){
			$packs[] = $pack;
		}
		usort($packs, fn(BehaviorPack $a, BehaviorPack $b) : int => $a->getName() <=> $b->getName());
		return $packs;
	}

	private function scan(string $directory) : void{
		if(is_file($directory . "/manifest.json")){
			$this->addPack($directory);
			return;
		}

		$entries = scandir($directory);
		if($entries === false){
			return;
		}
		foreach($entries as $entry){
			if($entry === "." || $entry === ".."){
				continue;
			}
			$path = $directory . "/" . $entry;
			if(is_dir($path)){
				$this->scan($path);
			}elseif(in_array(strtolower(pathinfo($entry, PATHINFO_EXTENSION)), self::ARCHIVE_EXTENSIONS, true)){
				$extracted = $this->extract($path);
				if($extracted !== null){
					$this->scan($extracted);
				}
			}
		}
	}

	private function extract(string $archive) : ?string{
		$hash = sha1_file($archive);
		if($hash === false){
			$this->logger->warning("Cannot read the archive " . basename($archive));
			return null;
		}

		$target = $this->cacheDirectory . "/" . pathinfo($archive, PATHINFO_FILENAME) . "-" . $hash;
		$marker = $target . "/.extracted";
		if(is_file($marker)){
			return $target;
		}

		$zip = new ZipArchive();
		if($zip->open($archive) !== true){
			$this->logger->warning("Cannot open the archive " . basename($archive));
			return null;
		}
		@mkdir($target, 0777, true);
		for($i = 0; $i < $zip->numFiles; $i++){
			$name = $zip->getNameIndex($i);
			if($name === false || !self::isSafeEntry($name)){
				$zip->close();
				$this->logger->warning("Unsafe path in the archive " . basename($archive) . ", skipped");
				return null;
			}
		}
		$extracted = $zip->extractTo($target);
		$zip->close();
		if(!$extracted){
			$this->logger->warning("Cannot extract the archive " . basename($archive));
			return null;
		}
		touch($marker);
		return $target;
	}

	private static function isSafeEntry(string $name) : bool{
		$name = str_replace("\\", "/", $name);
		if($name === "" || $name[0] === "/" || (isset($name[1]) && $name[1] === ":")){
			return false;
		}
		foreach(explode("/", $name) as $part){
			if($part === ".."){
				return false;
			}
		}
		return true;
	}

	private function addPack(string $directory) : void{
		try{
			$manifest = BehaviorPack::readJson($directory . "/manifest.json");
		}catch(BehaviorPackException $e){
			$this->logger->warning($e->getMessage());
			return;
		}

		$types = [];
		foreach($manifest["modules"] ?? [] as $module){
			if(is_array($module) && isset($module["type"])){
				$types[] = $module["type"];
			}
		}
		if(!in_array("data", $types, true) && !in_array("script", $types, true) && !in_array("javascript", $types, true)){
			return;
		}

		$pack = new BehaviorPack($directory, $manifest);
		$key = $pack->getUuid() !== "" ? $pack->getUuid() : $pack->getPath();
		if(isset($this->packs[$key])){
			$this->logger->warning("Behavior pack " . $pack->getName() . " found twice, keeping the first one");
			return;
		}
		$this->packs[$key] = $pack;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack;

use Ahc\Json\Comment;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
use Throwable;
use function file_get_contents;
use function implode;
use function is_array;
use function is_dir;
use function is_string;
use function rtrim;
use function sort;
use function str_replace;
use function strlen;
use function strtolower;
use function substr;

/**
 * A behavior pack found on disk, identified by its manifest.json.
 */
final class BehaviorPack{

	/**
	 * @param array<string, mixed> $manifest
	 */
	public function __construct(
		private string $path,
		private array $manifest
	){
		$this->path = rtrim(str_replace("\\", "/", $path), "/");
	}

	public function getPath() : string{
		return $this->path;
	}

	/**
	 * @return array<string, mixed>
	 */
	public function getManifest() : array{
		return $this->manifest;
	}

	public function getName() : string{
		$name = $this->manifest["header"]["name"] ?? null;
		return is_string($name) ? $name : $this->path;
	}

	public function getUuid() : string{
		$uuid = $this->manifest["header"]["uuid"] ?? null;
		return is_string($uuid) ? strtolower($uuid) : "";
	}

	public function getVersion() : string{
		$version = $this->manifest["header"]["version"] ?? null;
		if(is_array($version)){
			return implode(".", $version);
		}
		return is_string($version) ? $version : "0.0.0";
	}

	/**
	 * Returns the manifest modules of the given type ("data", "script", ...).
	 *
	 * @return list<array<string, mixed>>
	 */
	public function getModules(string $type) : array{
		$modules = [];
		foreach($this->manifest["modules"] ?? [] as $module){
			if(is_array($module) && ($module["type"] ?? null) === $type){
				$modules[] = $module;
			}
		}
		return $modules;
	}

	/**
	 * Returns the path of the script entry relative to the pack root, or null
	 * when the pack declares no script module.
	 */
	public function getScriptEntry() : ?string{
		foreach(["script", "javascript"] as $type){
			foreach($this->getModules($type) as $module){
				$entry = $module["entry"] ?? null;
				if(is_string($entry) && $entry !== ""){
					return $entry;
				}
			}
		}
		return null;
	}

	/**
	 * Returns the "module_name => version" pairs of the manifest dependencies
	 * that reference a script module, such as "@minecraft/server".
	 *
	 * @return array<string, string>
	 */
	public function getScriptDependencies() : array{
		$dependencies = [];
		foreach($this->manifest["dependencies"] ?? [] as $dependency){
			if(!is_array($dependency)){
				continue;
			}
			$name = $dependency["module_name"] ?? null;
			$version = $dependency["version"] ?? null;
			if(is_string($name)){
				$dependencies[$name] = is_array($version) ? implode(".", $version) : (is_string($version) ? $version : "");
			}
		}
		return $dependencies;
	}

	public function hasDirectory(string $directory) : bool{
		return is_dir($this->path . "/" . $directory);
	}

	/**
	 * Returns the absolute paths of the files with the given extension found
	 * recursively in a directory of the pack, sorted, or an empty list when
	 * the directory does not exist.
	 *
	 * @return list<string>
	 */
	public function listFiles(string $directory, string $extension = "json") : array{
		$root = $this->path . "/" . $directory;
		if(!is_dir($root)){
			return [];
		}

		$suffix = "." . strtolower($extension);
		$files = [];
		/** @var SplFileInfo $file */
		foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS)) as $file){
			$name = strtolower($file->getFilename());
			if($file->isFile() && substr($name, -strlen($suffix)) === $suffix){
				$files[] = str_replace("\\", "/", $file->getPathname());
			}
		}
		sort($files);
		return $files;
	}

	/**
	 * Returns a path relative to the pack root, as used by the Bedrock files
	 * that reference each other (for example "loot_tables/blocks/ore.json").
	 */
	public function relativePath(string $absolutePath) : string{
		$absolutePath = str_replace("\\", "/", $absolutePath);
		$prefix = $this->path . "/";
		return substr($absolutePath, 0, strlen($prefix)) === $prefix ? substr($absolutePath, strlen($prefix)) : $absolutePath;
	}

	/**
	 * Reads a Bedrock JSON file, which may contain comments.
	 *
	 * @return array<mixed>
	 * @throws BehaviorPackException
	 */
	public static function readJson(string $absolutePath) : array{
		$contents = @file_get_contents($absolutePath);
		if($contents === false){
			throw new BehaviorPackException("Cannot read $absolutePath");
		}
		try{
			$decoded = (new Comment())->decode($contents, true);
		}catch(Throwable $e){
			throw new BehaviorPackException("Invalid JSON in $absolutePath: " . $e->getMessage(), 0, $e);
		}
		if(!is_array($decoded)){
			throw new BehaviorPackException("Expected a JSON object in $absolutePath");
		}
		return $decoded;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack;

/**
 * Loads one kind of behavior pack content. Loaders run once, in the order
 * declared by BehaviorPackModule, after every pack has been discovered.
 */
interface ContentLoader{

	/**
	 * Short name used in the console, for example "custom items and blocks".
	 */
	public function getName() : string;

	/**
	 * Whether this loader needs the Customies plugin. Such loaders run when
	 * Customies enables, and are skipped with a warning when it is missing.
	 */
	public function requiresCustomies() : bool;

	/**
	 * Loads the content of every pack. Called on the main thread. A problem
	 * in one file must be logged and skipped, not thrown, so that one broken
	 * pack does not stop the others.
	 *
	 * @param list<BehaviorPack> $packs
	 */
	public function load(array $packs) : void;

	/**
	 * Releases what the loader holds, when the plugin is disabled.
	 */
	public function close() : void;
}
<?php

declare(strict_types=1);

namespace behaviorpack\custom;

use customiesdevs\customies\block\component\BlockComponent;
use pocketmine\nbt\tag\CompoundTag;
use function round;

/**
 * The minecraft:transformation block component, which Customies only builds
 * inside RotatableTrait.
 */
final class TransformationComponent implements BlockComponent{

	/**
	 * @param array{float, float, float} $rotation
	 * @param array{float, float, float} $translation
	 * @param array{float, float, float} $scale
	 */
	public function __construct(
		private array $rotation,
		private array $translation,
		private array $scale
	){
	}

	public function getName() : string{
		return "minecraft:transformation";
	}

	public function getValue() : CompoundTag{
		return CompoundTag::create()
			->setInt("RX", self::quarterTurns($this->rotation[0]))
			->setInt("RY", self::quarterTurns($this->rotation[1]))
			->setInt("RZ", self::quarterTurns($this->rotation[2]))
			->setFloat("SX", $this->scale[0])
			->setFloat("SY", $this->scale[1])
			->setFloat("SZ", $this->scale[2])
			->setFloat("TX", $this->translation[0])
			->setFloat("TY", $this->translation[1])
			->setFloat("TZ", $this->translation[2]);
	}

	private static function quarterTurns(float $degrees) : int{
		return (((int) round($degrees / 90)) % 4 + 4) % 4;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\custom;

use customiesdevs\customies\item\ItemComponents;
use pocketmine\item\Durable;
use pocketmine\item\ItemIdentifier;

/**
 * A behavior pack item with minecraft:durability.
 *
 * @phpstan-import-type ItemDefinition from BehaviorItemTrait
 */
final class BehaviorDurableItem extends Durable implements ItemComponents{
	use BehaviorItemTrait;

	/**
	 * @phpstan-param ItemDefinition $definition
	 */
	public function __construct(ItemIdentifier $identifier, array $definition){
		$this->definition = $definition;
		parent::__construct($identifier, $definition["name"]);
	}

	public function getMaxDurability() : int{
		return $this->definition["durability"];
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\custom;

use customiesdevs\customies\item\component\ItemComponent;
use pocketmine\block\Block;
use pocketmine\item\StringToItemParser;

/**
 * Shared behaviour of the items defined by a behavior pack minecraft:item
 * file: Customies components and the server-side values read from the JSON.
 *
 * @phpstan-type ItemDefinition array{
 *     identifier: string,
 *     name: string,
 *     maxStackSize: int,
 *     attackPoints: int,
 *     fuelTicks: int,
 *     cooldownTicks: int,
 *     cooldownTag: string|null,
 *     blockPlacer: string|null,
 *     durability: int,
 *     nutrition: int,
 *     saturation: float,
 *     canAlwaysEat: bool,
 *     residue: string|null,
 *     useTicks: int,
 *     armorSlot: int|null,
 *     protection: int
 * }
 */
trait BehaviorItemTrait{

	/**
	 * @phpstan-var ItemDefinition
	 */
	protected array $definition;

	/** @var array<string, ItemComponent> */
	private array $itemComponents = [];

	public function getIdentifier() : string{
		return $this->definition["identifier"];
	}

	public function addComponent(ItemComponent $component) : void{
		$this->itemComponents[$component->getName()] = $component;
	}

	public function hasComponent(string $name) : bool{
		return isset($this->itemComponents[$name]);
	}

	/**
	 * @return array<string, ItemComponent>
	 */
	public function getComponents() : array{
		return $this->itemComponents;
	}

	public function getMaxStackSize() : int{
		return $this->definition["maxStackSize"];
	}

	public function getAttackPoints() : int{
		return $this->definition["attackPoints"];
	}

	public function getFuelTime() : int{
		return $this->definition["fuelTicks"];
	}

	public function getCooldownTicks() : int{
		return $this->definition["cooldownTicks"];
	}

	public function getCooldownTag() : ?string{
		return $this->definition["cooldownTag"];
	}

	public function getBlock(?int $clickedFace = null) : Block{
		$identifier = $this->definition["blockPlacer"];
		if($identifier !== null){
			$item = StringToItemParser::getInstance()->parse($identifier);
			if($item !== null){
				return $item->getBlock($clickedFace);
			}
		}
		return parent::getBlock($clickedFace);
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\custom;

use customiesdevs\customies\item\ItemComponents;
use pocketmine\item\Item;
use pocketmine\item\ItemIdentifier;

/**
 * A plain behavior pack item, reused for every identifier.
 *
 * @phpstan-import-type ItemDefinition from BehaviorItemTrait
 */
final class BehaviorItem extends Item implements ItemComponents{
	use BehaviorItemTrait;

	/**
	 * @phpstan-param ItemDefinition $definition
	 */
	public function __construct(ItemIdentifier $identifier, array $definition){
		$this->definition = $definition;
		parent::__construct($identifier, $definition["name"]);
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\custom;

use customiesdevs\customies\item\ItemComponents;
use pocketmine\item\Food;
use pocketmine\item\Item;
use pocketmine\item\ItemIdentifier;
use pocketmine\item\StringToItemParser;

/**
 * A behavior pack item with minecraft:food.
 *
 * @phpstan-import-type ItemDefinition from BehaviorItemTrait
 */
final class BehaviorFoodItem extends Food implements ItemComponents{
	use BehaviorItemTrait;

	/**
	 * @phpstan-param ItemDefinition $definition
	 */
	public function __construct(ItemIdentifier $identifier, array $definition){
		$this->definition = $definition;
		parent::__construct($identifier, $definition["name"]);
	}

	public function getFoodRestore() : int{
		return $this->definition["nutrition"];
	}

	public function getSaturationRestore() : float{
		return $this->definition["saturation"];
	}

	public function requiresHunger() : bool{
		return !$this->definition["canAlwaysEat"];
	}

	public function getResidue() : Item{
		$residue = $this->definition["residue"];
		if($residue !== null){
			$item = StringToItemParser::getInstance()->parse($residue);
			if($item !== null){
				return $item;
			}
		}
		return parent::getResidue();
	}

	public function getMinUseDuration() : int{
		return $this->definition["useTicks"] > 0 ? $this->definition["useTicks"] : parent::getMinUseDuration();
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\custom;

use customiesdevs\customies\item\ItemComponents;
use pocketmine\item\Armor;
use pocketmine\item\ArmorTypeInfo;
use pocketmine\item\ItemIdentifier;

/**
 * A behavior pack item with minecraft:wearable in an armor slot.
 *
 * @phpstan-import-type ItemDefinition from BehaviorItemTrait
 */
final class BehaviorArmorItem extends Armor implements ItemComponents{
	use BehaviorItemTrait;

	/**
	 * @phpstan-param ItemDefinition $definition
	 */
	public function __construct(ItemIdentifier $identifier, array $definition){
		$this->definition = $definition;
		parent::__construct($identifier, $definition["name"], new ArmorTypeInfo(
			$definition["protection"],
			$definition["durability"],
			$definition["armorSlot"] ?? 0
		));
		if($definition["durability"] <= 0){
			$this->setUnbreakable();
		}
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\custom;

use behaviorpack\BehaviorPackException;
use customiesdevs\customies\block\component\BlockComponent;
use customiesdevs\customies\block\component\CollisionBoxComponent;
use customiesdevs\customies\block\component\DestructibleByExplosionComponent;
use customiesdevs\customies\block\component\DestructibleByMiningComponent;
use customiesdevs\customies\block\component\DisplayNameComponent;
use customiesdevs\customies\block\component\FlammableComponent;
use customiesdevs\customies\block\component\FrictionComponent;
use customiesdevs\customies\block\component\GeometryComponent;
use customiesdevs\customies\block\component\LightDampeningComponent;
use customiesdevs\customies\block\component\LightEmissionComponent;
use customiesdevs\customies\block\component\MaterialInstancesComponent;
use customiesdevs\customies\block\component\SelectionBoxComponent;
use customiesdevs\customies\block\Material;
use pocketmine\math\Vector3;
use function count;
use function in_array;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function max;
use function min;

/**
 * Turns the JSON components of a minecraft:block definition into Customies
 * block components, and reads the server-side values they imply.
 */
final class BlockComponentMapper{

	public const UNBREAKABLE_EXPLOSION_RESISTANCE = 3600000.0;

	/** Components handled by other loaders or by scripts, never reported. */
	public const IGNORED = [
		"minecraft:loot",
		"minecraft:custom_components"
	];

	public const SUPPORTED = [
		"minecraft:geometry",
		"minecraft:unit_cube",
		"minecraft:material_instances",
		"minecraft:collision_box",
		"minecraft:selection_box",
		"minecraft:destructible_by_mining",
		"minecraft:destructible_by_explosion",
		"minecraft:friction",
		"minecraft:light_emission",
		"minecraft:light_dampening",
		"minecraft:flammable",
		"minecraft:display_name",
		"minecraft:transformation"
	];

	private function __construct(){
	}

	public static function isSupported(string $name) : bool{
		return in_array($name, self::SUPPORTED, true);
	}

	/**
	 * Returns the Customies component for a JSON component, or null when it
	 * is not supported.
	 *
	 * @throws BehaviorPackException
	 */
	public static function map(string $name, mixed $value) : ?BlockComponent{
		return match($name){
			"minecraft:geometry" => self::geometry($value),
			"minecraft:unit_cube" => new GeometryComponent(),
			"minecraft:material_instances" => self::materialInstances($value),
			"minecraft:collision_box" => self::collisionBox($value),
			"minecraft:selection_box" => self::selectionBox($value),
			"minecraft:destructible_by_mining" => new DestructibleByMiningComponent(self::hardness($value)),
			"minecraft:destructible_by_explosion" => new DestructibleByExplosionComponent(self::explosionResistance($value)),
			"minecraft:friction" => new FrictionComponent(self::friction($value)),
			"minecraft:light_emission" => new LightEmissionComponent(self::lightLevel($value, 0)),
			"minecraft:light_dampening" => new LightDampeningComponent(self::lightLevel($value, 15)),
			"minecraft:flammable" => self::flammable($value),
			"minecraft:display_name" => new DisplayNameComponent(self::displayName($value)),
			"minecraft:transformation" => self::transformation($value),
			default => null
		};
	}

	/**
	 * Returns the seconds_to_destroy of a destructible_by_mining value, or -1
	 * when the block cannot be mined.
	 *
	 * @throws BehaviorPackException
	 */
	public static function hardness(mixed $value) : float{
		if($value === false){
			return -1.0;
		}
		if($value === true){
			return 0.0;
		}
		return self::number(self::field($value, "seconds_to_destroy", 0.0), "destructible_by_mining");
	}

	/**
	 * @throws BehaviorPackException
	 */
	public static function explosionResistance(mixed $value) : float{
		if($value === false){
			return self::UNBREAKABLE_EXPLOSION_RESISTANCE;
		}
		if($value === true){
			return 0.0;
		}
		return self::number(self::field($value, "explosion_resistance", 0.0), "destructible_by_explosion");
	}

	/**
	 * @throws BehaviorPackException
	 */
	public static function friction(mixed $value) : float{
		return max(0.0, min(0.9, self::number(self::field($value, "value", 0.4), "friction")));
	}

	/**
	 * @throws BehaviorPackException
	 */
	public static function lightLevel(mixed $value, int $default) : int{
		return (int) max(0, min(15, self::number(self::field($value, "value", $default), "light level")));
	}

	/**
	 * Returns the catch and destroy chance modifiers, or null when the block
	 * does not burn.
	 *
	 * @return array{int, int}|null
	 * @throws BehaviorPackException
	 */
	public static function flammability(mixed $value) : ?array{
		if($value === false){
			return null;
		}
		if($value === true){
			return [5, 20];
		}
		if(!is_array($value)){
			throw new BehaviorPackException("minecraft:flammable must be a boolean or an object");
		}
		return [
			(int) self::number($value["catch_chance_modifier"] ?? 5, "catch_chance_modifier"),
			(int) self::number($value["destroy_chance_modifier"] ?? 20, "destroy_chance_modifier")
		];
	}

	/**
	 * Returns the box of a collision_box or selection_box value in block
	 * units as [minX, minY, minZ, maxX, maxY, maxZ], or null when disabled.
	 *
	 * @return list<float>|null
	 * @throws BehaviorPackException
	 */
	public static function box(mixed $value) : ?array{
		if($value === false){
			return null;
		}
		if($value === true){
			return [0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
		}
		[$origin, $size] = self::boxVectors($value);
		$minX = ($origin->x + 8) / 16;
		$minY = $origin->y / 16;
		$minZ = ($origin->z + 8) / 16;
		return [$minX, $minY, $minZ, $minX + $size->x / 16, $minY + $size->y / 16, $minZ + $size->z / 16];
	}

	/**
	 * @throws BehaviorPackException
	 */
	public static function displayName(mixed $value) : string{
		$name = self::field($value, "value", null);
		if(!is_string($name)){
			throw new BehaviorPackException("minecraft:display_name must be a string");
		}
		return $name;
	}

	/**
	 * @throws BehaviorPackException
	 */
	private static function geometry(mixed $value) : GeometryComponent{
		if(is_string($value)){
			return new GeometryComponent($value);
		}
		if(!is_array($value) || !is_string($value["identifier"] ?? null)){
			throw new BehaviorPackException("minecraft:geometry needs an identifier");
		}
		$component = new GeometryComponent($value["identifier"]);
		$bones = $value["bone_visibility"] ?? [];
		if(is_array($bones)){
			foreach($bones as $bone => $visibility){
				if(is_bool($visibility) || is_string($visibility)){
					$component->addBoneVisibility((string) $bone, $visibility);
				}
			}
		}
		return $component;
	}

	/**
	 * @throws BehaviorPackException
	 */
	private static function materialInstances(mixed $value) : MaterialInstancesComponent{
		if(!is_array($value) || count($value) === 0){
			throw new BehaviorPackException("minecraft:material_instances must be a non-empty object");
		}
		$materials = [];
		foreach($value as $target => $instance){
			$resolved = $instance;
			$depth = 0;
			while(is_string($resolved) && $depth < 8){
				$resolved = $value[$resolved] ?? null;
				$depth++;
			}
			if(!is_array($resolved)){
				throw new BehaviorPackException("Material instance \"$target\" cannot be resolved");
			}
			$texture = $resolved["texture"] ?? null;
			if(!is_string($texture)){
				throw new BehaviorPackException("Material instance \"$target\" has no texture");
			}
			$renderMethod = $resolved["render_method"] ?? Material::RENDER_METHOD_OPAQUE;
			$materials[] = new Material(
				(string) $target,
				$texture,
				is_string($renderMethod) ? $renderMethod : Material::RENDER_METHOD_OPAQUE,
				($resolved["face_dimming"] ?? true) !== false,
				($resolved["ambient_occlusion"] ?? true) !== false
			);
		}
		return new MaterialInstancesComponent($materials);
	}

	/**
	 * @throws BehaviorPackException
	 */
	private static function collisionBox(mixed $value) : CollisionBoxComponent{
		if(is_bool($value)){
			return new CollisionBoxComponent($value);
		}
		[$origin, $size] = self::boxVectors($value);
		return new CollisionBoxComponent(true, $origin, $size);
	}

	/**
	 * @throws BehaviorPackException
	 */
	private static function selectionBox(mixed $value) : SelectionBoxComponent{
		if(is_bool($value)){
			return new SelectionBoxComponent($value);
		}
		[$origin, $size] = self::boxVectors($value);
		return new SelectionBoxComponent(true, $origin, $size);
	}

	/**
	 * @throws BehaviorPackException
	 */
	private static function flammable(mixed $value) : FlammableComponent{
		$flammability = self::flammability($value);
		if($flammability === null){
			return new FlammableComponent(0, 0);
		}
		return new FlammableComponent($flammability[0], $flammability[1]);
	}

	/**
	 * @throws BehaviorPackException
	 */
	private static function transformation(mixed $value) : TransformationComponent{
		if(!is_array($value)){
			throw new BehaviorPackException("minecraft:transformation must be an object");
		}
		return new TransformationComponent(
			self::triple($value["rotation"] ?? null, 0.0),
			self::triple($value["translation"] ?? null, 0.0),
			self::triple($value["scale"] ?? null, 1.0)
		);
	}

	/**
	 * @return array{Vector3, Vector3}
	 * @throws BehaviorPackException
	 */
	private static function boxVectors(mixed $value) : array{
		if(!is_array($value)){
			throw new BehaviorPackException("A block box must be a boolean or an object");
		}
		$origin = self::triple($value["origin"] ?? [-8, 0, -8], 0.0);
		$size = self::triple($value["size"] ?? [16, 16, 16], 16.0);
		return [new Vector3($origin[0], $origin[1], $origin[2]), new Vector3($size[0], $size[1], $size[2])];
	}

	/**
	 * @return array{float, float, float}
	 * @throws BehaviorPackException
	 */
	private static function triple(mixed $value, float $default) : array{
		if($value === null){
			return [$default, $default, $default];
		}
		if(!is_array($value) || count($value) !== 3){
			throw new BehaviorPackException("Expected an array of 3 numbers");
		}
		return [
			self::number($value[0] ?? null, "vector"),
			self::number($value[1] ?? null, "vector"),
			self::number($value[2] ?? null, "vector")
		];
	}

	private static function field(mixed $value, string $key, mixed $default) : mixed{
		if(is_array($value)){
			return $value[$key] ?? $default;
		}
		return $value ?? $default;
	}

	/**
	 * @throws BehaviorPackException
	 */
	private static function number(mixed $value, string $what) : float{
		if(!is_int($value) && !is_float($value)){
			throw new BehaviorPackException("Expected a number for $what");
		}
		return (float) $value;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\custom;

use behaviorpack\BehaviorPack;
use behaviorpack\ContentLoader;
use Closure;
use customiesdevs\customies\item\CreativeInventoryInfo;
use pocketmine\plugin\PluginBase;
use Throwable;
use function in_array;
use function is_array;
use function is_string;
use function str_starts_with;
use function substr;

/**
 * Registers the custom items (items/) and blocks (blocks/) of the behavior
 * packs through Customies. Blocks are registered first so that items can
 * reference them, and an item sharing the identifier of a block of the same
 * pack is skipped since the block already has its item.
 */
final class CustomContentLoader implements ContentLoader{

	private const CATEGORIES = [
		"construction",
		"equipment",
		"items",
		"nature"
	];

	public function __construct(
		private PluginBase $plugin
	){
	}

	public function getName() : string{
		return "custom items and blocks";
	}

	public function requiresCustomies() : bool{
		return true;
	}

	public function load(array $packs) : void{
		$blockRegistrar = new CustomBlockRegistrar($this->plugin);
		$itemRegistrar = new CustomItemRegistrar($this->plugin);
		$blockCount = 0;
		$itemCount = 0;

		/** @var array<string, array<string, true>> $packBlocks */
		$packBlocks = [];
		foreach($packs as $pack){
			$packBlocks[$pack->getPath()] = [];
			foreach($pack->listFiles("blocks") as $file){
				$identifier = $blockRegistrar->readIdentifier($file);
				if($identifier !== null){
					$packBlocks[$pack->getPath()][$identifier] = true;
				}
				if($this->guard($pack, $file, "block", static fn() : bool => $blockRegistrar->register($pack, $file))){
					$blockCount++;
				}
			}
		}

		foreach($packs as $pack){
			foreach($pack->listFiles("items") as $file){
				$identifier = $itemRegistrar->readIdentifier($file);
				if($identifier !== null && isset($packBlocks[$pack->getPath()][$identifier])){
					continue;
				}
				if($this->guard($pack, $file, "item", static fn() : bool => $itemRegistrar->register($pack, $file))){
					$itemCount++;
				}
			}
		}

		if($itemCount > 0 || $blockCount > 0){
			$this->plugin->getLogger()->info("Behavior packs: $itemCount custom items, $blockCount custom blocks");
		}
	}

	public function close() : void{
	}

	/**
	 * Returns the Customies creative inventory info of a description, or null
	 * when the content is not shown in the creative inventory.
	 *
	 * @param array<mixed> $description
	 */
	public static function creativeInfo(array $description) : ?CreativeInventoryInfo{
		$menu = $description["menu_category"] ?? null;
		$category = is_array($menu) ? ($menu["category"] ?? null) : ($description["category"] ?? null);
		if(!is_string($category) || !in_array($category, self::CATEGORIES, true)){
			return null;
		}
		$group = is_array($menu) ? ($menu["group"] ?? null) : null;
		if(!is_string($group) || $group === ""){
			return new CreativeInventoryInfo($category, CreativeInventoryInfo::NONE);
		}
		if(str_starts_with($group, "minecraft:")){
			$group = substr($group, 10);
		}
		return new CreativeInventoryInfo($category, $group);
	}

	/**
	 * @param Closure() : bool $register
	 */
	private function guard(BehaviorPack $pack, string $file, string $kind, Closure $register) : bool{
		try{
			return $register();
		}catch(Throwable $e){
			$this->plugin->getLogger()->warning("Behavior packs: skipped $kind " . $pack->getName() . "/" . $pack->relativePath($file) . ": " . $e->getMessage());
			return false;
		}
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\custom;

use customiesdevs\customies\block\BlockComponents;
use customiesdevs\customies\block\component\BlockComponent;
use pocketmine\block\Block;
use pocketmine\block\BlockBreakInfo;
use pocketmine\block\BlockIdentifier;
use pocketmine\block\BlockToolType;
use pocketmine\block\BlockTypeInfo;
use pocketmine\math\AxisAlignedBB;
use Throwable;
use function count;

/**
 * A block defined by a behavior pack minecraft:block file. Every setting
 * comes from a plain definition array built by CustomBlockRegistrar, so the
 * block can be rebuilt on the async workers.
 *
 * @phpstan-type Definition array{
 *     identifier: string,
 *     name: string,
 *     hardness: float,
 *     blastResistance: float,
 *     lightLevel: int,
 *     lightFilter: int,
 *     frictionFactor: float,
 *     flammability: array{int, int}|null,
 *     collision: list<float>|null,
 *     components: array<string, mixed>,
 *     properties: list<array{string, list<bool|int|string>}>,
 *     permutations: list<array{string, array<string, mixed>}>,
 *     traits: array{cardinal: bool, yRotationOffset: int, facing: bool, blockFace: bool, verticalHalf: bool}
 * }
 */
class BehaviorBlock extends Block implements BlockComponents{

	/** @var array<string, BlockComponent> */
	private array $extraComponents = [];

	/**
	 * @phpstan-param Definition $definition
	 */
	public function __construct(
		BlockIdentifier $idInfo,
		BlockTypeInfo $typeInfo,
		protected array $definition
	){
		parent::__construct($idInfo, $definition["name"], $typeInfo);
	}

	/**
	 * @phpstan-param Definition $definition
	 */
	public static function create(int $typeId, array $definition) : Block{
		$typeInfo = new BlockTypeInfo(new BlockBreakInfo(
			$definition["hardness"],
			BlockToolType::NONE,
			0,
			$definition["blastResistance"]
		));
		if(count($definition["properties"]) > 0){
			return new BehaviorPermutableBlock(new BlockIdentifier($typeId), $typeInfo, $definition);
		}
		return new BehaviorBlock(new BlockIdentifier($typeId), $typeInfo, $definition);
	}

	public function getIdentifier() : string{
		return $this->definition["identifier"];
	}

	public function addComponent(BlockComponent $component) : void{
		$this->extraComponents[$component->getName()] = $component;
	}

	public function hasComponent(string $name) : bool{
		return isset($this->getComponents()[$name]);
	}

	/**
	 * @return array<string, BlockComponent>
	 */
	public function getComponents() : array{
		$components = [];
		foreach($this->definition["components"] as $name => $value){
			try{
				$component = BlockComponentMapper::map($name, $value);
			}catch(Throwable){
				$component = null;
			}
			if($component !== null){
				$components[$component->getName()] = $component;
			}
		}
		foreach($this->extraComponents as $name => $component){
			$components[$name] = $component;
		}
		return $components;
	}

	public function getLightLevel() : int{
		return $this->definition["lightLevel"];
	}

	public function getLightFilter() : int{
		return $this->definition["lightFilter"];
	}

	public function getFrictionFactor() : float{
		return $this->definition["frictionFactor"];
	}

	public function getFlameEncouragement() : int{
		return $this->definition["flammability"][0] ?? 0;
	}

	public function getFlammability() : int{
		return $this->definition["flammability"][1] ?? 0;
	}

	public function isSolid() : bool{
		return $this->definition["collision"] !== null;
	}

	protected function recalculateCollisionBoxes() : array{
		$box = $this->definition["collision"];
		if($box === null){
			return [];
		}
		return [new AxisAlignedBB($box[0], $box[1], $box[2], $box[3], $box[4], $box[5])];
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\custom;

use behaviorpack\BehaviorPack;
use behaviorpack\BehaviorPackException;
use customiesdevs\customies\item\component\AllowOffHandComponent;
use customiesdevs\customies\item\component\BlockPlacerComponent;
use customiesdevs\customies\item\component\BundleInteractionComponent;
use customiesdevs\customies\item\component\CanDestroyInCreativeComponent;
use customiesdevs\customies\item\component\CooldownComponent;
use customiesdevs\customies\item\component\DamageAbsorptionComponent;
use customiesdevs\customies\item\component\DamageComponent;
use customiesdevs\customies\item\component\DiggerComponent;
use customiesdevs\customies\item\component\DisplayNameComponent;
use customiesdevs\customies\item\component\DurabilityComponent;
use customiesdevs\customies\item\component\DyeableComponent;
use customiesdevs\customies\item\component\EnchantableSlotComponent;
use customiesdevs\customies\item\component\EnchantableValueComponent;
use customiesdevs\customies\item\component\FoodComponent;
use customiesdevs\customies\item\component\FuelComponent;
use customiesdevs\customies\item\component\GlintComponent;
use customiesdevs\customies\item\component\HandEquippedComponent;
use customiesdevs\customies\item\component\HoverTextColorComponent;
use customiesdevs\customies\item\component\IconComponent;
use customiesdevs\customies\item\component\InteractButtonComponent;
use customiesdevs\customies\item\component\LiquidClippedComponent;
use customiesdevs\customies\item\component\MaxStackSizeComponent;
use customiesdevs\customies\item\component\ProjectileComponent;
use customiesdevs\customies\item\component\RarityComponent;
use customiesdevs\customies\item\component\RecordComponent;
use customiesdevs\customies\item\component\ShooterComponent;
use customiesdevs\customies\item\component\ShouldDespawnComponent;
use customiesdevs\customies\item\component\StackedByDataComponent;
use customiesdevs\customies\item\component\ThrowableComponent;
use customiesdevs\customies\item\component\UseAnimationComponent;
use customiesdevs\customies\item\component\UseDurationComponent;
use customiesdevs\customies\item\component\UseModifiersComponent;
use customiesdevs\customies\item\component\WearableComponent;
use customiesdevs\customies\item\CustomiesItemFactory;
use customiesdevs\customies\item\ItemComponents;
use pocketmine\block\Block;
use pocketmine\block\BlockTypeIds;
use pocketmine\inventory\ArmorInventory;
use pocketmine\item\Item;
use pocketmine\item\ItemIdentifier;
use pocketmine\item\ItemTypeIds;
use pocketmine\item\StringToItemParser;
use pocketmine\plugin\PluginBase;
use function count;
use function implode;
use function in_array;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function preg_match_all;
use function round;
use function str_contains;
use function strrpos;
use function substr;

/**
 * Registers the minecraft:item files of the behavior packs through
 * Customies. Customies registers an item from a closure, so the same few
 * classes serve every identifier.
 *
 * @phpstan-import-type ItemDefinition from BehaviorItemTrait
 */
final class CustomItemRegistrar{

	/** Components handled by other loaders or by scripts, never reported. */
	private const IGNORED = [
		"minecraft:custom_components"
	];

	private const SATURATION = [
		"poor" => 0.1,
		"low" => 0.3,
		"normal" => 0.6,
		"good" => 0.8,
		"max" => 1.0,
		"supernatural" => 1.2
	];

	private const ANIMATIONS = [
		"none" => UseAnimationComponent::ANIMATION_NONE,
		"eat" => UseAnimationComponent::ANIMATION_EAT,
		"drink" => UseAnimationComponent::ANIMATION_DRINK,
		"block" => UseAnimationComponent::ANIMATION_BLOCK,
		"bow" => UseAnimationComponent::ANIMATION_BOW,
		"camera" => UseAnimationComponent::ANIMATION_CAMERA,
		"spear" => UseAnimationComponent::ANIMATION_SPEAR,
		"crossbow" => UseAnimationComponent::ANIMATION_CROSSBOW,
		"spyglass" => UseAnimationComponent::ANIMATION_SPYGLASS,
		"brush" => UseAnimationComponent::ANIMATION_BRUSH
	];

	private const ARMOR_SLOTS = [
		WearableComponent::SLOT_ARMOR_HEAD => ArmorInventory::SLOT_HEAD,
		WearableComponent::SLOT_ARMOR_CHEST => ArmorInventory::SLOT_CHEST,
		WearableComponent::SLOT_ARMOR_LEGS => ArmorInventory::SLOT_LEGS,
		WearableComponent::SLOT_ARMOR_FEET => ArmorInventory::SLOT_FEET
	];

	public function __construct(
		private PluginBase $plugin
	){
	}

	/**
	 * Reads the identifier of an item file without registering it, or null
	 * when the file is not a valid item definition.
	 */
	public function readIdentifier(string $file) : ?string{
		try{
			$identifier = BehaviorPack::readJson($file)["minecraft:item"]["description"]["identifier"] ?? null;
		}catch(BehaviorPackException){
			return null;
		}
		return is_string($identifier) ? $identifier : null;
	}

	/**
	 * Registers one item file, returns whether an item was registered.
	 */
	public function register(BehaviorPack $pack, string $file) : bool{
		$path = $pack->getName() . "/" . $pack->relativePath($file);
		try{
			return $this->registerFile($file);
		}catch(BehaviorPackException $e){
			$this->plugin->getLogger()->warning("Behavior packs: skipped item $path: " . $e->getMessage());
			return false;
		}
	}

	/**
	 * @throws BehaviorPackException
	 */
	private function registerFile(string $file) : bool{
		$json = BehaviorPack::readJson($file)["minecraft:item"] ?? null;
		if(!is_array($json)){
			throw new BehaviorPackException("no minecraft:item object");
		}
		$description = $json["description"] ?? null;
		$identifier = is_array($description) ? ($description["identifier"] ?? null) : null;
		if(!is_array($description) || !is_string($identifier) || !str_contains($identifier, ":")){
			throw new BehaviorPackException("missing or invalid description.identifier");
		}
		if(StringToItemParser::getInstance()->parse($identifier) !== null){
			throw new BehaviorPackException("$identifier is already registered");
		}
		$components = $json["components"] ?? [];
		if(!is_array($components)){
			throw new BehaviorPackException("components must be an object");
		}

		$list = [];
		$unsupported = [];
		/** @phpstan-var ItemDefinition $definition */
		$definition = [
			"identifier" => $identifier,
			"name" => $identifier,
			"maxStackSize" => 64,
			"attackPoints" => 0,
			"fuelTicks" => 0,
			"cooldownTicks" => 0,
			"cooldownTag" => null,
			"blockPlacer" => null,
			"durability" => 0,
			"nutrition" => 0,
			"saturation" => 0.0,
			"canAlwaysEat" => false,
			"residue" => null,
			"useTicks" => 0,
			"armorSlot" => null,
			"protection" => 0
		];
		$food = false;
		$texture = substr($identifier, (int) strrpos($identifier, ":") + 1);
		$dyedTexture = "";
		$trimTexture = "";

		foreach($components as $name => $value){
			$name = (string) $name;
			switch($name){
				case "minecraft:icon":
					[$texture, $dyedTexture, $trimTexture] = $this->icon($value);
					break;
				case "minecraft:display_name":
					$definition["name"] = $this->string($value, "value", $name);
					$list[] = new DisplayNameComponent($definition["name"]);
					break;
				case "minecraft:max_stack_size":
					$definition["maxStackSize"] = (int) $this->number($value, "value", $name);
					break;
				case "minecraft:durability":
					$definition["durability"] = (int) $this->number($value, "max_durability", $name);
					$chance = is_array($value) ? ($value["damage_chance"] ?? []) : [];
					$min = is_array($chance) ? ($chance["min"] ?? 100) : 100;
					$max = is_array($chance) ? ($chance["max"] ?? 100) : 100;
					$list[] = new DurabilityComponent($definition["durability"], is_int($min) ? $min : 100, is_int($max) ? $max : 100);
					break;
				case "minecraft:damage":
					$definition["attackPoints"] = (int) $this->number($value, "value", $name);
					$list[] = new DamageComponent($definition["attackPoints"]);
					break;
				case "minecraft:digger":
					$digger = $this->digger($value);
					if($digger !== null){
						$list[] = $digger;
					}
					break;
				case "minecraft:food":
					$food = true;
					$list[] = $this->food($value, $definition);
					break;
				case "minecraft:fuel":
					$duration = $this->number($value, "duration", $name);
					$definition["fuelTicks"] = (int) round($duration * 20);
					$list[] = new FuelComponent($duration);
					break;
				case "minecraft:hand_equipped":
					$list[] = new HandEquippedComponent($this->bool($value, $name));
					break;
				case "minecraft:wearable":
					$slot = $this->string($value, "slot", $name);
					$definition["protection"] = is_array($value) ? (int) $this->optionalNumber($value["protection"] ?? 0, $name) : 0;
					$definition["armorSlot"] = self::ARMOR_SLOTS[$slot] ?? null;
					$list[] = new WearableComponent($slot, $definition["protection"], !is_array($value) || ($value["dispensable"] ?? true) !== false);
					break;
				case "minecraft:cooldown":
					$category = $this->string($value, "category", $name);
					$duration = $this->number($value, "duration", $name);
					$definition["cooldownTag"] = $category;
					$definition["cooldownTicks"] = (int) round($duration * 20);
					$list[] = new CooldownComponent($category, $duration);
					break;
				case "minecraft:throwable":
					$list[] = $this->throwable($value);
					break;
				case "minecraft:projectile":
					$list[] = new ProjectileComponent(
						is_array($value) ? $this->optionalNumber($value["minimum_critical_power"] ?? 1.25, $name) : 1.25,
						$this->string($value, "projectile_entity", $name)
					);
					break;
				case "minecraft:shooter":
					$list[] = $this->shooter($value);
					break;
				case "minecraft:allow_off_hand":
					$list[] = new AllowOffHandComponent($this->bool($value, $name));
					break;
				case "minecraft:glint":
					$list[] = new GlintComponent($this->bool($value, $name));
					break;
				case "minecraft:use_animation":
					$animation = $this->string($value, "value", $name);
					if(!isset(self::ANIMATIONS[$animation])){
						throw new BehaviorPackException("unknown use animation $animation");
					}
					$list[] = new UseAnimationComponent(self::ANIMATIONS[$animation]);
					break;
				case "minecraft:use_modifiers":
					$seconds = is_array($value) ? $this->optionalNumber($value["use_duration"] ?? 0, $name) : 0.0;
					$movement = is_array($value) ? $this->optionalNumber($value["movement_modifier"] ?? 1.0, $name) : 1.0;
					$definition["useTicks"] = (int) round($seconds * 20);
					$list[] = new UseModifiersComponent($movement, $seconds);
					$list[] = new UseDurationComponent($definition["useTicks"]);
					break;
				case "minecraft:use_duration":
					$definition["useTicks"] = (int) $this->number($value, "value", $name);
					$list[] = new UseDurationComponent($definition["useTicks"]);
					break;
				case "minecraft:block_placer":
					$list[] = $this->blockPlacer($value, $definition);
					break;
				case "minecraft:rarity":
					$list[] = new RarityComponent($this->string($value, "value", $name));
					break;
				case "minecraft:hover_text_color":
					$list[] = new HoverTextColorComponent($this->string($value, "value", $name));
					break;
				case "minecraft:stacked_by_data":
					$list[] = new StackedByDataComponent($this->bool($value, $name));
					break;
				case "minecraft:should_despawn":
					$list[] = new ShouldDespawnComponent($this->bool($value, $name));
					break;
				case "minecraft:liquid_clipped":
					$list[] = new LiquidClippedComponent($this->bool($value, $name));
					break;
				case "minecraft:can_destroy_in_creative":
					$list[] = new CanDestroyInCreativeComponent($this->bool($value, $name));
					break;
				case "minecraft:enchantable":
					if(!is_array($value)){
						throw new BehaviorPackException("$name must be an object");
					}
					$list[] = new EnchantableSlotComponent(is_string($value["slot"] ?? null) ? $value["slot"] : EnchantableSlotComponent::SLOT_ALL);
					$list[] = new EnchantableValueComponent((int) $this->optionalNumber($value["value"] ?? 1, $name));
					break;
				case "minecraft:interact_button":
					$list[] = new InteractButtonComponent(is_string($value) ? $value : true);
					break;
				case "minecraft:record":
					$list[] = new RecordComponent(
						(int) $this->number($value, "comparator_signal", $name),
						$this->number($value, "duration", $name),
						$this->string($value, "sound_event", $name)
					);
					break;
				case "minecraft:dyeable":
					$list[] = new DyeableComponent($this->string($value, "default_color", $name));
					break;
				case "minecraft:damage_absorption":
					$causes = is_array($value) ? ($value["absorbable_causes"] ?? null) : null;
					if(!is_array($causes)){
						throw new BehaviorPackException("$name needs absorbable_causes");
					}
					$list[] = new DamageAbsorptionComponent($causes);
					break;
				case "minecraft:bundle_interaction":
					$list[] = new BundleInteractionComponent((int) $this->number($value, "num_viewable_slots", $name));
					break;
				default:
					if(!in_array($name, self::IGNORED, true)){
						$unsupported[] = $name;
					}
			}
		}

		if(!isset($components["minecraft:max_stack_size"]) && ($definition["durability"] > 0 || $definition["armorSlot"] !== null)){
			$definition["maxStackSize"] = 1;
		}
		if($food && !isset($components["minecraft:use_animation"])){
			$list[] = new UseAnimationComponent(UseAnimationComponent::ANIMATION_EAT);
		}
		if($food && $definition["useTicks"] === 0){
			$list[] = new UseDurationComponent(32);
		}
		$list[] = new IconComponent($texture, $dyedTexture, $trimTexture);
		$list[] = new MaxStackSizeComponent($definition["maxStackSize"]);
		if(!isset($components["minecraft:can_destroy_in_creative"])){
			$list[] = new CanDestroyInCreativeComponent();
		}

		if(count($unsupported) > 0){
			$this->plugin->getLogger()->warning("Behavior packs: item $identifier: unsupported components ignored: " . implode(", ", $unsupported));
		}

		$item = $this->createItem($definition, $food);
		foreach($list as $component){
			$item->addComponent($component);
		}
		CustomiesItemFactory::getInstance()->registerItem(
			static fn() : Item => $item,
			$identifier,
			CustomContentLoader::creativeInfo($description)
		);
		return true;
	}

	/**
	 * @phpstan-param ItemDefinition $definition
	 */
	private function createItem(array $definition, bool $food) : Item&ItemComponents{
		$identifier = new ItemIdentifier(ItemTypeIds::newId());
		if($definition["armorSlot"] !== null){
			return new BehaviorArmorItem($identifier, $definition);
		}
		if($food){
			return new BehaviorFoodItem($identifier, $definition);
		}
		if($definition["durability"] > 0){
			return new BehaviorDurableItem($identifier, $definition);
		}
		return new BehaviorItem($identifier, $definition);
	}

	/**
	 * @return array{string, string, string}
	 * @throws BehaviorPackException
	 */
	private function icon(mixed $value) : array{
		if(is_string($value)){
			return [$value, "", ""];
		}
		if(!is_array($value)){
			throw new BehaviorPackException("minecraft:icon must be a string or an object");
		}
		$textures = $value["textures"] ?? $value["texture"] ?? null;
		if(is_string($textures)){
			return [$textures, "", ""];
		}
		if(!is_array($textures) || !is_string($textures["default"] ?? null)){
			throw new BehaviorPackException("minecraft:icon needs a default texture");
		}
		return [
			$textures["default"],
			is_string($textures["dyed"] ?? null) ? $textures["dyed"] : "",
			is_string($textures["icon_trim"] ?? null) ? $textures["icon_trim"] : ""
		];
	}

	/**
	 * @phpstan-param ItemDefinition $definition
	 * @throws BehaviorPackException
	 */
	private function food(mixed $value, array &$definition) : FoodComponent{
		if(!is_array($value)){
			throw new BehaviorPackException("minecraft:food must be an object");
		}
		$nutrition = (int) $this->optionalNumber($value["nutrition"] ?? 0, "nutrition");
		$modifier = $value["saturation_modifier"] ?? 0.6;
		if(is_string($modifier)){
			$modifier = self::SATURATION[$modifier] ?? throw new BehaviorPackException("unknown saturation_modifier $modifier");
		}
		$modifier = $this->optionalNumber($modifier, "saturation_modifier");
		$canAlwaysEat = ($value["can_always_eat"] ?? false) === true;
		$residue = $value["using_converts_to"] ?? null;
		if(is_array($residue)){
			$residue = $residue["name"] ?? $residue["item"] ?? null;
		}
		$residue = is_string($residue) && $residue !== "" ? $residue : null;

		$definition["nutrition"] = $nutrition;
		$definition["saturation"] = $nutrition * $modifier * 2;
		$definition["canAlwaysEat"] = $canAlwaysEat;
		$definition["residue"] = $residue;
		return new FoodComponent($canAlwaysEat, $nutrition, $modifier, $residue ?? "");
	}

	/**
	 * @throws BehaviorPackException
	 */
	private function digger(mixed $value) : ?DiggerComponent{
		if(!is_array($value)){
			throw new BehaviorPackException("minecraft:digger must be an object");
		}
		$digger = new DiggerComponent(($value["use_efficiency"] ?? false) === true);
		$added = false;
		foreach($value["destroy_speeds"] ?? [] as $entry){
			if(!is_array($entry)){
				continue;
			}
			$speed = (int) $this->optionalNumber($entry["speed"] ?? 1, "speed");
			$target = $entry["block"] ?? null;
			if(is_array($target) && is_string($target["tags"] ?? null)){
				preg_match_all("/'([^']+)'/", $target["tags"], $matches);
				if(count($matches[1]) > 0){
					$digger->withTags($speed, ...$matches[1]);
					$added = true;
				}
				continue;
			}
			if(is_array($target)){
				$target = $target["name"] ?? null;
			}
			$block = is_string($target) ? $this->resolveBlock($target) : null;
			if($block !== null){
				$digger->withBlocks($speed, $block);
				$added = true;
			}
		}
		return $added ? $digger : null;
	}

	/**
	 * @throws BehaviorPackException
	 */
	private function throwable(mixed $value) : ThrowableComponent{
		if(!is_array($value)){
			$value = [];
		}
		return new ThrowableComponent(
			($value["do_swing_animation"] ?? false) === true,
			$this->optionalNumber($value["launch_power_scale"] ?? 1.0, "launch_power_scale"),
			$this->optionalNumber($value["max_draw_duration"] ?? 0.0, "max_draw_duration"),
			$this->optionalNumber($value["max_launch_power"] ?? 1.0, "max_launch_power"),
			$this->optionalNumber($value["min_draw_duration"] ?? 0.0, "min_draw_duration"),
			($value["scale_power_by_draw_duration"] ?? false) === true
		);
	}

	/**
	 * Customies only sends the first ammunition entry.
	 *
	 * @throws BehaviorPackException
	 */
	private function shooter(mixed $value) : ShooterComponent{
		$ammunition = is_array($value) ? ($value["ammunition"][0] ?? null) : null;
		$item = is_array($ammunition) ? ($ammunition["item"] ?? null) : null;
		if(is_array($item)){
			$item = $item["name"] ?? null;
		}
		if(!is_array($value) || !is_array($ammunition) || !is_string($item)){
			throw new BehaviorPackException("minecraft:shooter needs an ammunition item");
		}
		return new ShooterComponent(
			$item,
			($ammunition["use_offhand"] ?? false) === true,
			($ammunition["search_inventory"] ?? false) === true,
			($ammunition["use_in_creative"] ?? false) === true,
			($value["charge_on_draw"] ?? false) === true,
			$this->optionalNumber($value["max_draw_duration"] ?? 0.0, "max_draw_duration"),
			($value["scale_power_by_draw_duration"] ?? false) === true
		);
	}

	/**
	 * @phpstan-param ItemDefinition $definition
	 * @throws BehaviorPackException
	 */
	private function blockPlacer(mixed $value, array &$definition) : BlockPlacerComponent{
		$target = is_array($value) ? ($value["block"] ?? null) : $value;
		if(is_array($target)){
			$target = $target["name"] ?? null;
		}
		$block = is_string($target) ? $this->resolveBlock($target) : null;
		if(!is_string($target) || $block === null){
			throw new BehaviorPackException("minecraft:block_placer block is unknown");
		}
		$definition["blockPlacer"] = $target;
		$component = new BlockPlacerComponent($block);
		$useOn = [];
		foreach(is_array($value) && is_array($value["use_on"] ?? null) ? $value["use_on"] : [] as $entry){
			$name = is_array($entry) ? ($entry["name"] ?? null) : $entry;
			$useOnBlock = is_string($name) ? $this->resolveBlock($name) : null;
			if($useOnBlock !== null){
				$useOn[] = $useOnBlock;
			}
		}
		if(count($useOn) > 0){
			$component->useOn(...$useOn);
		}
		return $component;
	}

	private function resolveBlock(string $name) : ?Block{
		$item = StringToItemParser::getInstance()->parse($name);
		if($item === null){
			return null;
		}
		$block = $item->getBlock();
		return $block->getTypeId() === BlockTypeIds::AIR ? null : $block;
	}

	/**
	 * @throws BehaviorPackException
	 */
	private function string(mixed $value, string $key, string $component) : string{
		$result = is_array($value) ? ($value[$key] ?? null) : $value;
		if(!is_string($result)){
			throw new BehaviorPackException("$component: expected a string for $key");
		}
		return $result;
	}

	/**
	 * @throws BehaviorPackException
	 */
	private function number(mixed $value, string $key, string $component) : float{
		$result = is_array($value) ? ($value[$key] ?? null) : $value;
		if(!is_int($result) && !is_float($result)){
			throw new BehaviorPackException("$component: expected a number for $key");
		}
		return (float) $result;
	}

	/**
	 * @throws BehaviorPackException
	 */
	private function optionalNumber(mixed $value, string $what) : float{
		if(!is_int($value) && !is_float($value)){
			throw new BehaviorPackException("expected a number for $what");
		}
		return (float) $value;
	}

	/**
	 * @throws BehaviorPackException
	 */
	private function bool(mixed $value, string $component) : bool{
		$result = is_array($value) ? ($value["value"] ?? true) : $value;
		if(!is_bool($result)){
			throw new BehaviorPackException("$component: expected a boolean");
		}
		return $result;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\custom;

use customiesdevs\customies\block\permutations\BlockProperty;
use customiesdevs\customies\block\permutations\Permutable;
use customiesdevs\customies\block\permutations\Permutation;
use pocketmine\block\Block;
use pocketmine\data\bedrock\block\convert\BlockStateReader;
use pocketmine\data\bedrock\block\convert\BlockStateWriter;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use Throwable;
use function array_search;
use function count;
use function intdiv;
use function is_bool;
use function is_int;

/**
 * A behavior pack block with states, from description.states or from the
 * placement traits. The state is stored as one index in the cartesian
 * product of every property, last property varying fastest.
 */
final class BehaviorPermutableBlock extends BehaviorBlock implements Permutable{

	public const CARDINAL_DIRECTION = "minecraft:cardinal_direction";
	public const FACING_DIRECTION = "minecraft:facing_direction";
	public const BLOCK_FACE = "minecraft:block_face";
	public const VERTICAL_HALF = "minecraft:vertical_half";

	public const FACING_NAMES = [
		Facing::DOWN => "down",
		Facing::UP => "up",
		Facing::NORTH => "north",
		Facing::SOUTH => "south",
		Facing::WEST => "west",
		Facing::EAST => "east"
	];

	private int $stateIndex = 0;

	/**
	 * @return list<BlockProperty>
	 */
	public function getBlockProperties() : array{
		$properties = [];
		foreach($this->definition["properties"] as [$name, $values]){
			$properties[] = new BlockProperty($name, $values);
		}
		return $properties;
	}

	/**
	 * @return list<Permutation>
	 */
	public function getPermutations() : array{
		$permutations = [];
		foreach($this->definition["permutations"] as [$condition, $components]){
			$permutation = new Permutation($condition);
			foreach($components as $name => $value){
				try{
					$component = BlockComponentMapper::map($name, $value);
				}catch(Throwable){
					$component = null;
				}
				if($component !== null){
					$permutation->withComponent($component->getName(), $component->getValue());
				}
			}
			$permutations[] = $permutation;
		}
		return $permutations;
	}

	/**
	 * @return list<bool|int|string>
	 */
	public function getCurrentBlockProperties() : array{
		$current = [];
		foreach($this->getValueIndexes() as $i => $valueIndex){
			$current[] = $this->definition["properties"][$i][1][$valueIndex];
		}
		return $current;
	}

	public function serializeState(BlockStateWriter $blockStateOut) : void{
		foreach($this->getCurrentBlockProperties() as $i => $value){
			$name = $this->definition["properties"][$i][0];
			if(is_bool($value)){
				$blockStateOut->writeBool($name, $value);
			}elseif(is_int($value)){
				$blockStateOut->writeInt($name, $value);
			}else{
				$blockStateOut->writeString($name, $value);
			}
		}
	}

	public function deserializeState(BlockStateReader $blockStateIn) : void{
		$indexes = [];
		foreach($this->definition["properties"] as [$name, $values]){
			$first = $values[0];
			if(is_bool($first)){
				$value = $blockStateIn->readBool($name);
			}elseif(is_int($first)){
				$value = $blockStateIn->readInt($name);
			}else{
				$value = $blockStateIn->readString($name);
			}
			$index = array_search($value, $values, true);
			$indexes[] = $index === false ? 0 : $index;
		}
		$this->setValueIndexes($indexes);
	}

	/**
	 * Sets the value of a property by name, ignored when the block has no
	 * such property or the value is not one of its values.
	 */
	public function setPropertyValue(string $name, bool|int|string $value) : void{
		$indexes = $this->getValueIndexes();
		foreach($this->definition["properties"] as $i => [$propertyName, $values]){
			if($propertyName !== $name){
				continue;
			}
			$index = array_search($value, $values, true);
			if($index !== false){
				$indexes[$i] = $index;
				$this->setValueIndexes($indexes);
			}
			return;
		}
	}

	public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
		$traits = $this->definition["traits"];
		if($player !== null && $traits["cardinal"]){
			$facing = $player->getHorizontalFacing();
			$turns = (($traits["yRotationOffset"] % 360 + 360) % 360) / 90;
			for($i = 0; $i < $turns; $i++){
				$facing = Facing::rotateY($facing, true);
			}
			$this->setPropertyValue(self::CARDINAL_DIRECTION, self::FACING_NAMES[$facing]);
		}
		if($player !== null && $traits["facing"]){
			$pitch = $player->getLocation()->getPitch();
			$facing = $pitch > 45 ? Facing::DOWN : ($pitch < -45 ? Facing::UP : $player->getHorizontalFacing());
			$this->setPropertyValue(self::FACING_DIRECTION, self::FACING_NAMES[$facing]);
		}
		if($traits["blockFace"]){
			$this->setPropertyValue(self::BLOCK_FACE, self::FACING_NAMES[$face] ?? "up");
		}
		if($traits["verticalHalf"]){
			$top = $face === Facing::DOWN || ($face !== Facing::UP && $clickVector->y > 0.5);
			$this->setPropertyValue(self::VERTICAL_HALF, $top ? "top" : "bottom");
		}
		return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
	}

	protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{
		$index = $this->stateIndex;
		$w->boundedIntAuto(0, $this->getStateCount() - 1, $index);
		$this->stateIndex = $index;
	}

	private function getStateCount() : int{
		$count = 1;
		foreach($this->definition["properties"] as [, $values]){
			$count *= count($values);
		}
		return $count;
	}

	/**
	 * @return list<int>
	 */
	private function getValueIndexes() : array{
		$indexes = [];
		$remaining = $this->stateIndex;
		for($i = count($this->definition["properties"]) - 1; $i >= 0; $i--){
			$size = count($this->definition["properties"][$i][1]);
			$indexes[$i] = $remaining % $size;
			$remaining = intdiv($remaining, $size);
		}
		$ordered = [];
		for($i = 0, $total = count($indexes); $i < $total; $i++){
			$ordered[] = $indexes[$i];
		}
		return $ordered;
	}

	/**
	 * @param array<int, int> $indexes
	 */
	private function setValueIndexes(array $indexes) : void{
		$state = 0;
		foreach($this->definition["properties"] as $i => [, $values]){
			$state = $state * count($values) + ($indexes[$i] ?? 0);
		}
		$this->stateIndex = $state;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\custom;

use behaviorpack\BehaviorPack;
use behaviorpack\BehaviorPackException;
use customiesdevs\customies\block\CustomiesBlockFactory;
use pocketmine\block\Block;
use pocketmine\block\BlockTypeIds;
use pocketmine\item\StringToItemParser;
use pocketmine\plugin\PluginBase;
use function array_keys;
use function array_merge;
use function array_unique;
use function array_values;
use function count;
use function implode;
use function in_array;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function range;
use function sort;
use function str_contains;
use const SORT_REGULAR;

/**
 * Registers the minecraft:block files of the behavior packs through
 * Customies.
 *
 * @phpstan-import-type Definition from BehaviorBlock
 */
final class CustomBlockRegistrar{

	public const MAX_STATES = 65536;

	private const DIRECTIONS = ["north", "south", "west", "east"];
	private const FACES = ["down", "up", "north", "south", "west", "east"];

	public function __construct(
		private PluginBase $plugin
	){
	}

	/**
	 * Reads the identifier of a block file without registering it, or null
	 * when the file is not a valid block definition.
	 */
	public function readIdentifier(string $file) : ?string{
		try{
			$identifier = BehaviorPack::readJson($file)["minecraft:block"]["description"]["identifier"] ?? null;
		}catch(BehaviorPackException){
			return null;
		}
		return is_string($identifier) ? $identifier : null;
	}

	/**
	 * Registers one block file, returns whether a block was registered.
	 */
	public function register(BehaviorPack $pack, string $file) : bool{
		$path = $pack->getName() . "/" . $pack->relativePath($file);
		try{
			return $this->registerFile($file, $path);
		}catch(BehaviorPackException $e){
			$this->plugin->getLogger()->warning("Behavior packs: skipped block $path: " . $e->getMessage());
			return false;
		}
	}

	/**
	 * @throws BehaviorPackException
	 */
	private function registerFile(string $file, string $path) : bool{
		$block = BehaviorPack::readJson($file)["minecraft:block"] ?? null;
		if(!is_array($block)){
			throw new BehaviorPackException("no minecraft:block object");
		}
		$description = $block["description"] ?? null;
		$identifier = is_array($description) ? ($description["identifier"] ?? null) : null;
		if(!is_array($description) || !is_string($identifier) || !str_contains($identifier, ":")){
			throw new BehaviorPackException("missing or invalid description.identifier");
		}
		if(StringToItemParser::getInstance()->parse($identifier) !== null){
			throw new BehaviorPackException("$identifier is already registered");
		}

		$components = $block["components"] ?? [];
		if(!is_array($components)){
			throw new BehaviorPackException("components must be an object");
		}
		$unsupported = $this->validateComponents($components);

		$properties = $this->readStates($description["states"] ?? $description["properties"] ?? []);
		$traits = $this->readTraits($description["traits"] ?? [], $properties);

		$permutations = [];
		$rawPermutations = $block["permutations"] ?? [];
		if(!is_array($rawPermutations)){
			throw new BehaviorPackException("permutations must be an array");
		}
		if(count($rawPermutations) > 0 && count($properties) === 0){
			$this->plugin->getLogger()->warning("Behavior packs: block $identifier has permutations but no states, permutations ignored ($path)");
		}elseif(count($rawPermutations) > 0){
			foreach($rawPermutations as $permutation){
				$condition = is_array($permutation) ? ($permutation["condition"] ?? null) : null;
				$permutationComponents = is_array($permutation) ? ($permutation["components"] ?? []) : null;
				if(!is_string($condition) || !is_array($permutationComponents)){
					throw new BehaviorPackException("a permutation needs a condition and components");
				}
				$unsupported = array_merge($unsupported, $this->validateComponents($permutationComponents));
				$permutations[] = [$condition, $permutationComponents];
			}
		}

		$stateCount = 1;
		foreach($properties as [, $values]){
			$stateCount *= count($values);
		}
		if($stateCount > self::MAX_STATES){
			throw new BehaviorPackException("$stateCount state combinations, the limit is " . self::MAX_STATES);
		}

		$flammability = isset($components["minecraft:flammable"]) ? BlockComponentMapper::flammability($components["minecraft:flammable"]) : null;
		$collision = isset($components["minecraft:collision_box"]) ? BlockComponentMapper::box($components["minecraft:collision_box"]) : [0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
		if($collision !== null && ($collision[3] < $collision[0] || $collision[4] < $collision[1] || $collision[5] < $collision[2])){
			throw new BehaviorPackException("minecraft:collision_box has a negative size");
		}

		/** @phpstan-var Definition $definition */
		$definition = [
			"identifier" => $identifier,
			"name" => isset($components["minecraft:display_name"]) ? BlockComponentMapper::displayName($components["minecraft:display_name"]) : $identifier,
			"hardness" => isset($components["minecraft:destructible_by_mining"]) ? BlockComponentMapper::hardness($components["minecraft:destructible_by_mining"]) : 0.0,
			"blastResistance" => isset($components["minecraft:destructible_by_explosion"]) ? BlockComponentMapper::explosionResistance($components["minecraft:destructible_by_explosion"]) : 0.0,
			"lightLevel" => isset($components["minecraft:light_emission"]) ? BlockComponentMapper::lightLevel($components["minecraft:light_emission"], 0) : 0,
			"lightFilter" => isset($components["minecraft:light_dampening"]) ? BlockComponentMapper::lightLevel($components["minecraft:light_dampening"], 15) : 15,
			"frictionFactor" => 1.0 - (isset($components["minecraft:friction"]) ? BlockComponentMapper::friction($components["minecraft:friction"]) : 0.4),
			"flammability" => $flammability,
			"collision" => $collision,
			"components" => $components,
			"properties" => $properties,
			"permutations" => $permutations,
			"traits" => $traits
		];

		$unsupported = array_values(array_unique($unsupported));
		if(count($unsupported) > 0){
			$this->plugin->getLogger()->warning("Behavior packs: block $identifier: unsupported components ignored: " . implode(", ", $unsupported));
		}

		$typeId = BlockTypeIds::newId();
		CustomiesBlockFactory::getInstance()->registerBlock(
			static fn() : Block => BehaviorBlock::create($typeId, $definition),
			$identifier,
			CustomContentLoader::creativeInfo($description)
		);
		return true;
	}

	/**
	 * Checks every supported component and returns the names of the
	 * unsupported ones.
	 *
	 * @param array<mixed> $components
	 * @return list<string>
	 * @throws BehaviorPackException
	 */
	private function validateComponents(array $components) : array{
		$unsupported = [];
		foreach($components as $name => $value){
			$name = (string) $name;
			if(in_array($name, BlockComponentMapper::IGNORED, true)){
				continue;
			}
			if(!BlockComponentMapper::isSupported($name)){
				$unsupported[] = $name;
				continue;
			}
			try{
				BlockComponentMapper::map($name, $value);
			}catch(BehaviorPackException $e){
				throw new BehaviorPackException("$name: " . $e->getMessage(), 0, $e);
			}
		}
		return $unsupported;
	}

	/**
	 * @return list<array{string, list<bool|int|string>}>
	 * @throws BehaviorPackException
	 */
	private function readStates(mixed $states) : array{
		if(!is_array($states)){
			throw new BehaviorPackException("description.states must be an object");
		}
		$properties = [];
		foreach($states as $name => $definition){
			$name = (string) $name;
			if(is_array($definition) && is_array($definition["values"] ?? null)){
				$min = $definition["values"]["min"] ?? null;
				$max = $definition["values"]["max"] ?? null;
				if(!is_int($min) || !is_int($max) || $max < $min){
					throw new BehaviorPackException("state $name has an invalid range");
				}
				$values = range($min, $max);
			}elseif(is_array($definition)){
				$values = array_values($definition);
			}else{
				throw new BehaviorPackException("state $name must be a list or a range");
			}
			$properties[] = [$name, $this->normalizeValues($name, $values)];
		}
		return $properties;
	}

	/**
	 * Sorts and deduplicates the values of a state, and puts its only falsy
	 * value first: Customies walks the values with next(), which stops on a
	 * falsy value.
	 *
	 * @param list<mixed> $values
	 * @return list<bool|int|string>
	 * @throws BehaviorPackException
	 */
	private function normalizeValues(string $name, array $values) : array{
		if(count($values) === 0){
			throw new BehaviorPackException("state $name has no values");
		}
		$first = $values[0];
		foreach($values as $value){
			$sameType = (is_bool($first) && is_bool($value)) || (is_int($first) && is_int($value)) || (is_string($first) && is_string($value));
			if(!$sameType){
				throw new BehaviorPackException("state $name mixes value types or uses unsupported values");
			}
		}
		$values = array_values(array_unique($values, SORT_REGULAR));
		sort($values);
		$falsy = [];
		$truthy = [];
		foreach($values as $value){
			if($value){
				$truthy[] = $value;
			}else{
				$falsy[] = $value;
			}
		}
		if(count($falsy) > 1){
			throw new BehaviorPackException("state $name cannot have both \"\" and \"0\" values");
		}
		return array_merge($falsy, $truthy);
	}

	/**
	 * Adds the states of the supported placement traits to the properties.
	 *
	 * @param list<array{string, list<bool|int|string>}> $properties
	 * @return array{cardinal: bool, yRotationOffset: int, facing: bool, blockFace: bool, verticalHalf: bool}
	 * @throws BehaviorPackException
	 */
	private function readTraits(mixed $traits, array &$properties) : array{
		$result = ["cardinal" => false, "yRotationOffset" => 0, "facing" => false, "blockFace" => false, "verticalHalf" => false];
		if(!is_array($traits)){
			throw new BehaviorPackException("description.traits must be an object");
		}
		$names = [];
		foreach($properties as [$name]){
			$names[$name] = true;
		}
		$add = static function(string $name, array $values) use (&$properties, &$names) : bool{
			if(isset($names[$name])){
				return false;
			}
			$names[$name] = true;
			$properties[] = [$name, $values];
			return true;
		};

		foreach(array_keys($traits) as $trait){
			$config = $traits[$trait];
			$enabled = is_array($config) && is_array($config["enabled_states"] ?? null) ? $config["enabled_states"] : [];
			if($trait === "minecraft:placement_direction"){
				if(in_array(BehaviorPermutableBlock::CARDINAL_DIRECTION, $enabled, true)){
					$result["cardinal"] = $add(BehaviorPermutableBlock::CARDINAL_DIRECTION, self::DIRECTIONS);
				}
				if(in_array(BehaviorPermutableBlock::FACING_DIRECTION, $enabled, true)){
					$result["facing"] = $add(BehaviorPermutableBlock::FACING_DIRECTION, self::FACES);
				}
				$offset = is_array($config) ? ($config["y_rotation_offset"] ?? 0) : 0;
				$result["yRotationOffset"] = is_int($offset) || is_float($offset) ? (int) $offset : 0;
			}elseif($trait === "minecraft:placement_position"){
				if(in_array(BehaviorPermutableBlock::BLOCK_FACE, $enabled, true)){
					$result["blockFace"] = $add(BehaviorPermutableBlock::BLOCK_FACE, self::FACES);
				}
				if(in_array(BehaviorPermutableBlock::VERTICAL_HALF, $enabled, true)){
					$result["verticalHalf"] = $add(BehaviorPermutableBlock::VERTICAL_HALF, ["bottom", "top"]);
				}
			}else{
				throw new BehaviorPackException("unsupported trait $trait");
			}
		}
		return $result;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\loot;

use pocketmine\item\Item;

/**
 * A behavior pack loot table.
 */
final class LootTable{

	public const MAX_DEPTH = 8;

	/**
	 * @param list<LootPool> $pools
	 */
	public function __construct(
		private string $path,
		private array $pools
	){
	}

	public function getPath() : string{
		return $this->path;
	}

	/**
	 * @return list<LootPool>
	 */
	public function getPools() : array{
		return $this->pools;
	}

	/**
	 * @return list<Item>
	 */
	public function roll(LootContext $context) : array{
		return $this->rollAtDepth($context, 0);
	}

	/**
	 * Rolls the table as a nested reference of another table.
	 *
	 * @return list<Item>
	 */
	public function rollAtDepth(LootContext $context, int $depth) : array{
		$items = [];
		foreach($this->pools as $pool){
			foreach($pool->roll($context, $depth) as $item){
				$items[] = $item;
			}
		}
		return $items;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\loot;

use function count;
use function ltrim;
use function str_replace;

/**
 * The loot tables of every behavior pack, keyed by their pack-relative path,
 * for example "loot_tables/blocks/ruby_ore.json".
 */
final class LootTableRegistry{

	/** @var array<string, LootTable> */
	private static array $tables = [];

	private function __construct(){
	}

	public static function normalizePath(string $path) : string{
		return ltrim(str_replace("\\", "/", $path), "/");
	}

	public static function register(LootTable $table) : void{
		self::$tables[self::normalizePath($table->getPath())] = $table;
	}

	public static function get(string $path) : ?LootTable{
		return self::$tables[self::normalizePath($path)] ?? null;
	}

	/**
	 * @return array<string, LootTable>
	 */
	public static function getAll() : array{
		return self::$tables;
	}

	public static function count() : int{
		return count(self::$tables);
	}

	public static function clear() : void{
		self::$tables = [];
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\loot;

use pocketmine\item\Item;
use function is_array;
use function is_bool;
use function is_string;

/**
 * A loot table condition. Unknown conditions always pass.
 */
final class LootCondition{

	/**
	 * @param array<mixed> $data
	 */
	public function __construct(
		private string $type,
		private array $data
	){
	}

	public function getType() : string{
		return $this->type;
	}

	public function test(LootContext $context) : bool{
		switch($this->type){
			case "random_chance":
				return LootRange::chance() < LootRange::number($this->data["chance"] ?? null, 1.0);
			case "random_chance_with_looting":
				$chance = LootRange::number($this->data["chance"] ?? null, 1.0);
				$multiplier = LootRange::number($this->data["looting_multiplier"] ?? null, 0.0);
				return LootRange::chance() < $chance + $multiplier * $context->getLootingLevel();
			case "killed_by_player":
				return $context->isKilledByPlayer();
			case "killed_by_player_or_pets":
				return $context->isKilledByPlayerOrPet();
			case "match_tool":
				return $this->matchTool($context->getTool());
			case "entity_properties":
				return $this->matchEntity($context);
			default:
				return true;
		}
	}

	private function matchTool(?Item $tool) : bool{
		if($tool === null){
			return false;
		}
		$item = $this->data["item"] ?? null;
		if(is_string($item) && LootItems::identifierOf($tool) !== LootItems::normalize($item)){
			return false;
		}
		if(isset($this->data["count"]) && !LootRange::contains($this->data["count"], $tool->getCount())){
			return false;
		}
		$enchantments = $this->data["enchantments"] ?? [];
		if(!is_array($enchantments)){
			return true;
		}
		foreach($enchantments as $requirement){
			if(!is_array($requirement) || !is_string($requirement["enchantment"] ?? null)){
				continue;
			}
			$enchantment = LootItems::enchantment($requirement["enchantment"]);
			if($enchantment === null){
				return false;
			}
			$level = $tool->getEnchantmentLevel($enchantment);
			if($level === 0){
				return false;
			}
			if(isset($requirement["levels"]) && !LootRange::contains($requirement["levels"], $level)){
				return false;
			}
		}
		return true;
	}

	private function matchEntity(LootContext $context) : bool{
		$target = $this->data["entity"] ?? "this";
		$entity = $target === "killer" ? $context->getKiller() : $context->getEntity();
		if($entity === null){
			return false;
		}
		$properties = $this->data["properties"] ?? [];
		if(is_array($properties) && is_bool($properties["on_fire"] ?? null) && $entity->isOnFire() !== $properties["on_fire"]){
			return false;
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\loot;

use pocketmine\entity\Entity;
use pocketmine\item\Item;
use pocketmine\player\Player;

/**
 * What a loot table is rolled for: the dropping entity, its killer (or the
 * player breaking a block), the tool used and the looting level.
 */
final class LootContext{

	public function __construct(
		private ?Entity $entity = null,
		private ?Entity $killer = null,
		private ?Item $tool = null,
		private int $lootingLevel = 0
	){
	}

	public function getEntity() : ?Entity{
		return $this->entity;
	}

	public function getKiller() : ?Entity{
		return $this->killer;
	}

	public function getTool() : ?Item{
		return $this->tool;
	}

	public function getLootingLevel() : int{
		return $this->lootingLevel;
	}

	public function isKilledByPlayer() : bool{
		return $this->killer instanceof Player;
	}

	public function isKilledByPlayerOrPet() : bool{
		if($this->killer instanceof Player){
			return true;
		}
		return $this->killer !== null && $this->killer->getOwningEntity() instanceof Player;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\loot;

use function is_array;
use function is_numeric;
use function max;
use function min;
use function mt_getrandmax;
use function mt_rand;

/**
 * Reads the numbers of a loot table, which are either a constant or a
 * {"min": a, "max": b} range.
 */
final class LootRange{

	private function __construct(){
	}

	public static function number(mixed $value, float $default) : float{
		return is_numeric($value) ? (float) $value : $default;
	}

	public static function chance() : float{
		return mt_rand() / mt_getrandmax();
	}

	public static function rollInt(mixed $value, int $default) : int{
		if(is_numeric($value)){
			return (int) $value;
		}
		if(is_array($value)){
			$low = (int) self::number($value["min"] ?? $value[0] ?? null, $default);
			$high = (int) self::number($value["max"] ?? $value[1] ?? null, $low);
			return mt_rand(min($low, $high), max($low, $high));
		}
		return $default;
	}

	public static function rollFloat(mixed $value, float $default) : float{
		if(is_numeric($value)){
			return (float) $value;
		}
		if(is_array($value)){
			$low = self::number($value["min"] ?? $value[0] ?? null, $default);
			$high = self::number($value["max"] ?? $value[1] ?? null, $low);
			return $low + ($high - $low) * self::chance();
		}
		return $default;
	}

	public static function contains(mixed $range, int $value) : bool{
		if(is_numeric($range)){
			return $value === (int) $range;
		}
		if(is_array($range)){
			if(isset($range["min"]) && is_numeric($range["min"]) && $value < (int) $range["min"]){
				return false;
			}
			if(isset($range["max"]) && is_numeric($range["max"]) && $value > (int) $range["max"]){
				return false;
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\loot;

use behaviorpack\BehaviorPack;
use behaviorpack\BehaviorPackException;
use behaviorpack\ContentLoader;
use pocketmine\event\HandlerListManager;
use pocketmine\plugin\PluginBase;
use function array_keys;
use function count;
use function is_array;
use function is_string;

/**
 * Loads the loot_tables of every pack and applies the loot tables declared
 * by pack blocks (minecraft:loot) and entities (minecraft:loot.table).
 */
final class LootTableLoader implements ContentLoader{

	private ?LootListener $listener = null;

	public function __construct(
		private PluginBase $plugin
	){
	}

	public function getName() : string{
		return "loot tables";
	}

	public function requiresCustomies() : bool{
		return false;
	}

	public function load(array $packs) : void{
		$logger = $this->plugin->getLogger();
		foreach($packs as $pack){
			foreach($pack->listFiles("loot_tables", "json") as $file){
				$path = $pack->relativePath($file);
				try{
					LootTableRegistry::register(LootTableParser::parse($path, BehaviorPack::readJson($file)));
				}catch(BehaviorPackException $e){
					$logger->warning("Behavior packs: skipped loot table $path of " . $pack->getName() . ": " . $e->getMessage());
				}
			}
		}

		$blockTables = [];
		$entityTables = [];
		foreach($packs as $pack){
			foreach($pack->listFiles("blocks", "json") as $file){
				$this->readDefinition($pack, $file, "minecraft:block", $blockTables);
			}
			foreach($pack->listFiles("entities", "json") as $file){
				$this->readDefinition($pack, $file, "minecraft:entity", $entityTables);
			}
		}

		if(count($blockTables) > 0 || count($entityTables) > 0){
			$this->listener = new LootListener($blockTables, $entityTables);
			$this->plugin->getServer()->getPluginManager()->registerEvents($this->listener, $this->plugin);
		}

		$logger->info("Behavior packs: " . LootTableRegistry::count() . " loot tables");
	}

	/**
	 * @param array<string, string> $tables
	 */
	private function readDefinition(BehaviorPack $pack, string $file, string $root, array &$tables) : void{
		try{
			$json = BehaviorPack::readJson($file);
		}catch(BehaviorPackException $e){
			$this->plugin->getLogger()->warning("Behavior packs: skipped " . $pack->relativePath($file) . " of " . $pack->getName() . ": " . $e->getMessage());
			return;
		}
		$definition = $json[$root] ?? null;
		if(!is_array($definition)){
			return;
		}
		$identifier = $definition["description"]["identifier"] ?? null;
		if(!is_string($identifier) || $identifier === ""){
			return;
		}
		$path = $root === "minecraft:block" ? self::blockLootPath($definition) : self::entityLootPath($definition);
		if($path === null){
			return;
		}
		if(LootTableRegistry::get($path) === null){
			$this->plugin->getLogger()->warning("Behavior packs: $identifier references the missing loot table $path");
			return;
		}
		$tables[$identifier] = LootTableRegistry::normalizePath($path);
	}

	/**
	 * Returns the loot table of a block, or the one shared by all of its
	 * permutations when the base components declare none.
	 *
	 * @param array<mixed> $definition
	 */
	private static function blockLootPath(array $definition) : ?string{
		$loot = $definition["components"]["minecraft:loot"] ?? null;
		if(is_string($loot) && $loot !== ""){
			return $loot;
		}
		$permutations = $definition["permutations"] ?? null;
		if(!is_array($permutations)){
			return null;
		}
		$paths = [];
		foreach($permutations as $permutation){
			$permutationLoot = is_array($permutation) ? ($permutation["components"]["minecraft:loot"] ?? null) : null;
			if(is_string($permutationLoot) && $permutationLoot !== ""){
				$paths[$permutationLoot] = true;
			}
		}
		$paths = array_keys($paths);
		return count($paths) === 1 ? (string) $paths[0] : null;
	}

	/**
	 * @param array<mixed> $definition
	 */
	private static function entityLootPath(array $definition) : ?string{
		$table = $definition["components"]["minecraft:loot"]["table"] ?? null;
		return is_string($table) && $table !== "" ? $table : null;
	}

	public function close() : void{
		if($this->listener !== null){
			HandlerListManager::global()->unregisterAll($this->listener);
			$this->listener = null;
		}
		LootTableRegistry::clear();
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\loot;

use pocketmine\crafting\FurnaceType;
use pocketmine\data\bedrock\EnchantmentIdMap;
use pocketmine\data\bedrock\EnchantmentIds;
use pocketmine\item\Durable;
use pocketmine\item\enchantment\AvailableEnchantmentRegistry;
use pocketmine\item\enchantment\Enchantment;
use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use pocketmine\Server;
use function array_filter;
use function array_rand;
use function array_values;
use function ceil;
use function count;
use function in_array;
use function is_array;
use function is_string;
use function max;
use function min;
use function mt_rand;
use function round;

/**
 * A loot table item function, applied when its own conditions pass.
 * Unknown functions leave the item unchanged.
 */
final class LootFunction{

	private const TREASURE_IDS = [
		EnchantmentIds::FROST_WALKER,
		EnchantmentIds::MENDING,
		EnchantmentIds::BINDING,
		EnchantmentIds::VANISHING,
		EnchantmentIds::SOUL_SPEED,
		EnchantmentIds::SWIFT_SNEAK
	];

	/**
	 * @param array<mixed>        $data
	 * @param list<LootCondition> $conditions
	 */
	public function __construct(
		private string $type,
		private array $data,
		private array $conditions
	){
	}

	public function getType() : string{
		return $this->type;
	}

	public function apply(Item $item, LootContext $context) : Item{
		foreach($this->conditions as $condition){
			if(!$condition->test($context)){
				return $item;
			}
		}

		switch($this->type){
			case "set_count":
				return $item->setCount(max(0, LootRange::rollInt($this->data["count"] ?? null, 1)));
			case "set_damage":
				return $this->setDamage($item);
			case "set_data":
				return $this->setData($item);
			case "set_name":
				$name = $this->data["name"] ?? null;
				return is_string($name) ? $item->setCustomName($name) : $item;
			case "set_lore":
				return $this->setLore($item);
			case "enchant_randomly":
				return $this->enchantRandomly($item, ($this->data["treasure"] ?? false) === true, 1, 0);
			case "enchant_with_levels":
				$levels = max(1, LootRange::rollInt($this->data["levels"] ?? null, 30));
				$amount = 1 + ($levels >= 15 ? 1 : 0) + ($levels >= 30 ? 1 : 0);
				return $this->enchantRandomly($item, ($this->data["treasure"] ?? false) === true, $amount, $levels);
			case "specific_enchants":
				return $this->specificEnchants($item);
			case "looting_enchant":
				return $this->lootingEnchant($item, $context->getLootingLevel());
			case "furnace_smelt":
				return $this->smelt($item);
			default:
				return $item;
		}
	}

	private function setDamage(Item $item) : Item{
		if(!$item instanceof Durable){
			return $item;
		}
		$fraction = min(1.0, max(0.0, LootRange::rollFloat($this->data["damage"] ?? null, 1.0)));
		$maxDurability = $item->getMaxDurability();
		return $item->setDamage(min($maxDurability, max(0, (int) round($maxDurability * (1 - $fraction)))));
	}

	private function setData(Item $item) : Item{
		$identifier = LootItems::identifierOf($item);
		if($identifier === null){
			return $item;
		}
		$result = LootItems::resolve($identifier, LootRange::rollInt($this->data["data"] ?? null, 0));
		return $result === null ? $item : $result->setCount($item->getCount());
	}

	private function setLore(Item $item) : Item{
		$lore = $this->data["lore"] ?? null;
		if(!is_array($lore)){
			return $item;
		}
		$lines = [];
		foreach($lore as $line){
			if(is_string($line)){
				$lines[] = $line;
			}
		}
		return $item->setLore($lines);
	}

	private function enchantRandomly(Item $item, bool $treasure, int $amount, int $levels) : Item{
		$book = $item->getTypeId() === VanillaItems::BOOK()->getTypeId();
		$registry = AvailableEnchantmentRegistry::getInstance();
		$candidates = array_values($book ? $registry->getAll() : $registry->getAllEnchantmentsForItem($item));
		if(!$treasure){
			$candidates = array_values(array_filter($candidates, fn(Enchantment $enchantment) : bool => !self::isTreasure($enchantment)));
		}
		if(count($candidates) === 0){
			return $item;
		}
		if($book){
			$item = VanillaItems::ENCHANTED_BOOK()->setCount($item->getCount());
		}

		for($i = 0; $i < $amount && count($candidates) > 0; ++$i){
			$key = array_rand($candidates);
			$enchantment = $candidates[$key];
			unset($candidates[$key]);
			$maxLevel = $enchantment->getMaxLevel();
			$level = $levels > 0 ? (int) ceil($maxLevel * min(1.0, $levels / 30)) : mt_rand(1, $maxLevel);
			$item->addEnchantment(new EnchantmentInstance($enchantment, max(1, min($maxLevel, $level))));
			$candidates = array_values(array_filter($candidates, fn(Enchantment $other) : bool => $other->isCompatibleWith($enchantment)));
		}
		return $item;
	}

	private static function isTreasure(Enchantment $enchantment) : bool{
		$map = EnchantmentIdMap::getInstance();
		foreach(self::TREASURE_IDS as $id){
			if($map->fromId($id) === $enchantment){
				return true;
			}
		}
		return false;
	}

	private function specificEnchants(Item $item) : Item{
		$enchants = $this->data["enchants"] ?? null;
		if(!is_array($enchants)){
			return $item;
		}
		foreach($enchants as $entry){
			$name = is_string($entry) ? $entry : (is_array($entry) ? ($entry["id"] ?? null) : null);
			if(!is_string($name)){
				continue;
			}
			$enchantment = LootItems::enchantment($name);
			if($enchantment === null){
				continue;
			}
			$level = is_array($entry) ? LootRange::rollInt($entry["level"] ?? null, 1) : 1;
			$item->addEnchantment(new EnchantmentInstance($enchantment, max(1, $level)));
		}
		return $item;
	}

	private function lootingEnchant(Item $item, int $lootingLevel) : Item{
		if($lootingLevel <= 0){
			return $item;
		}
		$count = $item->getCount();
		for($i = 0; $i < $lootingLevel; ++$i){
			$count += max(0, LootRange::rollInt($this->data["count"] ?? null, 0));
		}
		$limit = LootRange::rollInt($this->data["limit"] ?? null, 0);
		if($limit > 0){
			$count = min($limit, $count);
		}
		return $item->setCount($count);
	}

	private function smelt(Item $item) : Item{
		$recipe = Server::getInstance()->getCraftingManager()->getFurnaceRecipeManager(FurnaceType::FURNACE)->match($item);
		return $recipe === null ? $item : $recipe->getResult()->setCount($item->getCount());
	}

	/**
	 * Whether a function type is supported by apply().
	 */
	public static function isKnown(string $type) : bool{
		return in_array($type, ["set_count", "set_damage", "set_data", "set_name", "set_lore", "enchant_randomly", "enchant_with_levels", "specific_enchants", "looting_enchant", "furnace_smelt"], true);
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\loot;

use pocketmine\data\bedrock\EnchantmentIdMap;
use pocketmine\data\bedrock\EnchantmentIds;
use pocketmine\data\bedrock\item\SavedItemData;
use pocketmine\item\enchantment\Enchantment;
use pocketmine\item\enchantment\StringToEnchantmentParser;
use pocketmine\item\Item;
use pocketmine\item\StringToItemParser;
use pocketmine\world\format\io\GlobalItemDataHandlers;
use Throwable;
use function str_contains;
use function str_replace;
use function strtolower;
use function trim;

/**
 * Resolves the item and enchantment identifiers used by loot tables.
 */
final class LootItems{

	private const ENCHANTMENT_IDS = [
		"looting" => EnchantmentIds::LOOTING,
		"smite" => EnchantmentIds::SMITE,
		"bane_of_arthropods" => EnchantmentIds::BANE_OF_ARTHROPODS,
		"luck_of_the_sea" => EnchantmentIds::LUCK_OF_THE_SEA,
		"lure" => EnchantmentIds::LURE,
		"depth_strider" => EnchantmentIds::DEPTH_STRIDER,
		"binding" => EnchantmentIds::BINDING,
		"soul_speed" => EnchantmentIds::SOUL_SPEED
	];

	private function __construct(){
	}

	public static function normalize(string $identifier) : string{
		$identifier = strtolower(trim($identifier));
		return str_contains($identifier, ":") ? $identifier : "minecraft:" . $identifier;
	}

	public static function resolve(string $identifier, int $meta = 0) : ?Item{
		$identifier = self::normalize($identifier);
		try{
			return GlobalItemDataHandlers::getDeserializer()->deserializeType(new SavedItemData($identifier, $meta));
		}catch(Throwable){
		}
		if($meta !== 0){
			return null;
		}
		return StringToItemParser::getInstance()->parse($identifier);
	}

	public static function identifierOf(Item $item) : ?string{
		try{
			return GlobalItemDataHandlers::getSerializer()->serializeType($item)->getName();
		}catch(Throwable){
			return null;
		}
	}

	public static function enchantment(string $name) : ?Enchantment{
		$name = str_replace("minecraft:", "", strtolower(trim($name)));
		$enchantment = StringToEnchantmentParser::getInstance()->parse($name);
		if($enchantment !== null){
			return $enchantment;
		}
		$id = self::ENCHANTMENT_IDS[$name] ?? null;
		return $id === null ? null : EnchantmentIdMap::getInstance()->fromId($id);
	}

	public static function lootingLevel(?Item $tool) : int{
		if($tool === null){
			return 0;
		}
		$looting = self::enchantment("looting");
		return $looting === null ? 0 : $tool->getEnchantmentLevel($looting);
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\loot;

use pocketmine\block\Block;
use pocketmine\entity\Human;
use pocketmine\event\block\BlockBreakEvent;
use pocketmine\event\entity\EntityDamageByEntityEvent;
use pocketmine\event\entity\EntityDeathEvent;
use pocketmine\event\Listener;
use pocketmine\item\enchantment\VanillaEnchantments;
use pocketmine\player\Player;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use Throwable;

/**
 * Replaces the drops of the blocks and entities whose behavior pack
 * definition declares a loot table.
 */
final class LootListener implements Listener{

	/**
	 * @param array<string, string> $blockTables  block identifier => loot table path
	 * @param array<string, string> $entityTables entity identifier => loot table path
	 */
	public function __construct(
		private array $blockTables,
		private array $entityTables
	){
	}

	/**
	 * @priority HIGH
	 */
	public function onBlockBreak(BlockBreakEvent $event) : void{
		$block = $event->getBlock();
		$identifier = self::blockIdentifier($block);
		if($identifier === null || !isset($this->blockTables[$identifier])){
			return;
		}
		$player = $event->getPlayer();
		if(!$player->hasFiniteResources()){
			$event->setDrops([]);
			return;
		}
		$tool = $event->getItem();
		if($tool->hasEnchantment(VanillaEnchantments::SILK_TOUCH())){
			$event->setDrops([$block->asItem()]);
			return;
		}
		$table = LootTableRegistry::get($this->blockTables[$identifier]);
		if($table === null){
			return;
		}
		$event->setDrops($table->roll(new LootContext(null, $player, $tool, LootItems::lootingLevel($tool))));
	}

	/**
	 * @priority HIGH
	 */
	public function onEntityDeath(EntityDeathEvent $event) : void{
		$entity = $event->getEntity();
		if($entity instanceof Player){
			return;
		}
		$path = $this->entityTables[$entity::getNetworkTypeId()] ?? null;
		if($path === null){
			return;
		}
		$table = LootTableRegistry::get($path);
		if($table === null){
			return;
		}
		$killer = null;
		$cause = $entity->getLastDamageCause();
		if($cause instanceof EntityDamageByEntityEvent){
			$killer = $cause->getDamager();
		}
		$tool = $killer instanceof Human ? $killer->getInventory()->getItemInHand() : null;
		$event->setDrops($table->roll(new LootContext($entity, $killer, $tool, LootItems::lootingLevel($tool))));
	}

	private static function blockIdentifier(Block $block) : ?string{
		try{
			return GlobalBlockStateHandlers::getSerializer()->serializeBlock($block)->getName();
		}catch(Throwable){
			return null;
		}
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\loot;

use pocketmine\item\Item;
use function count;
use function max;
use function min;
use function mt_rand;

/**
 * A loot pool: rolls weighted entries a number of times, or picks one entry
 * by tier when "tiers" is set.
 */
final class LootPool{

	/**
	 * @param list<LootEntry>     $entries
	 * @param list<LootCondition> $conditions
	 * @param list<LootFunction>  $functions
	 * @param array<mixed>|null   $tiers
	 */
	public function __construct(
		private mixed $rolls,
		private float $bonusRolls,
		private ?array $tiers,
		private array $entries,
		private array $conditions,
		private array $functions
	){
	}

	/**
	 * @return list<LootEntry>
	 */
	public function getEntries() : array{
		return $this->entries;
	}

	/**
	 * @return list<Item>
	 */
	public function roll(LootContext $context, int $depth) : array{
		foreach($this->conditions as $condition){
			if(!$condition->test($context)){
				return [];
			}
		}
		if(count($this->entries) === 0){
			return [];
		}

		$items = [];
		if($this->tiers !== null){
			$entry = $this->pickTier();
			if($entry->canBeChosen($context)){
				$items = $entry->generate($context, $depth);
			}
		}else{
			$rolls = max(0, LootRange::rollInt($this->rolls, 1));
			for($i = 0; $i < $rolls; ++$i){
				$entry = $this->pickWeighted($context);
				if($entry !== null){
					foreach($entry->generate($context, $depth) as $item){
						$items[] = $item;
					}
				}
			}
		}

		$result = [];
		foreach($items as $item){
			foreach($this->functions as $function){
				$item = $function->apply($item, $context);
			}
			if(!$item->isNull() && $item->getCount() > 0){
				$result[] = $item;
			}
		}
		return $result;
	}

	public function getBonusRolls() : float{
		return $this->bonusRolls;
	}

	private function pickTier() : LootEntry{
		$initialRange = max(1, LootRange::rollInt($this->tiers["initial_range"] ?? null, 1));
		$bonusRolls = max(0, LootRange::rollInt($this->tiers["bonus_rolls"] ?? null, 0));
		$bonusChance = LootRange::number($this->tiers["bonus_chance"] ?? null, 0.0);
		$tier = mt_rand(1, $initialRange);
		for($i = 0; $i < $bonusRolls; ++$i){
			if(LootRange::chance() < $bonusChance){
				++$tier;
			}
		}
		return $this->entries[min($tier, count($this->entries)) - 1];
	}

	private function pickWeighted(LootContext $context) : ?LootEntry{
		$candidates = [];
		$total = 0;
		foreach($this->entries as $entry){
			if($entry->getWeight() > 0 && $entry->canBeChosen($context)){
				$candidates[] = $entry;
				$total += $entry->getWeight();
			}
		}
		if($total <= 0){
			return null;
		}
		$pick = mt_rand(1, $total);
		foreach($candidates as $entry){
			$pick -= $entry->getWeight();
			if($pick <= 0){
				return $entry;
			}
		}
		return null;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\loot;

use pocketmine\item\Item;

/**
 * A pool entry: an item, a nested loot table or nothing.
 */
final class LootEntry{

	public const TYPE_ITEM = "item";
	public const TYPE_LOOT_TABLE = "loot_table";
	public const TYPE_EMPTY = "empty";

	/**
	 * @param list<LootCondition> $conditions
	 * @param list<LootFunction>  $functions
	 */
	public function __construct(
		private string $type,
		private string $name,
		private int $weight,
		private int $quality,
		private array $conditions,
		private array $functions
	){
	}

	public function getType() : string{
		return $this->type;
	}

	public function getName() : string{
		return $this->name;
	}

	public function getWeight() : int{
		return $this->weight;
	}

	public function getQuality() : int{
		return $this->quality;
	}

	public function canBeChosen(LootContext $context) : bool{
		foreach($this->conditions as $condition){
			if(!$condition->test($context)){
				return false;
			}
		}
		return true;
	}

	/**
	 * @return list<Item>
	 */
	public function generate(LootContext $context, int $depth) : array{
		if($this->type === self::TYPE_ITEM){
			$item = LootItems::resolve($this->name);
			return $item === null ? [] : $this->applyFunctions([$item], $context);
		}
		if($this->type === self::TYPE_LOOT_TABLE){
			$table = LootTableRegistry::get($this->name);
			if($table === null || $depth >= LootTable::MAX_DEPTH){
				return [];
			}
			return $this->applyFunctions($table->rollAtDepth($context, $depth + 1), $context);
		}
		return [];
	}

	/**
	 * @param list<Item> $items
	 *
	 * @return list<Item>
	 */
	private function applyFunctions(array $items, LootContext $context) : array{
		$result = [];
		foreach($items as $item){
			foreach($this->functions as $function){
				$item = $function->apply($item, $context);
			}
			if(!$item->isNull() && $item->getCount() > 0){
				$result[] = $item;
			}
		}
		return $result;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\loot;

use behaviorpack\BehaviorPackException;
use function is_array;
use function is_string;
use function str_replace;
use function strtolower;

/**
 * Builds a LootTable from the decoded JSON of a loot_tables file.
 */
final class LootTableParser{

	private function __construct(){
	}

	/**
	 * @param array<mixed> $json
	 *
	 * @throws BehaviorPackException
	 */
	public static function parse(string $path, array $json) : LootTable{
		$pools = $json["pools"] ?? [];
		if(!is_array($pools)){
			throw new BehaviorPackException("\"pools\" must be a list");
		}
		$result = [];
		foreach($pools as $index => $pool){
			if(!is_array($pool)){
				throw new BehaviorPackException("Pool $index must be an object");
			}
			$result[] = self::parsePool($pool, (string) $index);
		}
		return new LootTable($path, $result);
	}

	/**
	 * @param array<mixed> $pool
	 *
	 * @throws BehaviorPackException
	 */
	private static function parsePool(array $pool, string $index) : LootPool{
		$entries = $pool["entries"] ?? [];
		if(!is_array($entries)){
			throw new BehaviorPackException("Pool $index: \"entries\" must be a list");
		}
		$parsed = [];
		foreach($entries as $entryIndex => $entry){
			if(!is_array($entry)){
				throw new BehaviorPackException("Pool $index entry $entryIndex must be an object");
			}
			$parsed[] = self::parseEntry($entry, "Pool $index entry $entryIndex");
		}
		$tiers = $pool["tiers"] ?? null;
		return new LootPool(
			$pool["rolls"] ?? 1,
			LootRange::number($pool["bonus_rolls"] ?? null, 0.0),
			is_array($tiers) ? $tiers : null,
			$parsed,
			self::parseConditions($pool["conditions"] ?? []),
			self::parseFunctions($pool["functions"] ?? [])
		);
	}

	/**
	 * @param array<mixed> $entry
	 *
	 * @throws BehaviorPackException
	 */
	private static function parseEntry(array $entry, string $where) : LootEntry{
		$type = self::stripNamespace($entry["type"] ?? LootEntry::TYPE_ITEM);
		$name = $entry["name"] ?? "";
		if($type !== LootEntry::TYPE_ITEM && $type !== LootEntry::TYPE_LOOT_TABLE && $type !== LootEntry::TYPE_EMPTY){
			throw new BehaviorPackException("$where: unknown entry type \"$type\"");
		}
		if($type !== LootEntry::TYPE_EMPTY && (!is_string($name) || $name === "")){
			throw new BehaviorPackException("$where: missing \"name\"");
		}
		return new LootEntry(
			$type,
			is_string($name) ? $name : "",
			(int) LootRange::number($entry["weight"] ?? null, 1),
			(int) LootRange::number($entry["quality"] ?? null, 0),
			self::parseConditions($entry["conditions"] ?? []),
			self::parseFunctions($entry["functions"] ?? [])
		);
	}

	/**
	 * @return list<LootCondition>
	 */
	private static function parseConditions(mixed $conditions) : array{
		if(!is_array($conditions)){
			return [];
		}
		$result = [];
		foreach($conditions as $condition){
			if(is_array($condition) && is_string($condition["condition"] ?? null)){
				$result[] = new LootCondition(self::stripNamespace($condition["condition"]), $condition);
			}
		}
		return $result;
	}

	/**
	 * @return list<LootFunction>
	 */
	private static function parseFunctions(mixed $functions) : array{
		if(!is_array($functions)){
			return [];
		}
		$result = [];
		foreach($functions as $function){
			if(!is_array($function) || !is_string($function["function"] ?? null)){
				continue;
			}
			$type = self::stripNamespace($function["function"]);
			if(LootFunction::isKnown($type)){
				$result[] = new LootFunction($type, $function, self::parseConditions($function["conditions"] ?? []));
			}
		}
		return $result;
	}

	private static function stripNamespace(mixed $value) : string{
		return is_string($value) ? str_replace("minecraft:", "", strtolower($value)) : "";
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\entity;

use pocketmine\entity\Entity;
use pocketmine\entity\Location;
use pocketmine\item\ItemIdentifier;
use pocketmine\item\SpawnEgg;
use pocketmine\math\Vector3;
use pocketmine\world\World;

/**
 * Spawn egg of a custom behavior pack entity.
 */
final class CustomSpawnEgg extends SpawnEgg{

	/**
	 * @phpstan-param class-string<BehaviorEntity> $entityClass
	 */
	public function __construct(
		ItemIdentifier $identifier,
		string $name,
		private string $entityClass
	){
		parent::__construct($identifier, $name);
	}

	protected function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity{
		$class = $this->entityClass;
		return new $class(Location::fromObject($pos, $world, $yaw, $pitch));
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\entity;

use pocketmine\block\Water;
use pocketmine\entity\animation\ArmSwingAnimation;
use pocketmine\entity\Entity;
use pocketmine\entity\EntitySizeInfo;
use pocketmine\entity\Living;
use pocketmine\event\entity\EntityDamageByEntityEvent;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\math\Vector3;
use pocketmine\nbt\NBT;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\ListTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\player\Player;
use pocketmine\utils\Utils;
use function array_is_list;
use function array_map;
use function array_search;
use function array_values;
use function atan2;
use function ceil;
use function cos;
use function count;
use function explode;
use function floor;
use function in_array;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function max;
use function min;
use function mt_rand;
use function sin;
use function sqrt;
use function str_replace;
use function strrchr;
use function strtolower;
use function substr;
use function ucwords;
use const M_PI;

/**
 * Base class of the custom entities defined by behavior packs. One tiny
 * subclass is generated per identifier because the network type id and the
 * save id are bound to the class; everything else is read from the entity
 * definition registered in EntityDefinitionRegistry.
 */
abstract class BehaviorEntity extends Living{

	private const TAG_COMPONENT_GROUPS = "BehaviorComponentGroups";
	private const TAG_SPAWNED = "BehaviorSpawned";

	private const MAX_EVENT_DEPTH = 16;

	private const TARGET_SEARCH_INTERVAL = 10;
	private const ATTACK_COOLDOWN = 20;
	private const PANIC_TICKS = 60;

	/** @var array<string, mixed>|null */
	private ?array $components = null;

	/** @var list<string> */
	private array $activeGroups = [];

	private bool $spawned = false;

	/** @var list<string> */
	private array $families = [];

	private bool $fireImmune = false;
	private bool $nameable = false;
	private bool $pushable = true;
	private float $attackDamage = 0.0;

	private ?Vector3 $strollTarget = null;
	private int $strollTicks = 0;
	private int $lookTicks = 0;
	private int $panicTicks = 0;
	private ?Vector3 $panicTarget = null;
	private ?Vector3 $panicSource = null;
	private ?Entity $attackTarget = null;
	private int $targetSearchTicks = 0;
	private int $attackCooldown = 0;
	private int $despawnCheckTicks = 0;

	public function getIdentifier() : string{
		return static::getNetworkTypeId();
	}

	/**
	 * @return array<string, mixed>
	 */
	public function getDefinition() : array{
		return EntityDefinitionRegistry::get($this->getIdentifier()) ?? [];
	}

	public function getName() : string{
		$identifier = $this->getIdentifier();
		$path = strrchr($identifier, ":");
		return ucwords(str_replace("_", " ", $path === false ? $identifier : substr($path, 1)));
	}

	/**
	 * @return list<string>
	 */
	public function getFamilies() : array{
		return $this->families;
	}

	/**
	 * @return list<string>
	 */
	public function getActiveComponentGroups() : array{
		return $this->activeGroups;
	}

	public function hasComponent(string $name) : bool{
		return isset($this->getComponents()[$name]);
	}

	/**
	 * Returns the base components merged with those of the active component
	 * groups, in activation order.
	 *
	 * @return array<string, mixed>
	 */
	public function getComponents() : array{
		return $this->components ?? $this->buildComponents();
	}

	protected function getInitialSizeInfo() : EntitySizeInfo{
		$box = $this->getComponents()["minecraft:collision_box"] ?? null;
		$width = 0.6;
		$height = 1.8;
		if(is_array($box)){
			$width = self::toFloat($box["width"] ?? null, $width);
			$height = self::toFloat($box["height"] ?? null, $height);
		}
		return new EntitySizeInfo(max($height, 0.01), max($width, 0.01));
	}

	public function isFireProof() : bool{
		return $this->fireImmune;
	}

	public function canBeRenamed() : bool{
		return $this->nameable;
	}

	protected function initEntity(CompoundTag $nbt) : void{
		$groups = $nbt->getListTag(self::TAG_COMPONENT_GROUPS, StringTag::class);
		if($groups !== null){
			foreach($groups as $group){
				$this->activeGroups[] = $group->getValue();
			}
		}
		$this->spawned = $nbt->getByte(self::TAG_SPAWNED, 0) !== 0;

		$this->components = $this->buildComponents();
		$this->applyComponents(false);

		parent::initEntity($nbt);

		if(!$this->spawned){
			$this->spawned = true;
			$this->triggerEvent("minecraft:entity_spawned");
			$this->applyComponents(true);
		}
	}

	public function saveNBT() : CompoundTag{
		$nbt = parent::saveNBT();
		if(count($this->activeGroups) > 0){
			$nbt->setTag(self::TAG_COMPONENT_GROUPS, new ListTag(array_map(function(string $group) : StringTag{
				return new StringTag($group);
			}, $this->activeGroups), NBT::TAG_String));
		}
		$nbt->setByte(self::TAG_SPAWNED, $this->spawned ? 1 : 0);
		return $nbt;
	}

	/**
	 * Runs an event of the entity definition: adds and removes component
	 * groups, following "sequence", "randomize" and "trigger".
	 */
	public function triggerEvent(string $event) : bool{
		$events = $this->getDefinition()["events"] ?? null;
		if(!is_array($events) || !is_array($events[$event] ?? null)){
			return false;
		}
		$before = $this->activeGroups;
		$this->runEventNode($events[$event], 0);
		if($before !== $this->activeGroups){
			$this->components = $this->buildComponents();
			$this->applyComponents(false);
		}
		return true;
	}

	/**
	 * @param array<mixed> $node
	 */
	private function runEventNode(array $node, int $depth) : void{
		if($depth > self::MAX_EVENT_DEPTH){
			return;
		}
		if(isset($node["filters"]) && is_array($node["filters"]) && !$this->matchesFilter($node["filters"], null, false)){
			return;
		}

		$groups = $this->getDefinition()["component_groups"] ?? [];
		foreach($node["remove"]["component_groups"] ?? [] as $group){
			if(is_string($group) && ($index = array_search($group, $this->activeGroups, true)) !== false){
				unset($this->activeGroups[$index]);
				$this->activeGroups = array_values($this->activeGroups);
			}
		}
		foreach($node["add"]["component_groups"] ?? [] as $group){
			if(is_string($group) && is_array($groups) && isset($groups[$group]) && !in_array($group, $this->activeGroups, true)){
				$this->activeGroups[] = $group;
			}
		}

		if(is_array($node["sequence"] ?? null)){
			foreach($node["sequence"] as $entry){
				if(is_array($entry)){
					$this->runEventNode($entry, $depth + 1);
				}
			}
		}

		if(is_array($node["randomize"] ?? null)){
			$total = 0.0;
			foreach($node["randomize"] as $entry){
				if(is_array($entry)){
					$total += max(0.0, self::toFloat($entry["weight"] ?? null, 1.0));
				}
			}
			if($total > 0){
				$roll = Utils::getRandomFloat() * $total;
				foreach($node["randomize"] as $entry){
					if(!is_array($entry)){
						continue;
					}
					$roll -= max(0.0, self::toFloat($entry["weight"] ?? null, 1.0));
					if($roll <= 0){
						$this->runEventNode($entry, $depth + 1);
						break;
					}
				}
			}
		}

		$trigger = $node["trigger"] ?? null;
		if(is_array($trigger)){
			$target = $trigger["target"] ?? "self";
			$trigger = $target === "self" ? ($trigger["event"] ?? null) : null;
		}
		if(is_string($trigger)){
			$events = $this->getDefinition()["events"] ?? [];
			if(is_array($events) && is_array($events[$trigger] ?? null)){
				$this->runEventNode($events[$trigger], $depth + 1);
			}
		}
	}

	/**
	 * @return array<string, mixed>
	 */
	private function buildComponents() : array{
		$definition = $this->getDefinition();
		$components = is_array($definition["components"] ?? null) ? $definition["components"] : [];
		$groups = is_array($definition["component_groups"] ?? null) ? $definition["component_groups"] : [];
		foreach($this->activeGroups as $group){
			if(is_array($groups[$group] ?? null)){
				foreach($groups[$group] as $name => $value){
					$components[$name] = $value;
				}
			}
		}
		return $components;
	}

	private function applyComponents(bool $resetHealth) : void{
		$components = $this->getComponents();

		$health = $components["minecraft:health"] ?? null;
		if(is_array($health)){
			$value = self::rangeValue($health["value"] ?? null, 20.0);
			$maxHealth = isset($health["max"]) ? self::rangeValue($health["max"], $value) : $value;
			$this->setMaxHealth(max(1, (int) ceil($maxHealth)));
			if($resetHealth){
				$this->setHealth(min($value, (float) $this->getMaxHealth()));
			}
		}

		$movement = $components["minecraft:movement"] ?? null;
		if(is_array($movement)){
			$this->setMovementSpeed(self::rangeValue($movement["value"] ?? null, 0.25), true);
		}

		$physics = $components["minecraft:physics"] ?? null;
		$this->setHasGravity(!is_array($physics) || ($physics["has_gravity"] ?? true) !== false);

		$scale = $components["minecraft:scale"] ?? null;
		$this->setScale(max(0.01, is_array($scale) ? self::toFloat($scale["value"] ?? null, 1.0) : 1.0));

		$families = $components["minecraft:type_family"]["family"] ?? [];
		$this->families = [];
		if(is_array($families)){
			foreach($families as $family){
				if(is_string($family)){
					$this->families[] = strtolower($family);
				}
			}
		}

		$this->fireImmune = isset($components["minecraft:fire_immune"]) || ($components["minecraft:is_immune_to_fire"] ?? false) !== false;

		$knockback = $components["minecraft:knockback_resistance"] ?? null;
		$this->knockbackResistanceAttr->setValue(is_array($knockback) ? max(0.0, min(1.0, self::toFloat($knockback["value"] ?? null, 0.0))) : 0.0, true);

		$nameable = $components["minecraft:nameable"] ?? null;
		$this->nameable = $nameable !== null;
		if(is_array($nameable)){
			$this->setNameTagAlwaysVisible(($nameable["always_show"] ?? false) === true);
		}

		$pushable = $components["minecraft:pushable"] ?? null;
		$this->pushable = !is_array($pushable) || ($pushable["is_pushable"] ?? true) !== false;

		$attack = $components["minecraft:attack"] ?? null;
		$this->attackDamage = is_array($attack) ? self::rangeValue($attack["damage"] ?? null, 0.0) : 0.0;
	}

	public function attack(EntityDamageEvent $source) : void{
		$cause = $source->getCause();
		if($this->fireImmune && ($cause === EntityDamageEvent::CAUSE_FIRE || $cause === EntityDamageEvent::CAUSE_FIRE_TICK || $cause === EntityDamageEvent::CAUSE_LAVA)){
			$source->cancel();
		}
		parent::attack($source);
		if($source->isCancelled() || !$this->hasComponent("minecraft:behavior.panic")){
			return;
		}
		$this->panicTicks = self::PANIC_TICKS;
		$this->panicTarget = null;
		$this->panicSource = null;
		if($source instanceof EntityDamageByEntityEvent && ($damager = $source->getDamager()) !== null){
			$this->panicSource = $damager->getPosition()->asVector3();
		}
	}

	protected function entityBaseTick(int $tickDiff = 1) : bool{
		$hasUpdate = parent::entityBaseTick($tickDiff);
		if($this->closed || !$this->isAlive()){
			return $hasUpdate;
		}

		if($this->tickDespawn($tickDiff)){
			return false;
		}
		$this->tickPush();
		$this->tickAi($tickDiff);

		return true;
	}

	private function tickDespawn(int $tickDiff) : bool{
		$despawn = $this->getComponents()["minecraft:despawn"] ?? null;
		if(!is_array($despawn) || $this->getNameTag() !== ""){
			return false;
		}
		$this->despawnCheckTicks += $tickDiff;
		if($this->despawnCheckTicks < 20){
			return false;
		}
		$this->despawnCheckTicks = 0;

		$distance = $despawn["despawn_from_distance"] ?? null;
		$maxDistance = is_array($distance) ? self::toFloat($distance["max_distance"] ?? null, 128.0) : 128.0;
		$minDistance = is_array($distance) ? self::toFloat($distance["min_distance"] ?? null, 32.0) : 32.0;

		$nearest = null;
		foreach($this->getWorld()->getPlayers() as $player){
			$d = $player->getPosition()->distanceSquared($this->location);
			if($nearest === null || $d < $nearest){
				$nearest = $d;
			}
		}
		if($nearest === null || $nearest > $maxDistance ** 2 || ($nearest > $minDistance ** 2 && mt_rand(1, 40) === 1)){
			$this->flagForDespawn();
			return true;
		}
		return false;
	}

	private function tickPush() : void{
		if(!$this->pushable){
			return;
		}
		foreach($this->getWorld()->getNearbyEntities($this->boundingBox, $this) as $entity){
			if(!$entity instanceof Living || ($entity instanceof Player && $entity->isSpectator())){
				continue;
			}
			$dx = $this->location->x - $entity->getLocation()->x;
			$dz = $this->location->z - $entity->getLocation()->z;
			$distance = max(sqrt($dx * $dx + $dz * $dz), 0.01);
			$this->motion = $this->motion->add($dx / $distance * 0.05, 0, $dz / $distance * 0.05);
		}
	}

	private function tickAi(int $tickDiff) : void{
		$components = $this->getComponents();

		if(isset($components["minecraft:behavior.float"]) && $this->isInWater()){
			$this->motion = $this->motion->withComponents(null, max($this->motion->y, 0.08), null);
		}

		if($this->attackCooldown > 0){
			$this->attackCooldown -= $tickDiff;
		}

		if($this->panicTicks > 0 && isset($components["minecraft:behavior.panic"])){
			$this->panicTicks -= $tickDiff;
			if($this->panicTarget === null || $this->horizontalDistanceSquared($this->panicTarget) < 1){
				$this->panicTarget = $this->pickPanicTarget();
			}
			$panic = $components["minecraft:behavior.panic"];
			$this->moveTowards($this->panicTarget, is_array($panic) ? self::toFloat($panic["speed_multiplier"] ?? null, 1.25) : 1.25);
			return;
		}

		if(isset($components["minecraft:behavior.nearest_attackable_target"])){
			$this->targetSearchTicks -= $tickDiff;
			if(!$this->isValidTarget($this->attackTarget)){
				$this->attackTarget = null;
			}
			if($this->attackTarget === null && $this->targetSearchTicks <= 0){
				$this->targetSearchTicks = self::TARGET_SEARCH_INTERVAL;
				$this->attackTarget = $this->findAttackTarget($components["minecraft:behavior.nearest_attackable_target"]);
			}
		}else{
			$this->attackTarget = null;
		}

		$melee = $components["minecraft:behavior.melee_attack"] ?? null;
		if($melee !== null && $this->attackTarget !== null){
			$this->tickMeleeAttack($this->attackTarget, is_array($melee) ? self::toFloat($melee["speed_multiplier"] ?? null, 1.0) : 1.0);
			return;
		}

		$look = $components["minecraft:behavior.look_at_player"] ?? null;
		if($look !== null){
			$lookDistance = is_array($look) ? self::toFloat($look["look_distance"] ?? null, 8.0) : 8.0;
			$probability = is_array($look) ? self::toFloat($look["probability"] ?? null, 0.02) : 0.02;
			$player = $this->findNearestPlayer($lookDistance);
			if($player === null){
				$this->lookTicks = 0;
			}elseif($this->lookTicks > 0){
				$this->lookTicks -= $tickDiff;
				$this->lookAt($player->getEyePos());
				return;
			}elseif($this->strollTarget === null && Utils::getRandomFloat() < $probability){
				$this->lookTicks = mt_rand(40, 80);
			}
		}

		$stroll = $components["minecraft:behavior.random_stroll"] ?? null;
		if($stroll !== null){
			$this->tickRandomStroll(is_array($stroll) ? $stroll : [], $tickDiff);
		}
	}

	/**
	 * @param array<mixed> $stroll
	 */
	private function tickRandomStroll(array $stroll, int $tickDiff) : void{
		if($this->strollTarget !== null){
			$this->strollTicks -= $tickDiff;
			if($this->strollTicks <= 0 || $this->horizontalDistanceSquared($this->strollTarget) < 1){
				$this->strollTarget = null;
				return;
			}
			$this->moveTowards($this->strollTarget, self::toFloat($stroll["speed_multiplier"] ?? null, 1.0));
			return;
		}
		$interval = max(1, (int) self::toFloat($stroll["interval"] ?? null, 120.0));
		if(mt_rand(1, $interval) !== 1){
			return;
		}
		$xz = max(1, (int) self::toFloat($stroll["xz_dist"] ?? null, 10.0));
		$this->strollTarget = new Vector3(
			$this->location->x + mt_rand(-$xz, $xz),
			$this->location->y,
			$this->location->z + mt_rand(-$xz, $xz)
		);
		$this->strollTicks = 100;
	}

	private function tickMeleeAttack(Entity $target, float $speedMultiplier) : void{
		$targetPosition = $target->getPosition();
		$this->moveTowards($targetPosition, $speedMultiplier);
		$this->lookAt($target->getEyePos());

		$reach = ($this->size->getWidth() * 2) ** 2 + $target->getSize()->getWidth();
		if($this->attackCooldown > 0 || $this->location->distanceSquared($targetPosition) > $reach){
			return;
		}
		$this->attackCooldown = self::ATTACK_COOLDOWN;
		$this->broadcastAnimation(new ArmSwingAnimation($this));
		$target->attack(new EntityDamageByEntityEvent($this, $target, EntityDamageEvent::CAUSE_ENTITY_ATTACK, max($this->attackDamage, 0.0)));
	}

	private function isValidTarget(?Entity $target) : bool{
		if($target === null || $target->isClosed() || !$target->isAlive() || $target->getWorld() !== $this->getWorld()){
			return false;
		}
		if($target instanceof Player && ($target->isCreative() || $target->isSpectator())){
			return false;
		}
		return $target->getPosition()->distanceSquared($this->location) <= 32 ** 2;
	}

	private function findAttackTarget(mixed $config) : ?Entity{
		if(!is_array($config)){
			return null;
		}
		$entries = $config["entity_types"] ?? [];
		if(!is_array($entries)){
			return null;
		}
		if(!array_is_list($entries)){
			$entries = [$entries];
		}
		$radius = self::toFloat($config["within_radius"] ?? null, 16.0);
		foreach($entries as $entry){
			if(is_array($entry)){
				$radius = max($radius, self::toFloat($entry["max_dist"] ?? null, $radius));
			}
		}

		$best = null;
		$bestDistance = null;
		foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy($radius, $radius, $radius), $this) as $entity){
			if(!$entity instanceof Living || !$this->isValidTarget($entity)){
				continue;
			}
			$distance = $entity->getPosition()->distanceSquared($this->location);
			foreach($entries as $entry){
				if(!is_array($entry)){
					continue;
				}
				$maxDistance = self::toFloat($entry["max_dist"] ?? null, self::toFloat($config["within_radius"] ?? null, 16.0));
				if($distance > $maxDistance ** 2){
					continue;
				}
				$filters = $entry["filters"] ?? null;
				if(is_array($filters) && !$this->matchesFilter($filters, $entity, true)){
					continue;
				}
				if($bestDistance === null || $distance < $bestDistance){
					$best = $entity;
					$bestDistance = $distance;
				}
				break;
			}
		}
		return $best;
	}

	/**
	 * Evaluates a subset of Bedrock filters: all_of, any_of, none_of,
	 * is_family and has_component. Unsupported tests return $unknownResult.
	 *
	 * @param array<mixed> $filter
	 */
	private function matchesFilter(array $filter, ?Entity $other, bool $unknownResult) : bool{
		if(array_is_list($filter)){
			foreach($filter as $entry){
				if(is_array($entry) && !$this->matchesFilter($entry, $other, $unknownResult)){
					return false;
				}
			}
			return true;
		}
		if(is_array($filter["all_of"] ?? null)){
			foreach($filter["all_of"] as $entry){
				if(is_array($entry) && !$this->matchesFilter($entry, $other, $unknownResult)){
					return false;
				}
			}
			return true;
		}
		if(is_array($filter["any_of"] ?? null)){
			foreach($filter["any_of"] as $entry){
				if(is_array($entry) && $this->matchesFilter($entry, $other, $unknownResult)){
					return true;
				}
			}
			return false;
		}
		if(is_array($filter["none_of"] ?? null)){
			foreach($filter["none_of"] as $entry){
				if(is_array($entry) && $this->matchesFilter($entry, $other, $unknownResult)){
					return false;
				}
			}
			return true;
		}

		$test = $filter["test"] ?? null;
		if(!is_string($test)){
			return $unknownResult;
		}
		$subject = ($filter["subject"] ?? "self") === "self" ? $this : $other;
		if($subject === null){
			return false;
		}
		$value = $filter["value"] ?? null;
		if($test === "is_family"){
			$result = is_string($value) && in_array(strtolower($value), self::familiesOf($subject), true);
		}elseif($test === "has_component"){
			$result = is_string($value) && $subject instanceof BehaviorEntity && $subject->hasComponent($value);
		}else{
			return $unknownResult;
		}
		$operator = $filter["operator"] ?? "==";
		return ($operator === "!=" || $operator === "not" || $operator === "<>") ? !$result : $result;
	}

	/**
	 * @return list<string>
	 */
	private static function familiesOf(Entity $entity) : array{
		if($entity instanceof BehaviorEntity){
			return $entity->getFamilies();
		}
		if($entity instanceof Player){
			return ["player", "mob"];
		}
		$parts = explode("\\", $entity::class);
		return [strtolower($parts[count($parts) - 1]), "mob"];
	}

	private function findNearestPlayer(float $distance) : ?Player{
		$best = null;
		$bestDistance = $distance ** 2;
		foreach($this->getWorld()->getPlayers() as $player){
			if($player->isSpectator() || !$player->isAlive()){
				continue;
			}
			$d = $player->getPosition()->distanceSquared($this->location);
			if($d <= $bestDistance){
				$best = $player;
				$bestDistance = $d;
			}
		}
		return $best;
	}

	private function pickPanicTarget() : Vector3{
		if($this->panicSource !== null){
			$angle = atan2($this->location->z - $this->panicSource->z, $this->location->x - $this->panicSource->x) + (Utils::getRandomFloat() - 0.5);
		}else{
			$angle = Utils::getRandomFloat() * 2 * M_PI;
		}
		return new Vector3($this->location->x + cos($angle) * 6, $this->location->y, $this->location->z + sin($angle) * 6);
	}

	private function moveTowards(Vector3 $target, float $speedMultiplier) : void{
		$dx = $target->x - $this->location->x;
		$dz = $target->z - $this->location->z;
		$distance = sqrt($dx * $dx + $dz * $dz);
		if($distance < 0.1){
			return;
		}
		$speed = $this->getMovementSpeed() * $speedMultiplier;
		$this->motion = new Vector3($dx / $distance * $speed, $this->motion->y, $dz / $distance * $speed);
		$yaw = atan2($dz, $dx) / M_PI * 180 - 90;
		$this->setRotation($yaw < 0 ? $yaw + 360.0 : $yaw, 0.0);
		if($this->isCollidedHorizontally && $this->onGround){
			$this->jump();
		}
	}

	private function horizontalDistanceSquared(Vector3 $target) : float{
		return ($target->x - $this->location->x) ** 2 + ($target->z - $this->location->z) ** 2;
	}

	private function isInWater() : bool{
		return $this->getWorld()->getBlockAt(
			(int) floor($this->location->x),
			(int) floor($this->location->y + 0.3),
			(int) floor($this->location->z)
		) instanceof Water;
	}

	private static function toFloat(mixed $value, float $default) : float{
		if(is_int($value) || is_float($value)){
			return (float) $value;
		}
		if(is_bool($value)){
			return $value ? 1.0 : 0.0;
		}
		return $default;
	}

	/**
	 * Reads a number, a [min, max] pair or a {range_min, range_max} object.
	 */
	private static function rangeValue(mixed $value, float $default) : float{
		if(is_int($value) || is_float($value)){
			return (float) $value;
		}
		if(!is_array($value)){
			return $default;
		}
		if(array_is_list($value) && count($value) === 2){
			$min = self::toFloat($value[0], $default);
			$max = self::toFloat($value[1], $min);
		}else{
			$min = self::toFloat($value["range_min"] ?? null, $default);
			$max = self::toFloat($value["range_max"] ?? null, $min);
		}
		return $min + Utils::getRandomFloat() * max(0.0, $max - $min);
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\entity;

/**
 * Parsed "minecraft:entity" definitions of the custom entities loaded from
 * behavior packs, indexed by identifier.
 */
final class EntityDefinitionRegistry{

	/** @var array<string, array<string, mixed>> */
	private static array $definitions = [];

	private function __construct(){
	}

	/**
	 * @param array<string, mixed> $definition
	 */
	public static function register(string $identifier, array $definition) : void{
		self::$definitions[$identifier] = $definition;
	}

	/**
	 * Returns the raw "minecraft:entity" object of a custom entity, or null
	 * when no behavior pack defines it.
	 *
	 * @return array<string, mixed>|null
	 */
	public static function get(string $identifier) : ?array{
		return self::$definitions[$identifier] ?? null;
	}

	/**
	 * @return array<string, array<string, mixed>>
	 */
	public static function all() : array{
		return self::$definitions;
	}

	public static function clear() : void{
		self::$definitions = [];
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\entity;

use behaviorpack\BehaviorPack;
use behaviorpack\BehaviorPackException;
use behaviorpack\ContentLoader;
use customiesdevs\customies\entity\CustomiesEntityFactory;
use customiesdevs\customies\item\CreativeInventoryInfo;
use customiesdevs\customies\item\CustomiesItemFactory;
use pocketmine\entity\EntityFactory;
use pocketmine\item\ItemIdentifier;
use pocketmine\item\ItemTypeIds;
use pocketmine\plugin\PluginBase;
use Throwable;
use function class_exists;
use function is_array;
use function is_string;
use function md5;
use function preg_match;
use function str_starts_with;
use function strtolower;
use function var_export;

/**
 * Registers the custom entities ("minecraft:entity" files in entities/) of
 * the behavior packs through Customies.
 */
final class EntityLoader implements ContentLoader{

	private const GENERATED_NAMESPACE = "behaviorpack\\entity\\generated";

	/** @var array<string, class-string<BehaviorEntity>> */
	private array $classes = [];

	public function __construct(
		private PluginBase $plugin
	){
	}

	public function getName() : string{
		return "custom entities";
	}

	public function requiresCustomies() : bool{
		return true;
	}

	public function load(array $packs) : void{
		$logger = $this->plugin->getLogger();
		$registered = 0;
		$vanilla = 0;

		foreach($packs as $pack){
			foreach($pack->listFiles("entities", "json") as $file){
				try{
					$result = $this->loadFile($pack, $file);
				}catch(Throwable $e){
					$logger->warning("Behavior packs: skipped entity " . $pack->getName() . "/" . $pack->relativePath($file) . ": " . $e->getMessage());
					continue;
				}
				if($result === true){
					$registered++;
				}elseif($result === false){
					$vanilla++;
				}
			}
		}

		if($vanilla > 0){
			$logger->debug("Behavior packs: $vanilla vanilla entity overrides ignored");
		}
		$logger->info("Behavior packs: $registered custom entities");
	}

	/**
	 * Returns true when a custom entity was registered, false for a vanilla
	 * override and null for a file that holds no entity.
	 *
	 * @throws BehaviorPackException
	 */
	private function loadFile(BehaviorPack $pack, string $file) : ?bool{
		$json = BehaviorPack::readJson($file);
		$definition = $json["minecraft:entity"] ?? null;
		if(!is_array($definition)){
			return null;
		}
		$identifier = $definition["description"]["identifier"] ?? null;
		if(!is_string($identifier) || preg_match('/^[a-z0-9_.\-]+:[a-z0-9_.\-\/]+$/', strtolower($identifier)) !== 1){
			throw new BehaviorPackException("missing or invalid description.identifier");
		}
		if(str_starts_with(strtolower($identifier), "minecraft:")){
			return false;
		}
		if(isset($this->classes[$identifier])){
			throw new BehaviorPackException("entity $identifier is already registered");
		}

		EntityDefinitionRegistry::register($identifier, $definition);
		$class = $this->generateClass($identifier);
		CustomiesEntityFactory::getInstance()->registerEntity($class, $identifier);
		$this->classes[$identifier] = $class;

		if(($definition["description"]["is_spawnable"] ?? false) === true){
			$this->registerSpawnEgg($pack, $identifier, $class);
		}
		return true;
	}

	/**
	 * @phpstan-param class-string<BehaviorEntity> $class
	 */
	private function registerSpawnEgg(BehaviorPack $pack, string $identifier, string $class) : void{
		$eggIdentifier = $identifier . "_spawn_egg";
		try{
			$egg = new CustomSpawnEgg(new ItemIdentifier(ItemTypeIds::newId()), "Spawn Egg", $class);
			CustomiesItemFactory::getInstance()->registerItem(static function() use ($egg) : CustomSpawnEgg{
				return clone $egg;
			}, $eggIdentifier, new CreativeInventoryInfo(CreativeInventoryInfo::CATEGORY_NATURE, CreativeInventoryInfo::GROUP_MOB_EGGS));
		}catch(Throwable $e){
			$this->plugin->getLogger()->warning("Behavior packs: no spawn egg for $identifier (" . $pack->getName() . "): " . $e->getMessage());
		}
	}

	/**
	 * Entity network and save ids are static, so each identifier needs its
	 * own class: a two-line subclass of BehaviorEntity is declared at runtime.
	 *
	 * @return class-string<BehaviorEntity>
	 */
	private function generateClass(string $identifier) : string{
		$shortName = "Entity" . md5($identifier);
		$class = self::GENERATED_NAMESPACE . "\\" . $shortName;
		if(!class_exists($class, false)){
			eval(
				"namespace " . self::GENERATED_NAMESPACE . ";\n" .
				"final class $shortName extends \\" . BehaviorEntity::class . "{\n" .
				"\tpublic static function getNetworkTypeId() : string{\n" .
				"\t\treturn " . var_export($identifier, true) . ";\n" .
				"\t}\n" .
				"}\n"
			);
		}
		/** @var class-string<BehaviorEntity> $class */
		return $class;
	}

	public function close() : void{
		$this->classes = [];
		EntityDefinitionRegistry::clear();
	}

	/**
	 * @return array<string, class-string<BehaviorEntity>>
	 */
	public function getClasses() : array{
		return $this->classes;
	}

	public function isRegistered(string $identifier) : bool{
		return isset($this->classes[$identifier]) && EntityFactory::getInstance()->isRegistered($this->classes[$identifier]);
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script;

use pocketmine\console\ConsoleCommandSender;
use pocketmine\lang\Translatable;

/**
 * Runs the commands of scripts with console permissions, without printing
 * their output.
 */
final class ScriptCommandSender extends ConsoleCommandSender{

	public function sendMessage(Translatable|string $message) : void{

	}

	public function getName() : string{
		return "Script";
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\binding;

use behaviorpack\script\js\Interpreter;
use behaviorpack\script\js\JsObject;
use Closure;
use function in_array;
use function is_array;
use function str_starts_with;

/**
 * Builds the host classes of the Minecraft modules. Reading a member that a
 * host class does not have is reported as an unimplemented API.
 */
final class ClassFactory{

	private const IGNORED = [
		"then", "catch", "finally", "toJSON", "inspect", "asymmetricMatch", "\$\$typeof", "nodeType", "tagName",
		"length", "name", "prototype", "valueOf", "toString", "constructor", "@@__IMMUTABLE_ITERABLE__@@"
	];

	/**
	 * @param Closure(string) : void $recordMissing
	 */
	public function __construct(
		private Interpreter $js,
		private Closure $recordMissing
	){}

	public function isIgnored(string $key) : bool{
		return in_array($key, self::IGNORED, true) || str_starts_with($key, "_") || str_starts_with($key, "#");
	}

	/**
	 * Returns a miss handler that records "$owner.$key" as unimplemented.
	 *
	 * @return Closure(JsObject, string) : mixed
	 */
	public function tracker(string $owner) : Closure{
		$record = $this->recordMissing;
		return function(JsObject $receiver, string $key) use ($owner, $record) : mixed{
			if(!$this->isIgnored($key)){
				$record($owner . "." . $key);
			}
			return null;
		};
	}

	/**
	 * Defines a class. Without $construct, "new" throws "Illegal constructor"
	 * as for the classes that scripts cannot instantiate.
	 *
	 * @param (Closure(list<mixed>, JsObject) : JsObject)|null $construct
	 */
	public function define(string $name, ?HostClass $parent = null, ?Closure $construct = null, int $length = 0) : HostClass{
		$js = $this->js;
		$prototype = new JsObject($parent?->prototype ?? $js->objectPrototype);
		$prototype->className = $name;
		$prototype->miss = $this->tracker($name);
		$construct ??= function(array $args, JsObject $newTarget) use ($js) : JsObject{
			$js->throwError("TypeError", "Illegal constructor");
		};
		$constructor = $js->makeClass($name, $prototype, $construct, null, $length, $parent?->constructor);
		$constructor->miss = $this->tracker($name);
		return new HostClass($name, $constructor, $prototype);
	}

	/**
	 * @param Closure(mixed, list<mixed>) : mixed $call
	 */
	public function method(HostClass $class, string $name, Closure $call, int $length = 0) : void{
		$this->js->defineMethod($class->prototype, $name, $length, $call);
	}

	/**
	 * @param Closure(mixed, list<mixed>) : mixed $call
	 */
	public function staticMethod(HostClass $class, string $name, Closure $call, int $length = 0) : void{
		$this->js->defineMethod($class->constructor, $name, $length, $call);
	}

	/**
	 * @param Closure(mixed) : mixed              $get
	 * @param (Closure(mixed, mixed) : void)|null $set
	 */
	public function getter(HostClass $class, string $name, Closure $get, ?Closure $set = null) : void{
		$this->js->defineGetter($class->prototype, $name, $get, $set);
	}

	public function staticValue(HostClass $class, string $name, mixed $value) : void{
		$this->js->defineHidden($class->constructor, $name, $value);
		$class->constructor->locked[$name] = true;
	}

	/**
	 * @param array<string, mixed> $host
	 */
	public function instance(HostClass $class, array $host) : JsObject{
		$object = new JsObject($class->prototype);
		$object->className = $class->name;
		$object->host = $host;
		return $object;
	}

	/**
	 * Returns the host data of $value when it is an object of the given
	 * kind, or throws a TypeError.
	 *
	 * @return array<string, mixed>
	 */
	public function host(mixed $value, string $kind) : array{
		if($value instanceof JsObject && is_array($value->host) && ($value->host["kind"] ?? null) === $kind){
			return $value->host;
		}
		$this->js->throwError("TypeError", "Illegal invocation: expected " . $kind);
	}

	/**
	 * @param array<string, mixed> $values
	 */
	public function enum(string $name, array $values) : JsObject{
		$object = $this->js->newObject();
		foreach($values as $key => $value){
			$object->props[$key] = $value;
			$object->locked[$key] = true;
		}
		$object->extensible = false;
		$object->miss = $this->tracker($name);
		return $object;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\binding;

use behaviorpack\script\js\Interpreter;
use behaviorpack\script\js\JsArray;
use behaviorpack\script\js\JsObject;
use behaviorpack\script\ScriptRuntime;
use Closure;
use function count;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function str_starts_with;

/**
 * The @minecraft/server-ui module: ActionFormData, MessageFormData and
 * ModalFormData, shown with the server form API. Headers, labels and
 * dividers are not buttons: an action form shows them in its body.
 */
final class UiModule{

	private Interpreter $js;

	private HostClass $formResponse;
	private HostClass $actionResponse;
	private HostClass $messageResponse;
	private HostClass $modalResponse;

	/** @var array<string, mixed> */
	private array $exports = [];

	public function __construct(
		private ScriptRuntime $runtime,
		private ClassFactory $f,
		private ServerModule $server
	){
		$this->js = $runtime->js;
		$this->defineResponses();
		$this->defineActionForm();
		$this->defineMessageForm();
		$this->defineModalForm();
		$this->exports["FormCancelationReason"] = $f->enum("FormCancelationReason", ["UserBusy" => "UserBusy", "UserClosed" => "UserClosed"]);
		$this->exports["FormRejectReason"] = $f->enum("FormRejectReason", ["MalformedResponse" => "MalformedResponse", "PlayerQuit" => "PlayerQuit", "ServerShutdown" => "ServerShutdown"]);
	}

	/**
	 * @return array<string, mixed>
	 */
	public function exports() : array{
		return $this->exports;
	}

	private function defineResponses() : void{
		$f = $this->f;
		$this->formResponse = $f->define("FormResponse");
		$this->actionResponse = $f->define("ActionFormResponse", $this->formResponse);
		$this->messageResponse = $f->define("MessageFormResponse", $this->formResponse);
		$this->modalResponse = $f->define("ModalFormResponse", $this->formResponse);
		foreach([$this->formResponse, $this->actionResponse, $this->messageResponse, $this->modalResponse] as $class){
			$this->exports[$class->name] = $class->constructor;
		}
	}

	/**
	 * @param array<string, mixed> $values
	 */
	private function response(HostClass $class, array $values) : JsObject{
		$response = $this->f->instance($class, ["kind" => "response"]);
		foreach($values as $key => $value){
			$response->props[$key] = $value;
		}
		return $response;
	}

	private function canceled(HostClass $class) : JsObject{
		return $this->response($class, ["canceled" => true, "cancelationReason" => "UserClosed"]);
	}

	/**
	 * @param array<string, mixed>          $initial
	 * @param Closure(array<string, mixed>) : array<string, mixed> $build
	 * @param Closure(mixed, array<string, mixed>) : JsObject      $map
	 */
	private function defineForm(string $name, array $initial, Closure $build, Closure $map) : HostClass{
		$js = $this->js;
		$class = $this->f->define($name, null, function(array $args, JsObject $newTarget) use ($js, $initial, $name, &$class) : JsObject{
			$form = $this->f->instance($class, ["kind" => $name] + $initial);
			$form->proto = $js->prototypeFor($newTarget, $class->prototype);
			return $form;
		});
		$this->f->method($class, "show", function(mixed $thisValue, array $args) use ($name, $build, $map) : mixed{
			$host = $this->f->host($thisValue, $name);
			$player = $args[0] ?? null;
			if(!$player instanceof JsObject || ($player->host["kind"] ?? null) !== "entity" || ($player->host["typeId"] ?? null) !== "minecraft:player"){
				$this->js->throwError("TypeError", "Forms can only be shown to players");
			}
			return $this->runtime->showForm($player->host["id"], $build($host), function(mixed $raw) use ($map, $host) : JsObject{
				return $map($raw, $host);
			});
		}, 1);
		$this->exports[$name] = $class->constructor;
		return $class;
	}

	/**
	 * @param Closure(mixed, list<mixed>) : void $mutate
	 */
	private function builder(HostClass $class, string $method, Closure $mutate, int $length = 1) : void{
		$this->f->method($class, $method, function(mixed $thisValue, array $args) use ($class, $mutate) : mixed{
			$this->f->host($thisValue, $class->name);
			$mutate($thisValue, $args);
			return $thisValue;
		}, $length);
	}

	private function defineActionForm() : void{
		$class = $this->defineForm("ActionFormData", ["title" => "", "body" => "", "buttons" => [], "extra" => []], function(array $host) : array{
			$content = $host["body"];
			foreach($host["extra"] as $line){
				$content .= ($content === "" ? "" : "\n") . $line;
			}
			return ["type" => "form", "title" => $host["title"], "content" => $content, "buttons" => $host["buttons"]];
		}, function(mixed $raw, array $host) : JsObject{
			if(!is_int($raw) || $raw < 0 || $raw >= count($host["buttons"])){
				return $this->canceled($this->actionResponse);
			}
			return $this->response($this->actionResponse, ["canceled" => false, "selection" => $raw]);
		});
		$this->builder($class, "title", function(mixed $thisValue, array $args) : void{
			$thisValue->host["title"] = $this->server->text($args[0] ?? "");
		});
		$this->builder($class, "body", function(mixed $thisValue, array $args) : void{
			$thisValue->host["body"] = $this->server->text($args[0] ?? "");
		});
		$this->builder($class, "button", function(mixed $thisValue, array $args) : void{
			$button = ["text" => $this->server->text($args[0] ?? "")];
			$icon = $args[1] ?? null;
			if(is_string($icon) && $icon !== ""){
				$button["image"] = ["type" => str_starts_with($icon, "http") ? "url" : "path", "data" => $icon];
			}
			$thisValue->host["buttons"][] = $button;
		}, 2);
		$this->builder($class, "header", function(mixed $thisValue, array $args) : void{
			$thisValue->host["extra"][] = "Â§l" . $this->server->text($args[0] ?? "") . "Â§r";
		});
		$this->builder($class, "label", function(mixed $thisValue, array $args) : void{
			$thisValue->host["extra"][] = $this->server->text($args[0] ?? "");
		});
		$this->builder($class, "divider", function(mixed $thisValue, array $args) : void{
			$thisValue->host["extra"][] = "";
		}, 0);
	}

	private function defineMessageForm() : void{
		$class = $this->defineForm("MessageFormData", ["title" => "", "body" => "", "button1" => "", "button2" => ""], function(array $host) : array{
			return ["type" => "modal", "title" => $host["title"], "content" => $host["body"], "button1" => $host["button1"], "button2" => $host["button2"]];
		}, function(mixed $raw) : JsObject{
			if(!is_bool($raw)){
				return $this->canceled($this->messageResponse);
			}
			return $this->response($this->messageResponse, ["canceled" => false, "selection" => $raw ? 0 : 1]);
		});
		foreach(["title", "body", "button1", "button2"] as $field){
			$this->builder($class, $field, function(mixed $thisValue, array $args) use ($field) : void{
				$thisValue->host[$field] = $this->server->text($args[0] ?? "");
			});
		}
	}

	private function option(mixed $options, string $key) : mixed{
		return $options instanceof JsObject && !$options instanceof JsArray ? $this->js->get($options, $key) : null;
	}

	private function defineModalForm() : void{
		$js = $this->js;
		$class = $this->defineForm("ModalFormData", ["title" => "", "content" => [], "kinds" => [], "submit" => null], function(array $host) : array{
			$form = ["type" => "custom_form", "title" => $host["title"], "content" => $host["content"]];
			if($host["submit"] !== null){
				$form["submit"] = $host["submit"];
			}
			return $form;
		}, function(mixed $raw, array $host) use ($js) : JsObject{
			if(!is_array($raw)){
				return $this->canceled($this->modalResponse);
			}
			$values = [];
			foreach($host["kinds"] as $index => $kind){
				$value = $raw[$index] ?? null;
				$values[] = match($kind){
					"toggle" => is_bool($value) ? $value : null,
					"slider" => is_int($value) || is_float($value) ? Interpreter::intOrFloat((float) $value) : null,
					"dropdown" => is_int($value) ? $value : null,
					"input" => is_string($value) ? $value : null,
					default => null
				};
			}
			return $this->response($this->modalResponse, ["canceled" => false, "formValues" => $js->newArray($values)]);
		});
		$this->builder($class, "title", function(mixed $thisValue, array $args) : void{
			$thisValue->host["title"] = $this->server->text($args[0] ?? "");
		});
		$this->builder($class, "submitButton", function(mixed $thisValue, array $args) : void{
			$thisValue->host["submit"] = $this->server->text($args[0] ?? "");
		});
		$add = function(JsObject $form, string $kind, array $element) : void{
			$form->host["content"][] = $element;
			$form->host["kinds"][] = $kind;
		};
		$this->builder($class, "textField", function(mixed $thisValue, array $args) use ($add) : void{
			$options = $args[2] ?? null;
			$default = is_string($options) ? $options : $this->option($options, "defaultValue");
			$add($thisValue, "input", [
				"type" => "input",
				"text" => $this->server->text($args[0] ?? ""),
				"placeholder" => $this->server->text($args[1] ?? ""),
				"default" => $default === null ? "" : $this->server->text($default)
			]);
		}, 3);
		$this->builder($class, "toggle", function(mixed $thisValue, array $args) use ($add, $js) : void{
			$options = $args[1] ?? null;
			$default = is_bool($options) ? $options : $this->option($options, "defaultValue");
			$add($thisValue, "toggle", ["type" => "toggle", "text" => $this->server->text($args[0] ?? ""), "default" => $js->toBoolean($default)]);
		}, 2);
		$this->builder($class, "slider", function(mixed $thisValue, array $args) use ($add, $js) : void{
			$minimum = $js->toNumber($args[1] ?? 0);
			$maximum = $js->toNumber($args[2] ?? 0);
			$options = $args[3] ?? null;
			if(is_int($options) || is_float($options)){
				$step = $options;
				$default = $args[4] ?? null;
			}else{
				$step = $this->option($options, "valueStep");
				$default = $this->option($options, "defaultValue");
			}
			$add($thisValue, "slider", [
				"type" => "slider",
				"text" => $this->server->text($args[0] ?? ""),
				"min" => $minimum,
				"max" => $maximum,
				"step" => $step === null ? 1 : $js->toNumber($step),
				"default" => $default === null ? $minimum : $js->toNumber($default)
			]);
		}, 4);
		$this->builder($class, "dropdown", function(mixed $thisValue, array $args) use ($add, $js) : void{
			$items = [];
			$list = $args[1] ?? null;
			if($list instanceof JsArray){
				foreach($list->items as $item){
					$items[] = $this->server->text($item);
				}
			}
			$options = $args[2] ?? null;
			$default = is_int($options) ? $options : $this->option($options, "defaultValueIndex");
			$add($thisValue, "dropdown", [
				"type" => "dropdown",
				"text" => $this->server->text($args[0] ?? ""),
				"options" => $items,
				"default" => $default === null ? 0 : (int) $js->toNumber($default)
			]);
		}, 3);
		$this->builder($class, "label", function(mixed $thisValue, array $args) use ($add) : void{
			$add($thisValue, "label", ["type" => "label", "text" => $this->server->text($args[0] ?? "")]);
		});
		$this->builder($class, "header", function(mixed $thisValue, array $args) use ($add) : void{
			$add($thisValue, "label", ["type" => "label", "text" => "Â§l" . $this->server->text($args[0] ?? "") . "Â§r"]);
		});
		$this->builder($class, "divider", function(mixed $thisValue, array $args) use ($add) : void{
			$add($thisValue, "label", ["type" => "label", "text" => ""]);
		}, 0);
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\binding;

use behaviorpack\script\js\JsObject;
use behaviorpack\script\js\NativeFunction;

/**
 * A JavaScript class implemented in PHP: its constructor and prototype.
 */
final class HostClass{

	public function __construct(
		public string $name,
		public NativeFunction $constructor,
		public JsObject $prototype
	){}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\binding;

use behaviorpack\script\js\Interpreter;
use behaviorpack\script\js\JsArray;
use behaviorpack\script\js\JsCallable;
use behaviorpack\script\js\JsNull;
use behaviorpack\script\js\JsObject;
use behaviorpack\script\js\JsSymbol;
use behaviorpack\script\js\JsThrow;
use behaviorpack\script\js\NativeFunction;
use behaviorpack\script\ScriptException;
use behaviorpack\script\ScriptRuntime;
use stdClass;
use function array_is_list;
use function array_key_exists;
use function count;
use function floor;
use function get_object_vars;
use function in_array;
use function intdiv;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_numeric;
use function is_string;
use function json_encode;
use function max;
use function min;
use function str_contains;
use function strlen;
use function strtolower;
use function substr;
use const JSON_PARTIAL_OUTPUT_ON_ERROR;

/**
 * The @minecraft/server module: its classes, enums, world and system. Host
 * objects keep an array with a "kind" key as their payload; every call to
 * the server goes through ScriptApi with the value encoding of ScriptValues.
 */
final class ServerModule{

	public const DIRECTIONS = ["Down", "Up", "North", "South", "West", "East"];

	private const BLOCK_HOOKS = ["onPlayerInteract", "onPlace", "onPlayerBreak"];
	private const ITEM_HOOKS = ["onUse", "onUseOn"];

	private Interpreter $js;

	public HostClass $blockType;
	public HostClass $itemType;
	public HostClass $entityType;
	public HostClass $effectType;
	public HostClass $effect;
	public HostClass $permutation;
	public HostClass $itemStack;
	public HostClass $component;
	public HostClass $itemComponent;
	public HostClass $durability;
	public HostClass $container;
	public HostClass $entityComponent;
	public HostClass $attributeComponent;
	public HostClass $health;
	public HostClass $inventory;
	public HostClass $equippable;
	public HostClass $blockComponent;
	public HostClass $blockInventory;
	public HostClass $entity;
	public HostClass $player;
	public HostClass $screenDisplay;
	public HostClass $block;
	public HostClass $dimension;
	public HostClass $signal;
	public HostClass $scoreboard;
	public HostClass $objective;
	public HostClass $identity;
	public HostClass $blockRegistry;
	public HostClass $itemRegistry;
	public HostClass $commandRegistry;
	public HostClass $world;
	public HostClass $system;

	public JsObject $worldObject;
	public JsObject $systemObject;

	/** @var array<string, mixed> */
	private array $exports = [];

	/** @var array<int, JsObject> */
	private array $entities = [];

	/** @var array<string, JsObject> */
	private array $dimensions = [];

	/** @var array<string, HostClass> */
	private array $eventClasses = [];

	/** @var array<string, JsObject> */
	private array $signals = [];

	/** @var array<string, JsObject> */
	private array $stubs = [];

	/** @var array{next: int, objectives: array<string, array{displayName: string, scores: array<string, int>}>, participants: array<string, array{id: int, displayName: string, type: string}>} */
	public array $scoreboardData = ["next" => 1, "objectives" => [], "participants" => []];

	public function __construct(
		private ScriptRuntime $runtime,
		private ClassFactory $f
	){
		$this->js = $runtime->js;
		$this->loadScoreboard($runtime->storage->getScoreboard());
		$this->defineTypes();
		$this->defineItems();
		$this->defineContainers();
		$this->defineEntities();
		$this->defineBlocks();
		$this->defineScoreboard();
		$this->defineEvents();
		$this->defineWorld();
		$this->defineSystem();
		$this->defineEnums();
	}

	/**
	 * @return array<string, mixed>
	 */
	public function exports() : array{
		return $this->exports;
	}

	private function export(string $name, mixed $value) : void{
		$this->exports[$name] = $value instanceof HostClass ? $value->constructor : $value;
	}

	/**
	 * Returns a stand-in for an export that is not implemented: using it in
	 * any way records it.
	 */
	public function stub(string $name) : JsObject{
		if(isset($this->stubs[$name])){
			return $this->stubs[$name];
		}
		$js = $this->js;
		$stub = new NativeFunction($js->functionPrototype, $name, 0, function(mixed $thisValue, array $args) use ($name) : mixed{
			$this->runtime->recordMissing($name);
			return null;
		}, function(array $args, JsObject $newTarget) use ($name, $js) : JsObject{
			$this->runtime->recordMissing($name);
			return $js->newObject();
		});
		$stub->miss = function(JsObject $receiver, string $key) use ($name) : mixed{
			if(!$this->f->isIgnored($key)){
				$this->runtime->recordMissing($name);
			}
			return null;
		};
		$js->defineHidden($stub, "prototype", $js->newObject());
		$this->stubs[$name] = $stub;
		return $stub;
	}

	private function api(string $method, mixed ...$args) : mixed{
		return $this->toJs($this->runtime->api->handle($method, $args));
	}

	private function raw(string $method, mixed ...$args) : mixed{
		return $this->runtime->api->handle($method, $args);
	}

	public static function namespaced(string $id) : string{
		$id = strtolower($id);
		return str_contains($id, ":") ? $id : "minecraft:" . $id;
	}

	private function string(mixed $value) : string{
		return $this->js->toString($value);
	}

	/**
	 * Flattens a string, a RawMessage or an array of them.
	 */
	public function text(mixed $message) : string{
		if($message === null || $message instanceof JsNull){
			return "";
		}
		if(is_string($message)){
			return $message;
		}
		if($message instanceof JsArray){
			$result = "";
			foreach($message->items as $item){
				$result .= $this->text($item);
			}
			return $result;
		}
		if($message instanceof JsObject && !$message instanceof JsCallable){
			$result = "";
			$text = $this->js->get($message, "text");
			if(is_string($text)){
				$result .= $text;
			}
			$translate = $this->js->get($message, "translate");
			if(is_string($translate)){
				$result .= "%" . $translate;
			}
			$raw = $this->js->get($message, "rawtext");
			if($raw instanceof JsArray){
				$result .= $this->text($raw);
			}
			return $result;
		}
		return $this->js->toString($message);
	}

	/**
	 * @return array{x: float, y: float, z: float}
	 */
	public function vector(mixed $value) : array{
		if(!$value instanceof JsObject){
			$this->js->throwError("TypeError", "Expected a Vector3");
		}
		return [
			"x" => (float) $this->js->toNumber($this->js->get($value, "x")),
			"y" => (float) $this->js->toNumber($this->js->get($value, "y")),
			"z" => (float) $this->js->toNumber($this->js->get($value, "z"))
		];
	}

	public function toJs(mixed $value) : mixed{
		if($value === null || is_bool($value) || is_int($value) || is_float($value) || is_string($value) || $value instanceof JsObject || $value instanceof JsNull || $value instanceof JsSymbol){
			return $value;
		}
		if($value instanceof stdClass){
			$object = $this->js->newObject();
			foreach(get_object_vars($value) as $key => $item){
				$object->props[$key] = $this->toJs($item);
			}
			return $object;
		}
		if(!is_array($value)){
			return null;
		}
		if(isset($value['$e'])){
			return $this->entityObject($value);
		}
		if(isset($value['$b'])){
			return $this->blockObject((string) $value["d"], (int) $value['$b'][0], (int) $value['$b'][1], (int) $value['$b'][2]);
		}
		if(array_key_exists('$p', $value)){
			return $this->permutationObject((string) $value['$p'], is_array($value["s"] ?? null) ? $value["s"] : []);
		}
		if(isset($value['$i'])){
			return $this->itemObject($value['$i']);
		}
		if(isset($value['$d'])){
			return $this->dimensionObject((string) $value['$d']);
		}
		if(array_is_list($value)){
			$items = [];
			foreach($value as $item){
				$items[] = $this->toJs($item);
			}
			return $this->js->newArray($items);
		}
		$object = $this->js->newObject();
		foreach($value as $key => $item){
			$object->props[$key] = $this->toJs($item);
		}
		return $object;
	}

	public function fromJs(mixed $value, int $depth = 0) : mixed{
		if($value === null || $value instanceof JsNull || $value instanceof JsSymbol || $value instanceof JsCallable){
			return null;
		}
		if(!$value instanceof JsObject){
			return $value;
		}
		if(is_array($value->host) && isset($value->host["kind"])){
			$host = $value->host;
			switch($host["kind"]){
				case "entity":
					return ['$e' => $host["id"]];
				case "block":
					return ['$b' => [$host["x"], $host["y"], $host["z"]], "d" => $host["d"]];
				case "permutation":
					return ['$p' => $host["type"], "s" => $host["states"]];
				case "item":
					return ['$i' => $this->itemData($host)];
				case "dimension":
					return ['$d' => $host["id"]];
				case "container":
					return ['$c' => $host["ref"]];
				case "type":
					return $host["id"];
				case "effecttype":
					return $host["id"];
			}
		}
		if($depth > 16){
			return null;
		}
		if($value instanceof JsArray){
			$items = [];
			foreach($value->items as $item){
				$items[] = $this->fromJs($item, $depth + 1);
			}
			return $items;
		}
		$result = [];
		foreach($this->js->ownKeys($value) as $key){
			$result[$key] = $this->fromJs($this->js->getFrom($value, $key, $value), $depth + 1);
		}
		return $result;
	}

	/**
	 * @param array<string, mixed> $ref
	 */
	public function entityObject(array $ref) : JsObject{
		$id = (int) $ref['$e'];
		$typeId = (string) ($ref["t"] ?? "minecraft:unknown");
		$existing = $this->entities[$id] ?? null;
		if($existing !== null && $existing->host["typeId"] === $typeId){
			return $existing;
		}
		if(count($this->entities) > 20000){
			$this->entities = [];
		}
		$isPlayer = $typeId === "minecraft:player";
		$entity = $this->f->instance($isPlayer ? $this->player : $this->entity, [
			"kind" => "entity",
			"id" => $id,
			"typeId" => $typeId,
			"name" => (string) ($ref["n"] ?? "")
		]);
		$this->entities[$id] = $entity;
		return $entity;
	}

	public function forgetEntity(int $id) : void{
		unset($this->entities[$id]);
	}

	public function dimensionObject(string $id) : JsObject{
		$key = str_contains($id, ":") || !in_array($id, ["overworld", "nether", "the_end"], true) ? $id : "minecraft:" . $id;
		return $this->dimensions[$key] ??= $this->f->instance($this->dimension, ["kind" => "dimension", "id" => $key]);
	}

	public function blockObject(string $dimension, int $x, int $y, int $z) : JsObject{
		return $this->f->instance($this->block, ["kind" => "block", "d" => $dimension, "x" => $x, "y" => $y, "z" => $z]);
	}

	/**
	 * @param array<string, mixed> $states
	 */
	public function permutationObject(string $type, array $states) : JsObject{
		return $this->f->instance($this->permutation, ["kind" => "permutation", "type" => $type, "states" => $states]);
	}

	/**
	 * @param array<string, mixed> $data
	 */
	public function itemObject(array $data) : JsObject{
		return $this->f->instance($this->itemStack, [
			"kind" => "item",
			"t" => (string) ($data["t"] ?? "minecraft:air"),
			"c" => (int) ($data["c"] ?? 1),
			"m" => (int) ($data["m"] ?? 64),
			"n" => isset($data["n"]) && is_string($data["n"]) ? $data["n"] : null,
			"l" => is_array($data["l"] ?? null) ? $data["l"] : [],
			"d" => isset($data["d"]) && is_int($data["d"]) ? $data["d"] : null,
			"md" => (int) ($data["md"] ?? 0),
			"nbt" => isset($data["nbt"]) && is_string($data["nbt"]) ? $data["nbt"] : null
		]);
	}

	/**
	 * @param array<string, mixed> $host
	 * @return array<string, mixed>
	 */
	private function itemData(array $host) : array{
		$data = ["t" => $host["t"], "c" => $host["c"], "l" => $host["l"]];
		if($host["n"] !== null){
			$data["n"] = $host["n"];
		}
		if($host["d"] !== null){
			$data["d"] = $host["d"];
		}
		if($host["nbt"] !== null){
			$data["nbt"] = $host["nbt"];
		}
		return $data;
	}

	private function entityId(mixed $thisValue) : int{
		return $this->f->host($thisValue, "entity")["id"];
	}

	private function typeName(mixed $value) : string{
		if($value instanceof JsObject && is_array($value->host) && isset($value->host["id"]) && in_array($value->host["kind"] ?? null, ["type", "effecttype"], true)){
			return $value->host["id"];
		}
		return $this->string($value);
	}

	private function defineTypes() : void{
		$f = $this->f;
		$typeGetter = function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "type")["id"];
		};
		$this->blockType = $f->define("BlockType");
		$f->getter($this->blockType, "id", $typeGetter);
		$this->itemType = $f->define("ItemType");
		$f->getter($this->itemType, "id", $typeGetter);
		$this->entityType = $f->define("EntityType");
		$f->getter($this->entityType, "id", $typeGetter);
		$this->effectType = $f->define("EffectType");
		$f->method($this->effectType, "getName", function(mixed $thisValue, array $args) : mixed{
			return $this->f->host($thisValue, "effecttype")["id"];
		});
		foreach(["BlockType" => $this->blockType, "ItemType" => $this->itemType, "EntityType" => $this->entityType, "EffectType" => $this->effectType] as $name => $class){
			$this->export($name, $class);
		}

		$blockTypes = $f->define("BlockTypes");
		$f->staticMethod($blockTypes, "get", function(mixed $thisValue, array $args) : mixed{
			return $this->f->instance($this->blockType, ["kind" => "type", "id" => self::namespaced($this->string($args[0] ?? ""))]);
		});
		$this->export("BlockTypes", $blockTypes);
		$itemTypes = $f->define("ItemTypes");
		$f->staticMethod($itemTypes, "get", function(mixed $thisValue, array $args) : mixed{
			$id = self::namespaced($this->string($args[0] ?? ""));
			try{
				$info = $this->raw("item.info", $id);
			}catch(ScriptException){
				return null;
			}
			return $this->f->instance($this->itemType, ["kind" => "type", "id" => (string) ($info["t"] ?? $id)]);
		});
		$this->export("ItemTypes", $itemTypes);
		$entityTypes = $f->define("EntityTypes");
		$f->staticMethod($entityTypes, "get", function(mixed $thisValue, array $args) : mixed{
			return $this->f->instance($this->entityType, ["kind" => "type", "id" => self::namespaced($this->string($args[0] ?? ""))]);
		});
		$this->export("EntityTypes", $entityTypes);
		$effectTypes = $f->define("EffectTypes");
		$f->staticMethod($effectTypes, "get", function(mixed $thisValue, array $args) : mixed{
			$name = $this->string($args[0] ?? "");
			return $this->f->instance($this->effectType, ["kind" => "effecttype", "id" => str_contains($name, ":") ? substr($name, (int) \strpos($name, ":") + 1) : $name]);
		});
		$this->export("EffectTypes", $effectTypes);

		$this->effect = $f->define("Effect");
		foreach(["typeId" => "t", "duration" => "d", "amplifier" => "a", "displayName" => "n"] as $name => $key){
			$f->getter($this->effect, $name, function(mixed $thisValue) use ($key) : mixed{
				return $this->f->host($thisValue, "effect")[$key];
			});
		}
		$f->getter($this->effect, "isValid", function(mixed $thisValue) : mixed{
			$host = $this->f->host($thisValue, "effect");
			return $this->raw("ent.valid", $host["entity"]) === true && $this->raw("ent.effect.get", $host["entity"], $host["t"]) !== null;
		});
		$this->export("Effect", $this->effect);

		$this->permutation = $f->define("BlockPermutation");
		$f->staticMethod($this->permutation, "resolve", function(mixed $thisValue, array $args) : mixed{
			$states = $args[1] ?? null;
			return $this->api("block.resolve", self::namespaced($this->string($args[0] ?? "")), $states instanceof JsObject ? $this->fromJs($states) : []);
		}, 2);
		$f->getter($this->permutation, "type", function(mixed $thisValue) : mixed{
			return $this->f->instance($this->blockType, ["kind" => "type", "id" => $this->f->host($thisValue, "permutation")["type"]]);
		});
		$f->method($this->permutation, "getState", function(mixed $thisValue, array $args) : mixed{
			return $this->f->host($thisValue, "permutation")["states"][$this->string($args[0] ?? "")] ?? null;
		});
		$f->method($this->permutation, "getAllStates", function(mixed $thisValue, array $args) : mixed{
			$object = $this->js->newObject();
			foreach($this->f->host($thisValue, "permutation")["states"] as $key => $value){
				$object->props[$key] = $value;
			}
			return $object;
		});
		$f->method($this->permutation, "withState", function(mixed $thisValue, array $args) : mixed{
			$host = $this->f->host($thisValue, "permutation");
			$states = $host["states"];
			$states[$this->string($args[0] ?? "")] = $this->fromJs($args[1] ?? null);
			return $this->api("block.resolve", $host["type"], $states);
		}, 2);
		$f->method($this->permutation, "matches", function(mixed $thisValue, array $args) : mixed{
			$host = $this->f->host($thisValue, "permutation");
			return $this->matchesPermutation($host["type"], $host["states"], $args[0] ?? null, $args[1] ?? null);
		}, 2);
		$f->method($this->permutation, "getItemStack", function(mixed $thisValue, array $args) : mixed{
			return $this->api("block.item", $this->fromJs($thisValue), $args[0] ?? 1);
		});
		$this->missingMethods($this->permutation, ["getTags" => [], "hasTag" => false]);
		$this->export("BlockPermutation", $this->permutation);
	}

	/**
	 * @param array<string, mixed> $states
	 */
	private function matchesPermutation(string $type, array $states, mixed $name, mixed $expected) : bool{
		if(self::namespaced($this->string($name)) !== $type){
			return false;
		}
		if($expected instanceof JsObject){
			foreach($this->js->ownKeys($expected) as $key){
				$value = $this->fromJs($this->js->getFrom($expected, $key, $expected));
				if(($states[$key] ?? null) !== $value && !(is_numeric($value) && is_numeric($states[$key] ?? null) && $value == $states[$key])){
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * Defines methods that are not implemented: they are recorded and return
	 * the given default.
	 *
	 * @param array<string, mixed> $methods
	 */
	private function missingMethods(HostClass $class, array $methods) : void{
		foreach($methods as $name => $default){
			$label = $class->name . "." . $name;
			$this->f->method($class, $name, function(mixed $thisValue, array $args) use ($label, $default) : mixed{
				$this->runtime->recordMissing($label);
				return is_array($default) ? $this->js->newArray($default) : $default;
			});
		}
	}

	private function defineItems() : void{
		$f = $this->f;
		$js = $this->js;
		$this->itemStack = $f->define("ItemStack", null, function(array $args, JsObject $newTarget) use ($js) : JsObject{
			$type = $args[0] ?? null;
			$amount = $args[1] ?? 1;
			$id = $this->typeName($type);
			if(!is_int($amount) || $amount < 1 || $amount > 255){
				$js->throwError("RangeError", "Amount must be an integer between 1 and 255");
			}
			$info = $this->raw("item.info", self::namespaced($id));
			$item = $this->itemObject(["t" => $info["t"], "c" => $amount, "m" => $info["m"], "l" => [], "d" => $info["md"] > 0 ? 0 : null, "md" => $info["md"]]);
			$item->proto = $js->prototypeFor($newTarget, $this->itemStack->prototype);
			return $item;
		}, 2);
		$item = $this->itemStack;
		$f->getter($item, "typeId", function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "item")["t"];
		});
		$f->getter($item, "type", function(mixed $thisValue) : mixed{
			return $this->f->instance($this->itemType, ["kind" => "type", "id" => $this->f->host($thisValue, "item")["t"]]);
		});
		$f->getter($item, "amount", function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "item")["c"];
		}, function(mixed $thisValue, mixed $value) use ($js) : void{
			$this->f->host($thisValue, "item");
			if(!is_int($value) || $value < 1 || $value > 255){
				$js->throwError("RangeError", "Amount must be an integer between 1 and 255");
			}
			$thisValue->host["c"] = $value;
		});
		$f->getter($item, "maxAmount", function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "item")["m"];
		});
		$f->getter($item, "isStackable", function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "item")["m"] > 1;
		});
		$f->getter($item, "nameTag", function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "item")["n"];
		}, function(mixed $thisValue, mixed $value) : void{
			$this->f->host($thisValue, "item");
			$thisValue->host["n"] = ($value === null || $value === "") ? null : $this->string($value);
		});
		$f->getter($item, "keepOnDeath", function(mixed $thisValue) : mixed{
			return false;
		});
		$f->getter($item, "lockMode", function(mixed $thisValue) : mixed{
			return "none";
		});
		$f->method($item, "getLore", function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->newArray($this->f->host($thisValue, "item")["l"]);
		});
		$f->method($item, "setLore", function(mixed $thisValue, array $args) : mixed{
			$this->f->host($thisValue, "item");
			$lore = [];
			$list = $args[0] ?? null;
			if($list instanceof JsArray){
				foreach($list->items as $line){
					$lore[] = $this->text($line);
				}
			}
			$thisValue->host["l"] = $lore;
			return null;
		}, 1);
		$f->method($item, "clone", function(mixed $thisValue, array $args) : mixed{
			$host = $this->f->host($thisValue, "item");
			return $this->f->instance($this->itemStack, $host);
		});
		$f->method($item, "matches", function(mixed $thisValue, array $args) : mixed{
			return self::namespaced($this->string($args[0] ?? "")) === $this->f->host($thisValue, "item")["t"];
		}, 1);
		$f->method($item, "isStackableWith", function(mixed $thisValue, array $args) : mixed{
			$a = $this->f->host($thisValue, "item");
			$other = $args[0] ?? null;
			if(!$other instanceof JsObject || ($other->host["kind"] ?? null) !== "item"){
				return false;
			}
			$b = $other->host;
			return $a["m"] > 1 && $a["t"] === $b["t"] && $a["n"] === $b["n"] && $a["d"] === $b["d"] && $a["l"] === $b["l"];
		}, 1);
		$f->method($item, "getComponent", function(mixed $thisValue, array $args) : mixed{
			$host = $this->f->host($thisValue, "item");
			$id = self::namespaced($this->string($args[0] ?? ""));
			if($id === "minecraft:durability"){
				return $host["md"] > 0 ? $this->f->instance($this->durability, ["kind" => "component", "owner" => $thisValue, "typeId" => $id]) : null;
			}
			$this->runtime->recordMissing("ItemStack.getComponent(" . $id . ")");
			return null;
		}, 1);
		$f->method($item, "hasComponent", function(mixed $thisValue, array $args) : mixed{
			$host = $this->f->host($thisValue, "item");
			return self::namespaced($this->string($args[0] ?? "")) === "minecraft:durability" && $host["md"] > 0;
		}, 1);
		$f->method($item, "getComponents", function(mixed $thisValue, array $args) use ($js) : mixed{
			$host = $this->f->host($thisValue, "item");
			if($host["md"] <= 0){
				return $js->newArray();
			}
			return $js->newArray([$this->f->instance($this->durability, ["kind" => "component", "owner" => $thisValue, "typeId" => "minecraft:durability"])]);
		});
		$this->missingMethods($item, ["getTags" => [], "hasTag" => false, "getDynamicProperty" => null, "setDynamicProperty" => null, "getCanDestroy" => [], "getCanPlaceOn" => []]);
		$this->export("ItemStack", $item);

		$this->component = $f->define("Component");
		$f->getter($this->component, "typeId", function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "component")["typeId"];
		});
		$f->getter($this->component, "isValid", function(mixed $thisValue) : mixed{
			$owner = $this->f->host($thisValue, "component")["owner"];
			if($owner instanceof JsObject && ($owner->host["kind"] ?? null) === "entity"){
				return $this->raw("ent.valid", $owner->host["id"]) === true;
			}
			return true;
		});
		$this->export("Component", $this->component);
		$this->itemComponent = $f->define("ItemComponent", $this->component);
		$this->export("ItemComponent", $this->itemComponent);
		$this->durability = $f->define("ItemDurabilityComponent", $this->itemComponent);
		$f->staticValue($this->durability, "componentId", "minecraft:durability");
		$f->getter($this->durability, "damage", function(mixed $thisValue) : mixed{
			$owner = $this->f->host($thisValue, "component")["owner"];
			return $owner->host["d"] ?? 0;
		}, function(mixed $thisValue, mixed $value) : void{
			$owner = $this->f->host($thisValue, "component")["owner"];
			$owner->host["d"] = (int) max(0, min($owner->host["md"], floor((float) $this->js->toNumber($value))));
		});
		$f->getter($this->durability, "maxDurability", function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "component")["owner"]->host["md"];
		});
		$f->method($this->durability, "getDamageChance", function(mixed $thisValue, array $args) : mixed{
			$level = (int) $this->js->toNumber($args[0] ?? 0);
			return Interpreter::intOrFloat(100 / ($level + 1));
		});
		$f->method($this->durability, "getDamageChanceRange", function(mixed $thisValue, array $args) : mixed{
			return $this->toJs(["min" => 0, "max" => 100]);
		});
		$this->export("ItemDurabilityComponent", $this->durability);
	}

	private function defineContainers() : void{
		$f = $this->f;
		$js = $this->js;
		$this->container = $f->define("Container");
		$container = $this->container;
		$ref = function(mixed $thisValue) : array{
			return $this->f->host($thisValue, "container")["ref"];
		};
		$f->getter($container, "isValid", function(mixed $thisValue) use ($ref) : mixed{
			return $this->raw("cont.valid", ['$c' => $ref($thisValue)]);
		});
		$f->getter($container, "size", function(mixed $thisValue) use ($ref) : mixed{
			return $this->raw("cont.size", ['$c' => $ref($thisValue)]);
		});
		$f->getter($container, "emptySlotsCount", function(mixed $thisValue) use ($ref) : mixed{
			return $this->raw("cont.empty", ['$c' => $ref($thisValue)]);
		});
		$f->method($container, "getItem", function(mixed $thisValue, array $args) use ($ref) : mixed{
			return $this->api("cont.get", ['$c' => $ref($thisValue)], $args[0] ?? 0);
		}, 1);
		$f->method($container, "setItem", function(mixed $thisValue, array $args) use ($ref) : mixed{
			$this->raw("cont.set", ['$c' => $ref($thisValue)], $args[0] ?? 0, $this->fromJs($args[1] ?? null));
			return null;
		}, 2);
		$f->method($container, "addItem", function(mixed $thisValue, array $args) use ($ref) : mixed{
			return $this->api("cont.add", ['$c' => $ref($thisValue)], $this->fromJs($args[0] ?? null));
		}, 1);
		$f->method($container, "clearAll", function(mixed $thisValue, array $args) use ($ref) : mixed{
			$this->raw("cont.clear", ['$c' => $ref($thisValue)]);
			return null;
		});
		$f->method($container, "moveItem", function(mixed $thisValue, array $args) use ($ref, $js) : mixed{
			$item = $this->raw("cont.get", ['$c' => $ref($thisValue)], $args[0] ?? 0);
			if($item === null){
				return null;
			}
			$this->raw("cont.set", ['$c' => $ref($args[2] ?? null)], $args[1] ?? 0, $item);
			$this->raw("cont.set", ['$c' => $ref($thisValue)], $args[0] ?? 0, null);
			return null;
		}, 3);
		$f->method($container, "swapItems", function(mixed $thisValue, array $args) use ($ref) : mixed{
			$first = $this->raw("cont.get", ['$c' => $ref($thisValue)], $args[0] ?? 0);
			$second = $this->raw("cont.get", ['$c' => $ref($args[2] ?? null)], $args[1] ?? 0);
			$this->raw("cont.set", ['$c' => $ref($thisValue)], $args[0] ?? 0, $second);
			$this->raw("cont.set", ['$c' => $ref($args[2] ?? null)], $args[1] ?? 0, $first);
			return null;
		}, 3);
		$f->method($container, "transferItem", function(mixed $thisValue, array $args) use ($ref) : mixed{
			$item = $this->raw("cont.get", ['$c' => $ref($thisValue)], $args[0] ?? 0);
			if($item === null){
				return null;
			}
			$leftover = $this->raw("cont.add", ['$c' => $ref($args[1] ?? null)], $item);
			$this->raw("cont.set", ['$c' => $ref($thisValue)], $args[0] ?? 0, $leftover);
			return $this->toJs($leftover);
		}, 2);
		$this->missingMethods($container, ["getSlot" => null, "contains" => false, "find" => null, "firstEmptySlot" => null, "firstItem" => null]);
		$this->export("Container", $container);
	}

	private function entityDynamicProperties(HostClass $class, \Closure $owner) : void{
		$f = $this->f;
		$f->method($class, "getDynamicProperty", function(mixed $thisValue, array $args) use ($owner) : mixed{
			return $this->api("dp.get", $owner($thisValue), $this->string($args[0] ?? ""));
		}, 1);
		$f->method($class, "setDynamicProperty", function(mixed $thisValue, array $args) use ($owner) : mixed{
			$value = $args[1] ?? null;
			if(!($value === null || is_bool($value) || is_int($value) || is_float($value) || is_string($value) || $value instanceof JsObject)){
				$this->js->throwError("TypeError", "Dynamic property values must be boolean, number, string or Vector3");
			}
			$stored = $value instanceof JsObject ? $this->vector($value) : $value;
			$this->raw("dp.set", $owner($thisValue), $this->string($args[0] ?? ""), $stored);
			return null;
		}, 2);
		$f->method($class, "getDynamicPropertyIds", function(mixed $thisValue, array $args) use ($owner) : mixed{
			return $this->api("dp.ids", $owner($thisValue));
		});
		$f->method($class, "clearDynamicProperties", function(mixed $thisValue, array $args) use ($owner) : mixed{
			$this->raw("dp.clear", $owner($thisValue));
			return null;
		});
		$f->method($class, "getDynamicPropertyTotalByteCount", function(mixed $thisValue, array $args) use ($owner) : mixed{
			$total = 0;
			foreach($this->raw("dp.ids", $owner($thisValue)) as $id){
				$total += strlen((string) $id) + strlen((string) json_encode($this->raw("dp.get", $owner($thisValue), $id), JSON_PARTIAL_OUTPUT_ON_ERROR));
			}
			return $total;
		});
	}

	private function defineEntities() : void{
		$f = $this->f;
		$js = $this->js;

		$this->entityComponent = $f->define("EntityComponent", $this->component);
		$f->getter($this->entityComponent, "entity", function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "component")["owner"];
		});
		$this->export("EntityComponent", $this->entityComponent);
		$this->attributeComponent = $f->define("EntityAttributeComponent", $this->entityComponent);
		$this->export("EntityAttributeComponent", $this->attributeComponent);
		$this->health = $f->define("EntityHealthComponent", $this->attributeComponent);
		$f->staticValue($this->health, "componentId", "minecraft:health");
		$ownerId = function(mixed $thisValue) : int{
			return $this->f->host($thisValue, "component")["owner"]->host["id"];
		};
		$f->getter($this->health, "currentValue", function(mixed $thisValue) use ($ownerId) : mixed{
			return Interpreter::intOrFloat((float) $this->raw("ent.health", $ownerId($thisValue))["c"]);
		});
		$f->getter($this->health, "effectiveMax", function(mixed $thisValue) use ($ownerId) : mixed{
			return Interpreter::intOrFloat((float) $this->raw("ent.health", $ownerId($thisValue))["m"]);
		});
		$f->getter($this->health, "defaultValue", function(mixed $thisValue) use ($ownerId) : mixed{
			return Interpreter::intOrFloat((float) $this->raw("ent.health", $ownerId($thisValue))["m"]);
		});
		$f->getter($this->health, "effectiveMin", function(mixed $thisValue) : mixed{
			return 0;
		});
		$f->method($this->health, "setCurrentValue", function(mixed $thisValue, array $args) use ($ownerId) : mixed{
			return $this->raw("ent.setHealth", $ownerId($thisValue), $this->js->toNumber($args[0] ?? 0));
		}, 1);
		foreach(["resetToDefaultValue", "resetToMaxValue"] as $name){
			$f->method($this->health, $name, function(mixed $thisValue, array $args) use ($ownerId) : mixed{
				$this->raw("ent.setHealth", $ownerId($thisValue), $this->raw("ent.health", $ownerId($thisValue))["m"]);
				return null;
			});
		}
		$f->method($this->health, "resetToMinValue", function(mixed $thisValue, array $args) use ($ownerId) : mixed{
			$this->raw("ent.setHealth", $ownerId($thisValue), 0);
			return null;
		});
		$this->export("EntityHealthComponent", $this->health);

		$this->inventory = $f->define("EntityInventoryComponent", $this->entityComponent);
		$f->staticValue($this->inventory, "componentId", "minecraft:inventory");
		$f->getter($this->inventory, "container", function(mixed $thisValue) use ($ownerId) : mixed{
			return $this->f->instance($this->container, ["kind" => "container", "ref" => ["e" => $ownerId($thisValue)]]);
		});
		$f->getter($this->inventory, "inventorySize", function(mixed $thisValue) use ($ownerId) : mixed{
			return $this->raw("cont.size", ['$c' => ["e" => $ownerId($thisValue)]]);
		});
		foreach(["containerType" => "inventory", "canBeSiphonedFrom" => false, "private" => false, "restrictToOwner" => false, "additionalSlotsPerStrength" => 0] as $name => $value){
			$f->getter($this->inventory, $name, function(mixed $thisValue) use ($value) : mixed{
				return $value;
			});
		}
		$this->export("EntityInventoryComponent", $this->inventory);

		$this->equippable = $f->define("EntityEquippableComponent", $this->entityComponent);
		$f->staticValue($this->equippable, "componentId", "minecraft:equippable");
		$f->method($this->equippable, "getEquipment", function(mixed $thisValue, array $args) use ($ownerId) : mixed{
			return $this->api("ent.equip.get", $ownerId($thisValue), $this->string($args[0] ?? ""));
		}, 1);
		$f->method($this->equippable, "setEquipment", function(mixed $thisValue, array $args) use ($ownerId) : mixed{
			return $this->raw("ent.equip.set", $ownerId($thisValue), $this->string($args[0] ?? ""), $this->fromJs($args[1] ?? null));
		}, 2);
		$this->missingMethods($this->equippable, ["getEquipmentSlot" => null]);
		$this->export("EntityEquippableComponent", $this->equippable);

		$components = [
			"minecraft:health" => $this->health,
			"minecraft:inventory" => $this->inventory,
			"minecraft:equippable" => $this->equippable
		];

		$this->entity = $f->define("Entity");
		$entity = $this->entity;
		$f->getter($entity, "id", function(mixed $thisValue) : mixed{
			return (string) $this->entityId($thisValue);
		});
		$f->getter($entity, "typeId", function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "entity")["typeId"];
		});
		$f->getter($entity, "isValid", function(mixed $thisValue) : mixed{
			return $this->raw("ent.valid", $this->entityId($thisValue));
		});
		$f->getter($entity, "location", function(mixed $thisValue) : mixed{
			return $this->api("ent.get", $this->entityId($thisValue), "location");
		});
		$f->getter($entity, "dimension", function(mixed $thisValue) : mixed{
			return $this->dimensionObject((string) $this->raw("ent.get", $this->entityId($thisValue), "dimension"));
		});
		$f->getter($entity, "nameTag", function(mixed $thisValue) : mixed{
			return $this->raw("ent.get", $this->entityId($thisValue), "nameTag");
		}, function(mixed $thisValue, mixed $value) : void{
			$this->raw("ent.set", $this->entityId($thisValue), "nameTag", $this->string($value));
		});
		$f->getter($entity, "isSneaking", function(mixed $thisValue) : mixed{
			return $this->raw("ent.get", $this->entityId($thisValue), "sneaking");
		}, function(mixed $thisValue, mixed $value) : void{
			$this->raw("ent.set", $this->entityId($thisValue), "sneaking", $this->js->toBoolean($value));
		});
		foreach(["isOnGround" => "onGround", "isSprinting" => "sprinting", "isSwimming" => "swimming", "isGliding" => "gliding", "isInWater" => "underwater"] as $name => $field){
			$f->getter($entity, $name, function(mixed $thisValue) use ($field) : mixed{
				return $this->raw("ent.get", $this->entityId($thisValue), $field);
			});
		}
		foreach(["getRotation" => "rotation", "getVelocity" => "velocity", "getHeadLocation" => "head", "getViewDirection" => "view"] as $name => $field){
			$f->method($entity, $name, function(mixed $thisValue, array $args) use ($field) : mixed{
				return $this->api("ent.get", $this->entityId($thisValue), $field);
			});
		}
		$f->method($entity, "setRotation", function(mixed $thisValue, array $args) : mixed{
			$id = $this->entityId($thisValue);
			$this->raw("ent.teleport", $id, $this->raw("ent.get", $id, "location"), ["rotation" => $this->fromJs($args[0] ?? null)]);
			return null;
		}, 1);
		$f->method($entity, "teleport", function(mixed $thisValue, array $args) : mixed{
			$this->raw("ent.teleport", $this->entityId($thisValue), $this->vector($args[0] ?? null), $this->fromJs($args[1] ?? null) ?? []);
			return null;
		}, 2);
		$f->method($entity, "tryTeleport", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("ent.teleport", $this->entityId($thisValue), $this->vector($args[0] ?? null), $this->fromJs($args[1] ?? null) ?? []);
		}, 2);
		$f->method($entity, "kill", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("ent.kill", $this->entityId($thisValue));
		});
		$f->method($entity, "remove", function(mixed $thisValue, array $args) : mixed{
			$this->raw("ent.remove", $this->entityId($thisValue));
			return null;
		});
		$f->method($entity, "triggerEvent", function(mixed $thisValue, array $args) : mixed{
			$this->raw("ent.trigger", $this->entityId($thisValue), $this->string($args[0] ?? ""));
			return null;
		}, 1);
		$f->method($entity, "applyDamage", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("ent.damage", $this->entityId($thisValue), $this->js->toNumber($args[0] ?? 0), $this->fromJs($args[1] ?? null) ?? []);
		}, 2);
		$f->method($entity, "applyImpulse", function(mixed $thisValue, array $args) : mixed{
			$this->raw("ent.impulse", $this->entityId($thisValue), $this->vector($args[0] ?? null));
			return null;
		}, 1);
		$f->method($entity, "applyKnockback", function(mixed $thisValue, array $args) : mixed{
			$first = $args[0] ?? null;
			if($first instanceof JsObject){
				$velocity = [
					"x" => (float) $this->js->toNumber($this->js->get($first, "x")),
					"y" => (float) $this->js->toNumber($args[1] ?? 0),
					"z" => (float) $this->js->toNumber($this->js->get($first, "z"))
				];
			}else{
				$strength = (float) $this->js->toNumber($args[2] ?? 0);
				$velocity = [
					"x" => (float) $this->js->toNumber($first) * $strength,
					"y" => (float) $this->js->toNumber($args[3] ?? 0),
					"z" => (float) $this->js->toNumber($args[1] ?? 0) * $strength
				];
			}
			$this->raw("ent.setVelocity", $this->entityId($thisValue), $velocity);
			return null;
		}, 2);
		$f->method($entity, "clearVelocity", function(mixed $thisValue, array $args) : mixed{
			$this->raw("ent.setVelocity", $this->entityId($thisValue), ["x" => 0.0, "y" => 0.0, "z" => 0.0]);
			return null;
		});
		$f->method($entity, "addEffect", function(mixed $thisValue, array $args) : mixed{
			$id = $this->entityId($thisValue);
			$type = $this->typeName($args[0] ?? null);
			$options = $args[2] ?? null;
			$amplifier = $options instanceof JsObject ? ($this->js->get($options, "amplifier") ?? 0) : 0;
			$particles = $options instanceof JsObject ? $this->js->get($options, "showParticles") : null;
			$added = $this->raw("ent.effect.add", $id, $type, $this->js->toNumber($args[1] ?? 1), $this->js->toNumber($amplifier), $particles !== false);
			if($added !== true){
				return null;
			}
			$data = $this->raw("ent.effect.get", $id, $type);
			return $data === null ? null : $this->effectObject($id, $data);
		}, 3);
		$f->method($entity, "removeEffect", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("ent.effect.remove", $this->entityId($thisValue), $this->typeName($args[0] ?? null));
		}, 1);
		$f->method($entity, "getEffect", function(mixed $thisValue, array $args) : mixed{
			$id = $this->entityId($thisValue);
			$data = $this->raw("ent.effect.get", $id, $this->typeName($args[0] ?? null));
			return $data === null ? null : $this->effectObject($id, $data);
		}, 1);
		$f->method($entity, "getEffects", function(mixed $thisValue, array $args) use ($js) : mixed{
			$id = $this->entityId($thisValue);
			$effects = [];
			foreach($this->raw("ent.effects", $id) as $data){
				$effects[] = $this->effectObject($id, $data);
			}
			return $js->newArray($effects);
		});
		$f->method($entity, "addTag", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("ent.addTag", $this->entityId($thisValue), $this->string($args[0] ?? ""));
		}, 1);
		$f->method($entity, "removeTag", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("ent.removeTag", $this->entityId($thisValue), $this->string($args[0] ?? ""));
		}, 1);
		$f->method($entity, "hasTag", function(mixed $thisValue, array $args) : mixed{
			return in_array($this->string($args[0] ?? ""), $this->raw("ent.tags", $this->entityId($thisValue)), true);
		}, 1);
		$f->method($entity, "getTags", function(mixed $thisValue, array $args) : mixed{
			return $this->api("ent.tags", $this->entityId($thisValue));
		});
		$f->method($entity, "getComponent", function(mixed $thisValue, array $args) use ($components) : mixed{
			$id = self::namespaced($this->string($args[0] ?? ""));
			$class = $components[$id] ?? null;
			if($class === null){
				$this->runtime->recordMissing("Entity.getComponent(" . $id . ")");
				return null;
			}
			if($this->raw("ent.comp", $this->entityId($thisValue), $id) !== true){
				return null;
			}
			return $this->f->instance($class, ["kind" => "component", "owner" => $thisValue, "typeId" => $id]);
		}, 1);
		$f->method($entity, "hasComponent", function(mixed $thisValue, array $args) use ($components) : mixed{
			$id = self::namespaced($this->string($args[0] ?? ""));
			return isset($components[$id]) && $this->raw("ent.comp", $this->entityId($thisValue), $id) === true;
		}, 1);
		$f->method($entity, "getComponents", function(mixed $thisValue, array $args) use ($components, $js) : mixed{
			$result = [];
			foreach($components as $id => $class){
				if($this->raw("ent.comp", $this->entityId($thisValue), $id) === true){
					$result[] = $this->f->instance($class, ["kind" => "component", "owner" => $thisValue, "typeId" => $id]);
				}
			}
			return $js->newArray($result);
		});
		$f->method($entity, "runCommand", function(mixed $thisValue, array $args) : mixed{
			return $this->toJs(["successCount" => $this->raw("ent.command", $this->entityId($thisValue), $this->string($args[0] ?? ""))]);
		}, 1);
		$f->method($entity, "setOnFire", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("ent.fire", $this->entityId($thisValue), $this->js->toNumber($args[0] ?? 0));
		}, 2);
		$f->method($entity, "extinguishFire", function(mixed $thisValue, array $args) : mixed{
			$this->raw("ent.extinguish", $this->entityId($thisValue));
			return true;
		});
		$this->entityDynamicProperties($entity, function(mixed $thisValue) : int{
			return $this->entityId($thisValue);
		});
		$this->export("Entity", $entity);

		$this->screenDisplay = $f->define("ScreenDisplay");
		$screenPlayer = function(mixed $thisValue) : int{
			return $this->f->host($thisValue, "screen")["player"];
		};
		$f->getter($this->screenDisplay, "isValid", function(mixed $thisValue) use ($screenPlayer) : mixed{
			return $this->raw("ent.valid", $screenPlayer($thisValue));
		});
		$f->method($this->screenDisplay, "setTitle", function(mixed $thisValue, array $args) use ($screenPlayer) : mixed{
			$options = $args[1] ?? null;
			$data = [];
			if($options instanceof JsObject){
				foreach(["fadeInDuration", "stayDuration", "fadeOutDuration"] as $key){
					$value = $this->js->get($options, $key);
					if($value !== null){
						$data[$key] = $this->js->toNumber($value);
					}
				}
				$subtitle = $this->js->get($options, "subtitle");
				if($subtitle !== null){
					$data["subtitle"] = $this->text($subtitle);
				}
			}
			$this->raw("pl.title", $screenPlayer($thisValue), $this->text($args[0] ?? ""), $data);
			return null;
		}, 2);
		$f->method($this->screenDisplay, "updateSubtitle", function(mixed $thisValue, array $args) use ($screenPlayer) : mixed{
			$this->raw("pl.title", $screenPlayer($thisValue), "", ["subtitle" => $this->text($args[0] ?? "")]);
			return null;
		}, 1);
		$f->method($this->screenDisplay, "setActionBar", function(mixed $thisValue, array $args) use ($screenPlayer) : mixed{
			$this->raw("pl.actionbar", $screenPlayer($thisValue), $this->text($args[0] ?? ""));
			return null;
		}, 1);
		$this->missingMethods($this->screenDisplay, ["clearTitle" => null, "resetHudElements" => null, "setHudVisibility" => null, "isForcedHidden" => false]);
		$this->export("ScreenDisplay", $this->screenDisplay);

		$this->player = $f->define("Player", $entity);
		$player = $this->player;
		$f->getter($player, "name", function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "entity")["name"];
		});
		$f->getter($player, "onScreenDisplay", function(mixed $thisValue) : mixed{
			return $this->f->instance($this->screenDisplay, ["kind" => "screen", "player" => $this->entityId($thisValue)]);
		});
		$f->getter($player, "level", function(mixed $thisValue) : mixed{
			return $this->raw("pl.xpInfo", $this->entityId($thisValue))["level"];
		});
		$f->getter($player, "xpEarnedAtCurrentLevel", function(mixed $thisValue) : mixed{
			return $this->raw("pl.xpInfo", $this->entityId($thisValue))["remainder"];
		});
		$f->getter($player, "totalXpNeededForNextLevel", function(mixed $thisValue) : mixed{
			$level = (int) $this->raw("pl.xpInfo", $this->entityId($thisValue))["level"];
			if($level >= 30){
				return 112 + ($level - 30) * 9;
			}
			if($level >= 15){
				return 37 + ($level - 15) * 5;
			}
			return 7 + $level * 2;
		});
		$f->getter($player, "selectedSlotIndex", function(mixed $thisValue) : mixed{
			return $this->raw("ent.get", $this->entityId($thisValue), "selectedSlot");
		}, function(mixed $thisValue, mixed $value) : void{
			$this->raw("pl.selectedSlot", $this->entityId($thisValue), $this->js->toNumber($value));
		});
		$f->getter($player, "isFlying", function(mixed $thisValue) : mixed{
			return $this->raw("ent.get", $this->entityId($thisValue), "flying");
		});
		$f->method($player, "getTotalXp", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("pl.xpInfo", $this->entityId($thisValue))["total"];
		});
		$f->method($player, "addExperience", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("pl.xp", $this->entityId($thisValue), $this->js->toNumber($args[0] ?? 0));
		}, 1);
		$f->method($player, "addLevels", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("pl.levels", $this->entityId($thisValue), $this->js->toNumber($args[0] ?? 0));
		}, 1);
		$f->method($player, "resetLevel", function(mixed $thisValue, array $args) : mixed{
			$id = $this->entityId($thisValue);
			$this->raw("pl.xp", $id, -$this->raw("pl.xpInfo", $id)["total"]);
			return null;
		});
		$f->method($player, "sendMessage", function(mixed $thisValue, array $args) : mixed{
			$this->raw("pl.message", $this->entityId($thisValue), $this->text($args[0] ?? ""));
			return null;
		}, 1);
		$f->method($player, "playSound", function(mixed $thisValue, array $args) : mixed{
			$this->raw("pl.sound", $this->entityId($thisValue), $this->string($args[0] ?? ""), $this->fromJs($args[1] ?? null) ?? []);
			return null;
		}, 2);
		$f->method($player, "getGameMode", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("pl.gamemode", $this->entityId($thisValue));
		});
		$f->method($player, "setGameMode", function(mixed $thisValue, array $args) : mixed{
			$mode = $args[0] ?? null;
			$this->raw("pl.setGamemode", $this->entityId($thisValue), $mode === null ? "Survival" : $this->string($mode));
			return null;
		}, 1);
		$f->method($player, "isOp", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("ent.get", $this->entityId($thisValue), "op");
		});
		$f->method($player, "getSpawnPoint", function(mixed $thisValue, array $args) : mixed{
			return $this->api("pl.spawnPoint", $this->entityId($thisValue));
		});
		$f->method($player, "setSpawnPoint", function(mixed $thisValue, array $args) : mixed{
			$this->raw("pl.setSpawnPoint", $this->entityId($thisValue), $this->fromJs($args[0] ?? null));
			return null;
		}, 1);
		$this->export("Player", $player);
	}

	/**
	 * @param array<string, mixed> $data
	 */
	private function effectObject(int $entity, array $data) : JsObject{
		return $this->f->instance($this->effect, [
			"kind" => "effect",
			"entity" => $entity,
			"t" => (string) ($data["t"] ?? ""),
			"d" => (int) ($data["d"] ?? 0),
			"a" => (int) ($data["a"] ?? 0),
			"n" => (string) ($data["n"] ?? "")
		]);
	}

	private function defineBlocks() : void{
		$f = $this->f;
		$js = $this->js;

		$this->blockComponent = $f->define("BlockComponent", $this->component);
		$f->getter($this->blockComponent, "block", function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "component")["owner"];
		});
		$this->export("BlockComponent", $this->blockComponent);
		$this->blockInventory = $f->define("BlockInventoryComponent", $this->blockComponent);
		$f->staticValue($this->blockInventory, "componentId", "minecraft:inventory");
		$f->getter($this->blockInventory, "container", function(mixed $thisValue) : mixed{
			$block = $this->f->host($thisValue, "component")["owner"]->host;
			return $this->f->instance($this->container, ["kind" => "container", "ref" => ["b" => [$block["d"], $block["x"], $block["y"], $block["z"]]]]);
		});
		$this->export("BlockInventoryComponent", $this->blockInventory);

		$this->block = $f->define("Block");
		$block = $this->block;
		$info = function(mixed $thisValue) use ($js) : array{
			$host = $this->f->host($thisValue, "block");
			$result = $this->raw("block.get", $host["d"], ["x" => $host["x"], "y" => $host["y"], "z" => $host["z"]]);
			if($result === null){
				$js->throwError("Error", "The block is in an unloaded chunk");
			}
			return $result;
		};
		$f->getter($block, "dimension", function(mixed $thisValue) : mixed{
			return $this->dimensionObject($this->f->host($thisValue, "block")["d"]);
		});
		foreach(["x", "y", "z"] as $axis){
			$f->getter($block, $axis, function(mixed $thisValue) use ($axis) : mixed{
				return $this->f->host($thisValue, "block")[$axis];
			});
		}
		$f->getter($block, "location", function(mixed $thisValue) : mixed{
			$host = $this->f->host($thisValue, "block");
			return $this->toJs(["x" => $host["x"], "y" => $host["y"], "z" => $host["z"]]);
		});
		$f->getter($block, "isValid", function(mixed $thisValue) : mixed{
			$host = $this->f->host($thisValue, "block");
			return $this->raw("block.get", $host["d"], ["x" => $host["x"], "y" => $host["y"], "z" => $host["z"]]) !== null;
		});
		$f->getter($block, "permutation", function(mixed $thisValue) use ($info) : mixed{
			return $this->toJs($info($thisValue)["p"]);
		});
		$f->getter($block, "typeId", function(mixed $thisValue) use ($info) : mixed{
			return $info($thisValue)["p"]['$p'];
		});
		$f->getter($block, "type", function(mixed $thisValue) use ($info) : mixed{
			return $this->f->instance($this->blockType, ["kind" => "type", "id" => $info($thisValue)["p"]['$p']]);
		});
		$f->getter($block, "isAir", function(mixed $thisValue) use ($info) : mixed{
			return $info($thisValue)["p"]['$p'] === "minecraft:air";
		});
		$f->getter($block, "isLiquid", function(mixed $thisValue) use ($info) : mixed{
			return $info($thisValue)["l"];
		});
		$f->getter($block, "isSolid", function(mixed $thisValue) use ($info) : mixed{
			return $info($thisValue)["s"];
		});
		$f->getter($block, "isWaterlogged", function(mixed $thisValue) : mixed{
			return false;
		});
		$location = function(mixed $thisValue) : array{
			$host = $this->f->host($thisValue, "block");
			return ["x" => $host["x"], "y" => $host["y"], "z" => $host["z"]];
		};
		$f->method($block, "setType", function(mixed $thisValue, array $args) use ($location) : mixed{
			$this->raw("dim.setBlock", $this->f->host($thisValue, "block")["d"], $location($thisValue), $this->typeName($args[0] ?? null));
			return null;
		}, 1);
		$f->method($block, "setPermutation", function(mixed $thisValue, array $args) use ($location) : mixed{
			$this->raw("dim.setBlock", $this->f->host($thisValue, "block")["d"], $location($thisValue), $this->fromJs($args[0] ?? null));
			return null;
		}, 1);
		$offset = function(mixed $thisValue, int $dx, int $dy, int $dz) : mixed{
			$host = $this->f->host($thisValue, "block");
			return $this->api("dim.block", $host["d"], ["x" => $host["x"] + $dx, "y" => $host["y"] + $dy, "z" => $host["z"] + $dz]);
		};
		$f->method($block, "offset", function(mixed $thisValue, array $args) use ($offset) : mixed{
			$vector = $this->vector($args[0] ?? null);
			return $offset($thisValue, (int) $vector["x"], (int) $vector["y"], (int) $vector["z"]);
		}, 1);
		foreach(["above" => [0, 1, 0], "below" => [0, -1, 0], "north" => [0, 0, -1], "south" => [0, 0, 1], "east" => [1, 0, 0], "west" => [-1, 0, 0]] as $name => [$dx, $dy, $dz]){
			$f->method($block, $name, function(mixed $thisValue, array $args) use ($offset, $dx, $dy, $dz) : mixed{
				$steps = ($args[0] ?? null) === null ? 1 : (int) $this->js->toNumber($args[0]);
				return $offset($thisValue, $dx * $steps, $dy * $steps, $dz * $steps);
			});
		}
		$f->method($block, "center", function(mixed $thisValue, array $args) : mixed{
			$host = $this->f->host($thisValue, "block");
			return $this->toJs(["x" => $host["x"] + 0.5, "y" => $host["y"] + 0.5, "z" => $host["z"] + 0.5]);
		});
		$f->method($block, "bottomCenter", function(mixed $thisValue, array $args) : mixed{
			$host = $this->f->host($thisValue, "block");
			return $this->toJs(["x" => $host["x"] + 0.5, "y" => $host["y"], "z" => $host["z"] + 0.5]);
		});
		$f->method($block, "matches", function(mixed $thisValue, array $args) use ($info) : mixed{
			$permutation = $info($thisValue)["p"];
			$states = $permutation["s"];
			return $this->matchesPermutation($permutation['$p'], is_array($states) ? $states : [], $args[0] ?? null, $args[1] ?? null);
		}, 2);
		$f->method($block, "getItemStack", function(mixed $thisValue, array $args) use ($info) : mixed{
			return $this->api("block.item", $info($thisValue)["p"], $args[0] ?? 1);
		});
		$f->method($block, "getComponent", function(mixed $thisValue, array $args) use ($info) : mixed{
			$id = self::namespaced($this->string($args[0] ?? ""));
			if($id !== "minecraft:inventory"){
				$this->runtime->recordMissing("Block.getComponent(" . $id . ")");
				return null;
			}
			return $info($thisValue)["i"] ? $this->f->instance($this->blockInventory, ["kind" => "component", "owner" => $thisValue, "typeId" => $id]) : null;
		}, 1);
		$this->missingMethods($block, ["getTags" => [], "hasTag" => false, "getRedstonePower" => null, "canPlace" => false]);
		$this->export("Block", $block);

		$this->dimension = $f->define("Dimension");
		$dimension = $this->dimension;
		$dimensionId = function(mixed $thisValue) : string{
			return $this->f->host($thisValue, "dimension")["id"];
		};
		$f->getter($dimension, "id", $dimensionId);
		$f->getter($dimension, "heightRange", function(mixed $thisValue) : mixed{
			return $this->toJs(["min" => -64, "max" => 320]);
		});
		$f->method($dimension, "getBlock", function(mixed $thisValue, array $args) use ($dimensionId) : mixed{
			return $this->api("dim.block", $dimensionId($thisValue), $this->vector($args[0] ?? null));
		}, 1);
		$f->method($dimension, "setBlockType", function(mixed $thisValue, array $args) use ($dimensionId) : mixed{
			$this->raw("dim.setBlock", $dimensionId($thisValue), $this->vector($args[0] ?? null), $this->typeName($args[1] ?? null));
			return null;
		}, 2);
		$f->method($dimension, "setBlockPermutation", function(mixed $thisValue, array $args) use ($dimensionId) : mixed{
			$this->raw("dim.setBlock", $dimensionId($thisValue), $this->vector($args[0] ?? null), $this->fromJs($args[1] ?? null));
			return null;
		}, 2);
		$f->method($dimension, "getEntities", function(mixed $thisValue, array $args) use ($dimensionId) : mixed{
			return $this->api("dim.entities", $dimensionId($thisValue), $this->queryOptions($args[0] ?? null));
		}, 1);
		$f->method($dimension, "getEntitiesAtBlockLocation", function(mixed $thisValue, array $args) use ($dimensionId) : mixed{
			return $this->api("dim.entities", $dimensionId($thisValue), ["_block" => $this->vector($args[0] ?? null)]);
		}, 1);
		$f->method($dimension, "getPlayers", function(mixed $thisValue, array $args) use ($dimensionId) : mixed{
			$options = $this->queryOptions($args[0] ?? null);
			$options["_players"] = true;
			return $this->api("dim.entities", $dimensionId($thisValue), $options);
		}, 1);
		$f->method($dimension, "spawnEntity", function(mixed $thisValue, array $args) use ($dimensionId) : mixed{
			return $this->api("dim.spawn", $dimensionId($thisValue), $this->typeName($args[0] ?? null), $this->vector($args[1] ?? null));
		}, 3);
		$f->method($dimension, "spawnItem", function(mixed $thisValue, array $args) use ($dimensionId) : mixed{
			return $this->api("dim.item", $dimensionId($thisValue), $this->fromJs($args[0] ?? null), $this->vector($args[1] ?? null));
		}, 2);
		$f->method($dimension, "runCommand", function(mixed $thisValue, array $args) use ($dimensionId) : mixed{
			return $this->toJs(["successCount" => $this->raw("dim.command", $dimensionId($thisValue), $this->string($args[0] ?? ""))]);
		}, 1);
		$f->method($dimension, "playSound", function(mixed $thisValue, array $args) use ($dimensionId) : mixed{
			$this->raw("dim.sound", $dimensionId($thisValue), $this->string($args[0] ?? ""), $this->vector($args[1] ?? null), $this->fromJs($args[2] ?? null) ?? []);
			return null;
		}, 3);
		$f->method($dimension, "createExplosion", function(mixed $thisValue, array $args) use ($dimensionId) : mixed{
			return $this->raw("dim.explode", $dimensionId($thisValue), $this->vector($args[0] ?? null), $this->js->toNumber($args[1] ?? 1), $this->fromJs($args[2] ?? null) ?? []);
		}, 3);
		$f->method($dimension, "getTopmostBlock", function(mixed $thisValue, array $args) use ($dimensionId) : mixed{
			$location = $args[0] ?? null;
			if(!$location instanceof JsObject){
				$this->js->throwError("TypeError", "Expected a VectorXZ");
			}
			return $this->api("dim.top", $dimensionId($thisValue), $this->js->toNumber($this->js->get($location, "x")), $this->js->toNumber($this->js->get($location, "z")));
		}, 2);
		$this->missingMethods($dimension, ["getBlockAbove" => null, "getBlockBelow" => null, "getBlockFromRay" => null, "getEntitiesFromRay" => [], "fillBlocks" => null, "spawnParticle" => null, "getWeather" => "Clear", "setWeather" => null, "containsBlock" => false, "getBlocks" => null]);
		$this->export("Dimension", $dimension);
	}

	/**
	 * @return array<string, mixed>
	 */
	private function queryOptions(mixed $options) : array{
		if(!$options instanceof JsObject){
			return [];
		}
		$result = $this->fromJs($options);
		if(!is_array($result)){
			return [];
		}
		foreach(["families", "excludeFamilies", "volume", "scoreOptions", "propertyOptions"] as $unsupported){
			if(isset($result[$unsupported])){
				$this->runtime->recordMissing("EntityQueryOptions." . $unsupported);
			}
		}
		return $result;
	}

	private function loadScoreboard(mixed $data) : void{
		if(!is_array($data)){
			return;
		}
		$this->scoreboardData = [
			"next" => (int) ($data["next"] ?? 1),
			"objectives" => [],
			"participants" => []
		];
		foreach(is_array($data["objectives"] ?? null) ? $data["objectives"] : [] as $id => $objective){
			if(is_array($objective)){
				$scores = [];
				foreach(is_array($objective["scores"] ?? null) ? $objective["scores"] : [] as $key => $score){
					$scores[(string) $key] = (int) $score;
				}
				$this->scoreboardData["objectives"][(string) $id] = ["displayName" => (string) ($objective["displayName"] ?? $id), "scores" => $scores];
			}
		}
		foreach(is_array($data["participants"] ?? null) ? $data["participants"] : [] as $key => $participant){
			if(is_array($participant)){
				$this->scoreboardData["participants"][(string) $key] = [
					"id" => (int) ($participant["id"] ?? 0),
					"displayName" => (string) ($participant["displayName"] ?? ""),
					"type" => (string) ($participant["type"] ?? "FakePlayer")
				];
			}
		}
	}

	private function participantKey(mixed $participant) : string{
		if($participant instanceof JsObject && is_array($participant->host)){
			$host = $participant->host;
			if(($host["kind"] ?? null) === "identity"){
				return $host["key"];
			}
			if(($host["kind"] ?? null) === "entity"){
				return $host["typeId"] === "minecraft:player" ? "p:" . $host["name"] : "e:" . $host["id"];
			}
		}
		if(is_string($participant)){
			return "f:" . $participant;
		}
		$this->js->throwError("TypeError", "Invalid scoreboard participant");
	}

	private function ensureParticipant(mixed $participant) : string{
		$key = $this->participantKey($participant);
		if(!isset($this->scoreboardData["participants"][$key])){
			$type = "FakePlayer";
			$name = is_string($participant) ? $participant : $key;
			if($participant instanceof JsObject && ($participant->host["kind"] ?? null) === "entity"){
				$type = $participant->host["typeId"] === "minecraft:player" ? "Player" : "Entity";
				$name = $type === "Player" ? $participant->host["name"] : $participant->host["typeId"];
			}
			$this->scoreboardData["participants"][$key] = ["id" => $this->scoreboardData["next"]++, "displayName" => $name, "type" => $type];
			$this->runtime->scoreboardDirty = true;
		}
		return $key;
	}

	private function identityObject(string $key) : JsObject{
		return $this->f->instance($this->identity, ["kind" => "identity", "key" => $key]);
	}

	/**
	 * @return array{displayName: string, scores: array<string, int>}
	 */
	private function objectiveData(mixed $thisValue) : array{
		$id = $this->f->host($thisValue, "objective")["id"];
		$data = $this->scoreboardData["objectives"][$id] ?? null;
		if($data === null){
			$this->js->throwError("Error", "The objective " . $id . " was removed");
		}
		return $data;
	}

	private function defineScoreboard() : void{
		$f = $this->f;
		$js = $this->js;
		$this->identity = $f->define("ScoreboardIdentity");
		$participant = function(mixed $thisValue) : array{
			$key = $this->f->host($thisValue, "identity")["key"];
			return $this->scoreboardData["participants"][$key] ?? ["id" => 0, "displayName" => "", "type" => "FakePlayer"];
		};
		$f->getter($this->identity, "id", function(mixed $thisValue) use ($participant) : mixed{
			return $participant($thisValue)["id"];
		});
		$f->getter($this->identity, "displayName", function(mixed $thisValue) use ($participant) : mixed{
			return $participant($thisValue)["displayName"];
		});
		$f->getter($this->identity, "type", function(mixed $thisValue) use ($participant) : mixed{
			return $participant($thisValue)["type"];
		});
		$f->getter($this->identity, "isValid", function(mixed $thisValue) : mixed{
			return isset($this->scoreboardData["participants"][$this->f->host($thisValue, "identity")["key"]]);
		});
		$f->method($this->identity, "getEntity", function(mixed $thisValue, array $args) : mixed{
			$key = $this->f->host($thisValue, "identity")["key"];
			if(substr($key, 0, 2) === "e:"){
				return $this->api("world.entity", (int) substr($key, 2));
			}
			if(substr($key, 0, 2) === "p:"){
				$name = substr($key, 2);
				foreach($this->raw("world.players", ["name" => $name]) as $ref){
					return $this->toJs($ref);
				}
			}
			return null;
		});
		$this->export("ScoreboardIdentity", $this->identity);

		$this->objective = $f->define("ScoreboardObjective");
		$f->getter($this->objective, "id", function(mixed $thisValue) : mixed{
			return $this->f->host($thisValue, "objective")["id"];
		});
		$f->getter($this->objective, "displayName", function(mixed $thisValue) : mixed{
			return $this->objectiveData($thisValue)["displayName"];
		});
		$f->getter($this->objective, "isValid", function(mixed $thisValue) : mixed{
			return isset($this->scoreboardData["objectives"][$this->f->host($thisValue, "objective")["id"]]);
		});
		$f->method($this->objective, "getScore", function(mixed $thisValue, array $args) : mixed{
			return $this->objectiveData($thisValue)["scores"][$this->participantKey($args[0] ?? null)] ?? null;
		}, 1);
		$f->method($this->objective, "setScore", function(mixed $thisValue, array $args) : mixed{
			$this->objectiveData($thisValue);
			$id = $thisValue->host["id"];
			$this->scoreboardData["objectives"][$id]["scores"][$this->ensureParticipant($args[0] ?? null)] = (int) $this->js->toNumber($args[1] ?? 0);
			$this->runtime->scoreboardDirty = true;
			return null;
		}, 2);
		$f->method($this->objective, "addScore", function(mixed $thisValue, array $args) : mixed{
			$this->objectiveData($thisValue);
			$id = $thisValue->host["id"];
			$key = $this->ensureParticipant($args[0] ?? null);
			$score = ($this->scoreboardData["objectives"][$id]["scores"][$key] ?? 0) + (int) $this->js->toNumber($args[1] ?? 0);
			$this->scoreboardData["objectives"][$id]["scores"][$key] = $score;
			$this->runtime->scoreboardDirty = true;
			return $score;
		}, 2);
		$f->method($this->objective, "hasParticipant", function(mixed $thisValue, array $args) : mixed{
			return isset($this->objectiveData($thisValue)["scores"][$this->participantKey($args[0] ?? null)]);
		}, 1);
		$f->method($this->objective, "removeParticipant", function(mixed $thisValue, array $args) : mixed{
			$this->objectiveData($thisValue);
			$id = $thisValue->host["id"];
			$key = $this->participantKey($args[0] ?? null);
			if(!isset($this->scoreboardData["objectives"][$id]["scores"][$key])){
				return false;
			}
			unset($this->scoreboardData["objectives"][$id]["scores"][$key]);
			$this->runtime->scoreboardDirty = true;
			return true;
		}, 1);
		$f->method($this->objective, "getParticipants", function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = [];
			foreach($this->objectiveData($thisValue)["scores"] as $key => $score){
				$result[] = $this->identityObject((string) $key);
			}
			return $js->newArray($result);
		});
		$f->method($this->objective, "getScores", function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = [];
			foreach($this->objectiveData($thisValue)["scores"] as $key => $score){
				$entry = $js->newObject();
				$entry->props = ["participant" => $this->identityObject((string) $key), "score" => $score];
				$result[] = $entry;
			}
			return $js->newArray($result);
		});
		$this->export("ScoreboardObjective", $this->objective);

		$this->scoreboard = $f->define("Scoreboard");
		$f->method($this->scoreboard, "addObjective", function(mixed $thisValue, array $args) use ($js) : mixed{
			$id = $this->string($args[0] ?? "");
			if(isset($this->scoreboardData["objectives"][$id])){
				$js->throwError("Error", "The objective " . $id . " already exists");
			}
			$displayName = ($args[1] ?? null) === null ? $id : $this->string($args[1]);
			$this->scoreboardData["objectives"][$id] = ["displayName" => $displayName, "scores" => []];
			$this->runtime->scoreboardDirty = true;
			return $this->f->instance($this->objective, ["kind" => "objective", "id" => $id]);
		}, 2);
		$f->method($this->scoreboard, "getObjective", function(mixed $thisValue, array $args) : mixed{
			$id = $this->string($args[0] ?? "");
			return isset($this->scoreboardData["objectives"][$id]) ? $this->f->instance($this->objective, ["kind" => "objective", "id" => $id]) : null;
		}, 1);
		$f->method($this->scoreboard, "getObjectives", function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = [];
			foreach($this->scoreboardData["objectives"] as $id => $unused){
				$result[] = $this->f->instance($this->objective, ["kind" => "objective", "id" => (string) $id]);
			}
			return $js->newArray($result);
		});
		$f->method($this->scoreboard, "removeObjective", function(mixed $thisValue, array $args) : mixed{
			$value = $args[0] ?? null;
			$id = $value instanceof JsObject && ($value->host["kind"] ?? null) === "objective" ? $value->host["id"] : $this->string($value);
			if(!isset($this->scoreboardData["objectives"][$id])){
				return false;
			}
			unset($this->scoreboardData["objectives"][$id]);
			$this->runtime->scoreboardDirty = true;
			return true;
		}, 1);
		$f->method($this->scoreboard, "getParticipants", function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = [];
			foreach($this->scoreboardData["participants"] as $key => $unused){
				$result[] = $this->identityObject((string) $key);
			}
			return $js->newArray($result);
		});
		$this->missingMethods($this->scoreboard, ["setObjectiveAtDisplaySlot" => null, "getObjectiveAtDisplaySlot" => null, "clearObjectiveAtDisplaySlot" => null]);
		$this->export("Scoreboard", $this->scoreboard);
	}

	private function defineEvents() : void{
		$f = $this->f;
		$this->signal = $f->define("EventSignal");
		$f->method($this->signal, "subscribe", function(mixed $thisValue, array $args) : mixed{
			$key = $this->f->host($thisValue, "signal")["key"];
			$callback = $args[0] ?? null;
			if(!$callback instanceof JsCallable){
				$this->js->throwError("TypeError", "The callback must be a function");
			}
			$this->runtime->subscribe($key, $callback);
			return $callback;
		}, 2);
		$f->method($this->signal, "unsubscribe", function(mixed $thisValue, array $args) : mixed{
			$key = $this->f->host($thisValue, "signal")["key"];
			$callback = $args[0] ?? null;
			if($callback instanceof JsCallable){
				$this->runtime->unsubscribe($key, $callback);
			}
			return null;
		}, 1);
		$this->export("EventSignal", $this->signal);

		$this->blockRegistry = $f->define("BlockComponentRegistry");
		$f->method($this->blockRegistry, "registerCustomComponent", function(mixed $thisValue, array $args) : mixed{
			$this->registerComponent("block", $args[0] ?? null, $args[1] ?? null);
			return null;
		}, 2);
		$this->export("BlockComponentRegistry", $this->blockRegistry);
		$this->itemRegistry = $f->define("ItemComponentRegistry");
		$f->method($this->itemRegistry, "registerCustomComponent", function(mixed $thisValue, array $args) : mixed{
			$this->registerComponent("item", $args[0] ?? null, $args[1] ?? null);
			return null;
		}, 2);
		$this->export("ItemComponentRegistry", $this->itemRegistry);
		$this->commandRegistry = $f->define("CustomCommandRegistry");
		$this->missingMethods($this->commandRegistry, ["registerCommand" => null, "registerEnum" => null]);
		$this->export("CustomCommandRegistry", $this->commandRegistry);
	}

	private function registerComponent(string $kind, mixed $name, mixed $component) : void{
		if(!is_string($name) || !str_contains($name, ":")){
			$this->js->throwError("Error", "Custom component names must have a namespace");
		}
		if(!$component instanceof JsObject){
			$this->js->throwError("TypeError", "The custom component must be an object");
		}
		$supported = $kind === "block" ? self::BLOCK_HOOKS : self::ITEM_HOOKS;
		$label = $kind === "block" ? "BlockCustomComponent" : "ItemCustomComponent";
		foreach($this->js->ownKeys($component) as $key){
			if(!in_array($key, $supported, true)){
				$this->runtime->recordMissing($label . "." . $key);
			}
		}
		$this->runtime->registerComponent($kind, $name, $component);
	}

	public function signalObject(string $key) : JsObject{
		return $this->signals[$key] ??= $this->f->instance($this->signal, ["kind" => "signal", "key" => $key]);
	}

	/**
	 * @param list<string> $names
	 */
	private function container(string $className, string $prefix, array $names) : JsObject{
		$class = $this->f->define($className);
		$container = $this->f->instance($class, ["kind" => "events"]);
		foreach($names as $name){
			$container->props[$name] = $this->signalObject($prefix . "." . $name);
		}
		$container->miss = function(JsObject $receiver, string $key) use ($className) : mixed{
			if($this->f->isIgnored($key)){
				return null;
			}
			$this->runtime->recordMissing($className . "." . $key);
			return $this->signalObject("inert." . $className . "." . $key);
		};
		return $container;
	}

	/**
	 * Creates an event object: its data is converted, block faces become
	 * Direction values, and before-events get a "cancel" property.
	 *
	 * @param array<string, mixed> $data
	 */
	public function createEvent(string $className, array $data, bool $before) : JsObject{
		$class = $this->eventClasses[$className] ??= $this->f->define($className);
		$event = new JsObject($class->prototype);
		$event->className = $className;
		foreach($data as $key => $value){
			if(($key === "blockFace" || $key === "face") && is_int($value)){
				$value = self::DIRECTIONS[$value] ?? "Up";
			}
			$event->props[$key] = $this->toJs($value);
		}
		if($before){
			$event->props["cancel"] = false;
		}
		return $event;
	}

	/**
	 * @return array<string, JsObject>
	 */
	public function createRegistries() : array{
		return [
			"blockComponentRegistry" => $this->f->instance($this->blockRegistry, ["kind" => "registry"]),
			"itemComponentRegistry" => $this->f->instance($this->itemRegistry, ["kind" => "registry"]),
			"customCommandRegistry" => $this->f->instance($this->commandRegistry, ["kind" => "registry"])
		];
	}

	private function defineWorld() : void{
		$f = $this->f;
		$js = $this->js;
		$this->world = $f->define("World");
		$world = $this->world;
		$f->method($world, "getAllPlayers", function(mixed $thisValue, array $args) : mixed{
			return $this->api("world.players", []);
		});
		$f->method($world, "getPlayers", function(mixed $thisValue, array $args) : mixed{
			return $this->api("world.players", $this->queryOptions($args[0] ?? null));
		}, 1);
		$f->method($world, "getDimension", function(mixed $thisValue, array $args) : mixed{
			return $this->dimensionObject($this->string($args[0] ?? "overworld"));
		}, 1);
		$f->method($world, "getEntity", function(mixed $thisValue, array $args) : mixed{
			$id = $args[0] ?? null;
			return is_numeric($id) ? $this->api("world.entity", (int) $id) : null;
		}, 1);
		$f->method($world, "sendMessage", function(mixed $thisValue, array $args) : mixed{
			$this->raw("world.message", $this->text($args[0] ?? ""));
			return null;
		}, 1);
		$f->method($world, "getAbsoluteTime", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("world.time");
		});
		$f->method($world, "setAbsoluteTime", function(mixed $thisValue, array $args) : mixed{
			$this->raw("world.setTime", (int) $this->js->toNumber($args[0] ?? 0));
			return null;
		}, 1);
		$f->method($world, "getTimeOfDay", function(mixed $thisValue, array $args) : mixed{
			return $this->raw("world.time") % 24000;
		});
		$f->method($world, "setTimeOfDay", function(mixed $thisValue, array $args) : mixed{
			$time = (int) $this->raw("world.time");
			$this->raw("world.setTime", $time - ($time % 24000) + ((int) $this->js->toNumber($args[0] ?? 0) % 24000));
			return null;
		}, 1);
		$f->method($world, "getDay", function(mixed $thisValue, array $args) : mixed{
			return intdiv((int) $this->raw("world.time"), 24000);
		});
		$f->method($world, "getMoonPhase", function(mixed $thisValue, array $args) : mixed{
			return intdiv((int) $this->raw("world.time"), 24000) % 8;
		});
		$f->method($world, "getDefaultSpawnLocation", function(mixed $thisValue, array $args) : mixed{
			return $this->api("world.spawn");
		});
		$f->method($world, "setDefaultSpawnLocation", function(mixed $thisValue, array $args) : mixed{
			$this->raw("world.setSpawn", $this->vector($args[0] ?? null));
			return null;
		}, 1);
		$f->method($world, "playSound", function(mixed $thisValue, array $args) : mixed{
			$this->raw("world.sound", $this->string($args[0] ?? ""), $this->vector($args[1] ?? null), $this->fromJs($args[2] ?? null) ?? []);
			return null;
		}, 3);
		$this->missingMethods($world, ["playMusic" => null, "queueMusic" => null, "stopMusic" => null, "getLootTableManager" => null, "broadcastClientMessage" => null]);
		$this->entityDynamicProperties($world, function(mixed $thisValue) : string{
			return "world";
		});
		$this->export("World", $world);

		$this->worldObject = $f->instance($world, ["kind" => "world"]);
		$this->worldObject->props["afterEvents"] = $this->container("WorldAfterEvents", "after", [
			"playerJoin", "playerLeave", "playerSpawn", "playerBreakBlock", "playerPlaceBlock", "playerInteractWithBlock",
			"playerInteractWithEntity", "itemUse", "entityHurt", "entityDie", "entitySpawn", "chatSend", "worldLoad", "worldInitialize"
		]);
		$this->worldObject->props["beforeEvents"] = $this->container("WorldBeforeEvents", "before", [
			"playerBreakBlock", "playerInteractWithBlock", "itemUse", "chatSend", "playerLeave"
		]);
		$this->worldObject->props["scoreboard"] = $f->instance($this->scoreboard, ["kind" => "scoreboard"]);
		foreach(["afterEvents", "beforeEvents", "scoreboard"] as $key){
			$this->worldObject->locked[$key] = true;
		}
		$this->export("world", $this->worldObject);
	}

	private function defineSystem() : void{
		$f = $this->f;
		$js = $this->js;
		$this->system = $f->define("System");
		$system = $this->system;
		$f->getter($system, "currentTick", function(mixed $thisValue) : mixed{
			return $this->runtime->currentTick;
		});
		$f->getter($system, "isEditorWorld", function(mixed $thisValue) : mixed{
			return false;
		});
		$callback = function(mixed $value) use ($js) : JsCallable{
			if(!$value instanceof JsCallable){
				$js->throwError("TypeError", "The callback must be a function");
			}
			return $value;
		};
		$f->method($system, "run", function(mixed $thisValue, array $args) use ($callback) : mixed{
			return $this->runtime->schedule($callback($args[0] ?? null), 1, 0);
		}, 1);
		$f->method($system, "runTimeout", function(mixed $thisValue, array $args) use ($callback) : mixed{
			return $this->runtime->schedule($callback($args[0] ?? null), (int) $this->js->toNumber($args[1] ?? 1), 0);
		}, 2);
		$f->method($system, "runInterval", function(mixed $thisValue, array $args) use ($callback) : mixed{
			$interval = max(1, (int) $this->js->toNumber($args[1] ?? 1));
			return $this->runtime->schedule($callback($args[0] ?? null), $interval, $interval);
		}, 2);
		$f->method($system, "clearRun", function(mixed $thisValue, array $args) : mixed{
			$this->runtime->clearRun((int) $this->js->toNumber($args[0] ?? 0));
			return null;
		}, 1);
		$f->method($system, "runJob", function(mixed $thisValue, array $args) use ($js) : mixed{
			$generator = $args[0] ?? null;
			if(!$generator instanceof JsObject || !$js->get($generator, "next") instanceof JsCallable){
				$js->throwError("TypeError", "runJob expects a generator");
			}
			return $this->runtime->addJob($generator);
		}, 1);
		$f->method($system, "clearJob", function(mixed $thisValue, array $args) : mixed{
			$this->runtime->clearJob((int) $this->js->toNumber($args[0] ?? 0));
			return null;
		}, 1);
		$f->method($system, "waitTicks", function(mixed $thisValue, array $args) use ($js) : mixed{
			$promise = $js->newPromise();
			$resolve = $js->native("", 0, function(mixed $unused, array $none) use ($js, $promise) : mixed{
				$js->resolvePromise($promise, null);
				return null;
			});
			$this->runtime->schedule($resolve, max(1, (int) $js->toNumber($args[0] ?? 1)), 0);
			return $promise;
		}, 1);
		$f->method($system, "sendScriptEvent", function(mixed $thisValue, array $args) : mixed{
			$this->runtime->sendScriptEvent($this->string($args[0] ?? ""), $this->string($args[1] ?? ""));
			return null;
		}, 2);
		$this->export("System", $system);

		$this->systemObject = $f->instance($system, ["kind" => "system"]);
		$this->systemObject->props["afterEvents"] = $this->container("SystemAfterEvents", "after", ["scriptEventReceive"]);
		$this->systemObject->props["beforeEvents"] = $this->container("SystemBeforeEvents", "system", ["startup", "watchdogTerminate", "shutdown"]);
		$this->systemObject->locked["afterEvents"] = true;
		$this->systemObject->locked["beforeEvents"] = true;
		$this->export("system", $this->systemObject);
	}

	private function errorClass(string $name) : void{
		$js = $this->js;
		$errorConstructor = $js->constructors["Error"];
		$proto = new JsObject($js->errorPrototype);
		$proto->className = "Error";
		$constructor = $js->makeClass($name, $proto, function(array $args, JsObject $newTarget) use ($js, $proto, $name) : JsObject{
			$error = $js->makeError("Error", ($args[0] ?? null) === null ? "" : $js->toString($args[0]));
			$error->proto = $js->prototypeFor($newTarget, $proto);
			return $error;
		}, null, 1, $errorConstructor);
		$js->defineHidden($proto, "name", $name);
		$this->export($name, $constructor);
	}

	private function defineEnums() : void{
		$f = $this->f;
		foreach(["LocationInUnloadedChunkError", "LocationOutOfWorldBoundariesError", "InvalidEntityError", "InvalidContainerSlotError", "ContainerRulesError", "CommandError", "InvalidIteratorError", "UnloadedChunksError"] as $name){
			$this->errorClass($name);
		}
		$this->export("TicksPerSecond", 20);
		$enums = [
			"Direction" => ["Down" => "Down", "East" => "East", "North" => "North", "South" => "South", "Up" => "Up", "West" => "West"],
			"GameMode" => ["Adventure" => "Adventure", "Creative" => "Creative", "Spectator" => "Spectator", "Survival" => "Survival"],
			"EquipmentSlot" => ["Chest" => "Chest", "Feet" => "Feet", "Head" => "Head", "Legs" => "Legs", "Mainhand" => "Mainhand", "Offhand" => "Offhand"],
			"ScriptEventSource" => ["Block" => "Block", "Entity" => "Entity", "NPCDialogue" => "NPCDialogue", "Server" => "Server"],
			"DisplaySlotId" => ["BelowName" => "BelowName", "List" => "List", "Sidebar" => "Sidebar"],
			"ObjectiveSortOrder" => ["Ascending" => 0, "Descending" => 1],
			"ScoreboardIdentityType" => ["Entity" => "Entity", "FakePlayer" => "FakePlayer", "Player" => "Player"],
			"EntityInitializationCause" => ["Born" => "Born", "Event" => "Event", "Loaded" => "Loaded", "Spawned" => "Spawned", "Transformed" => "Transformed"],
			"ItemLockMode" => ["inventory" => "inventory", "none" => "none", "slot" => "slot"],
			"TimeOfDay" => ["Day" => 1000, "Midnight" => 18000, "Night" => 13000, "Noon" => 6000, "Sunrise" => 23000, "Sunset" => 12000],
			"MinecraftDimensionTypes" => ["Nether" => "minecraft:nether", "Overworld" => "minecraft:overworld", "TheEnd" => "minecraft:the_end"],
			"EntityComponentTypes" => ["Equippable" => "minecraft:equippable", "Health" => "minecraft:health", "Inventory" => "minecraft:inventory"],
			"ItemComponentTypes" => ["Durability" => "minecraft:durability"],
			"BlockComponentTypes" => ["Inventory" => "minecraft:inventory"],
			"SignSide" => ["Back" => "Back", "Front" => "Front"]
		];
		$causes = [];
		foreach(["anvil", "blockExplosion", "campfire", "charging", "contact", "drowning", "entityAttack", "entityExplosion", "fall", "fallingBlock", "fire", "fireTick", "fireworks", "flyIntoWall", "freezing", "lava", "lightning", "maceSmash", "magic", "magma", "none", "override", "piston", "projectile", "ramAttack", "selfDestruct", "sonicBoom", "soulCampfire", "stalactite", "stalagmite", "starve", "suffocation", "temperature", "thorns", "void", "wither"] as $cause){
			$causes[$cause] = $cause;
		}
		$enums["EntityDamageCause"] = $causes;
		foreach($enums as $name => $values){
			$this->export($name, $f->enum($name, $values));
		}
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script;

use Closure;
use pocketmine\form\Form;
use pocketmine\player\Player;

/**
 * A form built by a script with @minecraft/server-ui. The response is handed
 * back to the script runtime as is, or null when the form was closed.
 */
final class ScriptForm implements Form{

	/**
	 * @param array<string, mixed>          $data
	 * @param Closure(Player, mixed) : void $onResponse
	 */
	public function __construct(
		private array $data,
		private Closure $onResponse
	){}

	public function handleResponse(Player $player, $data) : void{
		($this->onResponse)($player, $data);
	}

	/**
	 * @return array<string, mixed>
	 */
	public function jsonSerialize() : array{
		return $this->data;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script;

use pocketmine\block\Block;
use pocketmine\block\VanillaBlocks;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\entity\Entity;
use pocketmine\entity\effect\Effect;
use pocketmine\entity\effect\StringToEffectParser;
use pocketmine\item\Durable;
use pocketmine\item\Item;
use pocketmine\item\StringToItemParser;
use pocketmine\math\Vector3;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\tag\ByteTag;
use pocketmine\nbt\tag\IntTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\nbt\tag\Tag;
use pocketmine\nbt\TreeRoot;
use pocketmine\player\Player;
use pocketmine\Server;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use pocketmine\world\format\io\GlobalItemDataHandlers;
use pocketmine\world\World;
use stdClass;
use Throwable;
use function array_values;
use function base64_decode;
use function base64_encode;
use function count;
use function is_array;
use function is_bool;
use function is_int;
use function is_numeric;
use function is_string;
use function spl_object_id;
use function str_contains;
use function str_ends_with;
use function str_starts_with;
use function strtolower;
use function substr;

/**
 * Converts server objects to the values exchanged with the script runtime,
 * and back.
 */
final class ScriptValues{

	public const OVERWORLD = "minecraft:overworld";
	public const NETHER = "minecraft:nether";
	public const THE_END = "minecraft:the_end";

	/** @var array<int, string>|null */
	private ?array $effectNames = null;

	public function __construct(
		private Server $server
	){}

	public function getServer() : Server{
		return $this->server;
	}

	public static function namespaced(string $id) : string{
		$id = strtolower($id);
		return str_contains($id, ":") ? $id : "minecraft:" . $id;
	}

	/**
	 * @return array<string, mixed>
	 */
	public function entityRef(Entity $entity) : array{
		$ref = ["\$e" => $entity->getId(), "t" => $this->entityTypeId($entity)];
		if($entity instanceof Player){
			$ref["n"] = $entity->getName();
		}
		return $ref;
	}

	public function entityTypeId(Entity $entity) : string{
		if($entity instanceof Player){
			return "minecraft:player";
		}
		return self::namespaced($entity::getNetworkTypeId());
	}

	public function findEntity(mixed $handle) : ?Entity{
		if(is_array($handle)){
			$handle = $handle["\$e"] ?? null;
		}
		if(!is_numeric($handle)){
			return null;
		}
		$entity = $this->server->getWorldManager()->findEntity((int) $handle);
		if($entity === null || $entity->isClosed() || $entity->isFlaggedForDespawn()){
			return null;
		}
		if($entity instanceof Player && !$entity->isConnected()){
			return null;
		}
		return $entity;
	}

	public function entity(mixed $handle) : Entity{
		$entity = $this->findEntity($handle);
		if($entity === null){
			throw new ScriptException("The entity is no longer valid");
		}
		return $entity;
	}

	public function player(mixed $handle) : Player{
		$entity = $this->entity($handle);
		if(!$entity instanceof Player){
			throw new ScriptException("The entity is not a player");
		}
		return $entity;
	}

	/**
	 * Returns the storage scope of an entity: players are keyed by UUID so
	 * their data survives reconnections, other entities by runtime id.
	 */
	public function scope(Entity $entity) : string{
		if($entity instanceof Player){
			return "p:" . $entity->getUniqueId()->toString();
		}
		return "e:" . $entity->getId();
	}

	public function dimensionId(World $world) : string{
		$default = $this->server->getWorldManager()->getDefaultWorld();
		if($default === $world){
			return self::OVERWORLD;
		}
		$name = strtolower($world->getFolderName());
		if($name === "nether" || $name === "dim-1" || str_ends_with($name, "_nether")){
			return self::NETHER;
		}
		if($name === "the_end" || $name === "end" || $name === "dim1" || str_ends_with($name, "_the_end")){
			return self::THE_END;
		}
		return $world->getFolderName();
	}

	public function world(mixed $dimension) : World{
		if(is_array($dimension)){
			$dimension = $dimension["\$d"] ?? null;
		}
		if(!is_string($dimension)){
			throw new ScriptException("Invalid dimension");
		}
		$id = strtolower($dimension);
		if(!str_contains($id, ":") && $this->server->getWorldManager()->getWorldByName($dimension) !== null){
			return $this->server->getWorldManager()->getWorldByName($dimension);
		}
		$id = self::namespaced($id);
		foreach($this->server->getWorldManager()->getWorlds() as $world){
			if($this->dimensionId($world) === $id){
				return $world;
			}
		}
		if($id === self::OVERWORLD){
			$default = $this->server->getWorldManager()->getDefaultWorld();
			if($default !== null){
				return $default;
			}
		}
		throw new ScriptException("Unknown dimension: " . $dimension);
	}

	public function vector(mixed $value) : Vector3{
		if(!is_array($value) || !isset($value["x"], $value["y"], $value["z"]) || !is_numeric($value["x"]) || !is_numeric($value["y"]) || !is_numeric($value["z"])){
			throw new ScriptException("Invalid Vector3");
		}
		return new Vector3((float) $value["x"], (float) $value["y"], (float) $value["z"]);
	}

	/**
	 * @return array{x: float, y: float, z: float}
	 */
	public function vectorOut(Vector3 $vector) : array{
		return ["x" => (float) $vector->x, "y" => (float) $vector->y, "z" => (float) $vector->z];
	}

	/**
	 * @return array<string, mixed>
	 */
	public function blockRef(Block $block) : array{
		$position = $block->getPosition();
		return [
			"\$b" => [$position->getFloorX(), $position->getFloorY(), $position->getFloorZ()],
			"d" => $this->dimensionId($position->getWorld())
		];
	}

	public function blockTypeId(Block $block) : string{
		try{
			return GlobalBlockStateHandlers::getSerializer()->serializeBlock($block)->getName();
		}catch(Throwable){
			return "minecraft:unknown";
		}
	}

	/**
	 * @return array<string, mixed>
	 */
	public function permutation(Block $block) : array{
		try{
			$data = GlobalBlockStateHandlers::getSerializer()->serializeBlock($block);
		}catch(Throwable){
			return ["\$p" => "minecraft:unknown", "s" => new stdClass()];
		}
		$states = [];
		foreach($data->getStates() as $name => $tag){
			$states[$name] = self::stateValue($tag);
		}
		return ["\$p" => $data->getName(), "s" => count($states) === 0 ? new stdClass() : $states];
	}

	/**
	 * Builds a block from a type id or a permutation value, completing the
	 * missing states with the default ones of the type.
	 */
	public function block(mixed $value) : Block{
		$states = [];
		if(is_array($value)){
			$typeId = $value["\$p"] ?? null;
			$states = is_array($value["s"] ?? null) ? $value["s"] : [];
		}else{
			$typeId = $value;
		}
		if(!is_string($typeId)){
			throw new ScriptException("Invalid block type");
		}
		return $this->resolveBlock(self::namespaced($typeId), $states);
	}

	/**
	 * @param array<string, mixed> $states
	 */
	public function resolveBlock(string $typeId, array $states) : Block{
		if($typeId === "minecraft:air"){
			return VanillaBlocks::AIR();
		}
		$base = null;
		$item = StringToItemParser::getInstance()->parse($typeId);
		if($item !== null){
			$candidate = $item->getBlock();
			if($this->blockTypeId($candidate) === $typeId){
				$base = $candidate;
			}
		}
		$serializer = GlobalBlockStateHandlers::getSerializer();
		$deserializer = GlobalBlockStateHandlers::getDeserializer();
		if($base === null){
			try{
				$base = $deserializer->deserializeBlock(BlockStateData::current($typeId, []));
			}catch(Throwable){
				throw new ScriptException("Unknown block type: " . $typeId);
			}
		}
		if(count($states) === 0){
			return $base;
		}
		$data = $serializer->serializeBlock($base);
		$tags = $data->getStates();
		foreach($states as $name => $stateValue){
			$template = $tags[$name] ?? null;
			if($template === null){
				throw new ScriptException("Block " . $typeId . " has no state " . $name);
			}
			$tags[$name] = self::stateTag($template, $stateValue);
		}
		try{
			return $deserializer->deserializeBlock(BlockStateData::current($data->getName(), $tags));
		}catch(Throwable){
			throw new ScriptException("Invalid states for block " . $typeId);
		}
	}

	private static function stateValue(Tag $tag) : mixed{
		if($tag instanceof ByteTag){
			return $tag->getValue() !== 0;
		}
		if($tag instanceof IntTag || $tag instanceof StringTag){
			return $tag->getValue();
		}
		return null;
	}

	private static function stateTag(Tag $template, mixed $value) : Tag{
		if($template instanceof ByteTag){
			return new ByteTag(($value === true || $value === 1 || $value === "true") ? 1 : 0);
		}
		if($template instanceof IntTag){
			if(!is_numeric($value)){
				throw new ScriptException("Expected a number for an integer block state");
			}
			return new IntTag((int) $value);
		}
		if(!is_string($value)){
			throw new ScriptException("Expected a string for a string block state");
		}
		return new StringTag($value);
	}

	public function itemTypeId(Item $item) : string{
		try{
			return GlobalItemDataHandlers::getSerializer()->serializeType($item)->getName();
		}catch(Throwable){
			return "minecraft:unknown";
		}
	}

	/**
	 * @return array<string, mixed>|null
	 */
	public function item(Item $item) : ?array{
		if($item->isNull()){
			return null;
		}
		$data = [
			"t" => $this->itemTypeId($item),
			"c" => $item->getCount(),
			"m" => $item->getMaxStackSize(),
			"l" => array_values($item->getLore())
		];
		if($item->hasCustomName()){
			$data["n"] = $item->getCustomName();
		}
		if($item instanceof Durable){
			$data["d"] = $item->getDamage();
			$data["md"] = $item->getMaxDurability();
		}
		try{
			$data["nbt"] = base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($item->nbtSerialize())));
		}catch(Throwable){
			unset($data["nbt"]);
		}
		return ["\$i" => $data];
	}

	public function decodeItem(mixed $value) : Item{
		if($value === null){
			return VanillaBlocks::AIR()->asItem();
		}
		$data = is_array($value) && is_array($value["\$i"] ?? null) ? $value["\$i"] : null;
		if($data === null || !is_string($data["t"] ?? null)){
			throw new ScriptException("Invalid ItemStack");
		}
		$typeId = self::namespaced($data["t"]);
		$item = null;
		if(is_string($data["nbt"] ?? null)){
			try{
				$decoded = base64_decode($data["nbt"], true);
				if($decoded !== false){
					$candidate = Item::nbtDeserialize((new LittleEndianNbtSerializer())->read($decoded)->mustGetCompoundTag());
					if($this->itemTypeId($candidate) === $typeId){
						$item = $candidate;
					}
				}
			}catch(Throwable){
				$item = null;
			}
		}
		if($item === null){
			$item = StringToItemParser::getInstance()->parse($typeId);
			if($item === null){
				throw new ScriptException("Unknown item type: " . $typeId);
			}
		}
		if(is_int($data["c"] ?? null)){
			$item->setCount($data["c"]);
		}
		if(is_string($data["n"] ?? null) && $data["n"] !== ""){
			$item->setCustomName($data["n"]);
		}else{
			$item->clearCustomName();
		}
		if(is_array($data["l"] ?? null)){
			$lore = [];
			foreach($data["l"] as $line){
				if(is_string($line)){
					$lore[] = $line;
				}
			}
			$item->setLore($lore);
		}
		if($item instanceof Durable && is_int($data["d"] ?? null)){
			$item->setDamage($data["d"]);
		}
		return $item;
	}

	public function effect(mixed $type) : Effect{
		if(is_array($type)){
			$type = $type["id"] ?? null;
		}
		if(!is_string($type)){
			throw new ScriptException("Invalid effect type");
		}
		$effect = StringToEffectParser::getInstance()->parse($type);
		if($effect === null){
			throw new ScriptException("Unknown effect type: " . $type);
		}
		return $effect;
	}

	public function effectName(Effect $effect) : string{
		if($this->effectNames === null){
			$this->effectNames = [];
			$parser = StringToEffectParser::getInstance();
			foreach($parser->getKnownAliases() as $alias){
				$alias = (string) $alias;
				if(str_contains($alias, " ")){
					continue;
				}
				$candidate = $parser->parse($alias);
				if($candidate !== null && !isset($this->effectNames[spl_object_id($candidate)])){
					$this->effectNames[spl_object_id($candidate)] = $alias;
				}
			}
		}
		$name = $this->effectNames[spl_object_id($effect)] ?? "unknown";
		return str_starts_with($name, "minecraft:") ? substr($name, 10) : $name;
	}

	public static function bool(mixed $value, bool $default = false) : bool{
		return is_bool($value) ? $value : $default;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script;

use pocketmine\entity\Entity;
use pocketmine\event\block\BlockBreakEvent;
use pocketmine\event\block\BlockPlaceEvent;
use pocketmine\event\entity\EntityDamageByChildEntityEvent;
use pocketmine\event\entity\EntityDamageByEntityEvent;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\event\entity\EntityDeathEvent;
use pocketmine\event\entity\EntityDespawnEvent;
use pocketmine\event\entity\EntitySpawnEvent;
use pocketmine\event\EventPriority;
use pocketmine\event\player\PlayerChatEvent;
use pocketmine\event\player\PlayerEntityInteractEvent;
use pocketmine\event\player\PlayerInteractEvent;
use pocketmine\event\player\PlayerItemUseEvent;
use pocketmine\event\player\PlayerJoinEvent;
use pocketmine\event\player\PlayerQuitEvent;
use pocketmine\event\player\PlayerRespawnEvent;
use pocketmine\player\Player;
use pocketmine\plugin\PluginBase;
use pocketmine\world\World;
use function is_string;

/**
 * Turns server events into script events. After-events are queued and
 * delivered on the next tick; before-events are dispatched synchronously so
 * that scripts can cancel them. Nothing is sent for events no script
 * subscribed to.
 */
final class ScriptEventListener{

	public function __construct(
		private ScriptLoader $loader,
		private ScriptValues $values,
		private ScriptStorage $storage
	){}

	public function register(PluginBase $plugin) : void{
		$manager = $plugin->getServer()->getPluginManager();
		$manager->registerEvent(PlayerJoinEvent::class, function(PlayerJoinEvent $event) : void{
			$this->onJoin($event);
		}, EventPriority::MONITOR, $plugin);
		$manager->registerEvent(PlayerQuitEvent::class, function(PlayerQuitEvent $event) : void{
			$this->onQuit($event);
		}, EventPriority::MONITOR, $plugin);
		$manager->registerEvent(PlayerRespawnEvent::class, function(PlayerRespawnEvent $event) : void{
			$this->onRespawn($event);
		}, EventPriority::MONITOR, $plugin);
		$manager->registerEvent(BlockBreakEvent::class, function(BlockBreakEvent $event) : void{
			$this->beforeBreak($event);
		}, EventPriority::HIGH, $plugin);
		$manager->registerEvent(BlockBreakEvent::class, function(BlockBreakEvent $event) : void{
			$this->afterBreak($event);
		}, EventPriority::MONITOR, $plugin);
		$manager->registerEvent(BlockPlaceEvent::class, function(BlockPlaceEvent $event) : void{
			$this->afterPlace($event);
		}, EventPriority::MONITOR, $plugin);
		$manager->registerEvent(PlayerInteractEvent::class, function(PlayerInteractEvent $event) : void{
			$this->beforeInteract($event);
		}, EventPriority::HIGH, $plugin);
		$manager->registerEvent(PlayerInteractEvent::class, function(PlayerInteractEvent $event) : void{
			$this->afterInteract($event);
		}, EventPriority::MONITOR, $plugin);
		$manager->registerEvent(PlayerEntityInteractEvent::class, function(PlayerEntityInteractEvent $event) : void{
			$this->afterEntityInteract($event);
		}, EventPriority::MONITOR, $plugin);
		$manager->registerEvent(PlayerItemUseEvent::class, function(PlayerItemUseEvent $event) : void{
			$this->beforeItemUse($event);
		}, EventPriority::HIGH, $plugin);
		$manager->registerEvent(PlayerItemUseEvent::class, function(PlayerItemUseEvent $event) : void{
			$this->afterItemUse($event);
		}, EventPriority::MONITOR, $plugin);
		$manager->registerEvent(EntityDamageEvent::class, function(EntityDamageEvent $event) : void{
			$this->afterHurt($event);
		}, EventPriority::MONITOR, $plugin);
		$manager->registerEvent(EntityDeathEvent::class, function(EntityDeathEvent $event) : void{
			$this->afterDeath($event);
		}, EventPriority::MONITOR, $plugin);
		$manager->registerEvent(EntitySpawnEvent::class, function(EntitySpawnEvent $event) : void{
			$this->afterSpawn($event);
		}, EventPriority::MONITOR, $plugin);
		$manager->registerEvent(EntityDespawnEvent::class, function(EntityDespawnEvent $event) : void{
			$entity = $event->getEntity();
			if(!$entity instanceof Player){
				$this->storage->forget("e:" . $entity->getId());
			}
		}, EventPriority::MONITOR, $plugin);
		$manager->registerEvent(PlayerChatEvent::class, function(PlayerChatEvent $event) : void{
			$this->beforeChat($event);
		}, EventPriority::HIGH, $plugin);
		$manager->registerEvent(PlayerChatEvent::class, function(PlayerChatEvent $event) : void{
			$this->afterChat($event);
		}, EventPriority::MONITOR, $plugin);
	}

	private function onJoin(PlayerJoinEvent $event) : void{
		$player = $event->getPlayer();
		if($this->loader->wants("after.playerJoin")){
			$this->loader->queueEvent("playerJoin", ["playerId" => (string) $player->getId(), "playerName" => $player->getName()]);
		}
		if($this->loader->wants("after.playerSpawn")){
			$this->loader->queueEvent("playerSpawn", ["player" => $this->values->entityRef($player), "initialSpawn" => true]);
		}
	}

	private function onQuit(PlayerQuitEvent $event) : void{
		$player = $event->getPlayer();
		if($this->loader->wants("before.playerLeave")){
			$this->loader->dispatchSync("playerLeave", ["player" => $this->values->entityRef($player)]);
		}
		if($this->loader->wants("after.playerLeave")){
			$this->loader->queueEvent("playerLeave", ["playerId" => (string) $player->getId(), "playerName" => $player->getName()]);
		}
		$this->loader->queueEvent("__quit", ["id" => $player->getId()]);
	}

	private function onRespawn(PlayerRespawnEvent $event) : void{
		if($this->loader->wants("after.playerSpawn")){
			$this->loader->queueEvent("playerSpawn", ["player" => $this->values->entityRef($event->getPlayer()), "initialSpawn" => false]);
		}
	}

	private function beforeBreak(BlockBreakEvent $event) : void{
		if(!$this->loader->wants("before.playerBreakBlock")){
			return;
		}
		$block = $event->getBlock();
		$done = $this->loader->dispatchSync("playerBreakBlock", [
			"player" => $this->values->entityRef($event->getPlayer()),
			"block" => $this->values->blockRef($block),
			"dimension" => $this->dimension($block->getPosition()->getWorld()),
			"itemStack" => $this->values->item($event->getItem())
		]);
		if(($done["c"] ?? false) === true){
			$event->cancel();
		}
	}

	private function afterBreak(BlockBreakEvent $event) : void{
		$block = $event->getBlock();
		$typeId = $this->values->blockTypeId($block);
		$wantsEvent = $this->loader->wants("after.playerBreakBlock");
		$wantsHook = $this->loader->hasBlockHook($typeId);
		if(!$wantsEvent && !$wantsHook){
			return;
		}
		$data = [
			"player" => $this->values->entityRef($event->getPlayer()),
			"block" => $this->values->blockRef($block),
			"dimension" => $this->dimension($block->getPosition()->getWorld()),
			"brokenBlockPermutation" => $this->values->permutation($block)
		];
		if($wantsHook){
			$this->loader->queueEvent("__comp", ["k" => "b", "h" => "onPlayerBreak", "t" => $typeId, "d" => $data]);
		}
		if($wantsEvent){
			$item = $this->values->item($event->getItem());
			$data["itemStackBeforeBreak"] = $item;
			$data["itemStackAfterBreak"] = $item;
			$this->loader->queueEvent("playerBreakBlock", $data);
		}
	}

	private function afterPlace(BlockPlaceEvent $event) : void{
		$wantsEvent = $this->loader->wants("after.playerPlaceBlock");
		$world = $event->getBlockAgainst()->getPosition()->getWorld();
		$dimension = $this->dimension($world);
		$player = $this->values->entityRef($event->getPlayer());
		foreach($event->getTransaction()->getBlocks() as [$x, $y, $z, $placed]){
			$ref = ["\$b" => [$x, $y, $z], "d" => $dimension["\$d"]];
			$typeId = $this->values->blockTypeId($placed);
			if($this->loader->hasBlockHook($typeId)){
				$this->loader->queueEvent("__comp", ["k" => "b", "h" => "onPlace", "t" => $typeId, "d" => [
					"block" => $ref,
					"dimension" => $dimension,
					"previousBlock" => $this->values->permutation($world->getBlockAt($x, $y, $z))
				]]);
			}
			if($wantsEvent){
				$this->loader->queueEvent("playerPlaceBlock", ["player" => $player, "block" => $ref, "dimension" => $dimension]);
			}
		}
	}

	/**
	 * @return array<string, mixed>
	 */
	private function interactData(PlayerInteractEvent $event) : array{
		$block = $event->getBlock();
		return [
			"player" => $this->values->entityRef($event->getPlayer()),
			"block" => $this->values->blockRef($block),
			"blockFace" => $event->getFace(),
			"faceLocation" => $this->values->vectorOut($event->getTouchVector()),
			"itemStack" => $this->values->item($event->getItem()),
			"isFirstEvent" => true
		];
	}

	private function beforeInteract(PlayerInteractEvent $event) : void{
		if($event->getAction() !== PlayerInteractEvent::RIGHT_CLICK_BLOCK || !$this->loader->wants("before.playerInteractWithBlock")){
			return;
		}
		$done = $this->loader->dispatchSync("playerInteractWithBlock", $this->interactData($event));
		if(($done["c"] ?? false) === true){
			$event->cancel();
		}
	}

	private function afterInteract(PlayerInteractEvent $event) : void{
		if($event->getAction() !== PlayerInteractEvent::RIGHT_CLICK_BLOCK){
			return;
		}
		$block = $event->getBlock();
		$blockTypeId = $this->values->blockTypeId($block);
		$itemTypeId = $event->getItem()->isNull() ? "" : $this->values->itemTypeId($event->getItem());
		$wantsEvent = $this->loader->wants("after.playerInteractWithBlock");
		$blockHook = $this->loader->hasBlockHook($blockTypeId);
		$itemHook = $itemTypeId !== "" && $this->loader->hasItemHook($itemTypeId);
		if(!$wantsEvent && !$blockHook && !$itemHook){
			return;
		}
		$data = $this->interactData($event);
		$dimension = $this->dimension($block->getPosition()->getWorld());
		if($blockHook){
			$this->loader->queueEvent("__comp", ["k" => "b", "h" => "onPlayerInteract", "t" => $blockTypeId, "d" => [
				"block" => $data["block"],
				"dimension" => $dimension,
				"player" => $data["player"],
				"face" => $data["blockFace"],
				"faceLocation" => $data["faceLocation"]
			]]);
		}
		if($itemHook){
			$this->loader->queueEvent("__comp", ["k" => "i", "h" => "onUseOn", "t" => $itemTypeId, "d" => [
				"itemStack" => $data["itemStack"],
				"source" => $data["player"],
				"block" => $data["block"],
				"blockFace" => $data["blockFace"],
				"faceLocation" => $data["faceLocation"],
				"usedOnBlockPermutation" => $this->values->permutation($block)
			]]);
		}
		if($wantsEvent){
			$this->loader->queueEvent("playerInteractWithBlock", $data);
		}
	}

	private function afterEntityInteract(PlayerEntityInteractEvent $event) : void{
		if(!$this->loader->wants("after.playerInteractWithEntity")){
			return;
		}
		$player = $event->getPlayer();
		$item = $this->values->item($player->getInventory()->getItemInHand());
		$this->loader->queueEvent("playerInteractWithEntity", [
			"player" => $this->values->entityRef($player),
			"target" => $this->values->entityRef($event->getEntity()),
			"itemStack" => $item,
			"beforeItemStack" => $item
		]);
	}

	private function beforeItemUse(PlayerItemUseEvent $event) : void{
		if($event->getItem()->isNull() || !$this->loader->wants("before.itemUse")){
			return;
		}
		$done = $this->loader->dispatchSync("itemUse", [
			"source" => $this->values->entityRef($event->getPlayer()),
			"itemStack" => $this->values->item($event->getItem())
		]);
		if(($done["c"] ?? false) === true){
			$event->cancel();
		}
	}

	private function afterItemUse(PlayerItemUseEvent $event) : void{
		$item = $event->getItem();
		if($item->isNull()){
			return;
		}
		$typeId = $this->values->itemTypeId($item);
		$wantsEvent = $this->loader->wants("after.itemUse");
		$wantsHook = $this->loader->hasItemHook($typeId);
		if(!$wantsEvent && !$wantsHook){
			return;
		}
		$data = [
			"source" => $this->values->entityRef($event->getPlayer()),
			"itemStack" => $this->values->item($item)
		];
		if($wantsHook){
			$this->loader->queueEvent("__comp", ["k" => "i", "h" => "onUse", "t" => $typeId, "d" => $data]);
		}
		if($wantsEvent){
			$this->loader->queueEvent("itemUse", $data);
		}
	}

	private function afterHurt(EntityDamageEvent $event) : void{
		if(!$this->loader->wants("after.entityHurt")){
			return;
		}
		$this->loader->queueEvent("entityHurt", [
			"hurtEntity" => $this->values->entityRef($event->getEntity()),
			"damage" => $event->getFinalDamage(),
			"damageSource" => $this->damageSource($event)
		]);
	}

	private function afterDeath(EntityDeathEvent $event) : void{
		if(!$this->loader->wants("after.entityDie")){
			return;
		}
		$entity = $event->getEntity();
		$cause = $entity->getLastDamageCause();
		$this->loader->queueEvent("entityDie", [
			"deadEntity" => $this->values->entityRef($entity),
			"damageSource" => $cause === null ? ["cause" => "none"] : $this->damageSource($cause)
		]);
	}

	private function afterSpawn(EntitySpawnEvent $event) : void{
		$entity = $event->getEntity();
		if($entity instanceof Player || !$this->loader->wants("after.entitySpawn")){
			return;
		}
		$this->loader->queueEvent("entitySpawn", ["entity" => $this->values->entityRef($entity), "cause" => "Spawned"]);
	}

	private function beforeChat(PlayerChatEvent $event) : void{
		if(!$this->loader->wants("before.chatSend")){
			return;
		}
		$done = $this->loader->dispatchSync("chatSend", [
			"sender" => $this->values->entityRef($event->getPlayer()),
			"message" => $event->getMessage()
		]);
		if(($done["c"] ?? false) === true){
			$event->cancel();
			return;
		}
		$message = $done["m"]["message"] ?? null;
		if(is_string($message)){
			$event->setMessage($message);
		}
	}

	private function afterChat(PlayerChatEvent $event) : void{
		if(!$this->loader->wants("after.chatSend")){
			return;
		}
		$this->loader->queueEvent("chatSend", [
			"sender" => $this->values->entityRef($event->getPlayer()),
			"message" => $event->getMessage()
		]);
	}

	/**
	 * @return array<string, mixed>
	 */
	private function damageSource(EntityDamageEvent $event) : array{
		$source = ["cause" => ScriptApi::damageCauseName($event->getCause())];
		if($event instanceof EntityDamageByChildEntityEvent){
			$child = $event->getChild();
			if($child !== null){
				$source["damagingProjectile"] = $this->values->entityRef($child);
			}
		}
		if($event instanceof EntityDamageByEntityEvent){
			$damager = $event->getDamager();
			if($damager instanceof Entity){
				$source["damagingEntity"] = $this->values->entityRef($damager);
			}
		}
		return $source;
	}

	/**
	 * @return array{"$d": string}
	 */
	private function dimension(World $world) : array{
		return ["\$d" => $this->values->dimensionId($world)];
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script;

use Throwable;
use function array_keys;
use function array_search;
use function array_values;
use function file_get_contents;
use function file_put_contents;
use function in_array;
use function is_array;
use function is_dir;
use function is_file;
use function json_decode;
use function json_encode;
use function mkdir;
use function str_starts_with;
use function substr;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;

/**
 * Keeps what scripts store: dynamic properties and tags. A scope is "world",
 * "p:<player uuid>" for players, saved to disk, or "e:<runtime id>" for other
 * entities, kept for the session only. The scoreboard is stored as the data
 * given by the script runtime.
 */
final class ScriptStorage{

	/** @var array<string, array{dp: array<string, mixed>, tags: list<string>}> */
	private array $scopes = [];

	private mixed $scoreboard = null;

	private bool $dirty = false;

	public function __construct(
		private string $directory
	){}

	public function load() : void{
		$data = $this->readFile("properties.json");
		foreach($data as $scope => $entry){
			if(!is_array($entry)){
				continue;
			}
			$this->scopes[(string) $scope] = [
				"dp" => is_array($entry["dp"] ?? null) ? $entry["dp"] : [],
				"tags" => is_array($entry["tags"] ?? null) ? array_values($entry["tags"]) : []
			];
		}
		$scoreboard = $this->readFile("scoreboard.json");
		$this->scoreboard = $scoreboard === [] ? null : $scoreboard;
	}

	public function save() : void{
		if(!$this->dirty){
			return;
		}
		$this->dirty = false;
		$persistent = [];
		foreach($this->scopes as $scope => $entry){
			if(!str_starts_with($scope, "e:") && ($entry["dp"] !== [] || $entry["tags"] !== [])){
				$persistent[$scope] = ["dp" => (object) $entry["dp"], "tags" => $entry["tags"]];
			}
		}
		$this->writeFile("properties.json", $persistent);
		if($this->scoreboard !== null){
			$this->writeFile("scoreboard.json", $this->scoreboard);
		}
	}

	public function get(string $scope, string $key) : mixed{
		return $this->scopes[$scope]["dp"][$key] ?? null;
	}

	public function set(string $scope, string $key, mixed $value) : void{
		$this->touch($scope);
		if($value === null){
			unset($this->scopes[$scope]["dp"][$key]);
		}else{
			$this->scopes[$scope]["dp"][$key] = $value;
		}
		$this->markDirty($scope);
	}

	/**
	 * @return list<string>
	 */
	public function ids(string $scope) : array{
		return array_keys($this->scopes[$scope]["dp"] ?? []);
	}

	public function clear(string $scope) : void{
		if(isset($this->scopes[$scope])){
			$this->scopes[$scope]["dp"] = [];
			$this->markDirty($scope);
		}
	}

	/**
	 * @return list<string>
	 */
	public function getTags(string $scope) : array{
		return $this->scopes[$scope]["tags"] ?? [];
	}

	public function addTag(string $scope, string $tag) : bool{
		$this->touch($scope);
		if(in_array($tag, $this->scopes[$scope]["tags"], true)){
			return false;
		}
		$this->scopes[$scope]["tags"][] = $tag;
		$this->markDirty($scope);
		return true;
	}

	public function removeTag(string $scope, string $tag) : bool{
		$index = array_search($tag, $this->scopes[$scope]["tags"] ?? [], true);
		if($index === false){
			return false;
		}
		unset($this->scopes[$scope]["tags"][$index]);
		$this->scopes[$scope]["tags"] = array_values($this->scopes[$scope]["tags"]);
		$this->markDirty($scope);
		return true;
	}

	public function forget(string $scope) : void{
		unset($this->scopes[$scope]);
	}

	public function getScoreboard() : mixed{
		return $this->scoreboard;
	}

	public function setScoreboard(mixed $scoreboard) : void{
		$this->scoreboard = $scoreboard;
		$this->dirty = true;
	}

	private function touch(string $scope) : void{
		if(!isset($this->scopes[$scope])){
			$this->scopes[$scope] = ["dp" => [], "tags" => []];
		}
	}

	private function markDirty(string $scope) : void{
		if(substr($scope, 0, 2) !== "e:"){
			$this->dirty = true;
		}
	}

	/**
	 * @return array<mixed>
	 */
	private function readFile(string $name) : array{
		$path = $this->directory . "/" . $name;
		if(!is_file($path)){
			return [];
		}
		try{
			$decoded = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
		}catch(Throwable){
			return [];
		}
		return is_array($decoded) ? $decoded : [];
	}

	private function writeFile(string $name, mixed $data) : void{
		if(!is_dir($this->directory)){
			@mkdir($this->directory, 0777, true);
		}
		try{
			file_put_contents($this->directory . "/" . $name, json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
		}catch(Throwable){
			$this->dirty = true;
		}
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script;

use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\entity\Entity;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\plugin\Plugin;
use pocketmine\plugin\PluginOwned;
use function array_shift;
use function count;
use function implode;
use function str_contains;

/**
 * The /scriptevent command, received by scripts through
 * system.afterEvents.scriptEventReceive.
 */
final class ScriptEventCommand extends Command implements PluginOwned{

	public function __construct(
		private Plugin $plugin,
		private ScriptLoader $loader
	){
		parent::__construct("scriptevent", "Sends an event to the scripts of the behavior packs", "/scriptevent <namespace:id> [message]");
		$this->setPermission(DefaultPermissionNames::GROUP_OPERATOR);
	}

	public function getOwningPlugin() : Plugin{
		return $this->plugin;
	}

	public function execute(CommandSender $sender, string $commandLabel, array $args){
		if(count($args) === 0){
			$sender->sendMessage("Usage: /scriptevent <namespace:id> [message]");
			return false;
		}
		$id = (string) array_shift($args);
		if(!str_contains($id, ":") || str_contains($id, "minecraft:")){
			$sender->sendMessage("The id must have a namespace other than minecraft");
			return false;
		}
		$this->loader->sendScriptEvent($id, implode(" ", $args), $sender instanceof Entity ? $sender : null);
		return true;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script;

use RuntimeException;

/**
 * A script request that cannot be served. Its message is thrown back to the
 * script as an Error.
 */
final class ScriptException extends RuntimeException{

}
<?php

declare(strict_types=1);

namespace behaviorpack\script;

use behaviorpack\entity\BehaviorEntity;
use pocketmine\block\Block;
use pocketmine\block\Liquid;
use pocketmine\block\tile\Container as ContainerTile;
use pocketmine\entity\Entity;
use pocketmine\entity\EntityFactory;
use pocketmine\entity\Human;
use pocketmine\entity\Living;
use pocketmine\entity\effect\EffectInstance;
use pocketmine\event\entity\EntityDamageByEntityEvent;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\inventory\ArmorInventory;
use pocketmine\inventory\Inventory;
use pocketmine\item\Durable;
use pocketmine\item\StringToItemParser;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\DoubleTag;
use pocketmine\nbt\tag\FloatTag;
use pocketmine\nbt\tag\ListTag;
use pocketmine\network\mcpe\protocol\PlaySoundPacket;
use pocketmine\player\GameMode;
use pocketmine\player\Player;
use pocketmine\Server;
use pocketmine\world\Explosion;
use pocketmine\world\Position;
use pocketmine\world\World;
use function abs;
use function array_map;
use function array_reverse;
use function array_slice;
use function count;
use function in_array;
use function is_array;
use function is_int;
use function is_numeric;
use function is_string;
use function ltrim;
use function max;
use function min;
use function str_replace;
use function strtolower;
use function usort;

/**
 * Serves the requests of the script runtime. Each method name maps to one
 * operation on the server; see ScriptValues for the value encoding.
 */
final class ScriptApi{

	private const DAMAGE_CAUSES = [
		"contact" => EntityDamageEvent::CAUSE_CONTACT,
		"entityAttack" => EntityDamageEvent::CAUSE_ENTITY_ATTACK,
		"projectile" => EntityDamageEvent::CAUSE_PROJECTILE,
		"suffocation" => EntityDamageEvent::CAUSE_SUFFOCATION,
		"fall" => EntityDamageEvent::CAUSE_FALL,
		"fire" => EntityDamageEvent::CAUSE_FIRE,
		"fireTick" => EntityDamageEvent::CAUSE_FIRE_TICK,
		"lava" => EntityDamageEvent::CAUSE_LAVA,
		"drowning" => EntityDamageEvent::CAUSE_DROWNING,
		"blockExplosion" => EntityDamageEvent::CAUSE_BLOCK_EXPLOSION,
		"entityExplosion" => EntityDamageEvent::CAUSE_ENTITY_EXPLOSION,
		"selfDestruct" => EntityDamageEvent::CAUSE_SUICIDE,
		"magic" => EntityDamageEvent::CAUSE_MAGIC,
		"none" => EntityDamageEvent::CAUSE_CUSTOM,
		"void" => EntityDamageEvent::CAUSE_VOID,
		"starve" => EntityDamageEvent::CAUSE_STARVATION,
		"fallingBlock" => EntityDamageEvent::CAUSE_FALLING_BLOCK
	];

	private const EQUIPMENT_SLOTS = [
		"Head" => ArmorInventory::SLOT_HEAD,
		"Chest" => ArmorInventory::SLOT_CHEST,
		"Legs" => ArmorInventory::SLOT_LEGS,
		"Feet" => ArmorInventory::SLOT_FEET
	];

	private Server $server;

	private ?ScriptCommandSender $commandSender = null;

	public function __construct(
		private ScriptLoader $loader,
		private ScriptValues $values,
		private ScriptStorage $storage
	){
		$this->server = $values->getServer();
	}

	public static function damageCauseName(int $cause) : string{
		foreach(self::DAMAGE_CAUSES as $name => $id){
			if($id === $cause){
				return $name;
			}
		}
		return "none";
	}

	/**
	 * @param list<mixed> $a
	 */
	public function handle(string $method, array $a) : mixed{
		return match($method){
			"world.players" => $this->queryPlayers($a[0] ?? []),
			"world.entity" => $this->findEntity($a[0] ?? null),
			"world.message" => $this->server->broadcastMessage($this->string($a[0] ?? "")) >= 0,
			"world.time" => $this->defaultWorld()->getTime(),
			"world.setTime" => $this->defaultWorld()->setTime($this->int($a[0] ?? 0)),
			"world.spawn" => $this->values->vectorOut($this->defaultWorld()->getSpawnLocation()),
			"world.setSpawn" => $this->defaultWorld()->setSpawnLocation($this->values->vector($a[0] ?? null)),
			"world.sound" => $this->playSound($this->defaultWorld()->getPlayers(), $a[0] ?? "", $this->values->vector($a[1] ?? null), $a[2] ?? []),
			"dp.get" => $this->storage->get($this->scope($a[0] ?? null), $this->string($a[1] ?? "")),
			"dp.set" => $this->storage->set($this->scope($a[0] ?? null), $this->string($a[1] ?? ""), $a[2] ?? null),
			"dp.ids" => $this->storage->ids($this->scope($a[0] ?? null)),
			"dp.clear" => $this->storage->clear($this->scope($a[0] ?? null)),
			"sb.save" => $this->storage->setScoreboard($a[0] ?? null),
			"dim.block" => $this->getBlock($a[0] ?? null, $a[1] ?? null),
			"dim.setBlock" => $this->setBlock($a[0] ?? null, $a[1] ?? null, $a[2] ?? null),
			"dim.entities" => $this->queryDimension($a[0] ?? null, $a[1] ?? []),
			"dim.spawn" => $this->spawnEntity($a[0] ?? null, $a[1] ?? null, $a[2] ?? null),
			"dim.item" => $this->spawnItem($a[0] ?? null, $a[1] ?? null, $a[2] ?? null),
			"dim.command" => $this->runCommand($this->string($a[1] ?? ""), null),
			"dim.sound" => $this->playSound($this->values->world($a[0] ?? null)->getPlayers(), $a[1] ?? "", $this->values->vector($a[2] ?? null), $a[3] ?? []),
			"dim.explode" => $this->explode($a[0] ?? null, $a[1] ?? null, $a[2] ?? 1, $a[3] ?? []),
			"dim.top" => $this->topBlock($a[0] ?? null, $a[1] ?? 0, $a[2] ?? 0),
			"block.get" => $this->blockInfo($a[0] ?? null, $a[1] ?? null),
			"block.resolve" => $this->values->permutation($this->values->resolveBlock(ScriptValues::namespaced($this->string($a[0] ?? "")), is_array($a[1] ?? null) ? $a[1] : [])),
			"block.item" => $this->blockItem($a[0] ?? null, $a[1] ?? 1),
			"item.info" => $this->itemInfo($this->string($a[0] ?? "")),
			"ent.valid" => $this->values->findEntity($a[0] ?? null) !== null,
			"ent.get" => $this->getEntityField($this->values->entity($a[0] ?? null), $this->string($a[1] ?? "")),
			"ent.set" => $this->setEntityField($this->values->entity($a[0] ?? null), $this->string($a[1] ?? ""), $a[2] ?? null),
			"ent.teleport" => $this->teleport($this->values->entity($a[0] ?? null), $a[1] ?? null, $a[2] ?? []),
			"ent.kill" => $this->kill($this->values->entity($a[0] ?? null)),
			"ent.trigger" => $this->triggerEvent($this->values->entity($a[0] ?? null), $this->string($a[1] ?? "")),
			"ent.remove" => $this->remove($this->values->entity($a[0] ?? null)),
			"ent.damage" => $this->damage($this->values->entity($a[0] ?? null), $a[1] ?? 0, $a[2] ?? []),
			"ent.impulse" => $this->impulse($this->values->entity($a[0] ?? null), $this->values->vector($a[1] ?? null)),
			"ent.setVelocity" => $this->values->entity($a[0] ?? null)->setMotion($this->values->vector($a[1] ?? null)),
			"ent.effect.add" => $this->addEffect($this->living($a[0] ?? null), $a[1] ?? null, $a[2] ?? 0, $a[3] ?? 0, $a[4] ?? true),
			"ent.effect.remove" => $this->removeEffect($this->living($a[0] ?? null), $a[1] ?? null),
			"ent.effect.get" => $this->getEffect($this->living($a[0] ?? null), $a[1] ?? null),
			"ent.effects" => $this->getEffects($this->living($a[0] ?? null)),
			"ent.tags" => $this->storage->getTags($this->values->scope($this->values->entity($a[0] ?? null))),
			"ent.addTag" => $this->storage->addTag($this->values->scope($this->values->entity($a[0] ?? null)), $this->string($a[1] ?? "")),
			"ent.removeTag" => $this->storage->removeTag($this->values->scope($this->values->entity($a[0] ?? null)), $this->string($a[1] ?? "")),
			"ent.comp" => $this->hasComponent($this->values->entity($a[0] ?? null), $this->string($a[1] ?? "")),
			"ent.health" => $this->health($this->values->entity($a[0] ?? null)),
			"ent.setHealth" => $this->setHealth($this->values->entity($a[0] ?? null), $a[1] ?? 0),
			"ent.equip.get" => $this->getEquipment($this->values->entity($a[0] ?? null), $this->string($a[1] ?? "")),
			"ent.equip.set" => $this->setEquipment($this->values->entity($a[0] ?? null), $this->string($a[1] ?? ""), $a[2] ?? null),
			"ent.command" => $this->runCommand($this->string($a[1] ?? ""), $this->values->entity($a[0] ?? null)),
			"ent.fire" => $this->setOnFire($this->values->entity($a[0] ?? null), $a[1] ?? 0),
			"ent.extinguish" => $this->values->entity($a[0] ?? null)->extinguish(),
			"pl.message" => $this->values->player($a[0] ?? null)->sendMessage($this->string($a[1] ?? "")),
			"pl.sound" => $this->playPlayerSound($this->values->player($a[0] ?? null), $a[1] ?? "", $a[2] ?? []),
			"pl.title" => $this->sendTitle($this->values->player($a[0] ?? null), $this->string($a[1] ?? ""), $a[2] ?? []),
			"pl.actionbar" => $this->values->player($a[0] ?? null)->sendActionBarMessage($this->string($a[1] ?? "")),
			"pl.gamemode" => self::gameModeName($this->values->player($a[0] ?? null)->getGamemode()),
			"pl.setGamemode" => $this->setGameMode($this->values->player($a[0] ?? null), $this->string($a[1] ?? "")),
			"pl.xp" => $this->addExperience($this->values->player($a[0] ?? null), $this->int($a[1] ?? 0)),
			"pl.levels" => $this->addLevels($this->values->player($a[0] ?? null), $this->int($a[1] ?? 0)),
			"pl.xpInfo" => $this->experience($this->values->player($a[0] ?? null)),
			"pl.spawnPoint" => $this->spawnPoint($this->values->player($a[0] ?? null)),
			"pl.setSpawnPoint" => $this->setSpawnPoint($this->values->player($a[0] ?? null), $a[1] ?? null),
			"pl.form" => $this->sendForm($this->values->player($a[0] ?? null), $this->int($a[1] ?? 0), $a[2] ?? null),
			"pl.selectedSlot" => $this->values->player($a[0] ?? null)->getInventory()->setHeldItemIndex(max(0, min(8, $this->int($a[1] ?? 0)))),
			"cont.valid" => $this->findInventory($a[0] ?? null) !== null,
			"cont.size" => $this->inventory($a[0] ?? null)->getSize(),
			"cont.empty" => $this->emptySlots($this->inventory($a[0] ?? null)),
			"cont.get" => $this->values->item($this->inventory($a[0] ?? null)->getItem($this->slot($a[0] ?? null, $a[1] ?? 0))),
			"cont.set" => $this->inventory($a[0] ?? null)->setItem($this->slot($a[0] ?? null, $a[1] ?? 0), $this->values->decodeItem($a[2] ?? null)),
			"cont.add" => $this->addItem($this->inventory($a[0] ?? null), $a[1] ?? null),
			"cont.clear" => $this->inventory($a[0] ?? null)->clearAll(),
			default => throw new ScriptException("Unknown request " . $method)
		};
	}

	public static function gameModeName(GameMode $mode) : string{
		return match($mode){
			GameMode::SURVIVAL => "Survival",
			GameMode::CREATIVE => "Creative",
			GameMode::ADVENTURE => "Adventure",
			GameMode::SPECTATOR => "Spectator"
		};
	}

	private function defaultWorld() : World{
		$world = $this->server->getWorldManager()->getDefaultWorld();
		if($world === null){
			throw new ScriptException("No world is loaded");
		}
		return $world;
	}

	private function string(mixed $value) : string{
		if(is_string($value)){
			return $value;
		}
		if(is_numeric($value)){
			return (string) $value;
		}
		throw new ScriptException("Expected a string");
	}

	private function int(mixed $value) : int{
		if(!is_numeric($value)){
			throw new ScriptException("Expected a number");
		}
		return (int) $value;
	}

	private function float(mixed $value) : float{
		if(!is_numeric($value)){
			throw new ScriptException("Expected a number");
		}
		return (float) $value;
	}

	private function scope(mixed $owner) : string{
		if($owner === "world"){
			return "world";
		}
		return $this->values->scope($this->values->entity($owner));
	}

	private function living(mixed $handle) : Living{
		$entity = $this->values->entity($handle);
		if(!$entity instanceof Living){
			throw new ScriptException("The entity has no effects");
		}
		return $entity;
	}

	/**
	 * @return array<string, mixed>|null
	 */
	private function findEntity(mixed $id) : ?array{
		$entity = $this->values->findEntity($id);
		return $entity === null ? null : $this->values->entityRef($entity);
	}

	/**
	 * @return list<array<string, mixed>>
	 */
	private function queryPlayers(mixed $options) : array{
		$players = [];
		foreach($this->server->getOnlinePlayers() as $player){
			if($player->isConnected()){
				$players[] = $player;
			}
		}
		return $this->query($players, is_array($options) ? $options : []);
	}

	/**
	 * @return list<array<string, mixed>>
	 */
	private function queryDimension(mixed $dimension, mixed $options) : array{
		$world = $this->values->world($dimension);
		$options = is_array($options) ? $options : [];
		$entities = [];
		if(($options["_players"] ?? false) === true){
			foreach($world->getPlayers() as $player){
				if($player->isConnected()){
					$entities[] = $player;
				}
			}
		}else{
			foreach($world->getEntities() as $entity){
				if(!$entity->isClosed() && !$entity->isFlaggedForDespawn() && (!$entity instanceof Player || $entity->isConnected())){
					$entities[] = $entity;
				}
			}
		}
		return $this->query($entities, $options);
	}

	/**
	 * Applies the common EntityQueryOptions to a list of entities.
	 *
	 * @param list<Entity>         $entities
	 * @param array<string, mixed> $options
	 * @return list<array<string, mixed>>
	 */
	private function query(array $entities, array $options) : array{
		$location = isset($options["location"]) ? $this->values->vector($options["location"]) : null;
		$block = isset($options["_block"]) ? $this->values->vector($options["_block"])->floor() : null;
		$type = is_string($options["type"] ?? null) ? ScriptValues::namespaced($options["type"]) : null;
		$excludeTypes = array_map(fn($t) => ScriptValues::namespaced((string) $t), is_array($options["excludeTypes"] ?? null) ? $options["excludeTypes"] : []);
		$name = is_string($options["name"] ?? null) ? $options["name"] : null;
		$excludeNames = is_array($options["excludeNames"] ?? null) ? $options["excludeNames"] : [];
		$tags = is_array($options["tags"] ?? null) ? $options["tags"] : [];
		$excludeTags = is_array($options["excludeTags"] ?? null) ? $options["excludeTags"] : [];
		$maxDistance = is_numeric($options["maxDistance"] ?? null) ? (float) $options["maxDistance"] : null;
		$minDistance = is_numeric($options["minDistance"] ?? null) ? (float) $options["minDistance"] : null;
		$gameMode = is_string($options["gameMode"] ?? null) ? strtolower($options["gameMode"]) : null;
		$excludeGameModes = array_map(fn($m) => strtolower((string) $m), is_array($options["excludeGameModes"] ?? null) ? $options["excludeGameModes"] : []);

		$matches = [];
		foreach($entities as $entity){
			$typeId = $this->values->entityTypeId($entity);
			if($type !== null && $typeId !== $type){
				continue;
			}
			if(in_array($typeId, $excludeTypes, true)){
				continue;
			}
			$entityName = $entity instanceof Player ? $entity->getName() : $entity->getNameTag();
			if($name !== null && $entityName !== $name){
				continue;
			}
			if(in_array($entityName, $excludeNames, true)){
				continue;
			}
			if(count($tags) > 0 || count($excludeTags) > 0){
				$entityTags = $this->storage->getTags($this->values->scope($entity));
				foreach($tags as $tag){
					if(!in_array($tag, $entityTags, true)){
						continue 2;
					}
				}
				foreach($excludeTags as $tag){
					if(in_array($tag, $entityTags, true)){
						continue 2;
					}
				}
			}
			if($gameMode !== null || count($excludeGameModes) > 0){
				if(!$entity instanceof Player){
					continue;
				}
				$mode = strtolower(self::gameModeName($entity->getGamemode()));
				if(($gameMode !== null && $mode !== $gameMode) || in_array($mode, $excludeGameModes, true)){
					continue;
				}
			}
			$position = $entity->getPosition();
			if($block !== null && !$position->floor()->equals($block)){
				continue;
			}
			if($location !== null){
				$distance = $position->distance($location);
				if(($maxDistance !== null && $distance > $maxDistance) || ($minDistance !== null && $distance < $minDistance)){
					continue;
				}
			}
			$matches[] = $entity;
		}

		$closest = is_numeric($options["closest"] ?? null) ? (int) $options["closest"] : null;
		$farthest = is_numeric($options["farthest"] ?? null) ? (int) $options["farthest"] : null;
		if($location !== null && ($closest !== null || $farthest !== null)){
			usort($matches, fn(Entity $x, Entity $y) : int => $x->getPosition()->distanceSquared($location) <=> $y->getPosition()->distanceSquared($location));
			if($farthest !== null){
				$matches = array_reverse($matches);
			}
			$matches = array_slice($matches, 0, max(0, $closest ?? $farthest ?? 0));
		}

		$result = [];
		foreach($matches as $entity){
			$result[] = $this->values->entityRef($entity);
		}
		return $result;
	}

	private function loadedBlock(mixed $dimension, mixed $location) : ?Block{
		$world = $this->values->world($dimension);
		$position = $this->values->vector($location)->floor();
		$x = (int) $position->x;
		$y = (int) $position->y;
		$z = (int) $position->z;
		if(!$world->isInWorld($x, $y, $z) || !$world->isChunkLoaded($x >> 4, $z >> 4)){
			return null;
		}
		return $world->getBlockAt($x, $y, $z);
	}

	/**
	 * @return array<string, mixed>|null
	 */
	private function getBlock(mixed $dimension, mixed $location) : ?array{
		$block = $this->loadedBlock($dimension, $location);
		return $block === null ? null : $this->values->blockRef($block);
	}

	/**
	 * @return array<string, mixed>|null
	 */
	private function blockInfo(mixed $dimension, mixed $location) : ?array{
		$block = $this->loadedBlock($dimension, $location);
		if($block === null){
			return null;
		}
		$tile = $block->getPosition()->getWorld()->getTile($block->getPosition());
		return [
			"p" => $this->values->permutation($block),
			"l" => $block instanceof Liquid,
			"s" => $block->isSolid(),
			"i" => $tile instanceof ContainerTile
		];
	}

	private function setBlock(mixed $dimension, mixed $location, mixed $permutation) : bool{
		$world = $this->values->world($dimension);
		$position = $this->values->vector($location)->floor();
		if(!$world->isInWorld((int) $position->x, (int) $position->y, (int) $position->z) || !$world->isChunkLoaded(((int) $position->x) >> 4, ((int) $position->z) >> 4)){
			throw new ScriptException("The location is not in a loaded chunk");
		}
		$world->setBlock($position, $this->values->block($permutation));
		return true;
	}

	/**
	 * @return array<string, mixed>|null
	 */
	private function topBlock(mixed $dimension, mixed $x, mixed $z) : ?array{
		$world = $this->values->world($dimension);
		$x = (int) $this->float($x);
		$z = (int) $this->float($z);
		if(!$world->isChunkLoaded($x >> 4, $z >> 4)){
			return null;
		}
		$y = $world->getHighestBlockAt($x, $z);
		return $y === null ? null : $this->values->blockRef($world->getBlockAt($x, $y, $z));
	}

	/**
	 * @return array<string, mixed>|null
	 */
	private function blockItem(mixed $permutation, mixed $amount) : ?array{
		$item = $this->values->block($permutation)->asItem();
		$item->setCount(max(1, $this->int($amount)));
		return $this->values->item($item);
	}

	/**
	 * @return array<string, mixed>
	 */
	private function itemInfo(string $typeId) : array{
		$item = StringToItemParser::getInstance()->parse(ScriptValues::namespaced($typeId));
		if($item === null || $item->isNull()){
			throw new ScriptException("Unknown item type: " . $typeId);
		}
		return [
			"t" => $this->values->itemTypeId($item),
			"m" => $item->getMaxStackSize(),
			"md" => $item instanceof Durable ? $item->getMaxDurability() : 0
		];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function spawnEntity(mixed $dimension, mixed $typeId, mixed $location) : array{
		$world = $this->values->world($dimension);
		$typeId = ScriptValues::namespaced($this->string($typeId));
		if($typeId === "minecraft:player"){
			throw new ScriptException("Players cannot be spawned");
		}
		$position = $this->values->vector($location);
		$nbt = CompoundTag::create()
			->setString(EntityFactory::TAG_IDENTIFIER, $typeId)
			->setTag(Entity::TAG_POS, new ListTag([new DoubleTag($position->x), new DoubleTag($position->y), new DoubleTag($position->z)]))
			->setTag(Entity::TAG_ROTATION, new ListTag([new FloatTag(0.0), new FloatTag(0.0)]));
		$entity = EntityFactory::getInstance()->createFromData($world, $nbt);
		if($entity === null){
			throw new ScriptException("Unknown entity type: " . $typeId);
		}
		$entity->spawnToAll();
		return $this->values->entityRef($entity);
	}

	/**
	 * @return array<string, mixed>|null
	 */
	private function spawnItem(mixed $dimension, mixed $item, mixed $location) : ?array{
		$entity = $this->values->world($dimension)->dropItem($this->values->vector($location), $this->values->decodeItem($item), new Vector3(0, 0, 0), 10);
		return $entity === null ? null : $this->values->entityRef($entity);
	}

	private function runCommand(string $command, ?Entity $source) : int{
		$command = ltrim($command, "/ ");
		if($source instanceof Player){
			$command = str_replace("@s", "\"" . $source->getName() . "\"", $command);
		}
		if($this->commandSender === null){
			$this->commandSender = new ScriptCommandSender($this->server, $this->server->getLanguage());
		}
		return $this->server->dispatchCommand($this->commandSender, $command) ? 1 : 0;
	}

	/**
	 * @param list<Player> $players
	 */
	private function playSound(array $players, mixed $soundId, Vector3 $location, mixed $options) : bool{
		$options = is_array($options) ? $options : [];
		$packet = PlaySoundPacket::create(
			$this->string($soundId),
			$location->x,
			$location->y,
			$location->z,
			is_numeric($options["volume"] ?? null) ? (float) $options["volume"] : 1.0,
			is_numeric($options["pitch"] ?? null) ? (float) $options["pitch"] : 1.0,
			0,
			false,
			null,
			null
		);
		foreach($players as $player){
			if($player->isConnected()){
				$player->getNetworkSession()->sendDataPacket($packet);
			}
		}
		return true;
	}

	private function playPlayerSound(Player $player, mixed $soundId, mixed $options) : bool{
		$options = is_array($options) ? $options : [];
		$location = isset($options["location"]) ? $this->values->vector($options["location"]) : $player->getPosition();
		return $this->playSound([$player], $soundId, $location, $options);
	}

	private function explode(mixed $dimension, mixed $location, mixed $radius, mixed $options) : bool{
		$world = $this->values->world($dimension);
		$vector = $this->values->vector($location);
		$options = is_array($options) ? $options : [];
		$source = isset($options["source"]) ? $this->values->findEntity($options["source"]) : null;
		$explosion = new Explosion(Position::fromObject($vector, $world), max(0.1, $this->float($radius)), $source);
		if(($options["causesFire"] ?? false) === true){
			$explosion->setFireChance(1 / 3);
		}
		if(($options["breaksBlocks"] ?? true) !== false && !$explosion->explodeA()){
			return false;
		}
		return $explosion->explodeB();
	}

	private function getEntityField(Entity $entity, string $field) : mixed{
		$location = $entity->getLocation();
		return match($field){
			"location" => $this->values->vectorOut($location),
			"rotation" => ["x" => $location->getPitch(), "y" => $location->getYaw()],
			"dimension" => $this->values->dimensionId($entity->getWorld()),
			"nameTag" => $entity->getNameTag(),
			"velocity" => $this->values->vectorOut($entity->getMotion()),
			"head" => $this->values->vectorOut($entity->getEyePos()),
			"view" => $this->values->vectorOut($entity->getDirectionVector()),
			"onGround" => $entity->isOnGround(),
			"onFire" => $entity->isOnFire(),
			"sneaking" => $entity instanceof Living && $entity->isSneaking(),
			"sprinting" => $entity instanceof Living && $entity->isSprinting(),
			"swimming" => $entity instanceof Living && $entity->isSwimming(),
			"gliding" => $entity instanceof Living && $entity->isGliding(),
			"underwater" => $entity instanceof Living && $entity->isUnderwater(),
			"flying" => $entity instanceof Player && $entity->isFlying(),
			"selectedSlot" => $entity instanceof Human ? $entity->getInventory()->getHeldItemIndex() : 0,
			"op" => $entity instanceof Player && $this->server->isOp($entity->getName()),
			default => throw new ScriptException("Unknown entity property " . $field)
		};
	}

	private function setEntityField(Entity $entity, string $field, mixed $value) : bool{
		if($field === "nameTag"){
			$entity->setNameTag($this->string($value));
			return true;
		}
		if($field === "sneaking" && $entity instanceof Living){
			$entity->setSneaking($value === true);
			return true;
		}
		throw new ScriptException("Cannot set entity property " . $field);
	}

	private function teleport(Entity $entity, mixed $location, mixed $options) : bool{
		$options = is_array($options) ? $options : [];
		$world = isset($options["dimension"]) ? $this->values->world($options["dimension"]) : $entity->getWorld();
		$vector = $this->values->vector($location);
		$yaw = null;
		$pitch = null;
		if(is_array($options["rotation"] ?? null)){
			$pitch = is_numeric($options["rotation"]["x"] ?? null) ? (float) $options["rotation"]["x"] : null;
			$yaw = is_numeric($options["rotation"]["y"] ?? null) ? (float) $options["rotation"]["y"] : null;
		}
		$velocity = $entity->getMotion();
		$result = $entity->teleport(Position::fromObject($vector, $world), $yaw, $pitch);
		if($result && isset($options["facingLocation"])){
			if($entity instanceof Living){
				$entity->lookAt($this->values->vector($options["facingLocation"]));
			}
		}
		if($result && ($options["keepVelocity"] ?? false) === true){
			$entity->setMotion($velocity);
		}
		return $result;
	}

	private function impulse(Entity $entity, Vector3 $impulse) : bool{
		$entity->addMotion($impulse->x, $impulse->y, $impulse->z);
		return true;
	}

	private function triggerEvent(Entity $entity, string $event) : bool{
		return $entity instanceof BehaviorEntity && $entity->triggerEvent($event);
	}

	private function kill(Entity $entity) : bool{
		if(!$entity->isAlive()){
			return false;
		}
		$entity->kill();
		return true;
	}

	private function remove(Entity $entity) : bool{
		if($entity instanceof Player){
			throw new ScriptException("Players cannot be removed");
		}
		$entity->flagForDespawn();
		return true;
	}

	private function damage(Entity $entity, mixed $amount, mixed $options) : bool{
		$options = is_array($options) ? $options : [];
		$cause = self::DAMAGE_CAUSES[is_string($options["cause"] ?? null) ? $options["cause"] : "none"] ?? EntityDamageEvent::CAUSE_CUSTOM;
		$damager = isset($options["damagingEntity"]) ? $this->values->findEntity($options["damagingEntity"]) : null;
		if($damager !== null){
			$event = new EntityDamageByEntityEvent($damager, $entity, $cause === EntityDamageEvent::CAUSE_CUSTOM ? EntityDamageEvent::CAUSE_ENTITY_ATTACK : $cause, $this->float($amount));
		}else{
			$event = new EntityDamageEvent($entity, $cause, $this->float($amount));
		}
		$entity->attack($event);
		return !$event->isCancelled();
	}

	private function addEffect(Living $entity, mixed $type, mixed $duration, mixed $amplifier, mixed $particles) : bool{
		$effect = $this->values->effect($type);
		$instance = new EffectInstance($effect, max(1, $this->int($duration)), max(0, min(255, $this->int($amplifier))), $particles !== false);
		return $entity->getEffects()->add($instance);
	}

	private function removeEffect(Living $entity, mixed $type) : bool{
		$effect = $this->values->effect($type);
		if(!$entity->getEffects()->has($effect)){
			return false;
		}
		$entity->getEffects()->remove($effect);
		return true;
	}

	/**
	 * @return array<string, mixed>|null
	 */
	private function getEffect(Living $entity, mixed $type) : ?array{
		$instance = $entity->getEffects()->get($this->values->effect($type));
		return $instance === null ? null : $this->effectData($instance);
	}

	/**
	 * @return list<array<string, mixed>>
	 */
	private function getEffects(Living $entity) : array{
		$effects = [];
		foreach($entity->getEffects()->all() as $instance){
			$effects[] = $this->effectData($instance);
		}
		return $effects;
	}

	/**
	 * @return array<string, mixed>
	 */
	private function effectData(EffectInstance $instance) : array{
		$name = $instance->getType()->getName();
		return [
			"t" => $this->values->effectName($instance->getType()),
			"d" => $instance->getDuration(),
			"a" => $instance->getAmplifier(),
			"n" => is_string($name) ? $name : $name->getText()
		];
	}

	private function hasComponent(Entity $entity, string $name) : bool{
		return match(ScriptValues::namespaced($name)){
			"minecraft:health" => $entity instanceof Living,
			"minecraft:inventory" => $entity instanceof Human,
			"minecraft:equippable" => $entity instanceof Living,
			"minecraft:onfire" => $entity->isOnFire(),
			default => false
		};
	}

	/**
	 * @return array<string, float>
	 */
	private function health(Entity $entity) : array{
		return ["c" => $entity->getHealth(), "m" => (float) $entity->getMaxHealth()];
	}

	private function setHealth(Entity $entity, mixed $value) : bool{
		$health = $this->float($value);
		if($health <= 0){
			$entity->kill();
			return true;
		}
		$entity->setHealth(min($health, (float) $entity->getMaxHealth()));
		return true;
	}

	/**
	 * @return array<string, mixed>|null
	 */
	private function getEquipment(Entity $entity, string $slot) : ?array{
		if(!$entity instanceof Living){
			throw new ScriptException("The entity has no equipment");
		}
		if(isset(self::EQUIPMENT_SLOTS[$slot])){
			return $this->values->item($entity->getArmorInventory()->getItem(self::EQUIPMENT_SLOTS[$slot]));
		}
		if(!$entity instanceof Human){
			return null;
		}
		return match($slot){
			"Mainhand" => $this->values->item($entity->getInventory()->getItemInHand()),
			"Offhand" => $this->values->item($entity->getOffHandInventory()->getItem(0)),
			default => throw new ScriptException("Unknown equipment slot " . $slot)
		};
	}

	private function setEquipment(Entity $entity, string $slot, mixed $item) : bool{
		if(!$entity instanceof Living){
			throw new ScriptException("The entity has no equipment");
		}
		$decoded = $this->values->decodeItem($item);
		if(isset(self::EQUIPMENT_SLOTS[$slot])){
			$entity->getArmorInventory()->setItem(self::EQUIPMENT_SLOTS[$slot], $decoded);
			return true;
		}
		if(!$entity instanceof Human){
			return false;
		}
		match($slot){
			"Mainhand" => $entity->getInventory()->setItemInHand($decoded),
			"Offhand" => $entity->getOffHandInventory()->setItem(0, $decoded),
			default => throw new ScriptException("Unknown equipment slot " . $slot)
		};
		return true;
	}

	private function setOnFire(Entity $entity, mixed $seconds) : bool{
		$duration = $this->int($seconds);
		if($duration <= 0){
			$entity->extinguish();
			return false;
		}
		$entity->setOnFire($duration);
		return true;
	}

	/**
	 * @param array<string, mixed>|mixed $options
	 */
	private function sendTitle(Player $player, string $title, mixed $options) : bool{
		$options = is_array($options) ? $options : [];
		$player->sendTitle(
			$title,
			is_string($options["subtitle"] ?? null) ? $options["subtitle"] : "",
			is_numeric($options["fadeInDuration"] ?? null) ? (int) $options["fadeInDuration"] : -1,
			is_numeric($options["stayDuration"] ?? null) ? (int) $options["stayDuration"] : -1,
			is_numeric($options["fadeOutDuration"] ?? null) ? (int) $options["fadeOutDuration"] : -1
		);
		return true;
	}

	private function setGameMode(Player $player, string $mode) : bool{
		$gameMode = GameMode::fromString(strtolower($mode));
		if($gameMode === null){
			throw new ScriptException("Unknown game mode " . $mode);
		}
		return $player->setGamemode($gameMode);
	}

	private function addExperience(Player $player, int $amount) : int{
		$manager = $player->getXpManager();
		if($amount >= 0){
			$manager->addXp($amount);
		}else{
			$manager->subtractXp(min(abs($amount), $manager->getCurrentTotalXp()));
		}
		return $manager->getCurrentTotalXp();
	}

	private function addLevels(Player $player, int $amount) : int{
		$manager = $player->getXpManager();
		if($amount >= 0){
			$manager->addXpLevels($amount);
		}else{
			$manager->subtractXpLevels(min(abs($amount), $manager->getXpLevel()));
		}
		return $manager->getXpLevel();
	}

	/**
	 * @return array<string, int|float>
	 */
	private function experience(Player $player) : array{
		$manager = $player->getXpManager();
		return [
			"level" => $manager->getXpLevel(),
			"total" => $manager->getCurrentTotalXp(),
			"remainder" => $manager->getRemainderXp(),
			"progress" => $manager->getXpProgress()
		];
	}

	/**
	 * @return array<string, mixed>|null
	 */
	private function spawnPoint(Player $player) : ?array{
		$spawn = $player->getSpawn();
		if(!$spawn instanceof Position || !$spawn->isValid()){
			return null;
		}
		$data = $this->values->vectorOut($spawn);
		$data["dimension"] = ["\$d" => $this->values->dimensionId($spawn->getWorld())];
		return $data;
	}

	private function setSpawnPoint(Player $player, mixed $point) : bool{
		if(!is_array($point)){
			$player->setSpawn(null);
			return true;
		}
		$world = isset($point["dimension"]) ? $this->values->world($point["dimension"]) : $player->getWorld();
		$player->setSpawn(Position::fromObject($this->values->vector($point), $world));
		return true;
	}

	private function sendForm(Player $player, int $formId, mixed $data) : bool{
		if(!is_array($data)){
			throw new ScriptException("Invalid form");
		}
		$player->sendForm(new ScriptForm($data, function(Player $player, mixed $response) use ($formId) : void{
			$this->loader->queueEvent("__form", ["id" => $formId, "r" => $response]);
		}));
		return true;
	}

	private function findInventory(mixed $ref) : ?Inventory{
		if(is_array($ref) && is_array($ref["\$c"] ?? null)){
			$ref = $ref["\$c"];
		}
		if(!is_array($ref)){
			return null;
		}
		if(isset($ref["e"])){
			$entity = $this->values->findEntity($ref["e"]);
			return $entity instanceof Human ? $entity->getInventory() : null;
		}
		if(is_array($ref["b"] ?? null) && count($ref["b"]) === 4){
			[$dimension, $x, $y, $z] = $ref["b"];
			$block = $this->loadedBlock($dimension, ["x" => $x, "y" => $y, "z" => $z]);
			if($block === null){
				return null;
			}
			$tile = $block->getPosition()->getWorld()->getTile($block->getPosition());
			return $tile instanceof ContainerTile ? $tile->getInventory() : null;
		}
		return null;
	}

	private function inventory(mixed $ref) : Inventory{
		$inventory = $this->findInventory($ref);
		if($inventory === null){
			throw new ScriptException("The container is no longer valid");
		}
		return $inventory;
	}

	private function slot(mixed $ref, mixed $slot) : int{
		$index = $this->int($slot);
		if($index < 0 || $index >= $this->inventory($ref)->getSize()){
			throw new ScriptException("Slot " . $index . " is out of bounds");
		}
		return $index;
	}

	private function emptySlots(Inventory $inventory) : int{
		$count = 0;
		for($slot = 0, $size = $inventory->getSize(); $slot < $size; ++$slot){
			if($inventory->isSlotEmpty($slot)){
				++$count;
			}
		}
		return $count;
	}

	/**
	 * @return array<string, mixed>|null
	 */
	private function addItem(Inventory $inventory, mixed $item) : ?array{
		$leftovers = $inventory->addItem($this->values->decodeItem($item));
		foreach($leftovers as $leftover){
			if(!$leftover->isNull()){
				return $this->values->item($leftover);
			}
		}
		return null;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script;

use behaviorpack\script\binding\ClassFactory;
use behaviorpack\script\binding\ServerModule;
use behaviorpack\script\binding\UiModule;
use behaviorpack\script\js\Interpreter;
use behaviorpack\script\js\JsCallable;
use behaviorpack\script\js\JsObject;
use behaviorpack\script\js\JsPromise;
use behaviorpack\script\js\JsThrow;
use behaviorpack\script\js\ModuleLoader;
use behaviorpack\script\js\ScriptTimeoutException;
use behaviorpack\script\js\SyntaxErrorException;
use Closure;
use Logger;
use Throwable;
use function array_keys;
use function array_shift;
use function count;
use function hrtime;
use function implode;
use function is_array;
use function is_string;
use function max;
use function substr;
use function ucfirst;

/**
 * Runs the scripts of the behavior packs in the embedded JavaScript
 * interpreter. Every entry into script code goes through run(): it names
 * the pack, starts the watchdog for the outermost entry, runs the promise
 * jobs, and disables the pack whose code exceeds the watchdog delay.
 */
final class ScriptRuntime{

	public const WATCHDOG_MS = 2000;

	private const JOB_BUDGET_NS = 4_000_000;

	public Interpreter $js;
	public ServerModule $server;
	private ModuleLoader $modules;

	/** @var array<string, list<array{0: JsCallable, 1: ?string}>> */
	private array $subscribers = [];

	/** @var array<int, array{callback: JsCallable, due: int, interval: int, pack: ?string}> */
	private array $timers = [];

	/** @var array<int, array{generator: JsObject, pack: ?string}> */
	private array $jobs = [];

	private int $nextRunId = 1;

	public int $currentTick = 0;

	/** @var list<array<string, mixed>> */
	private array $localEvents = [];

	/** @var array<string, array{0: JsObject, 1: ?string}> */
	private array $blockComponents = [];

	/** @var array<string, array{0: JsObject, 1: ?string}> */
	private array $itemComponents = [];

	/** @var array<string, array<string, mixed>> */
	private array $declaredBlocks = [];

	/** @var array<string, array<string, mixed>> */
	private array $declaredItems = [];

	/** @var array<string, true> */
	private array $blockHooks = [];

	/** @var array<string, true> */
	private array $itemHooks = [];

	/** @var array<int, array{promise: JsPromise, player: int, map: Closure, pack: ?string}> */
	private array $forms = [];

	private int $nextFormId = 1;

	/** @var array<string, true> */
	private array $missing = [];

	/** @var list<string> */
	private array $newMissing = [];

	/** @var array<string, true> */
	private array $disabled = [];

	private bool $allDisabled = false;

	private int $depth = 0;

	public bool $scoreboardDirty = false;

	public function __construct(
		public ScriptApi $api,
		public ScriptStorage $storage,
		private Logger $logger,
		private int $watchdogMs = self::WATCHDOG_MS
	){
		$this->js = new Interpreter();
		$this->js->printer = function(string $level, string $message, ?string $context) : void{
			$this->log($level, ($context !== null ? "[" . $context . "] " : "") . $message);
		};
		$factory = new ClassFactory($this->js, function(string $name) : void{
			$this->recordMissing($name);
		});
		$this->server = new ServerModule($this, $factory);
		$ui = new UiModule($this, $factory, $this->server);
		$this->modules = new ModuleLoader($this->js, function(string $module, string $name) : mixed{
			return $this->server->stub($module . ":" . $name);
		});
		$this->modules->addHostModule("@minecraft/server", $this->server->exports(), function(string $name) : mixed{
			return $this->server->stub($name);
		});
		$this->modules->addHostModule("@minecraft/server-ui", $ui->exports(), function(string $name) : mixed{
			return $this->server->stub("@minecraft/server-ui:" . $name);
		});
		$this->modules->addHostModule("@minecraft/common", $this->commonExports(), function(string $name) : mixed{
			return $this->server->stub("@minecraft/common:" . $name);
		});
	}

	/**
	 * @return array<string, mixed>
	 */
	private function commonExports() : array{
		$exports = [];
		foreach(["ArgumentOutOfBoundsError", "EngineError", "InvalidArgumentError", "PropertyOutOfBoundsError", "UnsupportedFunctionalityError"] as $name){
			$exports[$name] = $this->server->exports()[$name] ?? $this->js->constructors["Error"];
		}
		return $exports;
	}

	public function log(string $level, string $message) : void{
		$message = "[script] " . $message;
		match($level){
			"error" => $this->logger->error($message),
			"warning" => $this->logger->warning($message),
			"debug" => $this->logger->debug($message),
			default => $this->logger->info($message)
		};
	}

	public function recordMissing(string $name) : void{
		if(!isset($this->missing[$name])){
			$this->missing[$name] = true;
			$this->newMissing[] = $name;
		}
	}

	/**
	 * Runs script code for a pack, with the watchdog, error reporting and
	 * the promise jobs.
	 *
	 * @param Closure() : void $code
	 */
	public function run(?string $pack, Closure $code) : void{
		if($this->allDisabled || ($pack !== null && isset($this->disabled[$pack]))){
			return;
		}
		$outer = $this->depth === 0;
		$savedContext = $this->js->context;
		$this->js->context = $pack;
		if($outer){
			$this->js->resetStack();
			$this->js->startWatchdog($this->watchdogMs);
		}
		$this->depth++;
		$failure = null;
		$offender = $pack;
		try{
			try{
				$code();
			}catch(JsThrow $e){
				$this->js->reportError($e->value);
			}catch(SyntaxErrorException $e){
				$this->log("error", ($pack !== null ? "[" . $pack . "] " : "") . "SyntaxError: " . $e->getMessage());
			}
			if($outer){
				$this->js->drainJobs();
			}
		}catch(ScriptTimeoutException $e){
			$failure = $e;
			$offender = $this->js->context ?? $pack;
		}catch(Throwable $e){
			$failure = $e;
		}
		$this->depth--;
		$this->js->context = $savedContext;
		if($outer){
			$this->js->stopWatchdog();
		}
		if($failure !== null){
			if(!$outer){
				throw $failure;
			}
			if($failure instanceof ScriptTimeoutException){
				$this->disable($offender);
			}else{
				$this->logger->error("Scripts: internal error while running " . ($pack === null ? "scripts" : "pack \"" . $pack . "\""));
				$this->logger->logException($failure);
			}
		}
		if($outer){
			$this->afterEntry();
		}
	}

	private function afterEntry() : void{
		$this->js->reportUnhandledRejections();
		if(count($this->newMissing) > 0){
			$this->logger->warning("Scripts: unimplemented API used: " . implode(", ", $this->newMissing));
			$this->newMissing = [];
		}
		if($this->scoreboardDirty){
			$this->scoreboardDirty = false;
			$this->storage->setScoreboard($this->server->scoreboardData);
		}
	}

	private function disable(?string $pack) : void{
		if($pack === null){
			$this->logger->error("Scripts: a script did not return control within " . $this->watchdogMs . " ms, scripts are disabled");
			$this->allDisabled = true;
			$this->subscribers = [];
			$this->timers = [];
			$this->jobs = [];
			$this->blockHooks = [];
			$this->itemHooks = [];
			return;
		}
		$this->logger->error("Scripts: pack \"" . $pack . "\" did not return control within " . $this->watchdogMs . " ms, its scripts are disabled");
		$this->disabled[$pack] = true;
		foreach($this->subscribers as $key => $list){
			$kept = [];
			foreach($list as $subscriber){
				if($subscriber[1] !== $pack){
					$kept[] = $subscriber;
				}
			}
			$this->subscribers[$key] = $kept;
		}
		foreach($this->timers as $id => $timer){
			if($timer["pack"] === $pack){
				unset($this->timers[$id]);
			}
		}
		foreach($this->jobs as $id => $job){
			if($job["pack"] === $pack){
				unset($this->jobs[$id]);
			}
		}
		foreach([&$this->blockComponents, &$this->itemComponents] as &$registry){
			foreach($registry as $name => $entry){
				if($entry[1] === $pack){
					unset($registry[$name]);
				}
			}
		}
		unset($registry);
		$this->js->removeJobs($pack);
		$this->computeHooks();
	}

	/**
	 * Imports the entry module of every pack, then fires the startup and
	 * world load events. Returns the number of packs whose scripts loaded.
	 *
	 * @param list<array{name: string, root: string, entry: string}> $entries
	 * @param array<string, array<string, mixed>>                   $blocks
	 * @param array<string, array<string, mixed>>                   $items
	 */
	public function start(array $entries, array $blocks, array $items, int $tick) : int{
		$this->declaredBlocks = $blocks;
		$this->declaredItems = $items;
		$this->currentTick = $tick;
		$loaded = 0;
		foreach($entries as $entry){
			$this->run($entry["name"], function() use ($entry, &$loaded) : void{
				$this->modules->run($entry["entry"], $entry["root"], $entry["name"]);
				$loaded++;
			});
		}
		$registries = $this->server->createRegistries();
		$startup = $this->server->createEvent("StartupEvent", $registries, false);
		foreach($this->subscribers["system.startup"] ?? [] as [$callback, $pack]){
			$this->run($pack, function() use ($callback, $startup) : void{
				$this->js->call($callback, null, [$startup]);
			});
		}
		$this->dispatch("after.worldInitialize", "WorldInitializeAfterEvent", [
			"blockComponentRegistry" => $registries["blockComponentRegistry"],
			"itemComponentRegistry" => $registries["itemComponentRegistry"]
		], false);
		$this->dispatch("after.worldLoad", "WorldLoadAfterEvent", [], false);
		return $loaded;
	}

	public function subscribe(string $key, JsCallable $callback) : void{
		if($this->js->context !== null && isset($this->disabled[$this->js->context])){
			return;
		}
		$this->subscribers[$key][] = [$callback, $this->js->context];
	}

	public function unsubscribe(string $key, JsCallable $callback) : void{
		$kept = [];
		foreach($this->subscribers[$key] ?? [] as $subscriber){
			if($subscriber[0] !== $callback){
				$kept[] = $subscriber;
			}
		}
		$this->subscribers[$key] = $kept;
	}

	public function wants(string $key) : bool{
		return count($this->subscribers[$key] ?? []) > 0;
	}

	/**
	 * Dispatches an event to its subscribers and returns the event object,
	 * or null when nobody subscribed.
	 *
	 * @param array<string, mixed> $data
	 */
	public function dispatch(string $key, string $className, array $data, bool $before) : ?JsObject{
		$subscribers = $this->subscribers[$key] ?? [];
		if(count($subscribers) === 0){
			return null;
		}
		$event = $this->server->createEvent($className, $data, $before);
		foreach($subscribers as [$callback, $pack]){
			$this->run($pack, function() use ($callback, $event) : void{
				$this->js->call($callback, null, [$event]);
			});
		}
		return $event;
	}

	/**
	 * Dispatches a before-event and returns whether it was cancelled and the
	 * changed fields.
	 *
	 * @param array<string, mixed> $data
	 * @return array{c: bool, m?: array<string, mixed>}|null
	 */
	public function dispatchBefore(string $name, array $data) : ?array{
		$event = $this->dispatch("before." . $name, ucfirst($name) . "BeforeEvent", $data, true);
		if($event === null){
			return null;
		}
		$result = ["c" => $this->js->toBoolean($event->props["cancel"] ?? false)];
		if($name === "chatSend"){
			$message = $event->props["message"] ?? null;
			if(is_string($message) && $message !== ($data["message"] ?? null)){
				$result["m"] = ["message" => $message];
			}
		}
		return $result;
	}

	public function hasPending() : bool{
		return count($this->timers) > 0 || count($this->jobs) > 0 || count($this->localEvents) > 0 || $this->js->hasJobs();
	}

	/**
	 * Runs a server tick: queued events, script events, due timers and jobs.
	 *
	 * @param list<array{e: string, d: mixed}> $events
	 */
	public function tick(int $tick, array $events) : void{
		$this->currentTick = $tick;
		foreach($events as $entry){
			$this->dispatchQueued($entry["e"], is_array($entry["d"]) ? $entry["d"] : []);
		}
		while(count($this->localEvents) > 0){
			$event = \array_shift($this->localEvents);
			$this->dispatch("after.scriptEventReceive", "ScriptEventCommandMessageAfterEvent", $event, false);
		}
		$this->runTimers();
		$this->runJobs();
	}

	/**
	 * @param array<string, mixed> $data
	 */
	private function dispatchQueued(string $name, array $data) : void{
		switch($name){
			case "__form":
				$this->resolveForm((int) ($data["id"] ?? 0), $data["r"] ?? null);
				return;
			case "__quit":
				$player = (int) ($data["id"] ?? 0);
				foreach($this->forms as $id => $form){
					if($form["player"] === $player){
						$this->resolveForm($id, null);
					}
				}
				$this->server->forgetEntity($player);
				return;
			case "__comp":
				$this->dispatchComponent($data);
				return;
		}
		$this->dispatch("after." . $name, ucfirst($name) . "AfterEvent", $data, false);
		if($name === "entityDie"){
			$dead = $data["deadEntity"] ?? null;
			if(is_array($dead) && ($dead["t"] ?? null) !== "minecraft:player" && isset($dead['$e'])){
				$this->server->forgetEntity((int) $dead['$e']);
			}
		}
	}

	public function sendScriptEvent(string $id, string $message, ?array $source = null) : void{
		$event = ["id" => $id, "message" => $message, "sourceType" => $source === null ? "Server" : "Entity"];
		if($source !== null){
			$event["sourceEntity"] = $source;
		}
		$this->localEvents[] = $event;
	}

	public function schedule(JsCallable $callback, int $delay, int $interval) : int{
		$id = $this->nextRunId++;
		$this->timers[$id] = ["callback" => $callback, "due" => $this->currentTick + max(1, $delay), "interval" => $interval, "pack" => $this->js->context];
		return $id;
	}

	public function clearRun(int $id) : void{
		unset($this->timers[$id]);
	}

	public function addJob(JsObject $generator) : int{
		$id = $this->nextRunId++;
		$this->jobs[$id] = ["generator" => $generator, "pack" => $this->js->context];
		return $id;
	}

	public function clearJob(int $id) : void{
		unset($this->jobs[$id]);
	}

	private function runTimers() : void{
		$due = [];
		foreach($this->timers as $id => $timer){
			if($timer["due"] <= $this->currentTick){
				$due[] = $id;
			}
		}
		foreach($due as $id){
			$timer = $this->timers[$id] ?? null;
			if($timer === null){
				continue;
			}
			if($timer["interval"] > 0){
				$this->timers[$id]["due"] = max($timer["due"] + $timer["interval"], $this->currentTick + 1);
			}else{
				unset($this->timers[$id]);
			}
			$callback = $timer["callback"];
			$this->run($timer["pack"], function() use ($callback) : void{
				$this->js->call($callback, null, []);
			});
		}
	}

	private function runJobs() : void{
		$deadline = hrtime(true) + self::JOB_BUDGET_NS;
		while(count($this->jobs) > 0 && hrtime(true) < $deadline){
			foreach(array_keys($this->jobs) as $id){
				$job = $this->jobs[$id] ?? null;
				if($job === null){
					continue;
				}
				$this->run($job["pack"], function() use ($id, $job) : void{
					try{
						$generator = $job["generator"];
						$result = $this->js->call($this->js->get($generator, "next"), $generator, []);
						if(!$result instanceof JsObject || $this->js->toBoolean($this->js->get($result, "done"))){
							unset($this->jobs[$id]);
						}
					}catch(JsThrow $e){
						unset($this->jobs[$id]);
						throw $e;
					}
				});
				if(hrtime(true) >= $deadline){
					break;
				}
			}
		}
	}

	public function registerComponent(string $kind, string $name, JsObject $component) : void{
		$entry = [$component, $this->js->context];
		if($kind === "block"){
			$this->blockComponents[$name] = $entry;
		}else{
			$this->itemComponents[$name] = $entry;
		}
		$this->computeHooks();
	}

	private function computeHooks() : void{
		$this->blockHooks = [];
		foreach($this->declaredBlocks as $typeId => $declared){
			foreach($declared as $name => $params){
				if(isset($this->blockComponents[$name])){
					$this->blockHooks[$typeId] = true;
					break;
				}
			}
		}
		$this->itemHooks = [];
		foreach($this->declaredItems as $typeId => $declared){
			foreach($declared as $name => $params){
				if(isset($this->itemComponents[$name])){
					$this->itemHooks[$typeId] = true;
					break;
				}
			}
		}
	}

	public function hasBlockHook(string $typeId) : bool{
		return isset($this->blockHooks[$typeId]);
	}

	public function hasItemHook(string $typeId) : bool{
		return isset($this->itemHooks[$typeId]);
	}

	/**
	 * @param array<string, mixed> $message
	 */
	private function dispatchComponent(array $message) : void{
		$block = ($message["k"] ?? null) === "b";
		$typeId = (string) ($message["t"] ?? "");
		$hook = (string) ($message["h"] ?? "");
		$declared = $block ? ($this->declaredBlocks[$typeId] ?? null) : ($this->declaredItems[$typeId] ?? null);
		if($declared === null){
			return;
		}
		$registry = $block ? $this->blockComponents : $this->itemComponents;
		$data = is_array($message["d"] ?? null) ? $message["d"] : [];
		$className = ($block ? "BlockComponent" : "ItemComponent") . ucfirst(\substr($hook, 2)) . "Event";
		foreach($declared as $name => $params){
			$entry = $registry[$name] ?? null;
			if($entry === null){
				continue;
			}
			[$component, $pack] = $entry;
			$this->run($pack, function() use ($component, $hook, $className, $data, $params) : void{
				$function = $this->js->get($component, $hook);
				if(!$function instanceof JsCallable){
					return;
				}
				$event = $this->server->createEvent($className, $data, false);
				$parameters = $this->js->newObject();
				$parameters->props["params"] = $this->server->toJs($params);
				$this->js->call($function, $component, [$event, $parameters]);
			});
		}
	}

	/**
	 * Shows a form to a player and returns the promise resolved with the
	 * response built by $map from the raw form data.
	 *
	 * @param array<string, mixed>        $form
	 * @param Closure(mixed) : JsObject   $map
	 */
	public function showForm(int $player, array $form, Closure $map) : JsPromise{
		$promise = $this->js->newPromise();
		$id = $this->nextFormId++;
		$this->forms[$id] = ["promise" => $promise, "player" => $player, "map" => $map, "pack" => $this->js->context];
		try{
			$this->api->handle("pl.form", [$player, $id, $form]);
		}catch(ScriptException $e){
			unset($this->forms[$id]);
			$this->js->rejectPromise($promise, $this->js->makeError("Error", $e->getMessage()));
		}
		return $promise;
	}

	private function resolveForm(int $id, mixed $raw) : void{
		$form = $this->forms[$id] ?? null;
		if($form === null){
			return;
		}
		unset($this->forms[$id]);
		$this->run($form["pack"], function() use ($form, $raw) : void{
			$this->js->resolvePromise($form["promise"], ($form["map"])($raw));
		});
	}

	public function isStopped() : bool{
		return $this->allDisabled;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script;

use behaviorpack\BehaviorPack;
use behaviorpack\BehaviorPackException;
use behaviorpack\ContentLoader;
use pocketmine\entity\Entity;
use pocketmine\plugin\PluginBase;
use pocketmine\scheduler\ClosureTask;
use pocketmine\scheduler\TaskHandler;
use Throwable;
use function count;
use function is_array;
use function is_file;
use function is_string;
use function rtrim;
use function str_replace;
use function str_starts_with;

/**
 * Runs the scripts of the behavior packs (the "script" modules of their
 * manifests) with the embedded JavaScript interpreter, which provides
 * @minecraft/server and @minecraft/server-ui. Server events are turned into
 * script events by ScriptEventListener; after-events are delivered on the
 * next tick, together with the due timers.
 */
final class ScriptLoader implements ContentLoader{

	private const SAVE_INTERVAL = 1200;

	private ScriptValues $values;
	private ScriptStorage $storage;
	private ScriptApi $api;
	private ?ScriptRuntime $runtime = null;
	private ?TaskHandler $tickTask = null;

	/** @var list<array{e: string, d: mixed}> */
	private array $queue = [];

	/** @var array<string, array<string, mixed>> */
	private array $blockComponents = [];

	/** @var array<string, array<string, mixed>> */
	private array $itemComponents = [];

	private bool $closed = false;

	public function __construct(
		private PluginBase $plugin,
		private int $timeoutMs = ScriptRuntime::WATCHDOG_MS
	){
		$this->values = new ScriptValues($plugin->getServer());
		$this->storage = new ScriptStorage(rtrim(str_replace("\\", "/", $plugin->getDataFolder()), "/") . "/scripts");
		$this->api = new ScriptApi($this, $this->values, $this->storage);
	}

	public function getName() : string{
		return "scripts";
	}

	public function requiresCustomies() : bool{
		return false;
	}

	public function load(array $packs) : void{
		$entries = [];
		foreach($packs as $pack){
			$entry = $pack->getScriptEntry();
			if($entry === null){
				continue;
			}
			$path = $pack->getPath() . "/" . $entry;
			if(!is_file($path)){
				$this->plugin->getLogger()->warning("Behavior packs: script entry " . $entry . " not found in " . $pack->getName());
				continue;
			}
			$entries[] = ["name" => $pack->getName(), "root" => $pack->getPath(), "entry" => $path];
			$this->collectComponents($pack);
		}
		if(count($entries) === 0){
			return;
		}
		$this->storage->load();
		$this->plugin->getScheduler()->scheduleDelayedTask(new ClosureTask(function() use ($entries) : void{
			$this->start($entries);
		}), 1);
	}

	/**
	 * @param list<array{name: string, root: string, entry: string}> $entries
	 */
	private function start(array $entries) : void{
		if($this->closed){
			return;
		}
		$server = $this->plugin->getServer();
		try{
			$this->runtime = new ScriptRuntime($this->api, $this->storage, $this->plugin->getLogger(), $this->timeoutMs);
		}catch(Throwable $e){
			$this->plugin->getLogger()->error("Behavior packs: the script runtime could not start");
			$this->plugin->getLogger()->logException($e);
			return;
		}
		(new ScriptEventListener($this, $this->values, $this->storage))->register($this->plugin);
		$server->getCommandMap()->register($this->plugin->getName(), new ScriptEventCommand($this->plugin, $this));
		$loaded = $this->runtime->start($entries, $this->blockComponents, $this->itemComponents, $server->getTick());
		$this->tickTask = $this->plugin->getScheduler()->scheduleRepeatingTask(new ClosureTask(function() : void{
			$this->tick();
		}), 1);
		$this->plugin->getLogger()->info("Behavior packs: " . $loaded . " scripts running");
	}

	public function close() : void{
		$this->closed = true;
		if($this->tickTask !== null){
			$this->tickTask->cancel();
			$this->tickTask = null;
		}
		$this->queue = [];
		$this->storage->save();
	}

	private function tick() : void{
		$tick = $this->plugin->getServer()->getTick();
		if($tick % self::SAVE_INTERVAL === 0){
			$this->storage->save();
		}
		$runtime = $this->runtime;
		if($runtime === null || $runtime->isStopped()){
			$this->queue = [];
			return;
		}
		if(count($this->queue) === 0 && !$runtime->hasPending()){
			return;
		}
		$events = $this->queue;
		$this->queue = [];
		$runtime->tick($tick, $events);
	}

	/**
	 * Whether a script subscribed to an event, named "after.<event>" or
	 * "before.<event>".
	 */
	public function wants(string $event) : bool{
		return $this->runtime !== null && $this->runtime->wants($event);
	}

	public function hasBlockHook(string $typeId) : bool{
		return $this->runtime !== null && $this->runtime->hasBlockHook($typeId);
	}

	public function hasItemHook(string $typeId) : bool{
		return $this->runtime !== null && $this->runtime->hasItemHook($typeId);
	}

	/**
	 * Queues an event delivered to the scripts on the next tick.
	 */
	public function queueEvent(string $event, mixed $data) : void{
		if($this->runtime !== null && !$this->runtime->isStopped()){
			$this->queue[] = ["e" => $event, "d" => $data];
		}
	}

	/**
	 * Dispatches a before-event right away and returns whether a script
	 * cancelled it ("c") and the fields it changed ("m").
	 *
	 * @param array<string, mixed> $data
	 * @return array<string, mixed>|null
	 */
	public function dispatchSync(string $event, array $data) : ?array{
		if($this->runtime === null || $this->runtime->isStopped()){
			return null;
		}
		return $this->runtime->dispatchBefore($event, $data);
	}

	public function sendScriptEvent(string $id, string $message, ?Entity $source) : void{
		if(!$this->wants("after.scriptEventReceive") || $this->runtime === null){
			return;
		}
		$this->runtime->sendScriptEvent($id, $message, $source === null ? null : $this->values->entityRef($source));
	}

	/**
	 * Records the custom components declared by the blocks and items of a
	 * pack, with their parameters: the names listed in
	 * "minecraft:custom_components" and the non-vanilla component keys.
	 */
	private function collectComponents(BehaviorPack $pack) : void{
		foreach(["blocks" => "minecraft:block", "items" => "minecraft:item"] as $directory => $root){
			foreach($pack->listFiles($directory) as $file){
				try{
					$json = BehaviorPack::readJson($file);
				}catch(BehaviorPackException){
					continue;
				}
				$definition = $json[$root] ?? null;
				$typeId = is_array($definition) ? ($definition["description"]["identifier"] ?? null) : null;
				if(!is_string($typeId)){
					continue;
				}
				$groups = [$definition["components"] ?? []];
				foreach(is_array($definition["permutations"] ?? null) ? $definition["permutations"] : [] as $permutation){
					if(is_array($permutation)){
						$groups[] = $permutation["components"] ?? [];
					}
				}
				$declared = [];
				foreach($groups as $components){
					if(!is_array($components)){
						continue;
					}
					foreach($components as $key => $value){
						$key = (string) $key;
						if($key === "minecraft:custom_components" && is_array($value)){
							foreach($value as $name){
								if(is_string($name) && !isset($declared[$name])){
									$declared[$name] = null;
								}
							}
						}elseif(!str_starts_with($key, "minecraft:") && !str_starts_with($key, "tag:")){
							$declared[$key] = $value;
						}
					}
				}
				if(count($declared) === 0){
					continue;
				}
				if($directory === "blocks"){
					$this->blockComponents[$typeId] = $declared;
				}else{
					$this->itemComponents[$typeId] = $declared;
				}
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use Closure;
use function in_array;

/**
 * A module record. Exports map an exported name to a binding:
 * ["local", Scope, name], ["value", value], ["reexport", Module, name] or
 * ["namespace", Module]. Host modules have value bindings only, and may
 * provide stand-ins for the names they do not export through $missing.
 */
final class Module{

	public const NEW = 0;
	public const EVALUATING = 1;
	public const EVALUATED = 2;
	public const FAILED = 3;

	public int $status = self::NEW;

	public ?Scope $scope = null;

	/** @var array<string, mixed>|null */
	public ?array $node = null;

	/** @var array<string, array<int, mixed>> */
	public array $exports = [];

	/** @var list<Module> */
	public array $starExports = [];

	public ?JsObject $namespace = null;

	public mixed $error = null;

	/** @var (Closure(string) : mixed)|null */
	public ?Closure $missing = null;

	public function __construct(
		public string $path,
		public ?string $pack = null
	){}

	/**
	 * @param list<Module> $visited
	 * @return array<int, mixed>|null
	 */
	public function resolve(string $name, array $visited = []) : ?array{
		if(in_array($this, $visited, true)){
			return null;
		}
		$visited[] = $this;
		$binding = $this->exports[$name] ?? null;
		if($binding !== null){
			if($binding[0] === "reexport"){
				return $binding[1]->resolve($binding[2], $visited);
			}
			return $binding;
		}
		if($name === "default"){
			return null;
		}
		foreach($this->starExports as $module){
			$resolved = $module->resolve($name, $visited);
			if($resolved !== null){
				return $resolved;
			}
		}
		return null;
	}

	/**
	 * @param list<Module> $visited
	 * @return list<string>
	 */
	public function exportNames(array $visited = []) : array{
		if(in_array($this, $visited, true)){
			return [];
		}
		$visited[] = $this;
		$names = [];
		foreach($this->exports as $name => $binding){
			$names[(string) $name] = true;
		}
		foreach($this->starExports as $module){
			foreach($module->exportNames($visited) as $name){
				if($name !== "default"){
					$names[$name] = true;
				}
			}
		}
		$list = [];
		foreach($names as $name => $unused){
			$list[] = (string) $name;
		}
		return $list;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js\builtin;

use behaviorpack\script\js\Interpreter;
use behaviorpack\script\js\JsCallable;
use behaviorpack\script\js\JsObject;
use behaviorpack\script\js\JsPromise;
use behaviorpack\script\js\JsThrow;
use behaviorpack\script\js\NativeFunction;
use function count;
use function spl_object_id;

/**
 * Promise and the generator prototype.
 */
final class PromiseBuiltins{

	public function __construct(
		private Interpreter $js
	){}

	public function install() : void{
		$this->installPromise();
		$this->installGenerator();
	}

	/**
	 * @return array{0: NativeFunction, 1: NativeFunction}
	 */
	public function resolvingFunctions(JsPromise $promise) : array{
		$js = $this->js;
		$resolve = $js->native("", 1, function(mixed $thisValue, array $args) use ($js, $promise) : mixed{
			$js->resolvePromise($promise, $args[0] ?? null);
			return null;
		});
		$reject = $js->native("", 1, function(mixed $thisValue, array $args) use ($js, $promise) : mixed{
			$js->rejectPromise($promise, $args[0] ?? null);
			return null;
		});
		return [$resolve, $reject];
	}

	private function thisPromise(mixed $thisValue) : JsPromise{
		if(!$thisValue instanceof JsPromise){
			$this->js->throwError("TypeError", "Method Promise.prototype.then called on incompatible receiver");
		}
		return $thisValue;
	}

	private function installPromise() : void{
		$js = $this->js;
		$proto = $js->promisePrototype;
		$proto->symbols[spl_object_id($js->symToStringTag)] = [$js->symToStringTag, "Promise"];
		$constructor = $js->makeClass("Promise", $proto, function(array $args, JsObject $newTarget) use ($js) : JsObject{
			$executor = $args[0] ?? null;
			if(!$executor instanceof JsCallable){
				$js->throwError("TypeError", "Promise resolver " . $js->describeValue($executor) . " is not a function");
			}
			$promise = new JsPromise($js->prototypeFor($newTarget, $js->promisePrototype));
			[$resolve, $reject] = $this->resolvingFunctions($promise);
			try{
				$js->call($executor, null, [$resolve, $reject]);
			}catch(JsThrow $e){
				$js->rejectPromise($promise, $e->value);
			}
			return $promise;
		}, null, 1);
		$js->constructors["Promise"] = $constructor;
		$js->defineHidden($js->global, "Promise", $constructor);

		$js->defineMethod($proto, "then", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->promiseThen($this->thisPromise($thisValue), $args[0] ?? null, $args[1] ?? null);
		});
		$js->defineMethod($proto, "catch", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->promiseThen($this->thisPromise($thisValue), null, $args[0] ?? null);
		});
		$js->defineMethod($proto, "finally", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$promise = $this->thisPromise($thisValue);
			$callback = $args[0] ?? null;
			if(!$callback instanceof JsCallable){
				return $js->promiseThen($promise, $callback, $callback);
			}
			$onFulfilled = $js->native("", 1, function(mixed $ignored, array $values) use ($js, $callback) : mixed{
				$value = $values[0] ?? null;
				$result = $js->promiseResolve($js->call($callback, null, []));
				return $js->promiseThen($result, $js->native("", 0, function(mixed $unused, array $none) use ($value) : mixed{
					return $value;
				}), null);
			});
			$onRejected = $js->native("", 1, function(mixed $ignored, array $values) use ($js, $callback) : mixed{
				$reason = $values[0] ?? null;
				$result = $js->promiseResolve($js->call($callback, null, []));
				return $js->promiseThen($result, $js->native("", 0, function(mixed $unused, array $none) use ($reason) : mixed{
					throw new JsThrow($reason);
				}), null);
			});
			return $js->promiseThen($promise, $onFulfilled, $onRejected);
		});

		$js->defineMethod($constructor, "resolve", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->promiseResolve($args[0] ?? null);
		});
		$js->defineMethod($constructor, "reject", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$promise = $js->newPromise();
			$js->rejectPromise($promise, $args[0] ?? null);
			return $promise;
		});
		$js->defineMethod($constructor, "withResolvers", 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			$promise = $js->newPromise();
			[$resolve, $reject] = $this->resolvingFunctions($promise);
			$result = $js->newObject();
			$result->props = ["promise" => $promise, "resolve" => $resolve, "reject" => $reject];
			return $result;
		});
		$js->defineMethod($constructor, "all", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = $js->newPromise();
			$items = $js->iterableToList($args[0] ?? null);
			$values = [];
			$remaining = count($items);
			if($remaining === 0){
				$js->resolvePromise($result, $js->newArray());
				return $result;
			}
			foreach($items as $index => $item){
				$values[$index] = null;
				$js->onSettled($js->promiseResolve($item), function(int $state, mixed $value) use ($js, $result, $index, &$values, &$remaining) : void{
					if($state === JsPromise::REJECTED){
						$js->rejectPromise($result, $value);
						return;
					}
					$values[$index] = $value;
					if(--$remaining === 0){
						$js->resolvePromise($result, $js->newArray($values));
					}
				});
			}
			return $result;
		});
		$js->defineMethod($constructor, "allSettled", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = $js->newPromise();
			$items = $js->iterableToList($args[0] ?? null);
			$values = [];
			$remaining = count($items);
			if($remaining === 0){
				$js->resolvePromise($result, $js->newArray());
				return $result;
			}
			foreach($items as $index => $item){
				$values[$index] = null;
				$js->onSettled($js->promiseResolve($item), function(int $state, mixed $value) use ($js, $result, $index, &$values, &$remaining) : void{
					$entry = $js->newObject();
					if($state === JsPromise::FULFILLED){
						$entry->props = ["status" => "fulfilled", "value" => $value];
					}else{
						$entry->props = ["status" => "rejected", "reason" => $value];
					}
					$values[$index] = $entry;
					if(--$remaining === 0){
						$js->resolvePromise($result, $js->newArray($values));
					}
				});
			}
			return $result;
		});
		$js->defineMethod($constructor, "race", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = $js->newPromise();
			foreach($js->iterableToList($args[0] ?? null) as $item){
				$js->onSettled($js->promiseResolve($item), function(int $state, mixed $value) use ($js, $result) : void{
					if($state === JsPromise::FULFILLED){
						$js->resolvePromise($result, $value);
					}else{
						$js->rejectPromise($result, $value);
					}
				});
			}
			return $result;
		});
		$js->defineMethod($constructor, "any", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = $js->newPromise();
			$items = $js->iterableToList($args[0] ?? null);
			$errors = [];
			$remaining = count($items);
			if($remaining === 0){
				$js->rejectPromise($result, $js->construct($js->constructors["AggregateError"], [$js->newArray(), "All promises were rejected"]));
				return $result;
			}
			foreach($items as $index => $item){
				$errors[$index] = null;
				$js->onSettled($js->promiseResolve($item), function(int $state, mixed $value) use ($js, $result, $index, &$errors, &$remaining) : void{
					if($state === JsPromise::FULFILLED){
						$js->resolvePromise($result, $value);
						return;
					}
					$errors[$index] = $value;
					if(--$remaining === 0){
						$js->rejectPromise($result, $js->construct($js->constructors["AggregateError"], [$js->newArray($errors), "All promises were rejected"]));
					}
				});
			}
			return $result;
		});
	}

	private function installGenerator() : void{
		$js = $this->js;
		$proto = $js->generatorPrototype;
		$proto->symbols[spl_object_id($js->symToStringTag)] = [$js->symToStringTag, "Generator"];
		$js->defineMethod($proto, "next", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->generatorResume($thisValue, 0, $args[0] ?? null);
		});
		$js->defineMethod($proto, "return", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->generatorResume($thisValue, 1, $args[0] ?? null);
		});
		$js->defineMethod($proto, "throw", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->generatorResume($thisValue, 2, $args[0] ?? null);
		});
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js\builtin;

use behaviorpack\script\js\Interpreter;
use behaviorpack\script\js\JsCallable;
use behaviorpack\script\js\JsNull;
use behaviorpack\script\js\JsObject;
use behaviorpack\script\js\JsSymbol;
use function array_key_exists;
use function array_keys;
use function count;
use function floor;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_nan;
use function is_string;
use function spl_object_id;

/**
 * Map, Set, WeakMap and WeakSet. Entries are kept in insertion order in PHP
 * arrays keyed by a normalized form of the key.
 */
final class CollectionBuiltins{

	private JsObject $iteratorPrototype;

	public function __construct(
		private Interpreter $js
	){}

	public function install() : void{
		$js = $this->js;
		$this->iteratorPrototype = new JsObject($js->iteratorPrototype);
		$js->defineMethod($this->iteratorPrototype, "next", 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			if(!$thisValue instanceof JsObject || !is_array($thisValue->host) || !isset($thisValue->host["collection"])){
				$js->throwError("TypeError", "next method called on incompatible receiver");
			}
			$state = &$thisValue->host;
			$collection = $state["collection"]->host;
			while($state["position"] < count($state["keys"])){
				$key = $state["keys"][$state["position"]++];
				if(!array_key_exists($key, $collection["keys"])){
					continue;
				}
				$entryKey = $collection["keys"][$key];
				$entryValue = $collection["values"][$key];
				return match($state["kind"]){
					"keys" => $js->iterResult($entryKey, false),
					"values" => $js->iterResult($entryValue, false),
					default => $js->iterResult($js->newArray([$entryKey, $entryValue]), false)
				};
			}
			return $js->iterResult(null, true);
		});
		$this->installMap("Map", false);
		$this->installMap("WeakMap", true);
		$this->installSet("Set", false);
		$this->installSet("WeakSet", true);
	}

	public function keyOf(mixed $value) : string{
		if($value instanceof JsObject || $value instanceof JsSymbol){
			return "o" . spl_object_id($value);
		}
		if(is_string($value)){
			return "s" . $value;
		}
		if(is_int($value)){
			return "n" . $value;
		}
		if(is_float($value)){
			if(is_nan($value)){
				return "nan";
			}
			if($value === floor($value) && $value > -9.0e15 && $value < 9.0e15){
				return "n" . (int) $value;
			}
			return "f" . Interpreter::numberToString($value);
		}
		if(is_bool($value)){
			return $value ? "b1" : "b0";
		}
		if($value instanceof JsNull){
			return "l";
		}
		return "u";
	}

	private function collection(mixed $thisValue, string $className) : JsObject{
		if(!$thisValue instanceof JsObject || $thisValue->className !== $className || !is_array($thisValue->host)){
			$this->js->throwError("TypeError", "Method " . $className . ".prototype called on incompatible receiver");
		}
		return $thisValue;
	}

	private function iterator(JsObject $collection, string $kind) : JsObject{
		$iterator = new JsObject($this->iteratorPrototype);
		$iterator->className = $collection->className . " Iterator";
		$iterator->host = ["collection" => $collection, "keys" => array_keys($collection->host["keys"]), "position" => 0, "kind" => $kind];
		return $iterator;
	}

	private function checkWeakKey(mixed $key, bool $weak) : void{
		if($weak && !$key instanceof JsObject && !$key instanceof JsSymbol){
			$this->js->throwError("TypeError", "Invalid value used as weak collection key");
		}
	}

	private function installMap(string $name, bool $weak) : void{
		$js = $this->js;
		$proto = new JsObject($js->objectPrototype);
		$proto->symbols[spl_object_id($js->symToStringTag)] = [$js->symToStringTag, $name];
		$constructor = $js->makeClass($name, $proto, function(array $args, JsObject $newTarget) use ($js, $proto, $name, $weak) : JsObject{
			$map = new JsObject($js->prototypeFor($newTarget, $proto));
			$map->className = $name;
			$map->host = ["keys" => [], "values" => []];
			$source = $args[0] ?? null;
			if($source !== null && !$source instanceof JsNull){
				foreach($js->iterate($source) as $entry){
					if(!$entry instanceof JsObject){
						$js->throwError("TypeError", "Iterator value " . $js->toStringSafe($entry) . " is not an entry object");
					}
					$key = $js->get($entry, "0");
					$this->checkWeakKey($key, $weak);
					$normalized = $this->keyOf($key);
					$map->host["keys"][$normalized] = $key;
					$map->host["values"][$normalized] = $js->get($entry, "1");
				}
			}
			return $map;
		}, null, 0);
		$js->constructors[$name] = $constructor;
		$js->defineHidden($js->global, $name, $constructor);

		$js->defineMethod($proto, "get", 1, function(mixed $thisValue, array $args) use ($name) : mixed{
			$map = $this->collection($thisValue, $name);
			return $map->host["values"][$this->keyOf($args[0] ?? null)] ?? null;
		});
		$js->defineMethod($proto, "set", 2, function(mixed $thisValue, array $args) use ($name, $weak) : mixed{
			$map = $this->collection($thisValue, $name);
			$key = $args[0] ?? null;
			$this->checkWeakKey($key, $weak);
			if(is_float($key) && $key == 0.0){
				$key = 0;
			}
			$normalized = $this->keyOf($key);
			if(!array_key_exists($normalized, $map->host["keys"])){
				$map->host["keys"][$normalized] = $key;
			}
			$map->host["values"][$normalized] = $args[1] ?? null;
			return $map;
		});
		$js->defineMethod($proto, "has", 1, function(mixed $thisValue, array $args) use ($name) : mixed{
			$map = $this->collection($thisValue, $name);
			return array_key_exists($this->keyOf($args[0] ?? null), $map->host["keys"]);
		});
		$js->defineMethod($proto, "delete", 1, function(mixed $thisValue, array $args) use ($name) : mixed{
			$map = $this->collection($thisValue, $name);
			$normalized = $this->keyOf($args[0] ?? null);
			if(!array_key_exists($normalized, $map->host["keys"])){
				return false;
			}
			unset($map->host["keys"][$normalized], $map->host["values"][$normalized]);
			return true;
		});
		if($weak){
			return;
		}
		$js->defineMethod($proto, "clear", 0, function(mixed $thisValue, array $args) use ($name) : mixed{
			$map = $this->collection($thisValue, $name);
			$map->host = ["keys" => [], "values" => []];
			return null;
		});
		$js->defineGetter($proto, "size", function(mixed $thisValue) use ($name) : mixed{
			return count($this->collection($thisValue, $name)->host["keys"]);
		});
		$js->defineMethod($proto, "forEach", 1, function(mixed $thisValue, array $args) use ($js, $name) : mixed{
			$map = $this->collection($thisValue, $name);
			$callback = $args[0] ?? null;
			if(!$callback instanceof JsCallable){
				$js->throwError("TypeError", $js->describeValue($callback) . " is not a function");
			}
			foreach(array_keys($map->host["keys"]) as $normalized){
				if(!array_key_exists($normalized, $map->host["keys"])){
					continue;
				}
				$js->call($callback, $args[1] ?? null, [$map->host["values"][$normalized], $map->host["keys"][$normalized], $map]);
			}
			return null;
		});
		$js->defineMethod($proto, "keys", 0, function(mixed $thisValue, array $args) use ($name) : mixed{
			return $this->iterator($this->collection($thisValue, $name), "keys");
		});
		$js->defineMethod($proto, "values", 0, function(mixed $thisValue, array $args) use ($name) : mixed{
			return $this->iterator($this->collection($thisValue, $name), "values");
		});
		$entries = $js->defineMethod($proto, "entries", 0, function(mixed $thisValue, array $args) use ($name) : mixed{
			return $this->iterator($this->collection($thisValue, $name), "entries");
		});
		$js->defineHidden($proto, $js->symIterator, $entries);
		$js->defineMethod($constructor, "groupBy", 2, function(mixed $thisValue, array $args) use ($js, $proto, $name) : mixed{
			$map = new JsObject($proto);
			$map->className = $name;
			$map->host = ["keys" => [], "values" => []];
			$index = 0;
			foreach($js->iterate($args[0] ?? null) as $value){
				$key = $js->call($args[1] ?? null, null, [$value, $index++]);
				$normalized = $this->keyOf($key);
				if(!array_key_exists($normalized, $map->host["keys"])){
					$map->host["keys"][$normalized] = $key;
					$map->host["values"][$normalized] = $js->newArray();
				}
				$map->host["values"][$normalized]->items[] = $value;
			}
			return $map;
		});
	}

	private function installSet(string $name, bool $weak) : void{
		$js = $this->js;
		$proto = new JsObject($js->objectPrototype);
		$proto->symbols[spl_object_id($js->symToStringTag)] = [$js->symToStringTag, $name];
		$constructor = $js->makeClass($name, $proto, function(array $args, JsObject $newTarget) use ($js, $proto, $name, $weak) : JsObject{
			$set = new JsObject($js->prototypeFor($newTarget, $proto));
			$set->className = $name;
			$set->host = ["keys" => [], "values" => []];
			$source = $args[0] ?? null;
			if($source !== null && !$source instanceof JsNull){
				foreach($js->iterate($source) as $value){
					$this->checkWeakKey($value, $weak);
					$normalized = $this->keyOf($value);
					$set->host["keys"][$normalized] = $value;
					$set->host["values"][$normalized] = $value;
				}
			}
			return $set;
		}, null, 0);
		$js->constructors[$name] = $constructor;
		$js->defineHidden($js->global, $name, $constructor);

		$js->defineMethod($proto, "add", 1, function(mixed $thisValue, array $args) use ($name, $weak) : mixed{
			$set = $this->collection($thisValue, $name);
			$value = $args[0] ?? null;
			$this->checkWeakKey($value, $weak);
			if(is_float($value) && $value == 0.0){
				$value = 0;
			}
			$normalized = $this->keyOf($value);
			if(!array_key_exists($normalized, $set->host["keys"])){
				$set->host["keys"][$normalized] = $value;
				$set->host["values"][$normalized] = $value;
			}
			return $set;
		});
		$js->defineMethod($proto, "has", 1, function(mixed $thisValue, array $args) use ($name) : mixed{
			$set = $this->collection($thisValue, $name);
			return array_key_exists($this->keyOf($args[0] ?? null), $set->host["keys"]);
		});
		$js->defineMethod($proto, "delete", 1, function(mixed $thisValue, array $args) use ($name) : mixed{
			$set = $this->collection($thisValue, $name);
			$normalized = $this->keyOf($args[0] ?? null);
			if(!array_key_exists($normalized, $set->host["keys"])){
				return false;
			}
			unset($set->host["keys"][$normalized], $set->host["values"][$normalized]);
			return true;
		});
		if($weak){
			return;
		}
		$js->defineMethod($proto, "clear", 0, function(mixed $thisValue, array $args) use ($name) : mixed{
			$set = $this->collection($thisValue, $name);
			$set->host = ["keys" => [], "values" => []];
			return null;
		});
		$js->defineGetter($proto, "size", function(mixed $thisValue) use ($name) : mixed{
			return count($this->collection($thisValue, $name)->host["keys"]);
		});
		$js->defineMethod($proto, "forEach", 1, function(mixed $thisValue, array $args) use ($js, $name) : mixed{
			$set = $this->collection($thisValue, $name);
			$callback = $args[0] ?? null;
			if(!$callback instanceof JsCallable){
				$js->throwError("TypeError", $js->describeValue($callback) . " is not a function");
			}
			foreach(array_keys($set->host["keys"]) as $normalized){
				if(!array_key_exists($normalized, $set->host["keys"])){
					continue;
				}
				$value = $set->host["keys"][$normalized];
				$js->call($callback, $args[1] ?? null, [$value, $value, $set]);
			}
			return null;
		});
		$values = $js->defineMethod($proto, "values", 0, function(mixed $thisValue, array $args) use ($name) : mixed{
			return $this->iterator($this->collection($thisValue, $name), "values");
		});
		$js->defineHidden($proto, "keys", $values);
		$js->defineMethod($proto, "entries", 0, function(mixed $thisValue, array $args) use ($name) : mixed{
			return $this->iterator($this->collection($thisValue, $name), "entries");
		});
		$js->defineHidden($proto, $js->symIterator, $values);
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js\builtin;

use behaviorpack\script\js\Interpreter;
use behaviorpack\script\js\JsArray;
use behaviorpack\script\js\JsCallable;
use behaviorpack\script\js\JsNull;
use behaviorpack\script\js\JsObject;
use behaviorpack\script\js\JsSymbol;
use behaviorpack\script\js\Lexer;
use function array_slice;
use function count;
use function ctype_digit;
use function ctype_xdigit;
use function explode;
use function implode;
use function is_array;
use function is_int;
use function is_string;
use function max;
use function mb_ord;
use function mb_str_split;
use function mb_strlen;
use function mb_strpos;
use function mb_strrpos;
use function mb_strtolower;
use function mb_strtoupper;
use function mb_substr;
use function min;
use function preg_match;
use function preg_replace;
use function spl_object_id;
use function str_contains;
use function str_ends_with;
use function str_repeat;
use function str_starts_with;
use function strcmp;
use function strlen;
use function strpos;
use function substr;
use const PREG_OFFSET_CAPTURE;
use const PREG_UNMATCHED_AS_NULL;

/**
 * String, RegExp and their prototypes. Strings are UTF-8 and indexed by
 * code point. Regular expressions are translated to PCRE.
 */
final class StringBuiltins{

	private JsObject $regexpPrototype;

	private ?JsCallable $regexpConstructor = null;

	public function __construct(
		private Interpreter $js
	){}

	public function install() : void{
		$this->installString();
		$this->installRegExp();
	}

	private function thisString(mixed $thisValue) : string{
		if(is_string($thisValue)){
			return $thisValue;
		}
		if($thisValue instanceof JsObject && is_string($thisValue->host)){
			return $thisValue->host;
		}
		if($thisValue === null || $thisValue instanceof JsNull){
			$this->js->throwError("TypeError", "String.prototype method called on null or undefined");
		}
		return $this->js->toString($thisValue);
	}

	private function length(string $value) : int{
		return mb_strlen($value, "UTF-8");
	}

	private function substring(string $value, int $start, ?int $length = null) : string{
		return mb_substr($value, $start, $length, "UTF-8");
	}

	private function clampIndex(mixed $value, int $length, int $default) : int{
		if($value === null){
			return $default;
		}
		$index = $this->js->toIntegerOrInfinity($value);
		return (int) max(0, min($index, $length));
	}

	private function relativeIndex(mixed $value, int $length, int $default) : int{
		if($value === null){
			return $default;
		}
		$index = $this->js->toIntegerOrInfinity($value);
		if($index < 0){
			return (int) max(0, $length + $index);
		}
		return (int) min($index, $length);
	}

	private function installString() : void{
		$js = $this->js;
		$proto = $js->stringPrototype;
		$proto->host = "";
		$string = $js->makeClass("String", $proto, function(array $args, JsObject $newTarget) use ($js) : JsObject{
			$wrapper = new JsObject($js->prototypeFor($newTarget, $js->stringPrototype));
			$wrapper->className = "String";
			$wrapper->host = count($args) === 0 ? "" : $js->toString($args[0]);
			return $wrapper;
		}, function(mixed $thisValue, array $args) use ($js) : mixed{
			if(count($args) === 0){
				return "";
			}
			if($args[0] instanceof JsSymbol){
				return "Symbol(" . ($args[0]->description ?? "") . ")";
			}
			return $js->toString($args[0]);
		}, 1);
		$js->constructors["String"] = $string;
		$js->defineHidden($js->global, "String", $string);

		$js->defineMethod($string, "fromCharCode", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = "";
			foreach($args as $arg){
				$result .= Lexer::codePoint($js->toUint32($arg) & 0xFFFF);
			}
			return $result;
		});
		$js->defineMethod($string, "fromCodePoint", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = "";
			foreach($args as $arg){
				$code = $js->toNumber($arg);
				if(!is_int($code) || $code < 0 || $code > 0x10FFFF){
					$js->throwError("RangeError", "Invalid code point " . $js->toStringSafe($arg));
				}
				$result .= Lexer::codePoint($code);
			}
			return $result;
		});
		$js->defineMethod($string, "raw", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$strings = $js->get($args[0] ?? null, "raw");
			$parts = $strings instanceof JsArray ? $strings->items : [];
			$result = "";
			foreach($parts as $index => $part){
				$result .= $js->toString($part);
				if($index + 1 < count($parts) && isset($args[$index + 1])){
					$result .= $js->toString($args[$index + 1]);
				}
			}
			return $result;
		});

		$js->defineMethod($proto, "toString", 0, function(mixed $thisValue, array $args) : mixed{
			return $this->thisString($thisValue);
		});
		$js->defineMethod($proto, "valueOf", 0, function(mixed $thisValue, array $args) : mixed{
			return $this->thisString($thisValue);
		});
		$js->defineMethod($proto, "charAt", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$index = (int) $js->toIntegerOrInfinity($args[0] ?? 0);
			return $index < 0 ? "" : $this->substring($value, $index, 1);
		});
		$js->defineMethod($proto, "charCodeAt", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$index = (int) $js->toIntegerOrInfinity($args[0] ?? 0);
			$char = $index < 0 ? "" : $this->substring($value, $index, 1);
			if($char === ""){
				return \NAN;
			}
			$code = mb_ord($char, "UTF-8");
			return $code === false ? \NAN : ($code > 0xFFFF ? 0xD800 + (($code - 0x10000) >> 10) : $code);
		});
		$js->defineMethod($proto, "codePointAt", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$index = (int) $js->toIntegerOrInfinity($args[0] ?? 0);
			$char = $index < 0 ? "" : $this->substring($value, $index, 1);
			if($char === ""){
				return null;
			}
			$code = mb_ord($char, "UTF-8");
			return $code === false ? null : $code;
		});
		$js->defineMethod($proto, "at", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$index = (int) $js->toIntegerOrInfinity($args[0] ?? 0);
			if($index < 0){
				$index += $this->length($value);
			}
			$char = $index < 0 ? "" : $this->substring($value, $index, 1);
			return $char === "" ? null : $char;
		});
		$js->defineMethod($proto, "indexOf", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$search = $js->toString($args[0] ?? null);
			$start = $this->clampIndex($args[1] ?? null, $this->length($value), 0);
			if($search === ""){
				return $start;
			}
			$position = mb_strpos($value, $search, $start, "UTF-8");
			return $position === false ? -1 : $position;
		});
		$js->defineMethod($proto, "lastIndexOf", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$search = $js->toString($args[0] ?? null);
			$length = $this->length($value);
			$from = ($args[1] ?? null) === null ? $length : $this->clampIndex($args[1], $length, $length);
			$haystack = $this->substring($value, 0, min($length, $from + $this->length($search)));
			if($search === ""){
				return min($from, $length);
			}
			$position = mb_strrpos($haystack, $search, 0, "UTF-8");
			return $position === false ? -1 : $position;
		});
		$js->defineMethod($proto, "includes", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			if($this->isRegExp($args[0] ?? null)){
				$js->throwError("TypeError", "First argument to String.prototype.includes must not be a regular expression");
			}
			$search = $js->toString($args[0] ?? null);
			$start = $this->clampIndex($args[1] ?? null, $this->length($value), 0);
			return $search === "" || mb_strpos($value, $search, $start, "UTF-8") !== false;
		});
		$js->defineMethod($proto, "startsWith", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$search = $js->toString($args[0] ?? null);
			$start = $this->clampIndex($args[1] ?? null, $this->length($value), 0);
			return str_starts_with($start === 0 ? $value : $this->substring($value, $start), $search);
		});
		$js->defineMethod($proto, "endsWith", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$search = $js->toString($args[0] ?? null);
			$length = $this->length($value);
			$end = $this->clampIndex($args[1] ?? null, $length, $length);
			return str_ends_with($end === $length ? $value : $this->substring($value, 0, $end), $search);
		});
		$js->defineMethod($proto, "slice", 2, function(mixed $thisValue, array $args) : mixed{
			$value = $this->thisString($thisValue);
			$length = $this->length($value);
			$start = $this->relativeIndex($args[0] ?? null, $length, 0);
			$end = $this->relativeIndex($args[1] ?? null, $length, $length);
			return $end > $start ? $this->substring($value, $start, $end - $start) : "";
		});
		$js->defineMethod($proto, "substring", 2, function(mixed $thisValue, array $args) : mixed{
			$value = $this->thisString($thisValue);
			$length = $this->length($value);
			$start = $this->clampIndex($args[0] ?? null, $length, 0);
			$end = $this->clampIndex($args[1] ?? null, $length, $length);
			if($start > $end){
				[$start, $end] = [$end, $start];
			}
			return $this->substring($value, $start, $end - $start);
		});
		$js->defineMethod($proto, "substr", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$length = $this->length($value);
			$start = $this->relativeIndex($args[0] ?? null, $length, 0);
			$count = ($args[1] ?? null) === null ? $length - $start : (int) max(0, min($js->toIntegerOrInfinity($args[1]), $length - $start));
			return $this->substring($value, $start, $count);
		});
		foreach(["toUpperCase", "toLocaleUpperCase"] as $name){
			$js->defineMethod($proto, $name, 0, function(mixed $thisValue, array $args) : mixed{
				return mb_strtoupper($this->thisString($thisValue), "UTF-8");
			});
		}
		foreach(["toLowerCase", "toLocaleLowerCase"] as $name){
			$js->defineMethod($proto, $name, 0, function(mixed $thisValue, array $args) : mixed{
				return mb_strtolower($this->thisString($thisValue), "UTF-8");
			});
		}
		$js->defineMethod($proto, "trim", 0, function(mixed $thisValue, array $args) : mixed{
			return preg_replace('/^[\s\x{FEFF}\x{A0}]+|[\s\x{FEFF}\x{A0}]+$/u', "", $this->thisString($thisValue)) ?? "";
		});
		foreach(["trimStart", "trimLeft"] as $name){
			$js->defineMethod($proto, $name, 0, function(mixed $thisValue, array $args) : mixed{
				return preg_replace('/^[\s\x{FEFF}\x{A0}]+/u', "", $this->thisString($thisValue)) ?? "";
			});
		}
		foreach(["trimEnd", "trimRight"] as $name){
			$js->defineMethod($proto, $name, 0, function(mixed $thisValue, array $args) : mixed{
				return preg_replace('/[\s\x{FEFF}\x{A0}]+$/u', "", $this->thisString($thisValue)) ?? "";
			});
		}
		foreach(["padStart" => true, "padEnd" => false] as $name => $start){
			$js->defineMethod($proto, $name, 2, function(mixed $thisValue, array $args) use ($js, $start) : mixed{
				$value = $this->thisString($thisValue);
				$target = (int) $js->toIntegerOrInfinity($args[0] ?? 0);
				$filler = ($args[1] ?? null) === null ? " " : $js->toString($args[1]);
				$length = $this->length($value);
				if($target <= $length || $filler === ""){
					return $value;
				}
				$needed = $target - $length;
				$fillLength = $this->length($filler);
				$padding = $this->substring(str_repeat($filler, (int) (($needed / $fillLength) + 1)), 0, $needed);
				return $start ? $padding . $value : $value . $padding;
			});
		}
		$js->defineMethod($proto, "repeat", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$count = $js->toIntegerOrInfinity($args[0] ?? 0);
			if($count < 0 || $count > 10_000_000 || strlen($value) * $count > 50_000_000){
				$js->throwError("RangeError", "Invalid count value: " . $js->toStringSafe($args[0] ?? null));
			}
			return str_repeat($value, (int) $count);
		});
		$js->defineMethod($proto, "concat", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = $this->thisString($thisValue);
			foreach($args as $arg){
				$result .= $js->toString($arg);
			}
			return $result;
		});
		$js->defineMethod($proto, "localeCompare", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = strcmp($this->thisString($thisValue), $js->toString($args[0] ?? null));
			return $result < 0 ? -1 : ($result > 0 ? 1 : 0);
		});
		$js->defineMethod($proto, "normalize", 0, function(mixed $thisValue, array $args) : mixed{
			return $this->thisString($thisValue);
		});
		$js->defineMethod($proto, "isWellFormed", 0, function(mixed $thisValue, array $args) : mixed{
			return true;
		});
		$js->defineMethod($proto, "split", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$separator = $args[0] ?? null;
			$limit = ($args[1] ?? null) === null ? 4294967295 : $js->toUint32($args[1]);
			if($limit === 0){
				return $js->newArray();
			}
			if($separator === null){
				return $js->newArray([$value]);
			}
			if($this->isRegExp($separator)){
				return $js->newArray(array_slice($this->splitRegExp($separator, $value), 0, $limit));
			}
			$separator = $js->toString($separator);
			if($separator === ""){
				$parts = $value === "" ? [] : mb_str_split($value, 1, "UTF-8");
			}else{
				$parts = explode($separator, $value);
			}
			return $js->newArray(array_slice($parts, 0, $limit));
		});
		$js->defineMethod($proto, "replace", 2, function(mixed $thisValue, array $args) : mixed{
			return $this->replace($this->thisString($thisValue), $args[0] ?? null, $args[1] ?? null, false);
		});
		$js->defineMethod($proto, "replaceAll", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$pattern = $args[0] ?? null;
			if($this->isRegExp($pattern) && !str_contains($pattern->host["flags"], "g")){
				$js->throwError("TypeError", "replaceAll must be called with a global RegExp");
			}
			return $this->replace($this->thisString($thisValue), $pattern, $args[1] ?? null, true);
		});
		$js->defineMethod($proto, "match", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$regexp = $this->toRegExp($args[0] ?? null);
			if(!str_contains($regexp->host["flags"], "g")){
				return $this->exec($regexp, $value);
			}
			$matches = [];
			foreach($this->allMatches($regexp, $value) as $match){
				$matches[] = $match["groups"][0][0];
			}
			$regexp->props["lastIndex"] = 0;
			return count($matches) === 0 ? JsNull::get() : $js->newArray($matches);
		});
		$js->defineMethod($proto, "matchAll", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$regexp = $this->toRegExp($args[0] ?? null, "g");
			if(!str_contains($regexp->host["flags"], "g")){
				$js->throwError("TypeError", "matchAll must be called with a global RegExp");
			}
			$results = [];
			foreach($this->allMatches($regexp, $value) as $match){
				$results[] = $this->matchArray($match, $value);
			}
			$array = $js->newArray($results);
			return $js->call($js->get($array, $js->symIterator), $array, []);
		});
		$js->defineMethod($proto, "search", 1, function(mixed $thisValue, array $args) : mixed{
			$value = $this->thisString($thisValue);
			$regexp = $this->toRegExp($args[0] ?? null);
			$match = $this->matchAt($regexp, $value, 0);
			return $match === null ? -1 : $match["index"];
		});
		$js->defineMethod($proto, $js->symIterator, 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisString($thisValue);
			$array = $js->newArray($value === "" ? [] : mb_str_split($value, 1, "UTF-8"));
			return $js->call($js->get($array, "values"), $array, []);
		});
	}

	public function isRegExp(mixed $value) : bool{
		return $value instanceof JsObject && is_array($value->host) && isset($value->host["pcre"]);
	}

	private function toRegExp(mixed $value, string $flags = "") : JsObject{
		if($this->isRegExp($value)){
			return $value;
		}
		$source = $value === null ? "(?:)" : $this->quoteMeta($this->js->toString($value));
		return $this->createRegExp($source, $flags);
	}

	private function quoteMeta(string $value) : string{
		return preg_replace('/[.*+?^${}()|[\]\\\\\/]/', '\\\\$0', $value) ?? $value;
	}

	private function installRegExp() : void{
		$js = $this->js;
		$proto = new JsObject($js->objectPrototype);
		$proto->className = "RegExp";
		$this->regexpPrototype = $proto;
		$construct = function(array $args, JsObject $newTarget) use ($js) : JsObject{
			$pattern = $args[0] ?? null;
			$flags = ($args[1] ?? null) === null ? null : $js->toString($args[1]);
			if($this->isRegExp($pattern)){
				$regexp = $this->createRegExp($pattern->host["source"], $flags ?? $pattern->host["flags"]);
			}else{
				$regexp = $this->createRegExp($pattern === null ? "(?:)" : $js->toString($pattern), $flags ?? "");
			}
			$regexp->proto = $js->prototypeFor($newTarget, $this->regexpPrototype);
			return $regexp;
		};
		$constructor = $js->makeClass("RegExp", $proto, $construct, function(mixed $thisValue, array $args) use ($construct) : mixed{
			return $construct($args, $this->regexpConstructor);
		}, 2);
		$this->regexpConstructor = $constructor;
		$js->constructors["RegExp"] = $constructor;
		$js->defineHidden($js->global, "RegExp", $constructor);

		$flag = function(string $letter) : \Closure{
			return function(mixed $thisValue) use ($letter) : mixed{
				return $this->isRegExp($thisValue) && str_contains($thisValue->host["flags"], $letter);
			};
		};
		foreach(["global" => "g", "ignoreCase" => "i", "multiline" => "m", "sticky" => "y", "unicode" => "u", "dotAll" => "s", "hasIndices" => "d"] as $name => $letter){
			$js->defineGetter($proto, $name, $flag($letter));
		}
		$js->defineGetter($proto, "source", function(mixed $thisValue) : mixed{
			return $this->isRegExp($thisValue) ? $thisValue->host["source"] : "(?:)";
		});
		$js->defineGetter($proto, "flags", function(mixed $thisValue) : mixed{
			return $this->isRegExp($thisValue) ? $thisValue->host["flags"] : "";
		});
		$js->defineMethod($proto, "exec", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			if(!$this->isRegExp($thisValue)){
				$js->throwError("TypeError", "RegExp.prototype.exec called on incompatible receiver");
			}
			return $this->exec($thisValue, $js->toString($args[0] ?? null));
		});
		$js->defineMethod($proto, "test", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			if(!$this->isRegExp($thisValue)){
				$js->throwError("TypeError", "RegExp.prototype.test called on incompatible receiver");
			}
			return !$this->exec($thisValue, $js->toString($args[0] ?? null)) instanceof JsNull;
		});
		$js->defineMethod($proto, "toString", 0, function(mixed $thisValue, array $args) : mixed{
			if(!$this->isRegExp($thisValue)){
				return "/(?:)/";
			}
			return "/" . $thisValue->host["source"] . "/" . $thisValue->host["flags"];
		});
	}

	public function createRegExp(string $source, string $flags) : JsObject{
		$js = $this->js;
		foreach(mb_str_split($flags, 1, "UTF-8") as $letter){
			if(!str_contains("dgimsuyv", $letter)){
				$js->throwError("SyntaxError", "Invalid regular expression flags '" . $flags . "'");
			}
		}
		$modifiers = "uD";
		foreach(["i", "m", "s"] as $letter){
			if(str_contains($flags, $letter)){
				$modifiers .= $letter;
			}
		}
		$pcre = "/" . $this->translate($source) . "/" . $modifiers;
		if(@preg_match($pcre, "") === false){
			$js->throwError("SyntaxError", "Invalid regular expression: /" . $source . "/");
		}
		$regexp = new JsObject($this->regexpPrototype);
		$regexp->className = "RegExp";
		$regexp->host = ["source" => $source === "" ? "(?:)" : $source, "flags" => $flags, "pcre" => $pcre];
		$regexp->props["lastIndex"] = 0;
		$regexp->hidden["lastIndex"] = true;
		return $regexp;
	}

	/**
	 * Translates the syntax that differs between JavaScript and PCRE.
	 */
	private function translate(string $source) : string{
		$result = "";
		$length = strlen($source);
		$inClass = false;
		for($i = 0; $i < $length; ++$i){
			$char = $source[$i];
			if($char === "\\" && $i + 1 < $length){
				$next = $source[$i + 1];
				if($next === "u" && $i + 2 < $length && $source[$i + 2] === "{"){
					$end = strpos($source, "}", $i + 3);
					if($end !== false){
						$result .= "\\x{" . substr($source, $i + 3, $end - $i - 3) . "}";
						$i = $end;
						continue;
					}
				}
				$hex = substr($source, $i + 2, 4);
				if($next === "u" && strlen($hex) === 4 && ctype_xdigit($hex)){
					$result .= "\\x{" . $hex . "}";
					$i += 5;
					continue;
				}
				$classes = $inClass ? ["d" => "0-9", "w" => "A-Za-z0-9_"] : ["d" => "[0-9]", "D" => "[^0-9]", "w" => "[A-Za-z0-9_]", "W" => "[^A-Za-z0-9_]"];
				if(isset($classes[$next])){
					$result .= $classes[$next];
					$i++;
					continue;
				}
				$result .= $char . $next;
				$i++;
				continue;
			}
			if($char === "[" && !$inClass){
				if(substr($source, $i, 3) === "[^]"){
					$result .= "[\\s\\S]";
					$i += 2;
					continue;
				}
				$inClass = true;
			}elseif($char === "]" && $inClass){
				$inClass = false;
			}elseif($char === "/"){
				$result .= "\\/";
				continue;
			}
			$result .= $char;
		}
		return $result;
	}

	private function charIndex(string $value, int $byteOffset) : int{
		return mb_strlen(substr($value, 0, $byteOffset), "UTF-8");
	}

	private function byteOffset(string $value, int $charIndex) : int{
		return strlen(mb_substr($value, 0, $charIndex, "UTF-8"));
	}

	/**
	 * Matches at or after a byte offset. Returns the groups as returned by
	 * preg_match with offsets, the match index and end in characters.
	 *
	 * @return array{groups: array<int|string, array{0: ?string, 1: int}>, index: int, byteStart: int, byteEnd: int}|null
	 */
	private function matchAt(JsObject $regexp, string $value, int $byteOffset) : ?array{
		if($byteOffset > strlen($value)){
			return null;
		}
		$groups = [];
		$result = @preg_match($regexp->host["pcre"], $value, $groups, PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL, $byteOffset);
		if($result !== 1){
			return null;
		}
		$start = $groups[0][1];
		if(str_contains($regexp->host["flags"], "y") && $start !== $byteOffset){
			return null;
		}
		return [
			"groups" => $groups,
			"index" => $this->charIndex($value, $start),
			"byteStart" => $start,
			"byteEnd" => $start + strlen($groups[0][0] ?? "")
		];
	}

	/**
	 * @return list<array{groups: array<int|string, array{0: ?string, 1: int}>, index: int, byteStart: int, byteEnd: int}>
	 */
	private function allMatches(JsObject $regexp, string $value) : array{
		$matches = [];
		$offset = 0;
		$length = strlen($value);
		while($offset <= $length){
			$this->js->tick();
			$match = $this->matchAt($regexp, $value, $offset);
			if($match === null){
				break;
			}
			$matches[] = $match;
			if($match["byteEnd"] === $match["byteStart"]){
				$offset = $match["byteEnd"] + $this->charLength($value, $match["byteEnd"]);
			}else{
				$offset = $match["byteEnd"];
			}
		}
		return $matches;
	}

	private function charLength(string $value, int $byteOffset) : int{
		if($byteOffset >= strlen($value)){
			return 1;
		}
		$byte = \ord($value[$byteOffset]);
		return match(true){
			$byte >= 0xF0 => 4,
			$byte >= 0xE0 => 3,
			$byte >= 0xC0 => 2,
			default => 1
		};
	}

	/**
	 * @param array{groups: array<int|string, array{0: ?string, 1: int}>, index: int, byteStart: int, byteEnd: int} $match
	 */
	private function matchArray(array $match, string $input) : JsArray{
		$js = $this->js;
		$items = [];
		$named = [];
		foreach($match["groups"] as $key => [$text, $unused]){
			if(is_int($key)){
				$items[] = $text;
			}else{
				$named[$key] = $text;
			}
		}
		$array = $js->newArray($items);
		$array->props["index"] = $match["index"];
		$array->props["input"] = $input;
		if(count($named) > 0){
			$groups = new JsObject(null);
			foreach($named as $name => $text){
				$groups->props[$name] = $text;
			}
			$array->props["groups"] = $groups;
		}else{
			$array->props["groups"] = null;
		}
		return $array;
	}

	private function exec(JsObject $regexp, string $value) : mixed{
		$js = $this->js;
		$flags = $regexp->host["flags"];
		$stateful = str_contains($flags, "g") || str_contains($flags, "y");
		$start = 0;
		if($stateful){
			$lastIndex = (int) $js->toIntegerOrInfinity($js->get($regexp, "lastIndex"));
			if($lastIndex > $this->length($value)){
				$js->set($regexp, "lastIndex", 0);
				return JsNull::get();
			}
			$start = $this->byteOffset($value, max(0, $lastIndex));
		}
		$match = $this->matchAt($regexp, $value, $start);
		if($match === null){
			if($stateful){
				$js->set($regexp, "lastIndex", 0);
			}
			return JsNull::get();
		}
		if($stateful){
			$js->set($regexp, "lastIndex", $this->charIndex($value, $match["byteEnd"]));
		}
		return $this->matchArray($match, $value);
	}

	/**
	 * @return list<mixed>
	 */
	private function splitRegExp(JsObject $regexp, string $value) : array{
		if($value === ""){
			return $this->matchAt($regexp, $value, 0) === null ? [""] : [];
		}
		$parts = [];
		$last = 0;
		$offset = 0;
		$length = strlen($value);
		while($offset < $length){
			$match = $this->matchAt($regexp, $value, $offset);
			if($match === null){
				break;
			}
			if($match["byteEnd"] === $match["byteStart"]){
				if($match["byteStart"] >= $length){
					break;
				}
				if($match["byteStart"] === $last){
					$offset = $match["byteStart"] + $this->charLength($value, $match["byteStart"]);
					continue;
				}
			}
			$parts[] = substr($value, $last, $match["byteStart"] - $last);
			foreach($match["groups"] as $key => [$text, $unused]){
				if(is_int($key) && $key > 0){
					$parts[] = $text;
				}
			}
			$last = $match["byteEnd"];
			$offset = $match["byteEnd"] === $match["byteStart"] ? $match["byteEnd"] + $this->charLength($value, $match["byteEnd"]) : $match["byteEnd"];
		}
		$parts[] = substr($value, $last);
		return $parts;
	}

	private function replace(string $value, mixed $pattern, mixed $replacement, bool $all) : string{
		$js = $this->js;
		$matches = [];
		if($this->isRegExp($pattern)){
			$global = str_contains($pattern->host["flags"], "g");
			if($global){
				$matches = $this->allMatches($pattern, $value);
				$pattern->props["lastIndex"] = 0;
			}else{
				$match = $this->matchAt($pattern, $value, 0);
				if($match !== null){
					$matches[] = $match;
				}
			}
		}else{
			$search = $js->toString($pattern);
			$offset = 0;
			while(true){
				$position = $search === "" ? ($offset <= strlen($value) ? $offset : false) : strpos($value, $search, $offset);
				if($position === false){
					break;
				}
				$matches[] = ["groups" => [0 => [$search, $position]], "index" => $this->charIndex($value, $position), "byteStart" => $position, "byteEnd" => $position + strlen($search)];
				if(!$all){
					break;
				}
				$offset = $search === "" ? $position + $this->charLength($value, $position) : $position + strlen($search);
				if($search === "" && $offset > strlen($value)){
					break;
				}
			}
		}
		if(count($matches) === 0){
			return $value;
		}
		$result = "";
		$last = 0;
		foreach($matches as $match){
			$result .= substr($value, $last, $match["byteStart"] - $last);
			$result .= $this->replacement($match, $value, $replacement);
			$last = $match["byteEnd"];
		}
		return $result . substr($value, $last);
	}

	/**
	 * @param array{groups: array<int|string, array{0: ?string, 1: int}>, index: int, byteStart: int, byteEnd: int} $match
	 */
	private function replacement(array $match, string $value, mixed $replacement) : string{
		$js = $this->js;
		$captures = [];
		$named = [];
		foreach($match["groups"] as $key => [$text, $unused]){
			if(is_int($key)){
				$captures[] = $text;
			}else{
				$named[$key] = $text;
			}
		}
		if($replacement instanceof JsCallable){
			$args = $captures;
			$args[] = $match["index"];
			$args[] = $value;
			if(count($named) > 0){
				$groups = new JsObject(null);
				foreach($named as $name => $text){
					$groups->props[$name] = $text;
				}
				$args[] = $groups;
			}
			return $js->toString($js->call($replacement, null, $args));
		}
		$template = $js->toString($replacement);
		if(!str_contains($template, "$")){
			return $template;
		}
		$output = "";
		$length = strlen($template);
		for($i = 0; $i < $length; ++$i){
			$char = $template[$i];
			if($char !== "$" || $i + 1 >= $length){
				$output .= $char;
				continue;
			}
			$next = $template[$i + 1];
			if($next === "$"){
				$output .= "$";
				$i++;
			}elseif($next === "&"){
				$output .= $captures[0] ?? "";
				$i++;
			}elseif($next === "`"){
				$output .= substr($value, 0, $match["byteStart"]);
				$i++;
			}elseif($next === "'"){
				$output .= substr($value, $match["byteEnd"]);
				$i++;
			}elseif(ctype_digit($next)){
				$digits = $next;
				if($i + 2 < $length && ctype_digit($template[$i + 2]) && (int) ($next . $template[$i + 2]) < count($captures)){
					$digits .= $template[$i + 2];
				}
				$index = (int) $digits;
				if($index > 0 && $index < count($captures)){
					$output .= $captures[$index] ?? "";
					$i += strlen($digits);
				}else{
					$output .= $char;
				}
			}elseif($next === "<" && count($named) > 0){
				$end = strpos($template, ">", $i + 2);
				if($end === false){
					$output .= $char;
					continue;
				}
				$output .= $named[substr($template, $i + 2, $end - $i - 2)] ?? "";
				$i = $end;
			}else{
				$output .= $char;
			}
		}
		return $output;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js\builtin;

use behaviorpack\script\js\Interpreter;
use behaviorpack\script\js\JsObject;
use DateTimeImmutable;
use DateTimeZone;
use Throwable;
use function count;
use function floor;
use function fmod;
use function gmdate;
use function is_float;
use function is_int;
use function is_nan;
use function is_string;
use function microtime;
use function preg_match;
use function sprintf;
use function str_pad;
use function strtotime;
use const NAN;

/**
 * A subset of Date: construction, the current time, the UTC and local
 * getters and the ISO string form.
 */
final class DateBuiltins{

	private JsObject $prototype;

	public function __construct(
		private Interpreter $js
	){}

	public static function now() : float{
		return floor(microtime(true) * 1000);
	}

	public function install() : void{
		$js = $this->js;
		$proto = new JsObject($js->objectPrototype);
		$proto->className = "Date";
		$this->prototype = $proto;
		$constructor = $js->makeClass("Date", $proto, function(array $args, JsObject $newTarget) use ($js) : JsObject{
			$date = new JsObject($js->prototypeFor($newTarget, $this->prototype));
			$date->className = "Date";
			$date->host = $this->timeFromArguments($args);
			return $date;
		}, function(mixed $thisValue, array $args) : mixed{
			return $this->format(self::now(), false);
		}, 7);
		$js->constructors["Date"] = $constructor;
		$js->defineHidden($js->global, "Date", $constructor);
		$js->defineMethod($constructor, "now", 0, function(mixed $thisValue, array $args) : mixed{
			return Interpreter::intOrFloat(self::now());
		});
		$js->defineMethod($constructor, "parse", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $this->parse($js->toString($args[0] ?? null));
		});
		$js->defineMethod($constructor, "UTC", 7, function(mixed $thisValue, array $args) : mixed{
			return $this->fromComponents($args);
		});

		$getters = [
			"getTime" => fn(float $t) : float => $t,
			"valueOf" => fn(float $t) : float => $t,
			"getFullYear" => fn(float $t) : float => (float) gmdate("Y", (int) floor($t / 1000)),
			"getUTCFullYear" => fn(float $t) : float => (float) gmdate("Y", (int) floor($t / 1000)),
			"getMonth" => fn(float $t) : float => (float) gmdate("n", (int) floor($t / 1000)) - 1,
			"getUTCMonth" => fn(float $t) : float => (float) gmdate("n", (int) floor($t / 1000)) - 1,
			"getDate" => fn(float $t) : float => (float) gmdate("j", (int) floor($t / 1000)),
			"getUTCDate" => fn(float $t) : float => (float) gmdate("j", (int) floor($t / 1000)),
			"getDay" => fn(float $t) : float => (float) gmdate("w", (int) floor($t / 1000)),
			"getUTCDay" => fn(float $t) : float => (float) gmdate("w", (int) floor($t / 1000)),
			"getHours" => fn(float $t) : float => (float) gmdate("G", (int) floor($t / 1000)),
			"getUTCHours" => fn(float $t) : float => (float) gmdate("G", (int) floor($t / 1000)),
			"getMinutes" => fn(float $t) : float => (float) gmdate("i", (int) floor($t / 1000)),
			"getUTCMinutes" => fn(float $t) : float => (float) gmdate("i", (int) floor($t / 1000)),
			"getSeconds" => fn(float $t) : float => (float) gmdate("s", (int) floor($t / 1000)),
			"getUTCSeconds" => fn(float $t) : float => (float) gmdate("s", (int) floor($t / 1000)),
			"getMilliseconds" => fn(float $t) : float => fmod($t, 1000),
			"getUTCMilliseconds" => fn(float $t) : float => fmod($t, 1000),
			"getTimezoneOffset" => fn(float $t) : float => 0.0
		];
		foreach($getters as $name => $getter){
			$js->defineMethod($proto, $name, 0, function(mixed $thisValue, array $args) use ($getter) : mixed{
				$time = $this->thisTime($thisValue);
				if(is_nan($time)){
					return NAN;
				}
				return Interpreter::intOrFloat($getter($time));
			});
		}
		$js->defineMethod($proto, "setTime", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$this->thisTime($thisValue);
			$thisValue->host = (float) $js->toNumber($args[0] ?? null);
			return Interpreter::intOrFloat($thisValue->host);
		});
		foreach(["toISOString", "toJSON"] as $name){
			$js->defineMethod($proto, $name, 0, function(mixed $thisValue, array $args) use ($js) : mixed{
				$time = $this->thisTime($thisValue);
				if(is_nan($time)){
					$js->throwError("RangeError", "Invalid time value");
				}
				return $this->format($time, true);
			});
		}
		foreach(["toString", "toUTCString", "toLocaleString", "toDateString", "toTimeString", "toLocaleDateString", "toLocaleTimeString"] as $name){
			$js->defineMethod($proto, $name, 0, function(mixed $thisValue, array $args) : mixed{
				$time = $this->thisTime($thisValue);
				return is_nan($time) ? "Invalid Date" : $this->format($time, false);
			});
		}
		$js->defineMethod($proto, $js->symToPrimitive, 1, function(mixed $thisValue, array $args) : mixed{
			$time = $this->thisTime($thisValue);
			if(($args[0] ?? null) === "number"){
				return Interpreter::intOrFloat($time);
			}
			return is_nan($time) ? "Invalid Date" : $this->format($time, false);
		});
	}

	private function thisTime(mixed $thisValue) : float{
		if(!$thisValue instanceof JsObject || $thisValue->className !== "Date" || !is_float($thisValue->host)){
			$this->js->throwError("TypeError", "this is not a Date object.");
		}
		return $thisValue->host;
	}

	/**
	 * @param list<mixed> $args
	 */
	private function timeFromArguments(array $args) : float{
		if(count($args) === 0){
			return self::now();
		}
		if(count($args) === 1){
			$value = $args[0];
			if($value instanceof JsObject && $value->className === "Date" && is_float($value->host)){
				return $value->host;
			}
			$primitive = $this->js->toPrimitive($value);
			if(is_string($primitive)){
				return $this->parse($primitive);
			}
			return (float) $this->js->toNumber($primitive);
		}
		return $this->fromComponents($args);
	}

	/**
	 * @param list<mixed> $args
	 */
	private function fromComponents(array $args) : float{
		$values = [];
		foreach([0, 0, 1, 0, 0, 0, 0] as $index => $default){
			$values[] = isset($args[$index]) ? (int) $this->js->toNumber($args[$index]) : $default;
		}
		[$year, $month, $day, $hour, $minute, $second, $millisecond] = $values;
		if($year >= 0 && $year <= 99){
			$year += 1900;
		}
		try{
			$date = (new DateTimeImmutable("now", new DateTimeZone("UTC")))->setDate($year, $month + 1, $day)->setTime($hour, $minute, $second);
			return (float) $date->getTimestamp() * 1000 + $millisecond;
		}catch(Throwable){
			return NAN;
		}
	}

	private function parse(string $text) : float{
		$timestamp = strtotime($text . (self::lacksTimeZone($text) ? " UTC" : ""));
		if($timestamp === false){
			return NAN;
		}
		$milliseconds = 0.0;
		if(preg_match('/\.(\d{1,3})/', $text, $match) === 1){
			$milliseconds = (float) str_pad($match[1], 3, "0");
		}
		return (float) $timestamp * 1000 + $milliseconds;
	}

	private function format(float $time, bool $iso) : string{
		$seconds = (int) floor($time / 1000);
		$milliseconds = (int) fmod($time, 1000);
		if($milliseconds < 0){
			$milliseconds += 1000;
		}
		if($iso){
			return gmdate("Y-m-d\\TH:i:s", $seconds) . sprintf(".%03dZ", $milliseconds);
		}
		return gmdate("D M d Y H:i:s", $seconds) . " GMT+0000 (Coordinated Universal Time)";
	}

	private static function lacksTimeZone(string $text) : bool{
		return preg_match('/^\d{4}-\d{2}-\d{2}$/', $text) === 1 || preg_match('/Z$|[+-]\d{2}:?\d{2}$/', $text) !== 1;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js\builtin;

use behaviorpack\script\js\Interpreter;
use behaviorpack\script\js\JsArray;
use behaviorpack\script\js\JsCallable;
use behaviorpack\script\js\JsNull;
use behaviorpack\script\js\JsObject;
use function array_fill;
use function array_merge;
use function array_reverse;
use function array_slice;
use function array_splice;
use function count;
use function implode;
use function is_array;
use function is_float;
use function is_int;
use function is_nan;
use function is_string;
use function max;
use function min;
use function spl_object_id;
use function strcmp;
use function usort;

/**
 * Array and its prototype, and array iterators.
 */
final class ArrayBuiltins{

	private JsObject $iteratorPrototype;

	public function __construct(
		private Interpreter $js
	){}

	public function install() : void{
		$js = $this->js;
		$proto = $js->arrayPrototype;
		$construct = function(array $args, JsObject $newTarget) use ($js) : JsObject{
			$array = new JsArray($js->prototypeFor($newTarget, $js->arrayPrototype));
			if(count($args) === 1 && (is_int($args[0]) || is_float($args[0]))){
				$length = $args[0];
				if(!is_int($length) || $length < 0 || $length > 50_000_000){
					$js->throwError("RangeError", "Invalid array length");
				}
				$array->items = $length === 0 ? [] : array_fill(0, $length, null);
				return $array;
			}
			$array->items = $args;
			return $array;
		};
		$array = $js->makeClass("Array", $proto, $construct, function(mixed $thisValue, array $args) use ($construct, $js) : mixed{
			return $construct($args, $js->constructors["Array"]);
		}, 1);
		$js->constructors["Array"] = $array;
		$js->defineHidden($js->global, "Array", $array);

		$js->defineMethod($array, "isArray", 1, function(mixed $thisValue, array $args) : mixed{
			$value = $args[0] ?? null;
			return $value instanceof JsArray && $value->className === "Array";
		});
		$js->defineMethod($array, "of", 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->newArray($args);
		});
		$js->defineMethod($array, "from", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$source = $args[0] ?? null;
			$mapper = $args[1] ?? null;
			if($source === null || $source instanceof JsNull){
				$js->throwError("TypeError", "Array.from requires an array-like object");
			}
			if(is_string($source) || ($source instanceof JsObject && $js->getSymbol($source, $js->symIterator) instanceof JsCallable)){
				$items = $js->iterableToList($source);
			}elseif($source instanceof JsObject){
				$length = $js->toIntegerOrInfinity($js->get($source, "length"));
				$items = [];
				for($i = 0; $i < $length && $i < 50_000_000; ++$i){
					$items[] = $js->get($source, (string) $i);
				}
			}else{
				$items = [];
			}
			if($mapper instanceof JsCallable){
				foreach($items as $index => $item){
					$items[$index] = $js->call($mapper, $args[2] ?? null, [$item, $index]);
				}
			}
			return $js->newArray($items);
		});

		$this->iteratorPrototype = new JsObject($js->iteratorPrototype);
		$this->iteratorPrototype->symbols[spl_object_id($js->symToStringTag)] = [$js->symToStringTag, "Array Iterator"];
		$js->defineMethod($this->iteratorPrototype, "next", 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			if(!$thisValue instanceof JsObject || !is_array($thisValue->host) || !isset($thisValue->host["kind"])){
				$js->throwError("TypeError", "next method called on incompatible receiver");
			}
			$state = $thisValue->host;
			$source = $state["source"];
			$items = $source instanceof JsArray ? $source->items : [];
			$index = $state["index"];
			if($state["done"] || $index >= count($items)){
				$thisValue->host["done"] = true;
				return $js->iterResult(null, true);
			}
			$thisValue->host["index"] = $index + 1;
			return match($state["kind"]){
				"keys" => $js->iterResult($index, false),
				"entries" => $js->iterResult($js->newArray([$index, $items[$index]]), false),
				default => $js->iterResult($items[$index], false)
			};
		});

		$this->installAccessors($proto);
		$this->installSearch($proto);
		$this->installIteration($proto);
		$this->installTransforms($proto);
	}

	/**
	 * Creates an iterator over an array, of the given kind: "values",
	 * "keys" or "entries".
	 */
	public function createIterator(JsArray $array, string $kind) : JsObject{
		$iterator = new JsObject($this->iteratorPrototype);
		$iterator->className = "Array Iterator";
		$iterator->host = ["source" => $array, "index" => 0, "kind" => $kind, "done" => false];
		return $iterator;
	}

	private function thisArray(mixed $thisValue) : JsArray{
		if($thisValue instanceof JsArray){
			return $thisValue;
		}
		$js = $this->js;
		if($thisValue instanceof JsObject){
			$length = $js->toIntegerOrInfinity($js->get($thisValue, "length"));
			$items = [];
			for($i = 0; $i < $length && $i < 1_000_000; ++$i){
				$items[] = $js->get($thisValue, (string) $i);
			}
			return $js->newArray($items);
		}
		if(is_string($thisValue)){
			return $js->newArray($js->iterableToList($thisValue));
		}
		$js->throwError("TypeError", "Array.prototype method called on incompatible receiver");
	}

	private function mutable(mixed $thisValue) : JsArray{
		if(!$thisValue instanceof JsArray){
			$this->js->throwError("TypeError", "Array.prototype method called on incompatible receiver");
		}
		if($thisValue->frozen){
			$this->js->throwError("TypeError", "Cannot modify a frozen array");
		}
		return $thisValue;
	}

	private function relativeIndex(mixed $value, int $length, int $default) : int{
		if($value === null){
			return $default;
		}
		$index = $this->js->toIntegerOrInfinity($value);
		if($index < 0){
			return (int) max(0, $length + $index);
		}
		return (int) min($index, $length);
	}

	private function callback(mixed $value) : JsCallable{
		if(!$value instanceof JsCallable){
			$this->js->throwError("TypeError", $this->js->describeValue($value) . " is not a function");
		}
		return $value;
	}

	private function installAccessors(JsObject $proto) : void{
		$js = $this->js;
		$js->defineMethod($proto, "push", 1, function(mixed $thisValue, array $args) : mixed{
			$array = $this->mutable($thisValue);
			foreach($args as $arg){
				$array->items[] = $arg;
			}
			return count($array->items);
		});
		$js->defineMethod($proto, "pop", 0, function(mixed $thisValue, array $args) : mixed{
			$array = $this->mutable($thisValue);
			if(count($array->items) === 0){
				return null;
			}
			return \array_pop($array->items);
		});
		$js->defineMethod($proto, "shift", 0, function(mixed $thisValue, array $args) : mixed{
			$array = $this->mutable($thisValue);
			if(count($array->items) === 0){
				return null;
			}
			return \array_shift($array->items);
		});
		$js->defineMethod($proto, "unshift", 1, function(mixed $thisValue, array $args) : mixed{
			$array = $this->mutable($thisValue);
			$array->items = array_merge($args, $array->items);
			return count($array->items);
		});
		$js->defineMethod($proto, "slice", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$array = $this->thisArray($thisValue);
			$length = count($array->items);
			$start = $this->relativeIndex($args[0] ?? null, $length, 0);
			$end = $this->relativeIndex($args[1] ?? null, $length, $length);
			return $js->newArray($end > $start ? array_slice($array->items, $start, $end - $start) : []);
		});
		$js->defineMethod($proto, "splice", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$array = $this->mutable($thisValue);
			$length = count($array->items);
			$start = $this->relativeIndex($args[0] ?? null, $length, 0);
			if(count($args) === 0){
				$deleteCount = 0;
			}elseif(count($args) === 1){
				$deleteCount = $length - $start;
			}else{
				$deleteCount = (int) max(0, min($js->toIntegerOrInfinity($args[1]), $length - $start));
			}
			$removed = array_splice($array->items, $start, $deleteCount, array_slice($args, 2));
			return $js->newArray($removed);
		});
		$js->defineMethod($proto, "toSpliced", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$items = $this->thisArray($thisValue)->items;
			$length = count($items);
			$start = $this->relativeIndex($args[0] ?? null, $length, 0);
			$deleteCount = count($args) < 2 ? $length - $start : (int) max(0, min($js->toIntegerOrInfinity($args[1]), $length - $start));
			array_splice($items, $start, $deleteCount, array_slice($args, 2));
			return $js->newArray($items);
		});
		$js->defineMethod($proto, "concat", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$items = $this->thisArray($thisValue)->items;
			foreach($args as $arg){
				if($arg instanceof JsArray && $arg->className === "Array"){
					foreach($arg->items as $item){
						$items[] = $item;
					}
				}else{
					$items[] = $arg;
				}
			}
			return $js->newArray($items);
		});
		$js->defineMethod($proto, "join", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$separator = ($args[0] ?? null) === null ? "," : $js->toString($args[0]);
			return $this->join($this->thisArray($thisValue), $separator);
		});
		$js->defineMethod($proto, "toString", 0, function(mixed $thisValue, array $args) : mixed{
			return $this->join($this->thisArray($thisValue), ",");
		});
		$js->defineMethod($proto, "toLocaleString", 0, function(mixed $thisValue, array $args) : mixed{
			return $this->join($this->thisArray($thisValue), ",");
		});
		$js->defineMethod($proto, "reverse", 0, function(mixed $thisValue, array $args) : mixed{
			$array = $this->mutable($thisValue);
			$array->items = array_reverse($array->items);
			return $array;
		});
		$js->defineMethod($proto, "toReversed", 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->newArray(array_reverse($this->thisArray($thisValue)->items));
		});
		$js->defineMethod($proto, "at", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$items = $this->thisArray($thisValue)->items;
			$index = (int) $js->toIntegerOrInfinity($args[0] ?? null);
			if($index < 0){
				$index += count($items);
			}
			return $items[$index] ?? null;
		});
		$js->defineMethod($proto, "with", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$items = $this->thisArray($thisValue)->items;
			$index = (int) $js->toIntegerOrInfinity($args[0] ?? null);
			if($index < 0){
				$index += count($items);
			}
			if($index < 0 || $index >= count($items)){
				$js->throwError("RangeError", "Invalid index");
			}
			$items[$index] = $args[1] ?? null;
			return $js->newArray($items);
		});
		$js->defineMethod($proto, "fill", 1, function(mixed $thisValue, array $args) : mixed{
			$array = $this->mutable($thisValue);
			$length = count($array->items);
			$start = $this->relativeIndex($args[1] ?? null, $length, 0);
			$end = $this->relativeIndex($args[2] ?? null, $length, $length);
			for($i = $start; $i < $end; ++$i){
				$array->items[$i] = $args[0] ?? null;
			}
			return $array;
		});
		$js->defineMethod($proto, "copyWithin", 2, function(mixed $thisValue, array $args) : mixed{
			$array = $this->mutable($thisValue);
			$length = count($array->items);
			$target = $this->relativeIndex($args[0] ?? null, $length, 0);
			$start = $this->relativeIndex($args[1] ?? null, $length, 0);
			$end = $this->relativeIndex($args[2] ?? null, $length, $length);
			$chunk = array_slice($array->items, $start, max(0, $end - $start));
			foreach($chunk as $offset => $item){
				if($target + $offset < $length){
					$array->items[$target + $offset] = $item;
				}
			}
			return $array;
		});
	}

	private function join(JsArray $array, string $separator) : string{
		$parts = [];
		foreach($array->items as $item){
			$parts[] = ($item === null || $item instanceof JsNull) ? "" : $this->js->toString($item);
		}
		return implode($separator, $parts);
	}

	private function installSearch(JsObject $proto) : void{
		$js = $this->js;
		$js->defineMethod($proto, "indexOf", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$items = $this->thisArray($thisValue)->items;
			$start = $this->relativeIndex($args[1] ?? null, count($items), 0);
			$search = $args[0] ?? null;
			for($i = $start, $count = count($items); $i < $count; ++$i){
				if($js->strictEquals($items[$i], $search)){
					return $i;
				}
			}
			return -1;
		});
		$js->defineMethod($proto, "lastIndexOf", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$items = $this->thisArray($thisValue)->items;
			$search = $args[0] ?? null;
			$start = count($args) > 1 ? (int) $js->toIntegerOrInfinity($args[1]) : count($items) - 1;
			if($start < 0){
				$start += count($items);
			}
			for($i = min($start, count($items) - 1); $i >= 0; --$i){
				if($js->strictEquals($items[$i], $search)){
					return $i;
				}
			}
			return -1;
		});
		$js->defineMethod($proto, "includes", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$items = $this->thisArray($thisValue)->items;
			$start = $this->relativeIndex($args[1] ?? null, count($items), 0);
			$search = $args[0] ?? null;
			for($i = $start, $count = count($items); $i < $count; ++$i){
				if($js->sameValueZero($items[$i], $search)){
					return true;
				}
			}
			return false;
		});
		$finders = [
			"find" => [false, false],
			"findIndex" => [false, true],
			"findLast" => [true, false],
			"findLastIndex" => [true, true]
		];
		foreach($finders as $name => [$reverse, $index]){
			$js->defineMethod($proto, $name, 1, function(mixed $thisValue, array $args) use ($js, $reverse, $index) : mixed{
				$array = $this->thisArray($thisValue);
				$callback = $this->callback($args[0] ?? null);
				$count = count($array->items);
				for($step = 0; $step < $count; ++$step){
					$i = $reverse ? $count - 1 - $step : $step;
					$item = $array->items[$i] ?? null;
					if($js->toBoolean($js->call($callback, $args[1] ?? null, [$item, $i, $array]))){
						return $index ? $i : $item;
					}
				}
				return $index ? -1 : null;
			});
		}
	}

	private function installIteration(JsObject $proto) : void{
		$js = $this->js;
		$js->defineMethod($proto, "forEach", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$array = $this->thisArray($thisValue);
			$callback = $this->callback($args[0] ?? null);
			for($i = 0; $i < count($array->items); ++$i){
				$js->call($callback, $args[1] ?? null, [$array->items[$i], $i, $array]);
			}
			return null;
		});
		$js->defineMethod($proto, "map", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$array = $this->thisArray($thisValue);
			$callback = $this->callback($args[0] ?? null);
			$result = [];
			$count = count($array->items);
			for($i = 0; $i < $count; ++$i){
				$result[] = $js->call($callback, $args[1] ?? null, [$array->items[$i] ?? null, $i, $array]);
			}
			return $js->newArray($result);
		});
		$js->defineMethod($proto, "filter", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$array = $this->thisArray($thisValue);
			$callback = $this->callback($args[0] ?? null);
			$result = [];
			$count = count($array->items);
			for($i = 0; $i < $count; ++$i){
				$item = $array->items[$i] ?? null;
				if($js->toBoolean($js->call($callback, $args[1] ?? null, [$item, $i, $array]))){
					$result[] = $item;
				}
			}
			return $js->newArray($result);
		});
		$js->defineMethod($proto, "some", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$array = $this->thisArray($thisValue);
			$callback = $this->callback($args[0] ?? null);
			for($i = 0; $i < count($array->items); ++$i){
				if($js->toBoolean($js->call($callback, $args[1] ?? null, [$array->items[$i], $i, $array]))){
					return true;
				}
			}
			return false;
		});
		$js->defineMethod($proto, "every", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$array = $this->thisArray($thisValue);
			$callback = $this->callback($args[0] ?? null);
			for($i = 0; $i < count($array->items); ++$i){
				if(!$js->toBoolean($js->call($callback, $args[1] ?? null, [$array->items[$i], $i, $array]))){
					return false;
				}
			}
			return true;
		});
		$js->defineMethod($proto, "reduce", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$array = $this->thisArray($thisValue);
			$callback = $this->callback($args[0] ?? null);
			$count = count($array->items);
			$index = 0;
			if(count($args) > 1){
				$accumulator = $args[1];
			}else{
				if($count === 0){
					$js->throwError("TypeError", "Reduce of empty array with no initial value");
				}
				$accumulator = $array->items[0];
				$index = 1;
			}
			for($i = $index; $i < $count; ++$i){
				$accumulator = $js->call($callback, null, [$accumulator, $array->items[$i] ?? null, $i, $array]);
			}
			return $accumulator;
		});
		$js->defineMethod($proto, "reduceRight", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$array = $this->thisArray($thisValue);
			$callback = $this->callback($args[0] ?? null);
			$index = count($array->items) - 1;
			if(count($args) > 1){
				$accumulator = $args[1];
			}else{
				if($index < 0){
					$js->throwError("TypeError", "Reduce of empty array with no initial value");
				}
				$accumulator = $array->items[$index];
				$index--;
			}
			for($i = $index; $i >= 0; --$i){
				$accumulator = $js->call($callback, null, [$accumulator, $array->items[$i] ?? null, $i, $array]);
			}
			return $accumulator;
		});
		$values = $js->defineMethod($proto, "values", 0, function(mixed $thisValue, array $args) : mixed{
			return $this->createIterator($this->thisArray($thisValue), "values");
		});
		$js->defineMethod($proto, "keys", 0, function(mixed $thisValue, array $args) : mixed{
			return $this->createIterator($this->thisArray($thisValue), "keys");
		});
		$js->defineMethod($proto, "entries", 0, function(mixed $thisValue, array $args) : mixed{
			return $this->createIterator($this->thisArray($thisValue), "entries");
		});
		$js->defineHidden($proto, $js->symIterator, $values);
		$js->setNativeArrayIterator($values);
	}

	private function installTransforms(JsObject $proto) : void{
		$js = $this->js;
		$sort = function(array $items, mixed $comparator) use ($js) : array{
			$defined = [];
			$undefined = 0;
			foreach($items as $item){
				if($item === null){
					$undefined++;
				}else{
					$defined[] = $item;
				}
			}
			if($comparator instanceof JsCallable){
				usort($defined, function(mixed $a, mixed $b) use ($js, $comparator) : int{
					$result = $js->toNumber($js->call($comparator, null, [$a, $b]));
					if(is_float($result) && is_nan($result)){
						return 0;
					}
					return $result < 0 ? -1 : ($result > 0 ? 1 : 0);
				});
			}elseif($comparator !== null){
				$js->throwError("TypeError", "The comparison function must be either a function or undefined");
			}else{
				usort($defined, function(mixed $a, mixed $b) use ($js) : int{
					return strcmp($js->toString($a), $js->toString($b));
				});
			}
			for($i = 0; $i < $undefined; ++$i){
				$defined[] = null;
			}
			return $defined;
		};
		$js->defineMethod($proto, "sort", 1, function(mixed $thisValue, array $args) use ($sort) : mixed{
			$array = $this->mutable($thisValue);
			$array->items = $sort($array->items, $args[0] ?? null);
			return $array;
		});
		$js->defineMethod($proto, "toSorted", 1, function(mixed $thisValue, array $args) use ($sort, $js) : mixed{
			return $js->newArray($sort($this->thisArray($thisValue)->items, $args[0] ?? null));
		});
		$js->defineMethod($proto, "flat", 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			$depth = ($args[0] ?? null) === null ? 1 : (int) $js->toIntegerOrInfinity($args[0]);
			return $js->newArray($this->flatten($this->thisArray($thisValue)->items, $depth));
		});
		$js->defineMethod($proto, "flatMap", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$array = $this->thisArray($thisValue);
			$callback = $this->callback($args[0] ?? null);
			$mapped = [];
			foreach($array->items as $i => $item){
				$mapped[] = $js->call($callback, $args[1] ?? null, [$item, $i, $array]);
			}
			return $js->newArray($this->flatten($mapped, 1));
		});
	}

	/**
	 * @param list<mixed> $items
	 * @return list<mixed>
	 */
	private function flatten(array $items, int $depth) : array{
		$result = [];
		foreach($items as $item){
			if($depth > 0 && $item instanceof JsArray && $item->className === "Array"){
				foreach($this->flatten($item->items, $depth - 1) as $inner){
					$result[] = $inner;
				}
			}else{
				$result[] = $item;
			}
		}
		return $result;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js\builtin;

use behaviorpack\script\js\Interpreter;
use behaviorpack\script\js\JsArray;
use behaviorpack\script\js\JsCallable;
use behaviorpack\script\js\JsFunction;
use behaviorpack\script\js\JsNull;
use behaviorpack\script\js\JsObject;
use behaviorpack\script\js\JsPromise;
use behaviorpack\script\js\JsSymbol;
use behaviorpack\script\js\JsThrow;
use behaviorpack\script\js\NativeFunction;
use stdClass;
use function abs;
use function acos;
use function acosh;
use function array_key_exists;
use function array_pop;
use function array_slice;
use function asin;
use function asinh;
use function atan;
use function atan2;
use function atanh;
use function base_convert;
use function ceil;
use function cos;
use function cosh;
use function count;
use function exp;
use function expm1;
use function floor;
use function fmod;
use function get_object_vars;
use function implode;
use function in_array;
use function intdiv;
use function is_array;
use function is_bool;
use function is_finite;
use function is_float;
use function is_infinite;
use function is_int;
use function is_nan;
use function is_string;
use function json_decode;
use function json_encode;
use function log;
use function log10;
use function log1p;
use function max;
use function min;
use function mt_getrandmax;
use function mt_rand;
use function number_format;
use function pack;
use function pow;
use function preg_match;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
use function round;
use function rtrim;
use function sin;
use function sinh;
use function spl_object_id;
use function sprintf;
use function sqrt;
use function str_contains;
use function str_repeat;
use function strlen;
use function strpos;
use function strtolower;
use function strtr;
use function substr;
use function tan;
use function tanh;
use function trim;
use function unpack;
use const INF;
use const JSON_INVALID_UTF8_SUBSTITUTE;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
use const M_E;
use const M_LN10;
use const M_LN2;
use const M_LOG10E;
use const M_LOG2E;
use const M_PI;
use const M_SQRT1_2;
use const M_SQRT2;
use const NAN;

/**
 * Object, Function, Symbol, errors, Boolean, Number, Math, JSON, Reflect,
 * console and the global functions.
 */
final class CoreBuiltins{

	private const ERROR_TYPES = ["TypeError", "RangeError", "SyntaxError", "ReferenceError", "EvalError", "URIError", "AggregateError"];

	/** @var array<string, JsSymbol> */
	private array $registry = [];

	public function __construct(
		private Interpreter $js
	){}

	public function install() : void{
		$js = $this->js;
		$global = $js->global;
		$js->defineHidden($global, "globalThis", $global);
		$global->props["undefined"] = null;
		$global->props["NaN"] = NAN;
		$global->props["Infinity"] = INF;
		$global->hidden["undefined"] = true;
		$global->hidden["NaN"] = true;
		$global->hidden["Infinity"] = true;
		$global->locked["undefined"] = true;
		$global->locked["NaN"] = true;
		$global->locked["Infinity"] = true;

		$this->installObject();
		$this->installFunction();
		$this->installSymbol();
		$this->installErrors();
		$this->installBoolean();
		$this->installNumber();
		$this->installMath();
		$this->installJson();
		$this->installReflect();
		$this->installGlobals();
		$this->installConsole();

		$js->defineMethod($js->iteratorPrototype, $js->symIterator, 0, function(mixed $thisValue, array $args) : mixed{
			return $thisValue;
		});
	}

	private function installObject() : void{
		$js = $this->js;
		$proto = $js->objectPrototype;
		$construct = function(array $args, JsObject $newTarget) use ($js) : JsObject{
			$value = $args[0] ?? null;
			if($value === null || $value instanceof JsNull){
				return $js->newObject($js->prototypeFor($newTarget, $js->objectPrototype));
			}
			return $js->toObject($value);
		};
		$object = $js->makeClass("Object", $proto, $construct, function(mixed $thisValue, array $args) use ($construct, $js) : mixed{
			return $construct($args, $js->constructors["Object"]);
		}, 1);
		$js->constructors["Object"] = $object;
		$js->defineHidden($js->global, "Object", $object);

		$js->defineMethod($object, "keys", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $js->toObject($args[0] ?? null);
			return $js->newArray($this->enumerableKeys($target));
		});
		$js->defineMethod($object, "values", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $js->toObject($args[0] ?? null);
			$values = [];
			foreach($this->enumerableKeys($target) as $key){
				$values[] = $js->getFrom($target, $key, $target);
			}
			return $js->newArray($values);
		});
		$js->defineMethod($object, "entries", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $js->toObject($args[0] ?? null);
			$entries = [];
			foreach($this->enumerableKeys($target) as $key){
				$entries[] = $js->newArray([$key, $js->getFrom($target, $key, $target)]);
			}
			return $js->newArray($entries);
		});
		$js->defineMethod($object, "assign", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $js->toObject($args[0] ?? null);
			foreach(array_slice($args, 1) as $source){
				if($source instanceof JsObject){
					foreach($this->enumerableKeys($source) as $key){
						$js->set($target, $key, $js->getFrom($source, $key, $source));
					}
					foreach($source->symbols as [$symbol, $value]){
						$js->set($target, $symbol, $value);
					}
				}elseif(is_string($source)){
					$js->copyDataProperties($target, $source);
				}
			}
			return $target;
		});
		$js->defineMethod($object, "freeze", 1, function(mixed $thisValue, array $args) : mixed{
			$target = $args[0] ?? null;
			if($target instanceof JsObject){
				$this->freeze($target);
			}
			return $target;
		});
		$js->defineMethod($object, "isFrozen", 1, function(mixed $thisValue, array $args) : mixed{
			$target = $args[0] ?? null;
			if(!$target instanceof JsObject){
				return true;
			}
			if($target->extensible || ($target instanceof JsArray && !$target->frozen && count($target->items) > 0)){
				return false;
			}
			foreach($target->props as $key => $unused){
				if(!isset($target->locked[$key])){
					return false;
				}
			}
			return true;
		});
		$js->defineMethod($object, "seal", 1, function(mixed $thisValue, array $args) : mixed{
			$target = $args[0] ?? null;
			if($target instanceof JsObject){
				$target->extensible = false;
			}
			return $target;
		});
		$js->defineMethod($object, "isSealed", 1, function(mixed $thisValue, array $args) : mixed{
			$target = $args[0] ?? null;
			return !$target instanceof JsObject || !$target->extensible;
		});
		$js->defineMethod($object, "preventExtensions", 1, function(mixed $thisValue, array $args) : mixed{
			$target = $args[0] ?? null;
			if($target instanceof JsObject){
				$target->extensible = false;
			}
			return $target;
		});
		$js->defineMethod($object, "isExtensible", 1, function(mixed $thisValue, array $args) : mixed{
			$target = $args[0] ?? null;
			return $target instanceof JsObject && $target->extensible;
		});
		$js->defineMethod($object, "create", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$proto = $args[0] ?? null;
			if(!$proto instanceof JsObject && !$proto instanceof JsNull){
				$js->throwError("TypeError", "Object prototype may only be an Object or null");
			}
			$created = new JsObject($proto instanceof JsObject ? $proto : null);
			if(($args[1] ?? null) instanceof JsObject){
				$this->defineProperties($created, $args[1]);
			}
			return $created;
		});
		$js->defineMethod($object, "getPrototypeOf", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $args[0] ?? null;
			if(is_string($target)){
				return $js->stringPrototype;
			}
			if(is_int($target) || is_float($target)){
				return $js->numberPrototype;
			}
			if(is_bool($target)){
				return $js->booleanPrototype;
			}
			$target = $js->toObject($target);
			return $target->proto ?? JsNull::get();
		});
		$js->defineMethod($object, "setPrototypeOf", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $args[0] ?? null;
			$proto = $args[1] ?? null;
			if($target instanceof JsObject){
				if($proto instanceof JsObject){
					$target->proto = $proto;
				}elseif($proto instanceof JsNull){
					$target->proto = null;
				}else{
					$js->throwError("TypeError", "Object prototype may only be an Object or null");
				}
			}
			return $target;
		});
		$js->defineMethod($object, "defineProperty", 3, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $args[0] ?? null;
			if(!$target instanceof JsObject){
				$js->throwError("TypeError", "Object.defineProperty called on non-object");
			}
			$descriptor = $args[2] ?? null;
			if(!$descriptor instanceof JsObject){
				$js->throwError("TypeError", "Property description must be an object");
			}
			$this->defineProperty($target, $js->toPropertyKey($args[1] ?? null), $descriptor);
			return $target;
		});
		$js->defineMethod($object, "defineProperties", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $args[0] ?? null;
			if(!$target instanceof JsObject || !($args[1] ?? null) instanceof JsObject){
				$js->throwError("TypeError", "Object.defineProperties called on non-object");
			}
			$this->defineProperties($target, $args[1]);
			return $target;
		});
		$js->defineMethod($object, "getOwnPropertyNames", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $js->toObject($args[0] ?? null);
			$keys = $js->ownKeys($target, true);
			if($target instanceof JsArray){
				$keys[] = "length";
			}
			return $js->newArray($keys);
		});
		$js->defineMethod($object, "getOwnPropertySymbols", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->newArray($js->ownSymbols($js->toObject($args[0] ?? null)));
		});
		$js->defineMethod($object, "getOwnPropertyDescriptor", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $this->descriptor($js->toObject($args[0] ?? null), $js->toPropertyKey($args[1] ?? null));
		});
		$js->defineMethod($object, "getOwnPropertyDescriptors", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $js->toObject($args[0] ?? null);
			$result = $js->newObject();
			foreach($js->ownKeys($target, true) as $key){
				$result->props[$key] = $this->descriptor($target, $key);
			}
			return $result;
		});
		$js->defineMethod($object, "fromEntries", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = $js->newObject();
			foreach($js->iterate($args[0] ?? null) as $entry){
				$key = $js->toPropertyKey($js->get($entry, "0"));
				$value = $js->get($entry, "1");
				if($key instanceof JsSymbol){
					$result->symbols[spl_object_id($key)] = [$key, $value];
				}else{
					$js->createDataProperty($result, $key, $value);
				}
			}
			return $result;
		});
		$js->defineMethod($object, "hasOwn", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->hasOwnProperty($js->toObject($args[0] ?? null), $js->toPropertyKey($args[1] ?? null));
		});
		$js->defineMethod($object, "is", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$a = $args[0] ?? null;
			$b = $args[1] ?? null;
			if(is_float($a) && is_float($b) && is_nan($a) && is_nan($b)){
				return true;
			}
			return $js->strictEquals($a, $b);
		});
		$js->defineMethod($object, "groupBy", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = new JsObject(null);
			$index = 0;
			foreach($js->iterate($args[0] ?? null) as $value){
				$key = $js->toPropertyKey($js->call($args[1] ?? null, null, [$value, $index++]));
				if($key instanceof JsSymbol){
					continue;
				}
				$group = $result->props[$key] ?? null;
				if(!$group instanceof JsArray){
					$group = $js->newArray();
					$result->props[$key] = $group;
				}
				$group->items[] = $value;
			}
			return $result;
		});

		$js->defineMethod($proto, "hasOwnProperty", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->hasOwnProperty($js->toObject($thisValue), $js->toPropertyKey($args[0] ?? null));
		});
		$js->defineMethod($proto, "isPrototypeOf", 1, function(mixed $thisValue, array $args) : mixed{
			$value = $args[0] ?? null;
			if(!$value instanceof JsObject || !$thisValue instanceof JsObject){
				return false;
			}
			for($current = $value->proto; $current !== null; $current = $current->proto){
				if($current === $thisValue){
					return true;
				}
			}
			return false;
		});
		$js->defineMethod($proto, "propertyIsEnumerable", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $js->toObject($thisValue);
			$key = $js->toPropertyKey($args[0] ?? null);
			if($key instanceof JsSymbol){
				return isset($target->symbols[spl_object_id($key)]);
			}
			return $js->hasOwnProperty($target, $key) && $js->isEnumerable($target, $key);
		});
		$js->defineMethod($proto, "toString", 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			return "[object " . $this->tag($thisValue) . "]";
		});
		$js->defineMethod($proto, "toLocaleString", 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->call($js->get($thisValue, "toString"), $thisValue, []);
		});
		$js->defineMethod($proto, "valueOf", 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->toObject($thisValue);
		});
		$getProto = $js->native("get __proto__", 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			if($thisValue instanceof JsObject){
				return $thisValue->proto ?? JsNull::get();
			}
			return JsNull::get();
		});
		$setProto = $js->native("set __proto__", 1, function(mixed $thisValue, array $args) : mixed{
			$value = $args[0] ?? null;
			if($thisValue instanceof JsObject){
				if($value instanceof JsObject){
					$thisValue->proto = $value;
				}elseif($value instanceof JsNull){
					$thisValue->proto = null;
				}
			}
			return null;
		});
		$proto->accessors["__proto__"] = [$getProto, $setProto];
		$proto->hidden["__proto__"] = true;
	}

	public function tag(mixed $value) : string{
		$js = $this->js;
		if($value === null){
			return "Undefined";
		}
		if($value instanceof JsNull){
			return "Null";
		}
		if($value instanceof JsObject){
			$tag = $js->getSymbol($value, $js->symToStringTag);
			if(is_string($tag)){
				return $tag;
			}
			if($value instanceof JsArray){
				return $value->className === "Arguments" ? "Arguments" : "Array";
			}
			if($value instanceof JsCallable){
				return "Function";
			}
			return in_array($value->className, ["Error", "Boolean", "Number", "String", "Date", "RegExp"], true) ? $value->className : "Object";
		}
		return match(true){
			is_string($value) => "String",
			is_bool($value) => "Boolean",
			default => "Number"
		};
	}

	/**
	 * @return list<string>
	 */
	public function enumerableKeys(JsObject $object) : array{
		return $this->js->ownKeys($object);
	}

	public function freeze(JsObject $object) : void{
		$object->extensible = false;
		foreach($object->props as $key => $unused){
			$object->locked[$key] = true;
		}
		if($object instanceof JsArray){
			$object->frozen = true;
		}
	}

	private function defineProperties(JsObject $target, JsObject $descriptors) : void{
		foreach($this->js->ownKeys($descriptors) as $key){
			$descriptor = $this->js->getFrom($descriptors, $key, $descriptors);
			if($descriptor instanceof JsObject){
				$this->defineProperty($target, $key, $descriptor);
			}
		}
	}

	public function defineProperty(JsObject $target, string|JsSymbol $key, JsObject $descriptor) : void{
		$js = $this->js;
		$has = function(string $name) use ($js, $descriptor) : bool{
			return $js->hasProperty($descriptor, $name);
		};
		if($key instanceof JsSymbol){
			$target->symbols[spl_object_id($key)] = [$key, $has("value") ? $js->get($descriptor, "value") : ($target->symbols[spl_object_id($key)][1] ?? null)];
			return;
		}
		$exists = $js->hasOwnProperty($target, $key);
		if(!$exists && !$target->extensible){
			$js->throwError("TypeError", "Cannot define property " . $key . ", object is not extensible");
		}
		if(isset($target->locked[$key]) && !isset($target->accessors[$key])){
			if($has("value") && !$js->strictEquals($js->get($descriptor, "value"), $target->props[$key] ?? null)){
				$js->throwError("TypeError", "Cannot redefine property: " . $key);
			}
		}
		if($has("get") || $has("set")){
			$getter = $js->get($descriptor, "get");
			$setter = $js->get($descriptor, "set");
			$pair = $target->accessors[$key] ?? [null, null];
			if($has("get")){
				$pair[0] = $getter instanceof JsCallable ? $getter : null;
			}
			if($has("set")){
				$pair[1] = $setter instanceof JsCallable ? $setter : null;
			}
			unset($target->props[$key], $target->locked[$key]);
			$target->accessors[$key] = $pair;
		}else{
			if($target instanceof JsArray && Interpreter::isIndex($key)){
				if($has("value")){
					$js->setOn($target, $key, $js->get($descriptor, "value"), $target);
				}
				return;
			}
			if($has("value") || !$exists || isset($target->accessors[$key])){
				unset($target->accessors[$key]);
				$target->props[$key] = $has("value") ? $js->get($descriptor, "value") : ($target->props[$key] ?? null);
			}
			if($has("writable")){
				if($js->toBoolean($js->get($descriptor, "writable"))){
					unset($target->locked[$key]);
				}else{
					$target->locked[$key] = true;
				}
			}elseif(!$exists){
				$target->locked[$key] = true;
			}
		}
		if($has("enumerable")){
			if($js->toBoolean($js->get($descriptor, "enumerable"))){
				unset($target->hidden[$key]);
			}else{
				$target->hidden[$key] = true;
			}
		}elseif(!$exists){
			$target->hidden[$key] = true;
		}
	}

	private function descriptor(JsObject $target, string|JsSymbol $key) : mixed{
		$js = $this->js;
		$result = $js->newObject();
		if($key instanceof JsSymbol){
			$entry = $target->symbols[spl_object_id($key)] ?? null;
			if($entry === null){
				return null;
			}
			$result->props = ["value" => $entry[1], "writable" => true, "enumerable" => true, "configurable" => true];
			return $result;
		}
		if(isset($target->accessors[$key])){
			$result->props = [
				"get" => $target->accessors[$key][0],
				"set" => $target->accessors[$key][1],
				"enumerable" => !isset($target->hidden[$key]),
				"configurable" => true
			];
			return $result;
		}
		if(!$js->hasOwnProperty($target, $key)){
			return null;
		}
		$writable = !isset($target->locked[$key]) && !($target instanceof JsArray && $target->frozen);
		$result->props = [
			"value" => $js->getFrom($target, $key, $target),
			"writable" => $writable,
			"enumerable" => $js->isEnumerable($target, $key),
			"configurable" => $writable
		];
		return $result;
	}

	private function installFunction() : void{
		$js = $this->js;
		$proto = $js->functionPrototype;
		$function = $js->makeClass("Function", $proto, function(array $args, JsObject $newTarget) use ($js) : JsObject{
			$js->throwError("EvalError", "Code generation from strings is not supported");
		}, function(mixed $thisValue, array $args) use ($js) : mixed{
			$js->throwError("EvalError", "Code generation from strings is not supported");
		}, 1);
		$js->constructors["Function"] = $function;
		$js->defineHidden($js->global, "Function", $function);
		$js->defineMethod($proto, "call", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->call($thisValue, $args[0] ?? null, array_slice($args, 1));
		});
		$js->defineMethod($proto, "apply", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$list = $args[1] ?? null;
			$arguments = ($list === null || $list instanceof JsNull) ? [] : $this->arrayLike($list);
			return $js->call($thisValue, $args[0] ?? null, $arguments);
		});
		$js->defineMethod($proto, "bind", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			if(!$thisValue instanceof JsCallable){
				$js->throwError("TypeError", "Bind must be called on a function");
			}
			$target = $thisValue;
			$boundThis = $args[0] ?? null;
			$boundArgs = array_slice($args, 1);
			$construct = null;
			if($js->isConstructor($target)){
				$construct = function(array $callArgs, JsObject $newTarget) use ($js, $target, $boundArgs) : JsObject{
					return $js->construct($target, [...$boundArgs, ...$callArgs]);
				};
			}
			$bound = new NativeFunction($js->functionPrototype, "bound " . $target->name, max(0, $target->length - count($boundArgs)), function(mixed $ignored, array $callArgs) use ($js, $target, $boundThis, $boundArgs) : mixed{
				return $js->call($target, $boundThis, [...$boundArgs, ...$callArgs]);
			}, $construct);
			return $bound;
		});
		$js->defineMethod($proto, "toString", 0, function(mixed $thisValue, array $args) : mixed{
			if($thisValue instanceof JsFunction && $thisValue->isClass){
				return "class " . $thisValue->name . " { }";
			}
			$name = $thisValue instanceof JsCallable ? $thisValue->name : "";
			return "function " . $name . "() { [native code] }";
		});
	}

	/**
	 * @return list<mixed>
	 */
	public function arrayLike(mixed $value) : array{
		$js = $this->js;
		if($value instanceof JsArray){
			return $value->items;
		}
		if(!$value instanceof JsObject){
			$js->throwError("TypeError", "CreateListFromArrayLike called on non-object");
		}
		$length = $js->toIntegerOrInfinity($js->get($value, "length"));
		$items = [];
		for($i = 0; $i < $length && $i < 100_000; ++$i){
			$items[] = $js->get($value, (string) $i);
		}
		return $items;
	}

	private function installSymbol() : void{
		$js = $this->js;
		$proto = $js->symbolPrototype;
		$symbol = $js->makeClass("Symbol", $proto, null, function(mixed $thisValue, array $args) use ($js) : mixed{
			$description = $args[0] ?? null;
			return new JsSymbol($description === null ? null : $js->toString($description));
		});
		$js->constructors["Symbol"] = $symbol;
		$js->defineHidden($js->global, "Symbol", $symbol);
		foreach(["iterator" => $js->symIterator, "asyncIterator" => $js->symAsyncIterator, "hasInstance" => $js->symHasInstance, "toPrimitive" => $js->symToPrimitive, "toStringTag" => $js->symToStringTag] as $name => $value){
			$js->defineHidden($symbol, $name, $value);
			$symbol->locked[$name] = true;
		}
		$js->defineMethod($symbol, "for", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$key = $js->toString($args[0] ?? null);
			return $this->registry[$key] ??= new JsSymbol($key);
		});
		$js->defineMethod($symbol, "keyFor", 1, function(mixed $thisValue, array $args) : mixed{
			foreach($this->registry as $key => $registered){
				if($registered === ($args[0] ?? null)){
					return (string) $key;
				}
			}
			return null;
		});
		$js->defineMethod($proto, "toString", 0, function(mixed $thisValue, array $args) : mixed{
			$symbol = $thisValue instanceof JsObject ? $thisValue->host : $thisValue;
			return $symbol instanceof JsSymbol ? "Symbol(" . ($symbol->description ?? "") . ")" : "Symbol()";
		});
		$js->defineMethod($proto, "valueOf", 0, function(mixed $thisValue, array $args) : mixed{
			return $thisValue instanceof JsObject ? $thisValue->host : $thisValue;
		});
		$js->defineGetter($proto, "description", function(mixed $thisValue) : mixed{
			$symbol = $thisValue instanceof JsObject ? $thisValue->host : $thisValue;
			return $symbol instanceof JsSymbol ? $symbol->description : null;
		});
	}

	private function installErrors() : void{
		$js = $this->js;
		$errorConstructor = $this->makeErrorClass("Error", $js->errorPrototype, null);
		$js->defineMethod($errorConstructor, "captureStackTrace", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $args[0] ?? null;
			if($target instanceof JsObject){
				$js->defineHidden($target, "stack", $js->toStringSafe($js->get($target, "name")) . ": " . $js->toStringSafe($js->get($target, "message")) . $js->stackTrace());
			}
			return null;
		});
		$js->defineMethod($js->errorPrototype, "toString", 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			if(!$thisValue instanceof JsObject){
				$js->throwError("TypeError", "Error.prototype.toString called on non-object");
			}
			$name = $js->get($thisValue, "name");
			$message = $js->get($thisValue, "message");
			$name = $name === null ? "Error" : $js->toString($name);
			$message = $message === null ? "" : $js->toString($message);
			if($name === ""){
				return $message;
			}
			return $message === "" ? $name : $name . ": " . $message;
		});
		foreach(self::ERROR_TYPES as $type){
			$proto = new JsObject($js->errorPrototype);
			$proto->className = "Error";
			$js->errorPrototypes[$type] = $proto;
			$this->makeErrorClass($type, $proto, $errorConstructor);
		}
		$js->errorPrototypes["Error"] = $js->errorPrototype;
	}

	private function makeErrorClass(string $type, JsObject $proto, ?JsObject $parent) : NativeFunction{
		$js = $this->js;
		$construct = function(array $args, JsObject $newTarget) use ($js, $proto, $type) : JsObject{
			$error = new JsObject($js->prototypeFor($newTarget, $proto));
			$error->className = "Error";
			$messageIndex = 0;
			if($type === "AggregateError"){
				$js->defineHidden($error, "errors", $js->newArray($js->iterableToList($args[0] ?? null)));
				$messageIndex = 1;
			}
			$message = $args[$messageIndex] ?? null;
			if($message !== null){
				$js->defineHidden($error, "message", $js->toString($message));
			}
			$options = $args[$messageIndex + 1] ?? null;
			if($options instanceof JsObject && $js->hasProperty($options, "cause")){
				$js->defineHidden($error, "cause", $js->get($options, "cause"));
			}
			$name = $js->get($error, "name");
			$js->defineHidden($error, "stack", $js->toStringSafe($name) . ": " . ($message === null ? "" : $js->toStringSafe($message)) . $js->stackTrace());
			return $error;
		};
		$constructor = $js->makeClass($type, $proto, $construct, function(mixed $thisValue, array $args) use ($construct, &$constructor) : mixed{
			return $construct($args, $constructor);
		}, 1, $parent);
		$js->defineHidden($proto, "name", $type);
		$js->defineHidden($proto, "message", "");
		$js->constructors[$type] = $constructor;
		$js->defineHidden($js->global, $type, $constructor);
		return $constructor;
	}

	private function installBoolean() : void{
		$js = $this->js;
		$proto = $js->booleanPrototype;
		$proto->host = false;
		$boolean = $js->makeClass("Boolean", $proto, function(array $args, JsObject $newTarget) use ($js) : JsObject{
			$wrapper = new JsObject($js->prototypeFor($newTarget, $js->booleanPrototype));
			$wrapper->className = "Boolean";
			$wrapper->host = $js->toBoolean($args[0] ?? null);
			return $wrapper;
		}, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->toBoolean($args[0] ?? null);
		}, 1);
		$js->constructors["Boolean"] = $boolean;
		$js->defineHidden($js->global, "Boolean", $boolean);
		$value = function(mixed $thisValue) use ($js) : bool{
			if(is_bool($thisValue)){
				return $thisValue;
			}
			if($thisValue instanceof JsObject && is_bool($thisValue->host)){
				return $thisValue->host;
			}
			$js->throwError("TypeError", "Boolean.prototype.valueOf requires that 'this' be a Boolean");
		};
		$js->defineMethod($proto, "toString", 0, function(mixed $thisValue, array $args) use ($value) : mixed{
			return $value($thisValue) ? "true" : "false";
		});
		$js->defineMethod($proto, "valueOf", 0, function(mixed $thisValue, array $args) use ($value) : mixed{
			return $value($thisValue);
		});
	}

	public function thisNumber(mixed $thisValue) : int|float{
		if(is_int($thisValue) || is_float($thisValue)){
			return $thisValue;
		}
		if($thisValue instanceof JsObject && (is_int($thisValue->host) || is_float($thisValue->host))){
			return $thisValue->host;
		}
		$this->js->throwError("TypeError", "Number.prototype method called on incompatible receiver");
	}

	private function installNumber() : void{
		$js = $this->js;
		$proto = $js->numberPrototype;
		$proto->host = 0;
		$number = $js->makeClass("Number", $proto, function(array $args, JsObject $newTarget) use ($js) : JsObject{
			$wrapper = new JsObject($js->prototypeFor($newTarget, $js->numberPrototype));
			$wrapper->className = "Number";
			$wrapper->host = count($args) === 0 ? 0 : $js->toNumber($args[0]);
			return $wrapper;
		}, function(mixed $thisValue, array $args) use ($js) : mixed{
			return count($args) === 0 ? 0 : $js->toNumber($args[0]);
		}, 1);
		$js->constructors["Number"] = $number;
		$js->defineHidden($js->global, "Number", $number);
		foreach([
			"MAX_SAFE_INTEGER" => 9007199254740991,
			"MIN_SAFE_INTEGER" => -9007199254740991,
			"EPSILON" => 2.220446049250313e-16,
			"MAX_VALUE" => 1.7976931348623157e308,
			"MIN_VALUE" => 5e-324,
			"POSITIVE_INFINITY" => INF,
			"NEGATIVE_INFINITY" => -INF,
			"NaN" => NAN
		] as $name => $value){
			$js->defineHidden($number, $name, $value);
			$number->locked[$name] = true;
		}
		$js->defineMethod($number, "isInteger", 1, function(mixed $thisValue, array $args) : mixed{
			$value = $args[0] ?? null;
			return is_int($value) || (is_float($value) && is_finite($value) && floor($value) === $value);
		});
		$js->defineMethod($number, "isSafeInteger", 1, function(mixed $thisValue, array $args) : mixed{
			$value = $args[0] ?? null;
			if(is_int($value)){
				return abs($value) <= 9007199254740991;
			}
			return is_float($value) && is_finite($value) && floor($value) === $value && abs($value) <= 9007199254740991;
		});
		$js->defineMethod($number, "isFinite", 1, function(mixed $thisValue, array $args) : mixed{
			$value = $args[0] ?? null;
			return is_int($value) || (is_float($value) && is_finite($value));
		});
		$js->defineMethod($number, "isNaN", 1, function(mixed $thisValue, array $args) : mixed{
			$value = $args[0] ?? null;
			return is_float($value) && is_nan($value);
		});

		$js->defineMethod($proto, "toString", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisNumber($thisValue);
			$radix = ($args[0] ?? null) === null ? 10 : (int) $js->toIntegerOrInfinity($args[0]);
			if($radix < 2 || $radix > 36){
				$js->throwError("RangeError", "toString() radix must be between 2 and 36");
			}
			if($radix === 10 || (is_float($value) && (is_nan($value) || self::isInfinite($value)))){
				return Interpreter::numberToString($value);
			}
			return $this->toRadix($value, $radix);
		});
		$js->defineMethod($proto, "toFixed", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $this->thisNumber($thisValue);
			$digits = (int) $js->toIntegerOrInfinity($args[0] ?? 0);
			if($digits < 0 || $digits > 100){
				$js->throwError("RangeError", "toFixed() digits argument must be between 0 and 100");
			}
			if(is_float($value) && (is_nan($value) || abs($value) >= 1e21 || self::isInfinite($value))){
				return Interpreter::numberToString($value);
			}
			$result = number_format((float) $value, $digits, ".", "");
			return preg_match('/^-0(\.0+)?$/', $result) === 1 ? substr($result, 1) : $result;
		});
		$js->defineMethod($proto, "toPrecision", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = (float) $this->thisNumber($thisValue);
			if(($args[0] ?? null) === null || is_nan($value) || self::isInfinite($value)){
				return Interpreter::numberToString($value);
			}
			$precision = (int) $js->toIntegerOrInfinity($args[0]);
			if($precision < 1 || $precision > 100){
				$js->throwError("RangeError", "toPrecision() argument must be between 1 and 100");
			}
			if($value == 0.0){
				return $precision === 1 ? "0" : "0." . str_repeat("0", $precision - 1);
			}
			$exponential = sprintf("%." . ($precision - 1) . "e", $value);
			$exponent = (int) substr($exponential, strpos($exponential, "e") + 1);
			if($exponent < -6 || $exponent >= $precision){
				[$mantissa, $power] = self::splitExponent($exponential);
				return $mantissa . "e" . ($power < 0 ? "-" : "+") . abs($power);
			}
			return number_format($value, max(0, $precision - 1 - $exponent), ".", "");
		});
		$js->defineMethod($proto, "toExponential", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = (float) $this->thisNumber($thisValue);
			if(is_nan($value) || self::isInfinite($value)){
				return Interpreter::numberToString($value);
			}
			$digits = ($args[0] ?? null) === null ? 6 : (int) $js->toIntegerOrInfinity($args[0]);
			[$mantissa, $power] = self::splitExponent(sprintf("%." . max(0, $digits) . "e", $value));
			return $mantissa . "e" . ($power < 0 ? "-" : "+") . abs($power);
		});
		$js->defineMethod($proto, "valueOf", 0, function(mixed $thisValue, array $args) : mixed{
			return $this->thisNumber($thisValue);
		});
		$js->defineMethod($proto, "toLocaleString", 0, function(mixed $thisValue, array $args) : mixed{
			$value = $this->thisNumber($thisValue);
			if(is_int($value) || (is_finite($value) && floor($value) === $value)){
				return number_format((float) $value, 0, ".", ",");
			}
			if(!is_finite($value)){
				return Interpreter::numberToString($value);
			}
			return rtrim(rtrim(number_format($value, 3, ".", ","), "0"), ".");
		});
	}

	private function toRadix(int|float $value, int $radix) : string{
		$negative = $value < 0;
		$value = abs($value);
		$integer = floor((float) $value);
		$fraction = (float) $value - $integer;
		$digits = "0123456789abcdefghijklmnopqrstuvwxyz";
		if($integer < 9.0e15){
			$result = base_convert((string) (int) $integer, 10, $radix);
		}else{
			$result = "";
			while($integer >= 1){
				$result = $digits[(int) fmod($integer, $radix)] . $result;
				$integer = floor($integer / $radix);
			}
		}
		if($fraction > 0){
			$result .= ".";
			for($i = 0; $i < 20 && $fraction > 0; ++$i){
				$fraction *= $radix;
				$digit = (int) floor($fraction);
				$result .= $digits[$digit];
				$fraction -= $digit;
			}
		}
		return ($negative ? "-" : "") . $result;
	}

	private function installMath() : void{
		$js = $this->js;
		$math = $js->newObject();
		$math->symbols[spl_object_id($js->symToStringTag)] = [$js->symToStringTag, "Math"];
		$js->defineHidden($js->global, "Math", $math);
		foreach(["PI" => M_PI, "E" => M_E, "LN2" => M_LN2, "LN10" => M_LN10, "LOG2E" => M_LOG2E, "LOG10E" => M_LOG10E, "SQRT2" => M_SQRT2, "SQRT1_2" => M_SQRT1_2] as $name => $value){
			$js->defineHidden($math, $name, $value);
			$math->locked[$name] = true;
		}
		$unary = [
			"abs" => fn(float $x) : float => abs($x),
			"floor" => fn(float $x) : float => floor($x),
			"ceil" => fn(float $x) : float => ceil($x),
			"round" => fn(float $x) : float => is_finite($x) ? floor($x + 0.5) : $x,
			"trunc" => fn(float $x) : float => $x < 0 ? ceil($x) : floor($x),
			"sign" => fn(float $x) : float => is_nan($x) ? NAN : ($x > 0 ? 1.0 : ($x < 0 ? -1.0 : $x)),
			"sqrt" => fn(float $x) : float => $x < 0 ? NAN : sqrt($x),
			"cbrt" => fn(float $x) : float => $x < 0 ? -pow(-$x, 1 / 3) : pow($x, 1 / 3),
			"sin" => fn(float $x) : float => sin($x),
			"cos" => fn(float $x) : float => cos($x),
			"tan" => fn(float $x) : float => tan($x),
			"asin" => fn(float $x) : float => asin($x),
			"acos" => fn(float $x) : float => acos($x),
			"atan" => fn(float $x) : float => atan($x),
			"sinh" => fn(float $x) : float => sinh($x),
			"cosh" => fn(float $x) : float => cosh($x),
			"tanh" => fn(float $x) : float => tanh($x),
			"asinh" => fn(float $x) : float => asinh($x),
			"acosh" => fn(float $x) : float => acosh($x),
			"atanh" => fn(float $x) : float => atanh($x),
			"exp" => fn(float $x) : float => exp($x),
			"expm1" => fn(float $x) : float => expm1($x),
			"log" => fn(float $x) : float => $x < 0 ? NAN : ($x == 0.0 ? -INF : log($x)),
			"log2" => fn(float $x) : float => $x < 0 ? NAN : ($x == 0.0 ? -INF : log($x, 2)),
			"log10" => fn(float $x) : float => $x < 0 ? NAN : ($x == 0.0 ? -INF : log10($x)),
			"log1p" => fn(float $x) : float => $x < -1 ? NAN : log1p($x),
			"fround" => fn(float $x) : float => is_finite($x) ? (float) unpack("g", pack("g", $x))[1] : $x
		];
		foreach($unary as $name => $function){
			$js->defineMethod($math, $name, 1, function(mixed $thisValue, array $args) use ($js, $function) : mixed{
				$value = $js->toNumber($args[0] ?? null);
				if(is_float($value) && is_nan($value)){
					return NAN;
				}
				return Interpreter::intOrFloat($function((float) $value));
			});
		}
		$js->defineMethod($math, "pow", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->binary("**", $args[0] ?? null, $args[1] ?? null);
		});
		$js->defineMethod($math, "atan2", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			return atan2((float) $js->toNumber($args[0] ?? null), (float) $js->toNumber($args[1] ?? null));
		});
		$js->defineMethod($math, "hypot", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$sum = 0.0;
			foreach($args as $arg){
				$value = (float) $js->toNumber($arg);
				$sum += $value * $value;
			}
			return Interpreter::intOrFloat(sqrt($sum));
		});
		$js->defineMethod($math, "min", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = INF;
			foreach($args as $arg){
				$value = $js->toNumber($arg);
				if(is_float($value) && is_nan($value)){
					return NAN;
				}
				if($value < $result){
					$result = $value;
				}
			}
			return $result;
		});
		$js->defineMethod($math, "max", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$result = -INF;
			foreach($args as $arg){
				$value = $js->toNumber($arg);
				if(is_float($value) && is_nan($value)){
					return NAN;
				}
				if($value > $result){
					$result = $value;
				}
			}
			return $result;
		});
		$js->defineMethod($math, "random", 0, function(mixed $thisValue, array $args) : mixed{
			return mt_rand() / (mt_getrandmax() + 1);
		});
		$js->defineMethod($math, "imul", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$a = $js->toInt32($args[0] ?? null);
			$b = $js->toInt32($args[1] ?? null);
			$low = (($a & 0xFFFF) * $b) & 0xFFFFFFFF;
			$high = ((($a >> 16) & 0xFFFF) * $b) & 0xFFFF;
			return Interpreter::wrapInt32($low + ($high << 16));
		});
		$js->defineMethod($math, "clz32", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $js->toUint32($args[0] ?? null);
			if($value === 0){
				return 32;
			}
			$count = 0;
			while(($value & 0x80000000) === 0){
				$value <<= 1;
				$count++;
			}
			return $count;
		});
	}

	private function installJson() : void{
		$js = $this->js;
		$json = $js->newObject();
		$json->symbols[spl_object_id($js->symToStringTag)] = [$js->symToStringTag, "JSON"];
		$js->defineHidden($js->global, "JSON", $json);
		$js->defineMethod($json, "parse", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$text = $js->toString($args[0] ?? null);
			try{
				$decoded = json_decode($text, false, 512, JSON_THROW_ON_ERROR);
			}catch(\JsonException $e){
				$js->throwError("SyntaxError", "Unexpected token in JSON: " . $e->getMessage());
			}
			$value = $this->fromJson($decoded);
			$reviver = $args[1] ?? null;
			if($reviver instanceof JsCallable){
				$root = $js->newObject();
				$root->props[""] = $value;
				return $this->revive($root, "", $reviver);
			}
			return $value;
		});
		$js->defineMethod($json, "stringify", 3, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $this->stringify($args[0] ?? null, $args[1] ?? null, $args[2] ?? null);
		});
	}

	public function fromJson(mixed $value) : mixed{
		if($value === null){
			return JsNull::get();
		}
		if(is_array($value)){
			$items = [];
			foreach($value as $item){
				$items[] = $this->fromJson($item);
			}
			return $this->js->newArray($items);
		}
		if($value instanceof stdClass){
			$object = $this->js->newObject();
			foreach(get_object_vars($value) as $key => $item){
				$object->props[$key] = $this->fromJson($item);
			}
			return $object;
		}
		if(is_float($value)){
			return Interpreter::intOrFloat($value);
		}
		return $value;
	}

	private function revive(JsObject $holder, string $key, JsCallable $reviver) : mixed{
		$js = $this->js;
		$value = $js->get($holder, $key);
		if($value instanceof JsObject){
			foreach($js->ownKeys($value) as $childKey){
				$revived = $this->revive($value, $childKey, $reviver);
				if($revived === null){
					$js->deleteProperty($value, $childKey);
				}else{
					$js->createDataProperty($value, $childKey, $revived);
				}
			}
		}
		return $js->call($reviver, $holder, [$key, $value]);
	}

	public function stringify(mixed $value, mixed $replacer = null, mixed $space = null) : mixed{
		$js = $this->js;
		$indent = "";
		if(is_int($space) || is_float($space)){
			$indent = str_repeat(" ", (int) max(0, min(10, $space)));
		}elseif(is_string($space)){
			$indent = substr($space, 0, 10);
		}
		$allowed = null;
		$function = null;
		if($replacer instanceof JsCallable){
			$function = $replacer;
		}elseif($replacer instanceof JsArray){
			$allowed = [];
			foreach($replacer->items as $item){
				if(is_string($item) || is_int($item) || is_float($item)){
					$allowed[] = $js->toString($item);
				}
			}
		}
		$root = $js->newObject();
		$root->props[""] = $value;
		$stack = [];
		$result = $this->serialize($root, "", $value, $function, $allowed, $indent, "", $stack);
		return $result;
	}

	/**
	 * @param list<string>|null  $allowed
	 * @param list<JsObject>     $stack
	 */
	private function serialize(JsObject $holder, string $key, mixed $value, ?JsCallable $replacer, ?array $allowed, string $indent, string $currentIndent, array &$stack) : ?string{
		$js = $this->js;
		if($value instanceof JsObject){
			$toJson = $js->get($value, "toJSON");
			if($toJson instanceof JsCallable){
				$value = $js->call($toJson, $value, [$key]);
			}
		}
		if($replacer !== null){
			$value = $js->call($replacer, $holder, [$key, $value]);
		}
		if($value instanceof JsObject && !$value instanceof JsCallable && !$value instanceof JsArray && !$value instanceof JsPromise){
			$host = $value->host;
			if(($value->className === "Number" || $value->className === "String" || $value->className === "Boolean") && (is_int($host) || is_float($host) || is_string($host) || is_bool($host))){
				$value = $host;
			}
		}
		if($value instanceof JsNull){
			return "null";
		}
		if($value === true){
			return "true";
		}
		if($value === false){
			return "false";
		}
		if(is_string($value)){
			return self::quote($value);
		}
		if(is_int($value)){
			return (string) $value;
		}
		if(is_float($value)){
			return is_finite($value) ? Interpreter::numberToString($value) : "null";
		}
		if($value === null || $value instanceof JsCallable || $value instanceof JsSymbol){
			return null;
		}
		if(!$value instanceof JsObject){
			return null;
		}
		foreach($stack as $entry){
			if($entry === $value){
				$js->throwError("TypeError", "Converting circular structure to JSON");
			}
		}
		$stack[] = $value;
		$innerIndent = $currentIndent . $indent;
		$parts = [];
		if($value instanceof JsArray){
			foreach($value->items as $index => $item){
				$serialized = $this->serialize($value, (string) $index, $item, $replacer, $allowed, $indent, $innerIndent, $stack);
				$parts[] = $serialized ?? "null";
			}
			array_pop($stack);
			if(count($parts) === 0){
				return "[]";
			}
			if($indent === ""){
				return "[" . implode(",", $parts) . "]";
			}
			return "[\n" . $innerIndent . implode(",\n" . $innerIndent, $parts) . "\n" . $currentIndent . "]";
		}
		$keys = $allowed ?? $js->ownKeys($value);
		foreach($keys as $propertyKey){
			if($allowed !== null && !$js->hasOwnProperty($value, $propertyKey)){
				continue;
			}
			$serialized = $this->serialize($value, $propertyKey, $js->getFrom($value, $propertyKey, $value), $replacer, $allowed, $indent, $innerIndent, $stack);
			if($serialized !== null){
				$parts[] = self::quote($propertyKey) . ($indent === "" ? ":" : ": ") . $serialized;
			}
		}
		array_pop($stack);
		if(count($parts) === 0){
			return "{}";
		}
		if($indent === ""){
			return "{" . implode(",", $parts) . "}";
		}
		return "{\n" . $innerIndent . implode(",\n" . $innerIndent, $parts) . "\n" . $currentIndent . "}";
	}

	public static function quote(string $value) : string{
		$encoded = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
		return $encoded === false ? "\"\"" : strtr($encoded, ["\\u2028" => "\u{2028}", "\\u2029" => "\u{2029}"]);
	}

	private function installReflect() : void{
		$js = $this->js;
		$reflect = $js->newObject();
		$js->defineHidden($js->global, "Reflect", $reflect);
		$object = $js->constructors["Object"];
		$js->defineMethod($reflect, "apply", 3, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $js->call($args[0] ?? null, $args[1] ?? null, $this->arrayLike($args[2] ?? null));
		});
		$js->defineMethod($reflect, "construct", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$newTarget = $args[2] ?? null;
			return $js->construct($args[0] ?? null, $this->arrayLike($args[1] ?? null), $newTarget instanceof JsObject ? $newTarget : null);
		});
		$js->defineMethod($reflect, "get", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $args[0] ?? null;
			if(!$target instanceof JsObject){
				$js->throwError("TypeError", "Reflect.get called on non-object");
			}
			$key = $js->toPropertyKey($args[1] ?? null);
			if($key instanceof JsSymbol){
				return $js->getSymbol($target, $key);
			}
			return $js->getFrom($target, $key, count($args) > 2 ? $args[2] : $target);
		});
		$js->defineMethod($reflect, "set", 3, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $args[0] ?? null;
			if(!$target instanceof JsObject){
				$js->throwError("TypeError", "Reflect.set called on non-object");
			}
			try{
				$js->set($target, $js->toPropertyKey($args[1] ?? null), $args[2] ?? null);
			}catch(JsThrow){
				return false;
			}
			return true;
		});
		$js->defineMethod($reflect, "has", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $args[0] ?? null;
			if(!$target instanceof JsObject){
				$js->throwError("TypeError", "Reflect.has called on non-object");
			}
			return $js->hasProperty($target, $js->toPropertyKey($args[1] ?? null));
		});
		$js->defineMethod($reflect, "ownKeys", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $args[0] ?? null;
			if(!$target instanceof JsObject){
				$js->throwError("TypeError", "Reflect.ownKeys called on non-object");
			}
			$keys = $js->ownKeys($target, true);
			foreach($js->ownSymbols($target) as $symbol){
				$keys[] = $symbol;
			}
			return $js->newArray($keys);
		});
		$js->defineMethod($reflect, "deleteProperty", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			$target = $args[0] ?? null;
			if(!$target instanceof JsObject){
				$js->throwError("TypeError", "Reflect.deleteProperty called on non-object");
			}
			try{
				return $js->deleteProperty($target, $js->toPropertyKey($args[1] ?? null));
			}catch(JsThrow){
				return false;
			}
		});
		foreach(["getPrototypeOf", "setPrototypeOf", "defineProperty", "getOwnPropertyDescriptor", "isExtensible", "preventExtensions"] as $name){
			$method = $object->props[$name] ?? null;
			if($method instanceof JsCallable){
				$js->defineHidden($reflect, $name, $method);
			}
		}
	}

	private function installGlobals() : void{
		$js = $this->js;
		$global = $js->global;
		$parseInt = $js->defineMethod($global, "parseInt", 2, function(mixed $thisValue, array $args) use ($js) : mixed{
			return $this->parseInt($js->toString($args[0] ?? null), ($args[1] ?? null) === null ? 0 : $js->toInt32($args[1]));
		});
		$parseFloat = $js->defineMethod($global, "parseFloat", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$text = trim($js->toString($args[0] ?? null));
			if(preg_match('/^[+-]?Infinity/', $text) === 1){
				return $text[0] === "-" ? -INF : INF;
			}
			if(preg_match('/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?/', $text, $match) !== 1){
				return NAN;
			}
			return Interpreter::stringToNumber($match[0]);
		});
		$number = $js->constructors["Number"];
		$js->defineHidden($number, "parseInt", $parseInt);
		$js->defineHidden($number, "parseFloat", $parseFloat);
		$js->defineMethod($global, "isNaN", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $js->toNumber($args[0] ?? null);
			return is_float($value) && is_nan($value);
		});
		$js->defineMethod($global, "isFinite", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$value = $js->toNumber($args[0] ?? null);
			return is_int($value) || is_finite($value);
		});
		$js->defineMethod($global, "encodeURIComponent", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			return strtr(rawurlencode($js->toString($args[0] ?? null)), ["%21" => "!", "%2A" => "*", "%27" => "'", "%28" => "(", "%29" => ")"]);
		});
		$js->defineMethod($global, "encodeURI", 1, function(mixed $thisValue, array $args) use ($js) : mixed{
			$encoded = rawurlencode($js->toString($args[0] ?? null));
			return strtr($encoded, ["%21" => "!", "%2A" => "*", "%27" => "'", "%28" => "(", "%29" => ")", "%3B" => ";", "%2F" => "/", "%3F" => "?", "%3A" => ":", "%40" => "@", "%26" => "&", "%3D" => "=", "%2B" => "+", "%24" => "$", "%2C" => ",", "%23" => "#"]);
		});
		$decode = function(mixed $thisValue, array $args) use ($js) : mixed{
			return rawurldecode($js->toString($args[0] ?? null));
		};
		$js->defineMethod($global, "decodeURIComponent", 1, $decode);
		$js->defineMethod($global, "decodeURI", 1, $decode);
	}

	private function parseInt(string $text, int $radix) : int|float{
		$text = trim($text, " \t\n\r\v\f\xC2\xA0");
		$sign = 1;
		if($text !== "" && ($text[0] === "-" || $text[0] === "+")){
			$sign = $text[0] === "-" ? -1 : 1;
			$text = substr($text, 1);
		}
		if($radix === 0){
			$radix = 10;
			if(strlen($text) > 1 && $text[0] === "0" && ($text[1] === "x" || $text[1] === "X")){
				$radix = 16;
				$text = substr($text, 2);
			}
		}elseif($radix === 16 && strlen($text) > 1 && $text[0] === "0" && ($text[1] === "x" || $text[1] === "X")){
			$text = substr($text, 2);
		}
		if($radix < 2 || $radix > 36){
			return NAN;
		}
		$digits = substr("0123456789abcdefghijklmnopqrstuvwxyz", 0, $radix);
		$lower = strtolower($text);
		$length = 0;
		while($length < strlen($lower) && str_contains($digits, $lower[$length])){
			$length++;
		}
		if($length === 0){
			return NAN;
		}
		$result = 0.0;
		for($i = 0; $i < $length; ++$i){
			$result = $result * $radix + (float) strpos($digits, $lower[$i]);
		}
		return Interpreter::intOrFloat($sign * $result);
	}

	private function installConsole() : void{
		$js = $this->js;
		$console = $js->newObject();
		$js->defineHidden($js->global, "console", $console);
		foreach(["log" => "info", "info" => "info", "debug" => "debug", "warn" => "warning", "error" => "error", "trace" => "info"] as $name => $level){
			$js->defineMethod($console, $name, 0, function(mixed $thisValue, array $args) use ($js, $level) : mixed{
				$js->print($level, $this->formatArgs($args));
				return null;
			});
		}
		$js->defineMethod($console, "assert", 0, function(mixed $thisValue, array $args) use ($js) : mixed{
			if(!$js->toBoolean($args[0] ?? null)){
				$js->print("error", "Assertion failed" . (count($args) > 1 ? ": " . $this->formatArgs(array_slice($args, 1)) : ""));
			}
			return null;
		});
	}

	/**
	 * @param list<mixed> $args
	 */
	public function formatArgs(array $args) : string{
		$js = $this->js;
		$parts = [];
		if(isset($args[0]) && is_string($args[0]) && str_contains($args[0], "%")){
			$index = 1;
			$first = preg_replace_callback('/%[sdifoOjc%]/', function(array $match) use (&$index, $args, $js) : string{
				if($match[0] === "%%"){
					return "%";
				}
				if(!array_key_exists($index, $args)){
					return $match[0];
				}
				$value = $args[$index++];
				return match($match[0]){
					"%s" => is_string($value) ? $value : $this->inspect($value, 1),
					"%d", "%i" => Interpreter::numberToString(Interpreter::intOrFloat(floor((float) $js->toNumber($value)))),
					"%f" => Interpreter::numberToString($js->toNumber($value)),
					"%c" => "",
					default => $this->inspect($value, 2)
				};
			}, $args[0]) ?? $args[0];
			$parts[] = $first;
			$args = array_slice($args, $index);
		}
		foreach($args as $arg){
			$parts[] = is_string($arg) ? $arg : $this->inspect($arg, 2);
		}
		return implode(" ", $parts);
	}

	/**
	 * @param list<JsObject> $seen
	 */
	public function inspect(mixed $value, int $depth, array $seen = [], bool $nested = false) : string{
		$js = $this->js;
		if(is_string($value)){
			return $nested ? "'" . $value . "'" : $value;
		}
		if(!$value instanceof JsObject){
			if($value instanceof JsSymbol){
				return "Symbol(" . ($value->description ?? "") . ")";
			}
			return $js->toStringSafe($value);
		}
		if(in_array($value, $seen, true)){
			return "[Circular]";
		}
		if($value instanceof JsCallable){
			if($value instanceof JsFunction && $value->isClass){
				return "[class " . ($value->name === "" ? "(anonymous)" : $value->name) . "]";
			}
			return "[Function: " . ($value->name === "" ? "(anonymous)" : $value->name) . "]";
		}
		if($value->className === "Error"){
			return $js->describeError($value);
		}
		if($value instanceof JsPromise){
			return "Promise { " . match($value->state){
				JsPromise::PENDING => "<pending>",
				JsPromise::FULFILLED => $this->inspect($value->value, $depth - 1, $seen, true),
				default => "<rejected> " . $this->inspect($value->value, $depth - 1, $seen, true)
			} . " }";
		}
		$seen[] = $value;
		if($value instanceof JsArray){
			if($depth < 0){
				return "[Array]";
			}
			$items = [];
			foreach(array_slice($value->items, 0, 100) as $item){
				$items[] = $this->inspect($item, $depth - 1, $seen, true);
			}
			if(count($value->items) > 100){
				$items[] = "... " . (count($value->items) - 100) . " more items";
			}
			return count($items) === 0 ? "[]" : "[ " . implode(", ", $items) . " ]";
		}
		$prefix = "";
		$constructor = $js->describeObject($value);
		if($constructor !== "#<Object>" && $constructor !== "#<Module>"){
			$prefix = substr($constructor, 2, -1) . " ";
		}
		if(is_string($value->host) || is_int($value->host) || is_float($value->host) || is_bool($value->host)){
			return "[" . $value->className . ": " . $this->inspect($value->host, 0, $seen, true) . "]";
		}
		if($depth < 0){
			return "[" . ($prefix === "" ? "Object" : trim($prefix)) . "]";
		}
		$entries = [];
		foreach($js->ownKeys($value) as $key){
			$propertyValue = isset($value->accessors[$key]) ? "[Getter/Setter]" : $this->inspect($js->getFrom($value, $key, $value), $depth - 1, $seen, true);
			$entries[] = (preg_match('/^[A-Za-z_$][\w$]*$/', $key) === 1 ? $key : self::quote($key)) . ": " . $propertyValue;
		}
		if(count($entries) === 0){
			return $prefix . "{}";
		}
		return $prefix . "{ " . implode(", ", $entries) . " }";
	}

	private static function isInfinite(float|int $value) : bool{
		return is_float($value) && is_infinite($value);
	}

	/**
	 * @return array{0: string, 1: int}
	 */
	private static function splitExponent(string $exponential) : array{
		$position = (int) strpos($exponential, "e");
		return [substr($exponential, 0, $position), (int) substr($exponential, $position + 1)];
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use Closure;

/**
 * A function implemented in PHP. $call receives the this value and the
 * arguments; $construct, when set, receives the arguments and new.target
 * and returns the constructed object.
 */
final class NativeFunction extends JsCallable{

	/**
	 * @param Closure(mixed, list<mixed>) : mixed                  $call
	 * @param (Closure(list<mixed>, JsObject) : JsObject)|null      $construct
	 */
	public function __construct(
		?JsObject $proto,
		string $name,
		int $length,
		public Closure $call,
		public ?Closure $construct = null
	){
		parent::__construct($proto);
		$this->name = $name;
		$this->length = $length;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

/**
 * The JavaScript null value. The PHP null value stands for undefined.
 */
final class JsNull{

	private static ?JsNull $instance = null;

	private function __construct(){

	}

	public static function get() : JsNull{
		return self::$instance ??= new JsNull();
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use Exception;

/**
 * Unwinds a suspended generator when its return() method is called, running
 * its finally blocks.
 */
final class GeneratorReturn extends Exception{

	public function __construct(
		public mixed $value
	){
		parent::__construct("Generator return");
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use Closure;

/**
 * A JavaScript promise. Reactions are PHP closures receiving the settled
 * state and value.
 */
final class JsPromise extends JsObject{

	public const PENDING = 0;
	public const FULFILLED = 1;
	public const REJECTED = 2;

	public int $state = self::PENDING;

	public mixed $value = null;

	public bool $resolving = false;

	public bool $handled = false;

	/** @var list<Closure(int, mixed) : void> */
	public array $reactions = [];

	public function __construct(?JsObject $proto){
		parent::__construct($proto);
		$this->className = "Promise";
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

/**
 * An abrupt completion of a statement: break, continue or return.
 */
final class Completion{

	public const BREAK = 1;
	public const CONTINUE = 2;
	public const RETURN = 3;

	public function __construct(
		public int $type,
		public mixed $value = null,
		public ?string $label = null
	){}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use function chr;
use function ctype_alnum;
use function ctype_digit;
use function ctype_xdigit;
use function hexdec;
use function in_array;
use function mb_chr;
use function octdec;
use function bindec;
use function ord;
use function str_replace;
use function strcspn;
use function strlen;
use function strpos;
use function substr;
use function substr_compare;
use function substr_count;

/**
 * Splits JavaScript source code into tokens. A token is an array with the
 * keys "t" (type: name, num, str, tpl, regex, punc, priv, eof), "v" (value),
 * "l" (line) and "n" (whether a line break precedes it).
 */
final class Lexer{

	private const PUNCTUATORS = [
		">>>=", "...", "===", "!==", "**=", "<<=", ">>=", ">>>", "&&=", "||=", "??=",
		"=>", "==", "!=", "<=", ">=", "&&", "||", "??", "?.", "++", "--", "+=", "-=", "*=", "/=", "%=",
		"&=", "|=", "^=", "<<", ">>", "**"
	];

	private const REGEX_KEYWORDS = ["return", "typeof", "instanceof", "in", "of", "new", "delete", "void", "throw", "case", "do", "else", "yield", "await"];

	private string $source;
	private int $length;
	private int $pos = 0;
	private int $line;

	public function __construct(string $source, private string $file, int $line = 1){
		$this->source = str_replace(["\r\n", "\r"], "\n", $source);
		$this->length = strlen($this->source);
		$this->line = $line;
	}

	/**
	 * @return list<array{t: string, v: mixed, l: int, n: bool}>
	 */
	public function tokenize() : array{
		$tokens = [];
		$previous = null;
		while(true){
			$newline = $this->skipTrivia();
			if($this->pos >= $this->length){
				$tokens[] = ["t" => "eof", "v" => null, "l" => $this->line, "n" => true];
				return $tokens;
			}
			$token = $this->next($previous);
			$token["n"] = $newline;
			$tokens[] = $token;
			$previous = $token;
		}
	}

	private function error(string $message) : SyntaxErrorException{
		return new SyntaxErrorException($message . " (" . $this->file . ":" . $this->line . ")");
	}

	private function skipTrivia() : bool{
		$newline = false;
		while($this->pos < $this->length){
			$char = $this->source[$this->pos];
			if($char === "\n"){
				$newline = true;
				$this->line++;
				$this->pos++;
			}elseif($char === " " || $char === "\t" || $char === "\f" || $char === "\v"){
				$this->pos++;
			}elseif($char === "\xEF" && substr($this->source, $this->pos, 3) === "\xEF\xBB\xBF"){
				$this->pos += 3;
			}elseif($char === "\xC2" && $this->pos + 1 < $this->length && $this->source[$this->pos + 1] === "\xA0"){
				$this->pos += 2;
			}elseif($char === "\xE2" && ($this->peekString(3) === "\xE2\x80\xA8" || $this->peekString(3) === "\xE2\x80\xA9")){
				$newline = true;
				$this->line++;
				$this->pos += 3;
			}elseif($char === "/" && $this->pos + 1 < $this->length && $this->source[$this->pos + 1] === "/"){
				$end = strpos($this->source, "\n", $this->pos);
				$this->pos = $end === false ? $this->length : $end;
			}elseif($char === "/" && $this->pos + 1 < $this->length && $this->source[$this->pos + 1] === "*"){
				$end = strpos($this->source, "*/", $this->pos + 2);
				if($end === false){
					throw $this->error("Unterminated comment");
				}
				$comment = substr($this->source, $this->pos, $end - $this->pos);
				$lines = substr_count($comment, "\n");
				if($lines > 0){
					$newline = true;
					$this->line += $lines;
				}
				$this->pos = $end + 2;
			}elseif($char === "#" && $this->pos === 0 && $this->pos + 1 < $this->length && $this->source[1] === "!"){
				$end = strpos($this->source, "\n");
				$this->pos = $end === false ? $this->length : $end;
			}else{
				break;
			}
		}
		return $newline;
	}

	private function peekString(int $length) : string{
		return substr($this->source, $this->pos, $length);
	}

	/**
	 * @param array<string, mixed>|null $previous
	 * @return array<string, mixed>
	 */
	private function next(?array $previous) : array{
		$char = $this->source[$this->pos];
		$line = $this->line;
		if(self::isIdentifierStart($char)){
			return ["t" => "name", "v" => $this->readIdentifier(), "l" => $line];
		}
		if($char === "#"){
			$this->pos++;
			return ["t" => "priv", "v" => "#" . $this->readIdentifier(), "l" => $line];
		}
		if(ctype_digit($char) || ($char === "." && $this->pos + 1 < $this->length && ctype_digit($this->source[$this->pos + 1]))){
			return ["t" => "num", "v" => $this->readNumber(), "l" => $line];
		}
		if($char === "\"" || $char === "'"){
			return ["t" => "str", "v" => $this->readString($char), "l" => $line];
		}
		if($char === "`"){
			return ["t" => "tpl", "v" => $this->readTemplate(), "l" => $line];
		}
		if($char === "/" && $this->regexAllowed($previous)){
			return ["t" => "regex", "v" => $this->readRegex(), "l" => $line];
		}
		foreach(self::PUNCTUATORS as $punctuator){
			if(substr_compare($this->source, $punctuator, $this->pos, strlen($punctuator)) === 0){
				if($punctuator === "?." && $this->pos + 2 < $this->length && ctype_digit($this->source[$this->pos + 2])){
					continue;
				}
				$this->pos += strlen($punctuator);
				return ["t" => "punc", "v" => $punctuator, "l" => $line];
			}
		}
		if(strpos("{}()[];,<>+-*/%&|^!~?:=.@", $char) !== false){
			$this->pos++;
			return ["t" => "punc", "v" => $char, "l" => $line];
		}
		throw $this->error("Unexpected character '" . $char . "'");
	}

	/**
	 * @param array<string, mixed>|null $previous
	 */
	private function regexAllowed(?array $previous) : bool{
		if($previous === null){
			return true;
		}
		if($previous["t"] === "punc"){
			return !in_array($previous["v"], [")", "]", "}"], true);
		}
		if($previous["t"] === "name"){
			return in_array($previous["v"], self::REGEX_KEYWORDS, true);
		}
		return false;
	}

	public static function isIdentifierStart(string $char) : bool{
		return ($char >= "a" && $char <= "z") || ($char >= "A" && $char <= "Z") || $char === "_" || $char === "$" || ord($char) >= 0x80 || $char === "\\";
	}

	private static function isIdentifierPart(string $char) : bool{
		return ctype_alnum($char) || $char === "_" || $char === "$" || ord($char) >= 0x80;
	}

	private function readIdentifier() : string{
		$start = $this->pos;
		while($this->pos < $this->length && self::isIdentifierPart($this->source[$this->pos])){
			$this->pos++;
		}
		if($this->pos < $this->length && $this->source[$this->pos] === "\\"){
			$name = substr($this->source, $start, $this->pos - $start);
			while($this->pos < $this->length && ($this->source[$this->pos] === "\\" || self::isIdentifierPart($this->source[$this->pos]))){
				if($this->source[$this->pos] === "\\"){
					$this->pos++;
					if(($this->source[$this->pos] ?? "") !== "u"){
						throw $this->error("Invalid escape in identifier");
					}
					$this->pos++;
					$name .= $this->readUnicodeEscape();
				}else{
					$name .= $this->source[$this->pos++];
				}
			}
			return $name;
		}
		return substr($this->source, $start, $this->pos - $start);
	}

	private function readNumber() : int|float{
		$start = $this->pos;
		$char = $this->source[$this->pos];
		if($char === "0" && $this->pos + 1 < $this->length){
			$prefix = $this->source[$this->pos + 1];
			$base = match($prefix){
				"x", "X" => 16,
				"o", "O" => 8,
				"b", "B" => 2,
				default => 0
			};
			if($base !== 0){
				$this->pos += 2;
				$digitsStart = $this->pos;
				while($this->pos < $this->length && (ctype_xdigit($this->source[$this->pos]) || $this->source[$this->pos] === "_")){
					$this->pos++;
				}
				$digits = str_replace("_", "", substr($this->source, $digitsStart, $this->pos - $digitsStart));
				if($this->pos < $this->length && $this->source[$this->pos] === "n"){
					$this->pos++;
				}
				$value = match($base){
					16 => hexdec($digits),
					8 => octdec($digits),
					default => bindec($digits)
				};
				return $value;
			}
		}
		while($this->pos < $this->length && (ctype_digit($this->source[$this->pos]) || $this->source[$this->pos] === "_")){
			$this->pos++;
		}
		$isFloat = false;
		if($this->pos < $this->length && $this->source[$this->pos] === "."){
			$isFloat = true;
			$this->pos++;
			while($this->pos < $this->length && (ctype_digit($this->source[$this->pos]) || $this->source[$this->pos] === "_")){
				$this->pos++;
			}
		}
		if($this->pos < $this->length && ($this->source[$this->pos] === "e" || $this->source[$this->pos] === "E")){
			$next = $this->source[$this->pos + 1] ?? "";
			$after = $this->source[$this->pos + 2] ?? "";
			if(ctype_digit($next) || (($next === "+" || $next === "-") && ctype_digit($after))){
				$isFloat = true;
				$this->pos += 2;
				while($this->pos < $this->length && ctype_digit($this->source[$this->pos])){
					$this->pos++;
				}
			}
		}
		$text = str_replace("_", "", substr($this->source, $start, $this->pos - $start));
		if($this->pos < $this->length && $this->source[$this->pos] === "n"){
			$this->pos++;
		}
		if($this->pos < $this->length && self::isIdentifierStart($this->source[$this->pos])){
			throw $this->error("Invalid number");
		}
		if(!$isFloat && strlen($text) > 1 && $text[0] === "0" && ctype_digit($text)){
			return octdec($text);
		}
		if(!$isFloat && strlen($text) < 19){
			return (int) $text;
		}
		return (float) $text;
	}

	private function readUnicodeEscape() : string{
		if(($this->source[$this->pos] ?? "") === "{"){
			$end = strpos($this->source, "}", $this->pos);
			if($end === false){
				throw $this->error("Invalid unicode escape");
			}
			$code = (int) hexdec(substr($this->source, $this->pos + 1, $end - $this->pos - 1));
			$this->pos = $end + 1;
			return self::codePoint($code);
		}
		$hex = substr($this->source, $this->pos, 4);
		if(strlen($hex) !== 4 || !ctype_xdigit($hex)){
			throw $this->error("Invalid unicode escape");
		}
		$this->pos += 4;
		$code = (int) hexdec($hex);
		if($code >= 0xD800 && $code <= 0xDBFF && substr($this->source, $this->pos, 2) === "\\u"){
			$low = substr($this->source, $this->pos + 2, 4);
			if(strlen($low) === 4 && ctype_xdigit($low)){
				$lowCode = (int) hexdec($low);
				if($lowCode >= 0xDC00 && $lowCode <= 0xDFFF){
					$this->pos += 6;
					return self::codePoint(0x10000 + (($code - 0xD800) << 10) + ($lowCode - 0xDC00));
				}
			}
		}
		return self::codePoint($code);
	}

	public static function codePoint(int $code) : string{
		if($code >= 0xD800 && $code <= 0xDFFF){
			return "\xEF\xBF\xBD";
		}
		$char = mb_chr($code, "UTF-8");
		return $char === false ? "\xEF\xBF\xBD" : $char;
	}

	/**
	 * Reads an escape sequence after a backslash and returns the cooked text,
	 * or null for an invalid escape in a template.
	 */
	private function readEscape(bool $template) : ?string{
		$char = $this->source[$this->pos] ?? "";
		$this->pos++;
		switch($char){
			case "n":
				return "\n";
			case "t":
				return "\t";
			case "r":
				return "\r";
			case "b":
				return "\x08";
			case "f":
				return "\f";
			case "v":
				return "\v";
			case "0":
				if(!ctype_digit($this->source[$this->pos] ?? "")){
					return "\0";
				}
				if($template){
					return null;
				}
				$start = $this->pos - 1;
				while($this->pos < $this->length && $this->pos - $start < 3 && $this->source[$this->pos] >= "0" && $this->source[$this->pos] <= "7"){
					$this->pos++;
				}
				return chr((int) octdec(substr($this->source, $start, $this->pos - $start)) & 0xff);
			case "x":
				$hex = substr($this->source, $this->pos, 2);
				if(strlen($hex) !== 2 || !ctype_xdigit($hex)){
					if($template){
						return null;
					}
					throw $this->error("Invalid hexadecimal escape");
				}
				$this->pos += 2;
				return self::codePoint((int) hexdec($hex));
			case "u":
				return $this->readUnicodeEscape();
			case "\n":
				$this->line++;
				return "";
			case "\xE2":
				$this->pos += 2;
				return "";
			default:
				if($char >= "1" && $char <= "7" && !$template){
					return chr((int) octdec($char));
				}
				return $char;
		}
	}

	private function readString(string $quote) : string{
		$this->pos++;
		$value = "";
		while(true){
			if($this->pos >= $this->length){
				throw $this->error("Unterminated string");
			}
			$char = $this->source[$this->pos];
			if($char === $quote){
				$this->pos++;
				return $value;
			}
			if($char === "\n"){
				throw $this->error("Unterminated string");
			}
			if($char === "\\"){
				$this->pos++;
				$value .= $this->readEscape(false);
				continue;
			}
			$next = strcspn($this->source, $quote . "\\\n", $this->pos);
			$value .= substr($this->source, $this->pos, $next);
			$this->pos += $next;
		}
	}

	/**
	 * @return array{cooked: list<string|null>, raw: list<string>, exprs: list<array{string, int}>}
	 */
	private function readTemplate() : array{
		$this->pos++;
		$cooked = [];
		$raw = [];
		$exprs = [];
		$chunk = "";
		$chunkRaw = "";
		$valid = true;
		while(true){
			if($this->pos >= $this->length){
				throw $this->error("Unterminated template literal");
			}
			$char = $this->source[$this->pos];
			if($char === "`"){
				$this->pos++;
				$cooked[] = $valid ? $chunk : null;
				$raw[] = $chunkRaw;
				return ["cooked" => $cooked, "raw" => $raw, "exprs" => $exprs];
			}
			if($char === "\\"){
				$start = $this->pos;
				$this->pos++;
				$escaped = $this->readEscape(true);
				if($escaped === null){
					$valid = false;
				}else{
					$chunk .= $escaped;
				}
				$chunkRaw .= substr($this->source, $start, $this->pos - $start);
				continue;
			}
			if($char === "$" && ($this->source[$this->pos + 1] ?? "") === "{"){
				$cooked[] = $valid ? $chunk : null;
				$raw[] = $chunkRaw;
				$chunk = "";
				$chunkRaw = "";
				$valid = true;
				$this->pos += 2;
				$line = $this->line;
				$start = $this->pos;
				$this->skipBalanced();
				$exprs[] = [substr($this->source, $start, $this->pos - $start), $line];
				$this->pos++;
				continue;
			}
			if($char === "\n"){
				$this->line++;
			}
			$chunk .= $char;
			$chunkRaw .= $char;
			$this->pos++;
		}
	}

	/**
	 * Moves to the brace closing a template substitution, skipping nested
	 * braces, strings, templates and comments.
	 */
	private function skipBalanced() : void{
		$depth = 0;
		while($this->pos < $this->length){
			$char = $this->source[$this->pos];
			if($char === "}"){
				if($depth === 0){
					return;
				}
				$depth--;
				$this->pos++;
			}elseif($char === "{"){
				$depth++;
				$this->pos++;
			}elseif($char === "\"" || $char === "'"){
				$this->readString($char);
			}elseif($char === "`"){
				$this->readTemplate();
			}elseif($char === "/" && ($this->source[$this->pos + 1] ?? "") === "/"){
				$end = strpos($this->source, "\n", $this->pos);
				$this->pos = $end === false ? $this->length : $end;
			}elseif($char === "/" && ($this->source[$this->pos + 1] ?? "") === "*"){
				$end = strpos($this->source, "*/", $this->pos + 2);
				if($end === false){
					throw $this->error("Unterminated comment");
				}
				$this->line += substr_count(substr($this->source, $this->pos, $end - $this->pos), "\n");
				$this->pos = $end + 2;
			}else{
				if($char === "\n"){
					$this->line++;
				}
				$this->pos++;
			}
		}
		throw $this->error("Unterminated template substitution");
	}

	/**
	 * @return array{pattern: string, flags: string}
	 */
	private function readRegex() : array{
		$this->pos++;
		$pattern = "";
		$inClass = false;
		while(true){
			if($this->pos >= $this->length || $this->source[$this->pos] === "\n"){
				throw $this->error("Unterminated regular expression");
			}
			$char = $this->source[$this->pos];
			if($char === "\\"){
				$pattern .= $char . ($this->source[$this->pos + 1] ?? "");
				$this->pos += 2;
				continue;
			}
			if($char === "["){
				$inClass = true;
			}elseif($char === "]"){
				$inClass = false;
			}elseif($char === "/" && !$inClass){
				$this->pos++;
				break;
			}
			$pattern .= $char;
			$this->pos++;
		}
		$flags = "";
		while($this->pos < $this->length && self::isIdentifierPart($this->source[$this->pos])){
			$flags .= $this->source[$this->pos++];
		}
		return ["pattern" => $pattern, "flags" => $flags];
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use RuntimeException;

/**
 * Invalid JavaScript source, raised by the lexer and the parser.
 */
final class SyntaxErrorException extends RuntimeException{

}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use Exception;

/**
 * A value thrown by JavaScript code, catchable by try/catch.
 */
final class JsThrow extends Exception{

	public function __construct(
		public mixed $value,
		string $message = "JavaScript exception"
	){
		parent::__construct($message);
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use RuntimeException;

/**
 * Raised when a script runs longer than the watchdog allows. Scripts cannot
 * catch it.
 */
final class ScriptTimeoutException extends RuntimeException{

}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use Closure;
use Throwable;
use function dirname;
use function file_get_contents;
use function is_dir;
use function is_file;
use function realpath;
use function rtrim;
use function str_replace;
use function str_starts_with;
use function strlen;
use function substr;

/**
 * Loads, links and evaluates ES modules. Relative specifiers are resolved
 * inside the pack of the importing module; "@minecraft/..." specifiers map
 * to host modules, and unknown ones to modules whose every export is a
 * stand-in built by $stubFactory.
 */
final class ModuleLoader{

	/** @var array<string, Module> */
	private array $modules = [];

	/** @var array<string, Module> */
	private array $hostModules = [];

	/** @var array<string, Module> */
	private array $byDisplayPath = [];

	/**
	 * @param Closure(string, string) : mixed $stubFactory receives the module and export names
	 */
	public function __construct(
		private Interpreter $js,
		private Closure $stubFactory
	){
		$js->dynamicImport = function(string $fromFile, string $specifier) : JsPromise{
			$promise = $this->js->newPromise();
			try{
				$from = $this->modules[$fromFile] ?? $this->byDisplayPath[($this->js->context ?? "") . "|" . $fromFile] ?? null;
				$module = $this->resolve($specifier, $from?->path ?? $fromFile, $from?->pack, $this->packRoot($from));
				$this->evaluate($module);
				$this->js->resolvePromise($promise, $this->js->moduleNamespace($module));
			}catch(JsThrow $e){
				$this->js->rejectPromise($promise, $e->value);
			}catch(SyntaxErrorException $e){
				$this->js->rejectPromise($promise, $this->js->makeError("SyntaxError", $e->getMessage()));
			}
			return $promise;
		};
	}

	/**
	 * @param array<string, mixed>              $exports
	 * @param (Closure(string) : mixed)|null    $missing
	 */
	public function addHostModule(string $specifier, array $exports, ?Closure $missing = null) : Module{
		$module = new Module($specifier);
		foreach($exports as $name => $value){
			$module->exports[(string) $name] = ["value", $value];
		}
		$module->missing = $missing;
		$module->status = Module::EVALUATED;
		$this->hostModules[$specifier] = $module;
		return $module;
	}

	/** @var array<string, string> */
	private array $packRoots = [];

	private function packRoot(?Module $module) : string{
		if($module === null){
			return "";
		}
		return $this->packRoots[$module->path] ?? "";
	}

	/**
	 * Loads and evaluates the entry module of a pack.
	 */
	public function run(string $entry, string $packRoot, string $pack) : Module{
		$path = $this->normalize($entry);
		$module = $this->load($path, $pack, $this->normalize($packRoot));
		$this->evaluate($module);
		return $module;
	}

	private function normalize(string $path) : string{
		$real = realpath($path);
		return str_replace("\\", "/", $real === false ? $path : $real);
	}

	private function resolve(string $specifier, string $fromFile, ?string $pack, string $packRoot) : Module{
		if(isset($this->hostModules[$specifier])){
			return $this->hostModules[$specifier];
		}
		if(str_starts_with($specifier, "@minecraft/")){
			$factory = $this->stubFactory;
			return $this->addHostModule($specifier, [], function(string $name) use ($factory, $specifier) : mixed{
				return $factory($specifier, $name);
			});
		}
		if(!str_starts_with($specifier, "./") && !str_starts_with($specifier, "../") && !str_starts_with($specifier, "/")){
			throw new SyntaxErrorException("Cannot find module '" . $specifier . "' imported from " . $fromFile);
		}
		$base = str_starts_with($specifier, "/") ? $packRoot : dirname($fromFile);
		$candidate = rtrim($base, "/") . "/" . $specifier;
		foreach([$candidate, $candidate . ".js", $candidate . ".mjs", $candidate . "/index.js"] as $option){
			if(is_file($option) && !is_dir($option)){
				$path = $this->normalize($option);
				if($packRoot !== "" && !str_starts_with($path, rtrim($packRoot, "/") . "/")){
					throw new SyntaxErrorException("Module '" . $specifier . "' is outside of its pack");
				}
				return $this->load($path, $pack, $packRoot);
			}
		}
		throw new SyntaxErrorException("Cannot find module '" . $specifier . "' imported from " . $fromFile);
	}

	private function load(string $path, ?string $pack, string $packRoot) : Module{
		if(isset($this->modules[$path])){
			return $this->modules[$path];
		}
		$source = @file_get_contents($path);
		if($source === false){
			throw new SyntaxErrorException("Cannot read module " . $path);
		}
		$module = new Module($path, $pack);
		$this->modules[$path] = $module;
		$this->packRoots[$path] = $packRoot;
		$this->byDisplayPath[($pack ?? "") . "|" . $this->displayPath($path, $packRoot)] = $module;
		$module->node = (new Parser($source, $this->displayPath($path, $packRoot)))->parseProgram();
		$this->link($module, $packRoot);
		return $module;
	}

	private function displayPath(string $path, string $packRoot) : string{
		$root = rtrim($packRoot, "/") . "/";
		if($packRoot !== "" && str_starts_with($path, $root)){
			return substr($path, strlen($root));
		}
		return $path;
	}

	private function link(Module $module, string $packRoot) : void{
		$js = $this->js;
		$node = $module->node;
		$scope = new Scope(null, true);
		$scope->vars["this"] = null;
		$module->scope = $scope;
		$savedFile = $js->file;
		$js->file = $this->displayPath($module->path, $packRoot);
		try{
			foreach($node["vars"] as $name){
				$scope->vars[$name] = null;
			}
			$js->hoistLexical($node, $scope);
			foreach($node["body"] as $statement){
				if($statement["type"] !== "ImportDeclaration"){
					continue;
				}
				$dependency = $this->resolve($statement["source"], $module->path, $module->pack, $packRoot);
				foreach($statement["specifiers"] as $specifier){
					if($specifier["kind"] === "namespace"){
						$scope->vars[$specifier["local"]] = $js->moduleNamespace($dependency);
						$scope->consts[$specifier["local"]] = true;
						continue;
					}
					$scope->imports[$specifier["local"]] = [$dependency, $specifier["imported"]];
				}
			}
			foreach($node["body"] as $statement){
				$this->collectExports($module, $statement, $packRoot);
			}
		}finally{
			$js->file = $savedFile;
		}
	}

	/**
	 * @param array<string, mixed> $statement
	 */
	private function collectExports(Module $module, array $statement, string $packRoot) : void{
		$scope = $module->scope;
		switch($statement["type"]){
			case "ExportNamedDeclaration":
				$declaration = $statement["declaration"];
				if($declaration !== null){
					foreach($this->declaredNames($declaration) as $name){
						$module->exports[$name] = ["local", $scope, $name];
					}
					return;
				}
				$source = $statement["source"] === null ? null : $this->resolve($statement["source"], $module->path, $module->pack, $packRoot);
				foreach($statement["specifiers"] as $specifier){
					$local = $specifier["local"];
					$exported = $specifier["exported"];
					if($source !== null){
						$module->exports[$exported] = ["reexport", $source, $local];
					}elseif(isset($scope->imports[$local])){
						$module->exports[$exported] = ["reexport", $scope->imports[$local][0], $scope->imports[$local][1]];
					}else{
						$module->exports[$exported] = ["local", $scope, $local];
					}
				}
				return;
			case "ExportDefaultDeclaration":
				$declaration = $statement["declaration"];
				if($declaration !== null){
					$names = $this->declaredNames($declaration);
					$module->exports["default"] = ["local", $scope, $names[0]];
					return;
				}
				$scope->vars["*default*"] = Tdz::get();
				$module->exports["default"] = ["local", $scope, "*default*"];
				return;
			case "ExportAllDeclaration":
				$source = $this->resolve($statement["source"], $module->path, $module->pack, $packRoot);
				if($statement["exported"] !== null){
					$module->exports[$statement["exported"]] = ["namespace", $source];
				}else{
					$module->starExports[] = $source;
				}
				return;
		}
	}

	/**
	 * @param array<string, mixed> $declaration
	 * @return list<string>
	 */
	private function declaredNames(array $declaration) : array{
		switch($declaration["type"]){
			case "VariableDeclaration":
				$names = [];
				foreach($declaration["declarations"] as $declarator){
					foreach(Parser::patternNames($declarator["id"]) as $name){
						$names[] = $name;
					}
				}
				return $names;
			case "FunctionDeclaration":
				return [$declaration["fn"]["id"]];
			case "ClassDeclaration":
				return [$declaration["class"]["id"]];
		}
		return [];
	}

	/**
	 * Evaluates a module after its dependencies.
	 */
	public function evaluate(Module $module) : void{
		if($module->status === Module::EVALUATED || $module->status === Module::EVALUATING){
			return;
		}
		if($module->status === Module::FAILED){
			throw new JsThrow($module->error);
		}
		$module->status = Module::EVALUATING;
		$js = $this->js;
		try{
			foreach($module->node["body"] as $statement){
				$source = match($statement["type"]){
					"ImportDeclaration", "ExportAllDeclaration" => $statement["source"],
					"ExportNamedDeclaration" => $statement["source"],
					default => null
				};
				if($source !== null){
					$this->evaluate($this->resolve($source, $module->path, $module->pack, $this->packRoots[$module->path] ?? ""));
				}
			}
			$savedFile = $js->file;
			$js->file = $this->displayPath($module->path, $this->packRoots[$module->path] ?? "");
			try{
				foreach($module->node["body"] as $statement){
					$js->execute($statement, $module->scope);
				}
			}finally{
				$js->file = $savedFile;
			}
		}catch(JsThrow $e){
			$module->status = Module::FAILED;
			$module->error = $e->value;
			throw $e;
		}catch(Throwable $e){
			$module->status = Module::FAILED;
			throw $e;
		}
		$module->status = Module::EVALUATED;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

/**
 * A function defined by a script. $node is the "Function" node, or null for
 * the implicit constructor of a class. Class constructors keep the instance
 * fields to initialize in $fields.
 */
final class JsFunction extends JsCallable{

	public ?JsObject $home = null;

	public bool $isClass = false;

	public bool $derived = false;

	public bool $needsPrototype = false;

	public string $file = "";

	/** @var list<array{0: string|JsSymbol, 1: array<string, mixed>|null, 2: bool}> */
	public array $fields = [];

	/**
	 * @param array<string, mixed>|null $node
	 */
	public function __construct(
		?JsObject $proto,
		public ?array $node,
		public Scope $scope
	){
		parent::__construct($proto);
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

/**
 * A lexical environment. Function scopes also hold the reserved bindings
 * "this", "%fn" (the running function) and "%nt" (new.target). Imported
 * bindings are live references to the export of another module.
 */
final class Scope{

	/** @var array<string, mixed> */
	public array $vars = [];

	/** @var array<string, true> */
	public array $consts = [];

	/** @var array<string, array{0: Module, 1: string}> */
	public array $imports = [];

	public function __construct(
		public ?Scope $parent = null,
		public bool $function = false
	){}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use function array_key_last;
use function array_merge;
use function array_pop;
use function count;
use function in_array;
use function is_array;
use function is_string;

/**
 * Builds the syntax tree of a JavaScript module. Nodes are arrays with a
 * "type" key, close to ESTree. Functions are "Function" nodes wrapped by
 * FunctionDeclaration, FunctionExpression and ArrowFunctionExpression; they
 * carry the names of their var declarations ("vars") and whether they use
 * "arguments". Blocks, switch statements, function bodies and programs carry
 * their lexical declarations ("lex") and function declarations ("funcs").
 */
final class Parser{

	private const BINARY_PRECEDENCE = [
		"??" => 1,
		"||" => 2,
		"&&" => 3,
		"|" => 4,
		"^" => 5,
		"&" => 6,
		"==" => 7, "!=" => 7, "===" => 7, "!==" => 7,
		"<" => 8, ">" => 8, "<=" => 8, ">=" => 8, "instanceof" => 8, "in" => 8,
		"<<" => 9, ">>" => 9, ">>>" => 9,
		"+" => 10, "-" => 10,
		"*" => 11, "/" => 11, "%" => 11,
		"**" => 12
	];

	private const ASSIGNMENT_OPERATORS = ["=", "+=", "-=", "*=", "/=", "%=", "**=", "<<=", ">>=", ">>>=", "&=", "|=", "^=", "&&=", "||=", "??="];

	private const RESERVED = [
		"break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "export",
		"extends", "finally", "for", "function", "if", "import", "in", "instanceof", "new", "return", "super", "switch",
		"this", "throw", "try", "typeof", "var", "void", "while", "with", "null", "true", "false", "enum"
	];

	/** @var list<array<string, mixed>> */
	private array $tokens;
	private int $index = 0;

	/** @var list<array{vars: array<string, true>, args: bool, arrow: bool, async: bool, generator: bool}> */
	private array $functions = [];

	public function __construct(string $source, private string $file, int $line = 1, private bool $module = true){
		$this->tokens = (new Lexer($source, $file, $line))->tokenize();
	}

	/**
	 * @return array<string, mixed>
	 */
	public function parseProgram() : array{
		$this->functions[] = ["vars" => [], "args" => false, "arrow" => false, "async" => $this->module, "generator" => false];
		$body = [];
		while(!$this->isEof()){
			$body[] = $this->parseStatement(true);
		}
		$context = array_pop($this->functions);
		$program = ["type" => "Program", "body" => $body, "vars" => self::keys($context["vars"]), "line" => 1];
		return $this->annotate($program, $body);
	}

	/**
	 * Parses a lone expression, used for template substitutions.
	 *
	 * @param list<array{vars: array<string, true>, args: bool, arrow: bool, async: bool, generator: bool}> $functions
	 * @return array<string, mixed>
	 */
	public static function parseEmbeddedExpression(string $source, string $file, int $line, array &$functions) : array{
		$parser = new self($source, $file, $line);
		$parser->functions = $functions;
		$expression = $parser->parseExpression();
		if(!$parser->isEof()){
			throw $parser->error("Unexpected token in template substitution");
		}
		$functions = $parser->functions;
		return $expression;
	}

	/**
	 * @param array<string, true> $set
	 * @return list<string>
	 */
	private static function keys(array $set) : array{
		$keys = [];
		foreach($set as $key => $unused){
			$keys[] = (string) $key;
		}
		return $keys;
	}

	/**
	 * @param array<string, mixed>       $node
	 * @param list<array<string, mixed>> $statements
	 * @return array<string, mixed>
	 */
	private function annotate(array $node, array $statements) : array{
		$lex = [];
		$funcs = [];
		foreach($statements as $statement){
			$this->collectDeclarations($statement, $lex, $funcs);
		}
		$node["lex"] = $lex;
		$node["funcs"] = $funcs;
		return $node;
	}

	/**
	 * @param array<string, mixed>       $statement
	 * @param list<array{string, string}> $lex
	 * @param list<array<string, mixed>> $funcs
	 */
	private function collectDeclarations(array $statement, array &$lex, array &$funcs) : void{
		$type = $statement["type"];
		if($type === "ExportNamedDeclaration" || $type === "ExportDefaultDeclaration"){
			$declaration = $statement["declaration"] ?? null;
			if(is_array($declaration)){
				$this->collectDeclarations($declaration, $lex, $funcs);
			}
			return;
		}
		if($type === "VariableDeclaration" && $statement["kind"] !== "var"){
			foreach($statement["declarations"] as $declarator){
				foreach(self::patternNames($declarator["id"]) as $name){
					$lex[] = [$name, $statement["kind"]];
				}
			}
		}elseif($type === "ClassDeclaration" && $statement["class"]["id"] !== null){
			$lex[] = [$statement["class"]["id"], "let"];
		}elseif($type === "FunctionDeclaration"){
			$funcs[] = $statement;
		}
	}

	/**
	 * @param array<string, mixed> $pattern
	 * @return list<string>
	 */
	public static function patternNames(array $pattern) : array{
		switch($pattern["type"]){
			case "Identifier":
				return [$pattern["name"]];
			case "AssignmentPattern":
				return self::patternNames($pattern["left"]);
			case "RestElement":
				return self::patternNames($pattern["argument"]);
			case "ArrayPattern":
				$names = [];
				foreach($pattern["elements"] as $element){
					if($element !== null){
						$names = array_merge($names, self::patternNames($element));
					}
				}
				return $names;
			case "ObjectPattern":
				$names = [];
				foreach($pattern["properties"] as $property){
					$names = array_merge($names, self::patternNames($property["type"] === "RestElement" ? $property : $property["value"]));
				}
				return $names;
			default:
				return [];
		}
	}

	private function error(string $message, ?array $token = null) : SyntaxErrorException{
		$token ??= $this->peek();
		return new SyntaxErrorException($message . " (" . $this->file . ":" . $token["l"] . ")");
	}

	/**
	 * @return array<string, mixed>
	 */
	private function peek(int $offset = 0) : array{
		return $this->tokens[$this->index + $offset] ?? $this->tokens[array_key_last($this->tokens)];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function next() : array{
		$token = $this->peek();
		if($token["t"] !== "eof"){
			$this->index++;
		}
		return $token;
	}

	private function isEof() : bool{
		return $this->peek()["t"] === "eof";
	}

	private function is(string $punctuator, int $offset = 0) : bool{
		$token = $this->peek($offset);
		return $token["t"] === "punc" && $token["v"] === $punctuator;
	}

	private function isName(string $name, int $offset = 0) : bool{
		$token = $this->peek($offset);
		return $token["t"] === "name" && $token["v"] === $name;
	}

	private function eat(string $punctuator) : bool{
		if($this->is($punctuator)){
			$this->index++;
			return true;
		}
		return false;
	}

	private function expect(string $punctuator) : void{
		if(!$this->eat($punctuator)){
			$token = $this->peek();
			throw $this->error("Expected '" . $punctuator . "' but found '" . self::describe($token) . "'");
		}
	}

	private function expectName(string $name) : void{
		if(!$this->isName($name)){
			throw $this->error("Expected '" . $name . "' but found '" . self::describe($this->peek()) . "'");
		}
		$this->index++;
	}

	/**
	 * @param array<string, mixed> $token
	 */
	private static function describe(array $token) : string{
		if($token["t"] === "eof"){
			return "end of input";
		}
		return is_string($token["v"]) ? $token["v"] : $token["t"];
	}

	private function identifier() : string{
		$token = $this->peek();
		if($token["t"] !== "name" || in_array($token["v"], self::RESERVED, true)){
			throw $this->error("Unexpected token '" . self::describe($token) . "'");
		}
		$this->index++;
		return $token["v"];
	}

	private function propertyName() : string{
		$token = $this->next();
		if($token["t"] === "name"){
			return $token["v"];
		}
		throw $this->error("Expected a property name", $token);
	}

	private function consumeSemicolon() : void{
		if($this->eat(";")){
			return;
		}
		$token = $this->peek();
		if($token["t"] === "eof" || $token["n"] || ($token["t"] === "punc" && $token["v"] === "}")){
			return;
		}
		throw $this->error("Unexpected token '" . self::describe($token) . "'");
	}

	private function line() : int{
		return $this->peek()["l"];
	}

	private function &context() : array{
		return $this->functions[count($this->functions) - 1];
	}

	private function markArguments() : void{
		for($i = count($this->functions) - 1; $i >= 0; --$i){
			if(!$this->functions[$i]["arrow"]){
				$this->functions[$i]["args"] = true;
				return;
			}
		}
	}

	private function inAsync() : bool{
		return $this->functions[count($this->functions) - 1]["async"];
	}

	private function inGenerator() : bool{
		return $this->functions[count($this->functions) - 1]["generator"];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseStatement(bool $topLevel = false) : array{
		$token = $this->peek();
		$line = $token["l"];
		if($token["t"] === "punc"){
			switch($token["v"]){
				case "{":
					return $this->parseBlock();
				case ";":
					$this->index++;
					return ["type" => "EmptyStatement", "line" => $line];
			}
		}elseif($token["t"] === "name"){
			switch($token["v"]){
				case "var":
				case "const":
					$declaration = $this->parseVariableDeclaration(false);
					$this->consumeSemicolon();
					return $declaration;
				case "let":
					$next = $this->peek(1);
					if($next["t"] === "name" || ($next["t"] === "punc" && ($next["v"] === "[" || $next["v"] === "{"))){
						$declaration = $this->parseVariableDeclaration(false);
						$this->consumeSemicolon();
						return $declaration;
					}
					break;
				case "function":
					return ["type" => "FunctionDeclaration", "fn" => $this->parseFunction(false, true), "line" => $line];
				case "async":
					if($this->isName("function", 1) && !$this->peek(1)["n"]){
						$this->index++;
						return ["type" => "FunctionDeclaration", "fn" => $this->parseFunction(true, true), "line" => $line];
					}
					break;
				case "class":
					return ["type" => "ClassDeclaration", "class" => $this->parseClass(true), "line" => $line];
				case "if":
					return $this->parseIf();
				case "for":
					return $this->parseFor();
				case "while":
					$this->index++;
					$this->expect("(");
					$test = $this->parseExpression();
					$this->expect(")");
					return ["type" => "WhileStatement", "test" => $test, "body" => $this->parseStatement(), "line" => $line];
				case "do":
					$this->index++;
					$body = $this->parseStatement();
					$this->expectName("while");
					$this->expect("(");
					$test = $this->parseExpression();
					$this->expect(")");
					$this->eat(";");
					return ["type" => "DoWhileStatement", "test" => $test, "body" => $body, "line" => $line];
				case "return":
					$this->index++;
					$argument = null;
					if(!$this->is(";") && !$this->is("}") && !$this->peek()["n"] && !$this->isEof()){
						$argument = $this->parseExpression();
					}
					$this->consumeSemicolon();
					return ["type" => "ReturnStatement", "argument" => $argument, "line" => $line];
				case "break":
				case "continue":
					$this->index++;
					$label = null;
					if($this->peek()["t"] === "name" && !$this->peek()["n"] && !in_array($this->peek()["v"], self::RESERVED, true)){
						$label = $this->next()["v"];
					}
					$this->consumeSemicolon();
					return ["type" => $token["v"] === "break" ? "BreakStatement" : "ContinueStatement", "label" => $label, "line" => $line];
				case "throw":
					$this->index++;
					if($this->peek()["n"]){
						throw $this->error("Illegal newline after throw");
					}
					$argument = $this->parseExpression();
					$this->consumeSemicolon();
					return ["type" => "ThrowStatement", "argument" => $argument, "line" => $line];
				case "try":
					return $this->parseTry();
				case "switch":
					return $this->parseSwitch();
				case "debugger":
					$this->index++;
					$this->consumeSemicolon();
					return ["type" => "EmptyStatement", "line" => $line];
				case "import":
					if($topLevel && !$this->is("(", 1) && !$this->is(".", 1)){
						return $this->parseImport();
					}
					break;
				case "export":
					if($topLevel){
						return $this->parseExport();
					}
					throw $this->error("Unexpected export");
				default:
					if($this->is(":", 1) && !in_array($token["v"], self::RESERVED, true)){
						$this->index += 2;
						return ["type" => "LabeledStatement", "label" => $token["v"], "body" => $this->parseStatement(), "line" => $line];
					}
			}
		}
		$expression = $this->parseExpression();
		$this->consumeSemicolon();
		return ["type" => "ExpressionStatement", "expression" => $expression, "line" => $line];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseBlock() : array{
		$line = $this->line();
		$this->expect("{");
		$body = [];
		while(!$this->eat("}")){
			if($this->isEof()){
				throw $this->error("Unexpected end of input");
			}
			$body[] = $this->parseStatement();
		}
		return $this->annotate(["type" => "BlockStatement", "body" => $body, "line" => $line], $body);
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseVariableDeclaration(bool $noIn) : array{
		$line = $this->line();
		$kind = $this->next()["v"];
		$declarations = [];
		do{
			$id = $this->parseBindingTarget();
			$init = null;
			if($this->eat("=")){
				$init = $this->parseAssign($noIn);
			}
			if($kind === "var"){
				$context = &$this->context();
				foreach(self::patternNames($id) as $name){
					$context["vars"][$name] = true;
				}
				unset($context);
			}
			$declarations[] = ["id" => $id, "init" => $init];
		}while($this->eat(","));
		return ["type" => "VariableDeclaration", "kind" => $kind, "declarations" => $declarations, "line" => $line];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseBindingTarget() : array{
		$line = $this->line();
		if($this->is("[")){
			return $this->parseArrayPattern();
		}
		if($this->is("{")){
			return $this->parseObjectPattern();
		}
		return ["type" => "Identifier", "name" => $this->identifier(), "line" => $line];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseBindingElement() : array{
		$target = $this->parseBindingTarget();
		if($this->eat("=")){
			return ["type" => "AssignmentPattern", "left" => $target, "right" => $this->parseAssign(false)];
		}
		return $target;
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseArrayPattern() : array{
		$this->expect("[");
		$elements = [];
		while(!$this->eat("]")){
			if($this->is(",")){
				$this->index++;
				$elements[] = null;
				continue;
			}
			if($this->eat("...")){
				$elements[] = ["type" => "RestElement", "argument" => $this->parseBindingTarget()];
			}else{
				$elements[] = $this->parseBindingElement();
			}
			if(!$this->is("]")){
				$this->expect(",");
			}
		}
		return ["type" => "ArrayPattern", "elements" => $elements];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseObjectPattern() : array{
		$this->expect("{");
		$properties = [];
		while(!$this->eat("}")){
			if($this->eat("...")){
				$properties[] = ["type" => "RestElement", "argument" => $this->parseBindingTarget()];
			}else{
				[$key, $computed] = $this->parsePropertyKey();
				if($this->eat(":")){
					$value = $this->parseBindingElement();
				}else{
					if($computed || $key["type"] !== "Literal" || !is_string($key["value"])){
						throw $this->error("Invalid destructuring pattern");
					}
					$value = ["type" => "Identifier", "name" => $key["value"]];
					if($this->eat("=")){
						$value = ["type" => "AssignmentPattern", "left" => $value, "right" => $this->parseAssign(false)];
					}
				}
				$properties[] = ["type" => "Property", "key" => $key, "computed" => $computed, "value" => $value];
			}
			if(!$this->is("}")){
				$this->expect(",");
			}
		}
		return ["type" => "ObjectPattern", "properties" => $properties];
	}

	/**
	 * @return array{0: array<string, mixed>, 1: bool}
	 */
	private function parsePropertyKey() : array{
		$token = $this->next();
		switch($token["t"]){
			case "name":
				return [["type" => "Literal", "value" => $token["v"]], false];
			case "str":
				return [["type" => "Literal", "value" => $token["v"]], false];
			case "num":
				return [["type" => "Literal", "value" => $token["v"]], false];
			case "priv":
				return [["type" => "Literal", "value" => $token["v"], "private" => true], false];
			case "punc":
				if($token["v"] === "["){
					$key = $this->parseAssign(false);
					$this->expect("]");
					return [$key, true];
				}
		}
		throw $this->error("Unexpected token '" . self::describe($token) . "'", $token);
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseIf() : array{
		$line = $this->line();
		$this->index++;
		$this->expect("(");
		$test = $this->parseExpression();
		$this->expect(")");
		$consequent = $this->parseStatement();
		$alternate = null;
		if($this->isName("else")){
			$this->index++;
			$alternate = $this->parseStatement();
		}
		return ["type" => "IfStatement", "test" => $test, "consequent" => $consequent, "alternate" => $alternate, "line" => $line];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseFor() : array{
		$line = $this->line();
		$this->index++;
		$await = false;
		if($this->isName("await")){
			$this->index++;
			$await = true;
		}
		$this->expect("(");
		$init = null;
		if($this->is(";")){
			$init = null;
		}elseif($this->isName("var") || $this->isName("const") || ($this->isName("let") && ($this->peek(1)["t"] === "name" || $this->is("[", 1) || $this->is("{", 1)))){
			$init = $this->parseVariableDeclaration(true);
		}else{
			$init = $this->parseExpression(true);
		}
		if($init !== null && ($this->isName("of") || $this->isName("in"))){
			$of = $this->next()["v"] === "of";
			if($init["type"] !== "VariableDeclaration"){
				$init = $this->toPattern($init);
			}
			$right = $of ? $this->parseAssign(false) : $this->parseExpression();
			$this->expect(")");
			$body = $this->parseStatement();
			return ["type" => $of ? "ForOfStatement" : "ForInStatement", "left" => $init, "right" => $right, "body" => $body, "await" => $await, "line" => $line];
		}
		$this->expect(";");
		$test = $this->is(";") ? null : $this->parseExpression();
		$this->expect(";");
		$update = $this->is(")") ? null : $this->parseExpression();
		$this->expect(")");
		$body = $this->parseStatement();
		return ["type" => "ForStatement", "init" => $init, "test" => $test, "update" => $update, "body" => $body, "line" => $line];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseTry() : array{
		$line = $this->line();
		$this->index++;
		$block = $this->parseBlock();
		$param = null;
		$handler = null;
		$finalizer = null;
		if($this->isName("catch")){
			$this->index++;
			if($this->eat("(")){
				$param = $this->parseBindingTarget();
				$this->expect(")");
			}
			$handler = $this->parseBlock();
		}
		if($this->isName("finally")){
			$this->index++;
			$finalizer = $this->parseBlock();
		}
		if($handler === null && $finalizer === null){
			throw $this->error("Missing catch or finally after try");
		}
		return ["type" => "TryStatement", "block" => $block, "param" => $param, "handler" => $handler, "finalizer" => $finalizer, "line" => $line];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseSwitch() : array{
		$line = $this->line();
		$this->index++;
		$this->expect("(");
		$discriminant = $this->parseExpression();
		$this->expect(")");
		$this->expect("{");
		$cases = [];
		$all = [];
		while(!$this->eat("}")){
			if($this->isName("case")){
				$this->index++;
				$test = $this->parseExpression();
			}else{
				$this->expectName("default");
				$test = null;
			}
			$this->expect(":");
			$consequent = [];
			while(!$this->isName("case") && !$this->isName("default") && !$this->is("}")){
				if($this->isEof()){
					throw $this->error("Unexpected end of input");
				}
				$statement = $this->parseStatement();
				$consequent[] = $statement;
				$all[] = $statement;
			}
			$cases[] = ["test" => $test, "consequent" => $consequent];
		}
		return $this->annotate(["type" => "SwitchStatement", "discriminant" => $discriminant, "cases" => $cases, "line" => $line], $all);
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseImport() : array{
		$line = $this->line();
		$this->index++;
		$specifiers = [];
		if($this->peek()["t"] === "str"){
			$source = $this->next()["v"];
			$this->skipImportAttributes();
			$this->consumeSemicolon();
			return ["type" => "ImportDeclaration", "specifiers" => [], "source" => $source, "line" => $line];
		}
		if($this->peek()["t"] === "name" && !$this->isName("from")){
			$specifiers[] = ["kind" => "default", "imported" => "default", "local" => $this->identifier()];
			$this->eat(",");
		}elseif($this->isName("from") && ($this->is(",", 1) || $this->isName("from", 1))){
			$specifiers[] = ["kind" => "default", "imported" => "default", "local" => $this->identifier()];
			$this->eat(",");
		}
		if($this->eat("*")){
			$this->expectName("as");
			$specifiers[] = ["kind" => "namespace", "imported" => "*", "local" => $this->identifier()];
		}elseif($this->eat("{")){
			while(!$this->eat("}")){
				$token = $this->next();
				if($token["t"] !== "name" && $token["t"] !== "str"){
					throw $this->error("Invalid import specifier", $token);
				}
				$imported = $token["v"];
				$local = $imported;
				if($this->isName("as")){
					$this->index++;
					$local = $this->identifier();
				}
				$specifiers[] = ["kind" => "named", "imported" => $imported, "local" => $local];
				if(!$this->is("}")){
					$this->expect(",");
				}
			}
		}
		$this->expectName("from");
		$token = $this->next();
		if($token["t"] !== "str"){
			throw $this->error("Expected a module specifier", $token);
		}
		$this->skipImportAttributes();
		$this->consumeSemicolon();
		return ["type" => "ImportDeclaration", "specifiers" => $specifiers, "source" => $token["v"], "line" => $line];
	}

	private function skipImportAttributes() : void{
		if(($this->isName("with") || $this->isName("assert")) && !$this->peek()["n"] && $this->is("{", 1)){
			$this->index++;
			$this->parseObjectLiteral();
		}
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseExport() : array{
		$line = $this->line();
		$this->index++;
		if($this->isName("default")){
			$this->index++;
			if($this->isName("function") || ($this->isName("async") && $this->isName("function", 1))){
				$async = $this->isName("async");
				if($async){
					$this->index++;
				}
				$fn = $this->parseFunction($async, true, true);
				return ["type" => "ExportDefaultDeclaration", "declaration" => $fn["id"] !== null ? ["type" => "FunctionDeclaration", "fn" => $fn, "line" => $line] : null, "expression" => $fn["id"] === null ? ["type" => "FunctionExpression", "fn" => $fn] : null, "line" => $line];
			}
			if($this->isName("class")){
				$class = $this->parseClass(false);
				return ["type" => "ExportDefaultDeclaration", "declaration" => $class["id"] !== null ? ["type" => "ClassDeclaration", "class" => $class, "line" => $line] : null, "expression" => $class["id"] === null ? ["type" => "ClassExpression", "class" => $class] : null, "line" => $line];
			}
			$expression = $this->parseAssign(false);
			$this->consumeSemicolon();
			return ["type" => "ExportDefaultDeclaration", "declaration" => null, "expression" => $expression, "line" => $line];
		}
		if($this->eat("*")){
			$exported = null;
			if($this->isName("as")){
				$this->index++;
				$token = $this->next();
				$exported = $token["v"];
			}
			$this->expectName("from");
			$source = $this->next()["v"];
			$this->skipImportAttributes();
			$this->consumeSemicolon();
			return ["type" => "ExportAllDeclaration", "source" => $source, "exported" => $exported, "line" => $line];
		}
		if($this->eat("{")){
			$specifiers = [];
			while(!$this->eat("}")){
				$token = $this->next();
				$local = $token["v"];
				$exported = $local;
				if($this->isName("as")){
					$this->index++;
					$exported = $this->next()["v"];
				}
				$specifiers[] = ["local" => $local, "exported" => $exported];
				if(!$this->is("}")){
					$this->expect(",");
				}
			}
			$source = null;
			if($this->isName("from")){
				$this->index++;
				$source = $this->next()["v"];
				$this->skipImportAttributes();
			}
			$this->consumeSemicolon();
			return ["type" => "ExportNamedDeclaration", "declaration" => null, "specifiers" => $specifiers, "source" => $source, "line" => $line];
		}
		$declaration = $this->parseStatement();
		if(!in_array($declaration["type"], ["VariableDeclaration", "FunctionDeclaration", "ClassDeclaration"], true)){
			throw $this->error("Unexpected export");
		}
		return ["type" => "ExportNamedDeclaration", "declaration" => $declaration, "specifiers" => [], "source" => null, "line" => $line];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseFunction(bool $async, bool $declaration, bool $optionalName = false) : array{
		$line = $this->line();
		$this->expectName("function");
		$generator = $this->eat("*");
		$id = null;
		if($this->peek()["t"] === "name" && !$this->is("(")){
			$id = $this->identifier();
		}elseif($declaration && !$optionalName){
			throw $this->error("Function name expected");
		}
		return $this->parseFunctionRest($id, $async, $generator, false, $line);
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseFunctionRest(?string $id, bool $async, bool $generator, bool $method, int $line) : array{
		$this->functions[] = ["vars" => [], "args" => false, "arrow" => false, "async" => $async, "generator" => $generator];
		$this->expect("(");
		$params = $this->parseParameters();
		$body = $this->parseFunctionBody();
		$context = array_pop($this->functions);
		return [
			"type" => "Function",
			"id" => $id,
			"params" => $params,
			"body" => $body,
			"expression" => false,
			"async" => $async,
			"generator" => $generator,
			"arrow" => false,
			"method" => $method,
			"vars" => self::keys($context["vars"]),
			"args" => $context["args"],
			"line" => $line
		];
	}

	/**
	 * Parses parameters after the opening parenthesis, up to and including
	 * the closing one.
	 *
	 * @return list<array<string, mixed>>
	 */
	private function parseParameters() : array{
		$params = [];
		while(!$this->eat(")")){
			if($this->eat("...")){
				$params[] = ["type" => "RestElement", "argument" => $this->parseBindingTarget()];
			}else{
				$params[] = $this->parseBindingElement();
			}
			if(!$this->is(")")){
				$this->expect(",");
			}
		}
		return $params;
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseFunctionBody() : array{
		$line = $this->line();
		$this->expect("{");
		$body = [];
		while(!$this->eat("}")){
			if($this->isEof()){
				throw $this->error("Unexpected end of input");
			}
			$body[] = $this->parseStatement();
		}
		return $this->annotate(["type" => "BlockStatement", "body" => $body, "line" => $line], $body);
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseClass(bool $requireName) : array{
		$line = $this->line();
		$this->expectName("class");
		$id = null;
		if($this->peek()["t"] === "name" && !$this->isName("extends") && !$this->is("{")){
			$id = $this->identifier();
		}elseif($requireName){
			throw $this->error("Class name expected");
		}
		$superClass = null;
		if($this->isName("extends")){
			$this->index++;
			$superClass = $this->parseLeftHandSide();
		}
		$this->expect("{");
		$members = [];
		while(!$this->eat("}")){
			if($this->eat(";")){
				continue;
			}
			$members[] = $this->parseClassMember();
		}
		return ["id" => $id, "superClass" => $superClass, "members" => $members, "line" => $line];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseClassMember() : array{
		$line = $this->line();
		$static = false;
		if($this->isName("static") && !$this->is("(", 1) && !$this->is("=", 1) && !$this->is(";", 1) && !$this->is("}", 1)){
			$this->index++;
			$static = true;
			if($this->is("{")){
				$this->functions[] = ["vars" => [], "args" => false, "arrow" => false, "async" => false, "generator" => false];
				$body = $this->parseFunctionBody();
				$context = array_pop($this->functions);
				$fn = ["type" => "Function", "id" => null, "params" => [], "body" => $body, "expression" => false, "async" => false, "generator" => false, "arrow" => false, "method" => true, "vars" => self::keys($context["vars"]), "args" => false, "line" => $line];
				return ["kind" => "block", "static" => true, "value" => $fn, "line" => $line];
			}
		}
		$kind = "method";
		$async = false;
		$generator = false;
		if($this->isName("async") && !$this->is("(", 1) && !$this->is("=", 1) && !$this->peek(1)["n"]){
			$this->index++;
			$async = true;
		}
		if($this->eat("*")){
			$generator = true;
		}
		if(($this->isName("get") || $this->isName("set")) && !$this->is("(", 1) && !$this->is("=", 1) && !$this->is(";", 1) && !$this->is("}", 1)){
			$kind = $this->next()["v"];
		}
		[$key, $computed] = $this->parsePropertyKey();
		if($this->is("(")){
			$name = !$computed && is_string($key["value"] ?? null) ? $key["value"] : null;
			if($kind === "method" && !$static && !$computed && $name === "constructor"){
				$kind = "constructor";
			}
			$fn = $this->parseFunctionRest($name, $async, $generator, true, $line);
			return ["kind" => $kind, "static" => $static, "key" => $key, "computed" => $computed, "value" => $fn, "line" => $line];
		}
		$value = null;
		if($this->eat("=")){
			$this->functions[] = ["vars" => [], "args" => false, "arrow" => true, "async" => false, "generator" => false];
			$value = $this->parseAssign(false);
			array_pop($this->functions);
		}
		$this->consumeSemicolon();
		return ["kind" => "field", "static" => $static, "key" => $key, "computed" => $computed, "value" => $value, "line" => $line];
	}

	/**
	 * @return array<string, mixed>
	 */
	public function parseExpression(bool $noIn = false) : array{
		$line = $this->line();
		$expression = $this->parseAssign($noIn);
		if($this->is(",")){
			$expressions = [$expression];
			while($this->eat(",")){
				$expressions[] = $this->parseAssign($noIn);
			}
			return ["type" => "SequenceExpression", "expressions" => $expressions, "line" => $line];
		}
		return $expression;
	}

	private function isArrowAhead() : bool{
		$token = $this->peek();
		if($token["t"] === "name" && !in_array($token["v"], self::RESERVED, true) && $this->is("=>", 1)){
			return true;
		}
		if(!$this->is("(")){
			return false;
		}
		$depth = 0;
		for($i = $this->index, $count = count($this->tokens); $i < $count; ++$i){
			$current = $this->tokens[$i];
			if($current["t"] === "punc"){
				if($current["v"] === "(" || $current["v"] === "[" || $current["v"] === "{"){
					$depth++;
				}elseif($current["v"] === ")" || $current["v"] === "]" || $current["v"] === "}"){
					$depth--;
					if($depth === 0){
						$after = $this->tokens[$i + 1] ?? null;
						return $after !== null && $after["t"] === "punc" && $after["v"] === "=>" && !$after["n"];
					}
				}
			}elseif($current["t"] === "eof"){
				return false;
			}
		}
		return false;
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseArrow(bool $async) : array{
		$line = $this->line();
		$this->functions[] = ["vars" => [], "args" => false, "arrow" => true, "async" => $async, "generator" => false];
		if($this->eat("(")){
			$params = $this->parseParameters();
		}else{
			$params = [["type" => "Identifier", "name" => $this->identifier(), "line" => $line]];
		}
		$this->expect("=>");
		if($this->is("{")){
			$body = $this->parseFunctionBody();
			$expression = false;
		}else{
			$body = $this->parseAssign(false);
			$expression = true;
		}
		$context = array_pop($this->functions);
		$fn = [
			"type" => "Function",
			"id" => null,
			"params" => $params,
			"body" => $body,
			"expression" => $expression,
			"async" => $async,
			"generator" => false,
			"arrow" => true,
			"method" => false,
			"vars" => self::keys($context["vars"]),
			"args" => false,
			"line" => $line
		];
		return ["type" => "ArrowFunctionExpression", "fn" => $fn, "line" => $line];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseAssign(bool $noIn = false) : array{
		$line = $this->line();
		if($this->isName("async") && !$this->peek(1)["n"]){
			$next = $this->peek(1);
			if(($next["t"] === "name" && $this->is("=>", 2)) || ($next["t"] === "punc" && $next["v"] === "(" && $this->isArrowAheadFrom($this->index + 1))){
				$this->index++;
				return $this->parseArrow(true);
			}
		}
		if($this->isArrowAhead()){
			return $this->parseArrow(false);
		}
		if($this->isName("yield") && $this->inGenerator()){
			$this->index++;
			$delegate = false;
			$argument = null;
			if(!$this->peek()["n"]){
				$delegate = $this->eat("*");
				$token = $this->peek();
				if($delegate || !($token["t"] === "eof" || ($token["t"] === "punc" && in_array($token["v"], [")", "]", "}", ",", ";", ":"], true)))){
					$argument = $this->parseAssign($noIn);
				}
			}
			return ["type" => "YieldExpression", "argument" => $argument, "delegate" => $delegate, "line" => $line];
		}
		$left = $this->parseConditional($noIn);
		$token = $this->peek();
		if($token["t"] === "punc" && in_array($token["v"], self::ASSIGNMENT_OPERATORS, true)){
			$this->index++;
			if($token["v"] === "="){
				$left = $this->toPattern($left);
			}elseif($left["type"] !== "Identifier" && $left["type"] !== "MemberExpression"){
				throw $this->error("Invalid assignment target");
			}
			$right = $this->parseAssign($noIn);
			return ["type" => "AssignmentExpression", "operator" => $token["v"], "left" => $left, "right" => $right, "line" => $line];
		}
		return $left;
	}

	private function isArrowAheadFrom(int $index) : bool{
		$saved = $this->index;
		$this->index = $index;
		$result = $this->isArrowAhead();
		$this->index = $saved;
		return $result;
	}

	/**
	 * Converts an expression parsed before "=" into an assignment target.
	 *
	 * @param array<string, mixed> $node
	 * @return array<string, mixed>
	 */
	private function toPattern(array $node) : array{
		switch($node["type"]){
			case "Identifier":
			case "MemberExpression":
			case "ObjectPattern":
			case "ArrayPattern":
			case "AssignmentPattern":
				return $node;
			case "ArrayExpression":
				$elements = [];
				foreach($node["elements"] as $element){
					if($element === null){
						$elements[] = null;
					}elseif($element["type"] === "SpreadElement"){
						$elements[] = ["type" => "RestElement", "argument" => $this->toPattern($element["argument"])];
					}else{
						$elements[] = $this->toPattern($element);
					}
				}
				return ["type" => "ArrayPattern", "elements" => $elements];
			case "ObjectExpression":
				$properties = [];
				foreach($node["properties"] as $property){
					if($property["kind"] === "spread"){
						$properties[] = ["type" => "RestElement", "argument" => $this->toPattern($property["value"])];
						continue;
					}
					$properties[] = ["type" => "Property", "key" => $property["key"], "computed" => $property["computed"], "value" => $this->toPattern($property["value"])];
				}
				return ["type" => "ObjectPattern", "properties" => $properties];
			case "AssignmentExpression":
				if($node["operator"] === "="){
					return ["type" => "AssignmentPattern", "left" => $this->toPattern($node["left"]), "right" => $node["right"]];
				}
		}
		throw $this->error("Invalid assignment target");
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseConditional(bool $noIn) : array{
		$line = $this->line();
		$test = $this->parseBinary(0, $noIn);
		if(!$this->eat("?")){
			return $test;
		}
		$consequent = $this->parseAssign(false);
		$this->expect(":");
		$alternate = $this->parseAssign($noIn);
		return ["type" => "ConditionalExpression", "test" => $test, "consequent" => $consequent, "alternate" => $alternate, "line" => $line];
	}

	private function binaryOperator(bool $noIn) : ?string{
		$token = $this->peek();
		if($token["t"] === "punc" && isset(self::BINARY_PRECEDENCE[$token["v"]])){
			return $token["v"];
		}
		if($token["t"] === "name" && ($token["v"] === "instanceof" || ($token["v"] === "in" && !$noIn))){
			return $token["v"];
		}
		return null;
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseBinary(int $minPrecedence, bool $noIn) : array{
		$line = $this->line();
		if($this->peek()["t"] === "priv" && $this->isName("in", 1)){
			$name = $this->next()["v"];
			$this->index++;
			$left = ["type" => "PrivateIn", "name" => $name, "right" => $this->parseBinary(9, $noIn), "line" => $line];
		}else{
			$left = $this->parseUnary();
		}
		while(true){
			$operator = $this->binaryOperator($noIn);
			if($operator === null){
				return $left;
			}
			$precedence = self::BINARY_PRECEDENCE[$operator];
			if($precedence <= $minPrecedence){
				return $left;
			}
			$this->index++;
			$right = $this->parseBinary($operator === "**" ? $precedence - 1 : $precedence, $noIn);
			$type = ($operator === "&&" || $operator === "||" || $operator === "??") ? "LogicalExpression" : "BinaryExpression";
			$left = ["type" => $type, "operator" => $operator, "left" => $left, "right" => $right, "line" => $line];
		}
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseUnary() : array{
		$token = $this->peek();
		$line = $token["l"];
		if($token["t"] === "punc"){
			if(in_array($token["v"], ["!", "~", "+", "-"], true)){
				$this->index++;
				return ["type" => "UnaryExpression", "operator" => $token["v"], "argument" => $this->parseUnary(), "line" => $line];
			}
			if($token["v"] === "++" || $token["v"] === "--"){
				$this->index++;
				$argument = $this->parseUnary();
				if($argument["type"] !== "Identifier" && $argument["type"] !== "MemberExpression"){
					throw $this->error("Invalid update target");
				}
				return ["type" => "UpdateExpression", "operator" => $token["v"], "prefix" => true, "argument" => $argument, "line" => $line];
			}
		}elseif($token["t"] === "name"){
			if(in_array($token["v"], ["typeof", "void", "delete"], true)){
				$this->index++;
				return ["type" => "UnaryExpression", "operator" => $token["v"], "argument" => $this->parseUnary(), "line" => $line];
			}
			if($token["v"] === "await" && $this->inAsync()){
				$this->index++;
				return ["type" => "AwaitExpression", "argument" => $this->parseUnary(), "line" => $line];
			}
		}
		$expression = $this->parseLeftHandSide();
		$next = $this->peek();
		if($next["t"] === "punc" && ($next["v"] === "++" || $next["v"] === "--") && !$next["n"]){
			if($expression["type"] !== "Identifier" && $expression["type"] !== "MemberExpression"){
				throw $this->error("Invalid update target");
			}
			$this->index++;
			return ["type" => "UpdateExpression", "operator" => $next["v"], "prefix" => false, "argument" => $expression, "line" => $line];
		}
		return $expression;
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseLeftHandSide() : array{
		$line = $this->line();
		if($this->isName("new")){
			$expression = $this->parseNew();
		}else{
			$expression = $this->parsePrimary();
		}
		$chain = false;
		while(true){
			$token = $this->peek();
			if($token["t"] === "punc"){
				if($token["v"] === "."){
					$this->index++;
					$next = $this->next();
					if($next["t"] === "priv"){
						$expression = ["type" => "MemberExpression", "object" => $expression, "property" => ["type" => "Literal", "value" => $next["v"]], "computed" => false, "optional" => false, "private" => true, "line" => $line];
						continue;
					}
					if($next["t"] !== "name"){
						throw $this->error("Unexpected token '" . self::describe($next) . "'", $next);
					}
					$expression = ["type" => "MemberExpression", "object" => $expression, "property" => ["type" => "Literal", "value" => $next["v"]], "computed" => false, "optional" => false, "line" => $line];
					continue;
				}
				if($token["v"] === "?."){
					$this->index++;
					$chain = true;
					if($this->eat("(")){
						$expression = ["type" => "CallExpression", "callee" => $expression, "arguments" => $this->parseArguments(), "optional" => true, "line" => $line];
					}elseif($this->eat("[")){
						$property = $this->parseExpression();
						$this->expect("]");
						$expression = ["type" => "MemberExpression", "object" => $expression, "property" => $property, "computed" => true, "optional" => true, "line" => $line];
					}else{
						$next = $this->next();
						$private = $next["t"] === "priv";
						if($next["t"] !== "name" && !$private){
							throw $this->error("Unexpected token '" . self::describe($next) . "'", $next);
						}
						$expression = ["type" => "MemberExpression", "object" => $expression, "property" => ["type" => "Literal", "value" => $next["v"]], "computed" => false, "optional" => true, "private" => $private, "line" => $line];
					}
					continue;
				}
				if($token["v"] === "["){
					$this->index++;
					$property = $this->parseExpression();
					$this->expect("]");
					$expression = ["type" => "MemberExpression", "object" => $expression, "property" => $property, "computed" => true, "optional" => false, "line" => $line];
					continue;
				}
				if($token["v"] === "("){
					$this->index++;
					$expression = ["type" => "CallExpression", "callee" => $expression, "arguments" => $this->parseArguments(), "optional" => false, "line" => $line];
					continue;
				}
			}elseif($token["t"] === "tpl"){
				$this->index++;
				$expression = ["type" => "TaggedTemplateExpression", "tag" => $expression, "quasi" => $this->templateNode($token), "line" => $line];
				continue;
			}
			break;
		}
		if($chain){
			return ["type" => "ChainExpression", "expression" => $expression, "line" => $line];
		}
		return $expression;
	}

	/**
	 * @return list<array<string, mixed>>
	 */
	private function parseArguments() : array{
		$arguments = [];
		while(!$this->eat(")")){
			if($this->eat("...")){
				$arguments[] = ["type" => "SpreadElement", "argument" => $this->parseAssign(false)];
			}else{
				$arguments[] = $this->parseAssign(false);
			}
			if(!$this->is(")")){
				$this->expect(",");
			}
		}
		return $arguments;
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseNew() : array{
		$line = $this->line();
		$this->expectName("new");
		if($this->eat(".")){
			$this->expectName("target");
			return ["type" => "NewTarget", "line" => $line];
		}
		if($this->isName("new")){
			$callee = $this->parseNew();
		}else{
			$callee = $this->parsePrimary();
		}
		while(true){
			if($this->eat(".")){
				$next = $this->next();
				$callee = ["type" => "MemberExpression", "object" => $callee, "property" => ["type" => "Literal", "value" => $next["v"]], "computed" => false, "optional" => false, "private" => $next["t"] === "priv", "line" => $line];
			}elseif($this->eat("[")){
				$property = $this->parseExpression();
				$this->expect("]");
				$callee = ["type" => "MemberExpression", "object" => $callee, "property" => $property, "computed" => true, "optional" => false, "line" => $line];
			}else{
				break;
			}
		}
		$arguments = [];
		if($this->eat("(")){
			$arguments = $this->parseArguments();
		}
		return ["type" => "NewExpression", "callee" => $callee, "arguments" => $arguments, "line" => $line];
	}

	/**
	 * @param array<string, mixed> $token
	 * @return array<string, mixed>
	 */
	private function templateNode(array $token) : array{
		$expressions = [];
		foreach($token["v"]["exprs"] as [$source, $line]){
			$expressions[] = self::parseEmbeddedExpression($source, $this->file, $line, $this->functions);
		}
		return ["type" => "TemplateLiteral", "cooked" => $token["v"]["cooked"], "raw" => $token["v"]["raw"], "expressions" => $expressions, "line" => $token["l"]];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parsePrimary() : array{
		$token = $this->next();
		$line = $token["l"];
		switch($token["t"]){
			case "num":
			case "str":
				return ["type" => "Literal", "value" => $token["v"], "line" => $line];
			case "tpl":
				return $this->templateNode($token);
			case "regex":
				return ["type" => "RegExpLiteral", "pattern" => $token["v"]["pattern"], "flags" => $token["v"]["flags"], "line" => $line];
			case "punc":
				switch($token["v"]){
					case "(":
						$expression = $this->parseExpression();
						$this->expect(")");
						return $expression;
					case "[":
						return $this->parseArrayLiteral($line);
					case "{":
						$this->index--;
						return $this->parseObjectLiteral();
				}
				break;
			case "name":
				switch($token["v"]){
					case "this":
						return ["type" => "ThisExpression", "line" => $line];
					case "super":
						return ["type" => "Super", "line" => $line];
					case "null":
						return ["type" => "Literal", "value" => null, "null" => true, "line" => $line];
					case "true":
						return ["type" => "Literal", "value" => true, "line" => $line];
					case "false":
						return ["type" => "Literal", "value" => false, "line" => $line];
					case "function":
						$this->index--;
						return ["type" => "FunctionExpression", "fn" => $this->parseFunction(false, false), "line" => $line];
					case "async":
						if($this->isName("function") && !$this->peek()["n"]){
							return ["type" => "FunctionExpression", "fn" => $this->parseFunction(true, false), "line" => $line];
						}
						return ["type" => "Identifier", "name" => "async", "line" => $line];
					case "class":
						$this->index--;
						return ["type" => "ClassExpression", "class" => $this->parseClass(false), "line" => $line];
					case "import":
						if($this->eat("(")){
							$source = $this->parseAssign(false);
							if($this->eat(",")){
								if(!$this->is(")")){
									$this->parseAssign(false);
								}
								$this->eat(",");
							}
							$this->expect(")");
							return ["type" => "ImportExpression", "source" => $source, "line" => $line];
						}
						if($this->eat(".")){
							$this->expectName("meta");
							return ["type" => "ImportMeta", "line" => $line];
						}
						break;
					default:
						if(in_array($token["v"], self::RESERVED, true)){
							break;
						}
						if($token["v"] === "arguments"){
							$this->markArguments();
						}
						return ["type" => "Identifier", "name" => $token["v"], "line" => $line];
				}
				break;
			case "priv":
				break;
		}
		throw $this->error("Unexpected token '" . self::describe($token) . "'", $token);
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseArrayLiteral(int $line) : array{
		$elements = [];
		while(!$this->eat("]")){
			if($this->is(",")){
				$this->index++;
				$elements[] = null;
				continue;
			}
			if($this->eat("...")){
				$elements[] = ["type" => "SpreadElement", "argument" => $this->parseAssign(false)];
			}else{
				$elements[] = $this->parseAssign(false);
			}
			if(!$this->is("]")){
				$this->expect(",");
			}
		}
		return ["type" => "ArrayExpression", "elements" => $elements, "line" => $line];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseObjectLiteral() : array{
		$line = $this->line();
		$this->expect("{");
		$properties = [];
		while(!$this->eat("}")){
			if($this->eat("...")){
				$properties[] = ["kind" => "spread", "value" => $this->parseAssign(false)];
			}else{
				$properties[] = $this->parseObjectMember();
			}
			if(!$this->is("}")){
				$this->expect(",");
			}
		}
		return ["type" => "ObjectExpression", "properties" => $properties, "line" => $line];
	}

	/**
	 * @return array<string, mixed>
	 */
	private function parseObjectMember() : array{
		$line = $this->line();
		$async = false;
		$generator = false;
		$kind = "init";
		if($this->isName("async") && !$this->is(",", 1) && !$this->is(":", 1) && !$this->is("(", 1) && !$this->is("}", 1) && !$this->is("=", 1)){
			$this->index++;
			$async = true;
		}
		if($this->eat("*")){
			$generator = true;
		}
		if(($this->isName("get") || $this->isName("set")) && !$this->is(",", 1) && !$this->is(":", 1) && !$this->is("(", 1) && !$this->is("}", 1) && !$this->is("=", 1)){
			$kind = $this->next()["v"];
		}
		$keyToken = $this->peek();
		[$key, $computed] = $this->parsePropertyKey();
		if($this->is("(")){
			$name = !$computed && is_string($key["value"] ?? null) ? $key["value"] : null;
			$fn = $this->parseFunctionRest($name, $async, $generator, true, $line);
			return ["kind" => $kind === "init" ? "init" : $kind, "key" => $key, "computed" => $computed, "value" => ["type" => "FunctionExpression", "fn" => $fn, "line" => $line], "method" => true];
		}
		if($this->eat(":")){
			return ["kind" => "init", "key" => $key, "computed" => $computed, "value" => $this->parseAssign(false), "method" => false];
		}
		if($computed || $keyToken["t"] !== "name"){
			throw $this->error("Unexpected token '" . self::describe($this->peek()) . "'");
		}
		if($keyToken["v"] === "arguments"){
			$this->markArguments();
		}
		$value = ["type" => "Identifier", "name" => $keyToken["v"], "line" => $line];
		if($this->is("=")){
			$this->index++;
			$value = ["type" => "AssignmentExpression", "operator" => "=", "left" => $value, "right" => $this->parseAssign(false), "line" => $line];
		}
		return ["kind" => "init", "key" => $key, "computed" => false, "value" => $value, "method" => false, "shorthand" => true];
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use Closure;

/**
 * A JavaScript object. Data properties live in $props, accessor properties
 * in $accessors as [getter, setter]; $hidden holds the non-enumerable keys
 * and $locked the non-writable ones. Symbol-keyed properties are kept in
 * $symbols by symbol id.
 *
 * Host objects keep their PHP payload in $host. When a property is found
 * nowhere on the prototype chain, the first $miss handler of the chain is
 * called with the receiver and the key, and its result is returned.
 */
class JsObject{

	/** @var array<string|int, mixed> */
	public array $props = [];

	/** @var array<string|int, array{0: JsCallable|null, 1: JsCallable|null}> */
	public array $accessors = [];

	/** @var array<string|int, true> */
	public array $hidden = [];

	/** @var array<string|int, true> */
	public array $locked = [];

	/** @var array<int, array{0: JsSymbol, 1: mixed}> */
	public array $symbols = [];

	public bool $extensible = true;

	public string $className = "Object";

	public mixed $host = null;

	/** @var Closure(JsObject, string) : mixed|null */
	public ?Closure $miss = null;

	public function __construct(
		public ?JsObject $proto = null
	){}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

/**
 * A JavaScript array: its elements are kept as a PHP list in $items, holes
 * being undefined.
 */
final class JsArray extends JsObject{

	/** @var list<mixed> */
	public array $items = [];

	public bool $frozen = false;

	/**
	 * @param list<mixed> $items
	 */
	public function __construct(?JsObject $proto, array $items = []){
		parent::__construct($proto);
		$this->items = $items;
		$this->className = "Array";
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

/**
 * A JavaScript function, either defined by a script or native.
 */
abstract class JsCallable extends JsObject{

	public string $name = "";

	public int $length = 0;

	public function __construct(?JsObject $proto){
		parent::__construct($proto);
		$this->className = "Function";
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use Fiber;

/**
 * The state of a generator object, whose body runs in a fiber.
 */
final class GeneratorState{

	public bool $started = false;

	public bool $running = false;

	public bool $done = false;

	public function __construct(
		public Fiber $fiber
	){}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

/**
 * Marks a let, const or class binding that is not initialized yet, and the
 * this value of a derived constructor before super() is called.
 */
final class Tdz{

	private static ?Tdz $instance = null;

	private function __construct(){

	}

	public static function get() : Tdz{
		return self::$instance ??= new Tdz();
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

use behaviorpack\script\js\builtin\ArrayBuiltins;
use behaviorpack\script\js\builtin\CollectionBuiltins;
use behaviorpack\script\js\builtin\CoreBuiltins;
use behaviorpack\script\js\builtin\DateBuiltins;
use behaviorpack\script\js\builtin\PromiseBuiltins;
use behaviorpack\script\js\builtin\StringBuiltins;
use Closure;
use Error;
use Fiber;
use Generator;
use RuntimeException;
use SplQueue;
use stdClass;
use Throwable;
use function array_key_exists;
use function array_merge;
use function array_pop;
use function array_slice;
use function bindec;
use function count;
use function ctype_digit;
use function ctype_xdigit;
use function fdiv;
use function floor;
use function fmod;
use function hrtime;
use function implode;
use function hexdec;
use function in_array;
use function intdiv;
use function is_bool;
use function is_finite;
use function is_float;
use function is_infinite;
use function is_int;
use function is_nan;
use function is_string;
use function ltrim;
use function mb_str_split;
use function mb_strlen;
use function mb_substr;
use function octdec;
use function pow;
use function preg_match;
use function rtrim;
use function sort;
use function spl_object_id;
use function sprintf;
use function str_repeat;
use function str_replace;
use function str_starts_with;
use function strcmp;
use function strlen;
use function strpos;
use function strtolower;
use function substr;
use function trim;
use const INF;
use const NAN;
use const PHP_INT_MAX;
use const PHP_INT_MIN;

/**
 * A tree-walking JavaScript interpreter. It evaluates the syntax trees built
 * by Parser, runs async functions and generators in fibers, keeps a
 * microtask queue for promises, and aborts code that runs past the watchdog
 * deadline with ScriptTimeoutException.
 */
final class Interpreter{

	public const MAX_DEPTH = 300;

	private const STEP_CHECK = 1024;

	public JsObject $objectPrototype;
	public NativeFunction $functionPrototype;
	public JsArray $arrayPrototype;
	public JsObject $stringPrototype;
	public JsObject $numberPrototype;
	public JsObject $booleanPrototype;
	public JsObject $symbolPrototype;
	public JsObject $errorPrototype;
	public JsObject $iteratorPrototype;
	public JsObject $generatorPrototype;
	public JsObject $promisePrototype;
	public JsObject $global;

	/** @var array<string, JsObject> */
	public array $errorPrototypes = [];

	/** @var array<string, JsCallable> */
	public array $constructors = [];

	public JsSymbol $symIterator;
	public JsSymbol $symAsyncIterator;
	public JsSymbol $symHasInstance;
	public JsSymbol $symToPrimitive;
	public JsSymbol $symToStringTag;

	public StringBuiltins $strings;

	public ?string $context = null;
	public string $file = "";
	public int $line = 0;
	public int $deadline = PHP_INT_MAX;

	/** @var (Closure(string, string, ?string) : void)|null */
	public ?Closure $printer = null;

	/** @var (Closure(string, string) : JsPromise)|null */
	public ?Closure $dynamicImport = null;

	/** @var list<array{0: string, 1: string, 2: int}> */
	private array $frames = [];
	private int $depth = 0;
	private int $steps = 0;

	/** @var SplQueue<array{0: Closure, 1: ?string}> */
	private SplQueue $jobs;

	/** @var array<int, JsPromise> */
	private array $unhandled = [];

	/** @var array<int, true> */
	private array $asyncFibers = [];

	private static ?stdClass $short = null;

	public function __construct(){
		self::$short ??= new stdClass();
		$this->jobs = new SplQueue();
		$this->objectPrototype = new JsObject(null);
		$this->functionPrototype = new NativeFunction($this->objectPrototype, "", 0, function(mixed $thisValue, array $args) : mixed{
			return null;
		});
		$this->arrayPrototype = new JsArray($this->objectPrototype);
		$this->stringPrototype = new JsObject($this->objectPrototype);
		$this->stringPrototype->className = "String";
		$this->numberPrototype = new JsObject($this->objectPrototype);
		$this->numberPrototype->className = "Number";
		$this->booleanPrototype = new JsObject($this->objectPrototype);
		$this->booleanPrototype->className = "Boolean";
		$this->symbolPrototype = new JsObject($this->objectPrototype);
		$this->symbolPrototype->className = "Symbol";
		$this->errorPrototype = new JsObject($this->objectPrototype);
		$this->errorPrototype->className = "Error";
		$this->iteratorPrototype = new JsObject($this->objectPrototype);
		$this->generatorPrototype = new JsObject($this->iteratorPrototype);
		$this->generatorPrototype->className = "Generator";
		$this->promisePrototype = new JsObject($this->objectPrototype);
		$this->global = new JsObject($this->objectPrototype);
		$this->global->className = "global";
		$this->symIterator = new JsSymbol("Symbol.iterator");
		$this->symAsyncIterator = new JsSymbol("Symbol.asyncIterator");
		$this->symHasInstance = new JsSymbol("Symbol.hasInstance");
		$this->symToPrimitive = new JsSymbol("Symbol.toPrimitive");
		$this->symToStringTag = new JsSymbol("Symbol.toStringTag");

		(new CoreBuiltins($this))->install();
		(new ArrayBuiltins($this))->install();
		$this->strings = new StringBuiltins($this);
		$this->strings->install();
		(new CollectionBuiltins($this))->install();
		(new PromiseBuiltins($this))->install();
		(new DateBuiltins($this))->install();
	}

	public static function short() : stdClass{
		return self::$short ??= new stdClass();
	}

	/**
	 * Starts the watchdog for a new entry from the server.
	 */
	public function startWatchdog(int $milliseconds) : void{
		$this->deadline = hrtime(true) + $milliseconds * 1_000_000;
		$this->steps = 0;
	}

	public function stopWatchdog() : void{
		$this->deadline = PHP_INT_MAX;
	}

	public function tick() : void{
		if(++$this->steps >= self::STEP_CHECK){
			$this->steps = 0;
			if(hrtime(true) > $this->deadline){
				throw new ScriptTimeoutException("Script watchdog deadline exceeded");
			}
		}
	}

	public function resetStack() : void{
		$this->frames = [];
		$this->depth = 0;
	}

	/**
	 * @return array{0: list<array{0: string, 1: string, 2: int}>, 1: int, 2: string, 3: int, 4: ?string}
	 */
	private function saveState() : array{
		return [$this->frames, $this->depth, $this->file, $this->line, $this->context];
	}

	/**
	 * @param array{0: list<array{0: string, 1: string, 2: int}>, 1: int, 2: string, 3: int, 4: ?string} $state
	 */
	private function restoreState(array $state) : void{
		[$this->frames, $this->depth, $this->file, $this->line, $this->context] = $state;
	}

	public function newObject(?JsObject $proto = null) : JsObject{
		return new JsObject($proto ?? $this->objectPrototype);
	}

	/**
	 * @param list<mixed> $items
	 */
	public function newArray(array $items = []) : JsArray{
		return new JsArray($this->arrayPrototype, $items);
	}

	/**
	 * @param Closure(mixed, list<mixed>) : mixed             $call
	 * @param (Closure(list<mixed>, JsObject) : JsObject)|null $construct
	 */
	public function native(string $name, int $length, Closure $call, ?Closure $construct = null) : NativeFunction{
		return new NativeFunction($this->functionPrototype, $name, $length, $call, $construct);
	}

	public function defineHidden(JsObject $object, string|JsSymbol $key, mixed $value) : void{
		if($key instanceof JsSymbol){
			$object->symbols[spl_object_id($key)] = [$key, $value];
			return;
		}
		$object->props[$key] = $value;
		$object->hidden[$key] = true;
	}

	/**
	 * @param Closure(mixed, list<mixed>) : mixed $call
	 */
	public function defineMethod(JsObject $target, string|JsSymbol $key, int $length, Closure $call) : NativeFunction{
		$name = $key instanceof JsSymbol ? "[" . $key->description . "]" : $key;
		$function = $this->native($name, $length, $call);
		$this->defineHidden($target, $key, $function);
		return $function;
	}

	/**
	 * @param Closure(mixed) : mixed              $get
	 * @param (Closure(mixed, mixed) : void)|null $set
	 */
	public function defineGetter(JsObject $target, string $name, Closure $get, ?Closure $set = null, bool $enumerable = false) : void{
		$getter = $this->native("get " . $name, 0, function(mixed $thisValue, array $args) use ($get) : mixed{
			return $get($thisValue);
		});
		$setter = null;
		if($set !== null){
			$setter = $this->native("set " . $name, 1, function(mixed $thisValue, array $args) use ($set) : mixed{
				$set($thisValue, $args[0] ?? null);
				return null;
			});
		}
		unset($target->props[$name]);
		$target->accessors[$name] = [$getter, $setter];
		if($enumerable){
			unset($target->hidden[$name]);
		}else{
			$target->hidden[$name] = true;
		}
	}

	/**
	 * Creates a native class: a constructor whose "prototype" is $prototype.
	 *
	 * @param (Closure(list<mixed>, JsObject) : JsObject)|null $construct
	 * @param (Closure(mixed, list<mixed>) : mixed)|null        $call
	 */
	public function makeClass(string $name, JsObject $prototype, ?Closure $construct, ?Closure $call = null, int $length = 0, ?JsObject $parent = null) : NativeFunction{
		$call ??= function(mixed $thisValue, array $args) use ($name) : mixed{
			$this->throwError("TypeError", "Class constructor " . $name . " cannot be invoked without 'new'");
		};
		$constructor = new NativeFunction($parent ?? $this->functionPrototype, $name, $length, $call, $construct);
		$this->defineHidden($constructor, "prototype", $prototype);
		$constructor->locked["prototype"] = true;
		$this->defineHidden($prototype, "constructor", $constructor);
		return $constructor;
	}

	public function iterResult(mixed $value, bool $done) : JsObject{
		$result = new JsObject($this->objectPrototype);
		$result->props["value"] = $value;
		$result->props["done"] = $done;
		return $result;
	}

	/**
	 * Returns the prototype to use for an object built by a native
	 * constructor called through new.target.
	 */
	public function prototypeFor(?JsObject $newTarget, JsObject $fallback) : JsObject{
		if($newTarget === null){
			return $fallback;
		}
		$proto = $this->get($newTarget, "prototype");
		return $proto instanceof JsObject ? $proto : $fallback;
	}

	public function makeError(string $type, string $message) : JsObject{
		$error = new JsObject($this->errorPrototypes[$type] ?? $this->errorPrototype);
		$error->className = "Error";
		$this->defineHidden($error, "message", $message);
		$this->defineHidden($error, "stack", $type . ": " . $message . $this->stackTrace());
		return $error;
	}

	public function throwError(string $type, string $message) : never{
		throw new JsThrow($this->makeError($type, $message), $type . ": " . $message);
	}

	public function stackTrace() : string{
		$lines = "";
		$line = $this->line;
		for($i = count($this->frames) - 1; $i >= 0 && $i >= count($this->frames) - 10; --$i){
			[$name, $file, $callerLine] = $this->frames[$i];
			$lines .= "\n    at " . ($name === "" ? "<anonymous>" : $name) . " (" . $file . ":" . $line . ")";
			$line = $callerLine;
		}
		$lines .= "\n    at " . $this->file . ":" . $line;
		return $lines;
	}

	/**
	 * Describes a thrown value for the console, with its stack when it is
	 * an error.
	 */
	public function describeError(mixed $value) : string{
		if($value instanceof JsObject){
			try{
				$stack = $this->getFrom($value, "stack", $value);
				if(is_string($stack) && $stack !== ""){
					$name = $this->getFrom($value, "name", $value);
					$message = $this->getFrom($value, "message", $value);
					$head = $this->toStringSafe($name) . ": " . $this->toStringSafe($message);
					$position = strpos($stack, "\n");
					return $head . ($position === false ? "" : substr($stack, $position));
				}
			}catch(JsThrow){
				return "[object]";
			}
		}
		return $this->toStringSafe($value);
	}

	public function toStringSafe(mixed $value) : string{
		try{
			return $this->toString($value);
		}catch(JsThrow){
			return "[object]";
		}
	}

	public function print(string $level, string $message) : void{
		if($this->printer !== null){
			($this->printer)($level, $message, $this->context);
		}
	}

	public function reportError(mixed $value) : void{
		$this->print("error", $this->describeError($value));
	}

	public function typeOf(mixed $value) : string{
		if($value === null){
			return "undefined";
		}
		if(is_bool($value)){
			return "boolean";
		}
		if(is_int($value) || is_float($value)){
			return "number";
		}
		if(is_string($value)){
			return "string";
		}
		if($value instanceof JsCallable){
			return "function";
		}
		if($value instanceof JsSymbol){
			return "symbol";
		}
		return "object";
	}

	public function toBoolean(mixed $value) : bool{
		if($value === null || $value instanceof JsNull){
			return false;
		}
		if(is_bool($value)){
			return $value;
		}
		if(is_int($value)){
			return $value !== 0;
		}
		if(is_float($value)){
			return !($value == 0.0 || is_nan($value));
		}
		if(is_string($value)){
			return $value !== "";
		}
		return true;
	}

	public function toNumber(mixed $value) : int|float{
		if(is_int($value) || is_float($value)){
			return $value;
		}
		if($value === null){
			return NAN;
		}
		if($value instanceof JsNull){
			return 0;
		}
		if(is_bool($value)){
			return $value ? 1 : 0;
		}
		if(is_string($value)){
			return self::stringToNumber($value);
		}
		if($value instanceof JsSymbol){
			$this->throwError("TypeError", "Cannot convert a Symbol value to a number");
		}
		return $this->toNumber($this->toPrimitive($value, "number"));
	}

	public static function stringToNumber(string $value) : int|float{
		$text = trim($value, " \t\n\r\v\f\xC2\xA0");
		if($text === ""){
			return 0;
		}
		$lower = strtolower($text);
		if(str_starts_with($lower, "0x") && strlen($text) > 2 && ctype_xdigit(substr($text, 2))){
			return self::intOrFloat((float) \hexdec(substr($text, 2)));
		}
		if(str_starts_with($lower, "0b") && \preg_match('/^[01]+$/', substr($text, 2)) === 1){
			return self::intOrFloat((float) \bindec(substr($text, 2)));
		}
		if(str_starts_with($lower, "0o") && \preg_match('/^[0-7]+$/', substr($text, 2)) === 1){
			return self::intOrFloat((float) \octdec(substr($text, 2)));
		}
		if($text === "Infinity" || $text === "+Infinity"){
			return INF;
		}
		if($text === "-Infinity"){
			return -INF;
		}
		if(\preg_match('/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/', $text) !== 1){
			return NAN;
		}
		if(\preg_match('/^[+-]?\d+$/', $text) === 1 && strlen(ltrim($text, "+-")) < 16){
			return (int) $text;
		}
		return (float) $text;
	}

	public static function intOrFloat(float $value) : int|float{
		if($value === floor($value) && $value >= -9007199254740991.0 && $value <= 9007199254740991.0 && !($value == 0.0 && fdiv(1.0, $value) < 0)){
			return (int) $value;
		}
		return $value;
	}

	public function toNumeric(mixed $value) : int|float{
		return $this->toNumber($value);
	}

	public function toIntegerOrInfinity(mixed $value) : int|float{
		$number = $this->toNumber($value);
		if(is_int($number)){
			return $number;
		}
		if(is_nan($number)){
			return 0;
		}
		if(is_infinite($number)){
			return $number;
		}
		$truncated = $number < 0 ? -floor(-$number) : floor($number);
		return self::intOrFloat($truncated);
	}

	public function toInt32(mixed $value) : int{
		$number = is_int($value) ? $value : $this->toNumber($value);
		if(is_int($number) && $number >= -2147483648 && $number <= 2147483647){
			return $number;
		}
		if(is_float($number) && (is_nan($number) || is_infinite($number))){
			return 0;
		}
		$number = (float) $number;
		$number = $number < 0 ? -floor(-$number) : floor($number);
		$number = fmod($number, 4294967296.0);
		if($number < 0){
			$number += 4294967296.0;
		}
		$int = (int) $number;
		return $int >= 2147483648 ? $int - 4294967296 : $int;
	}

	public function toUint32(mixed $value) : int{
		$int = $this->toInt32($value);
		return $int < 0 ? $int + 4294967296 : $int;
	}

	public static function wrapInt32(int $value) : int{
		$value &= 0xFFFFFFFF;
		return $value >= 2147483648 ? $value - 4294967296 : $value;
	}

	public function toString(mixed $value) : string{
		if(is_string($value)){
			return $value;
		}
		if(is_int($value)){
			return (string) $value;
		}
		if(is_float($value)){
			return self::numberToString($value);
		}
		if($value === null){
			return "undefined";
		}
		if($value instanceof JsNull){
			return "null";
		}
		if(is_bool($value)){
			return $value ? "true" : "false";
		}
		if($value instanceof JsSymbol){
			$this->throwError("TypeError", "Cannot convert a Symbol value to a string");
		}
		return $this->toString($this->toPrimitive($value, "string"));
	}

	public static function numberToString(int|float $number) : string{
		if(is_int($number)){
			return (string) $number;
		}
		if(is_nan($number)){
			return "NaN";
		}
		if($number == 0.0){
			return "0";
		}
		if(is_infinite($number)){
			return $number > 0 ? "Infinity" : "-Infinity";
		}
		if($number < 0){
			return "-" . self::numberToString(-$number);
		}
		$repr = "";
		for($precision = 1; $precision <= 17; ++$precision){
			$repr = sprintf("%." . ($precision - 1) . "e", $number);
			if((float) $repr === $number){
				break;
			}
		}
		$ePosition = strpos($repr, "e");
		$mantissa = substr($repr, 0, $ePosition);
		$exponent = (int) substr($repr, $ePosition + 1);
		$digits = rtrim(str_replace(".", "", $mantissa), "0");
		if($digits === ""){
			$digits = "0";
		}
		$k = strlen($digits);
		$n = $exponent + 1;
		if($k <= $n && $n <= 21){
			return $digits . str_repeat("0", $n - $k);
		}
		if(0 < $n && $n <= 21){
			return substr($digits, 0, $n) . "." . substr($digits, $n);
		}
		if(-6 < $n && $n <= 0){
			return "0." . str_repeat("0", -$n) . $digits;
		}
		$e = $n - 1;
		$sign = $e < 0 ? "-" : "+";
		$e = $e < 0 ? -$e : $e;
		if($k === 1){
			return $digits . "e" . $sign . $e;
		}
		return $digits[0] . "." . substr($digits, 1) . "e" . $sign . $e;
	}

	public function toPrimitive(mixed $value, string $hint = "default") : mixed{
		if(!$value instanceof JsObject){
			return $value;
		}
		$exotic = $this->getSymbol($value, $this->symToPrimitive);
		if($exotic instanceof JsCallable){
			$result = $this->call($exotic, $value, [$hint]);
			if(!$result instanceof JsObject){
				return $result;
			}
			$this->throwError("TypeError", "Cannot convert object to primitive value");
		}
		$order = $hint === "string" ? ["toString", "valueOf"] : ["valueOf", "toString"];
		foreach($order as $method){
			$function = $this->get($value, $method);
			if($function instanceof JsCallable){
				$result = $this->call($function, $value, []);
				if(!$result instanceof JsObject){
					return $result;
				}
			}
		}
		$this->throwError("TypeError", "Cannot convert object to primitive value");
	}

	public function toObject(mixed $value) : JsObject{
		if($value instanceof JsObject){
			return $value;
		}
		if($value === null || $value instanceof JsNull){
			$this->throwError("TypeError", "Cannot convert undefined or null to object");
		}
		$wrapper = new JsObject(match(true){
			is_string($value) => $this->stringPrototype,
			is_bool($value) => $this->booleanPrototype,
			$value instanceof JsSymbol => $this->symbolPrototype,
			default => $this->numberPrototype
		});
		$wrapper->className = match(true){
			is_string($value) => "String",
			is_bool($value) => "Boolean",
			$value instanceof JsSymbol => "Symbol",
			default => "Number"
		};
		$wrapper->host = $value;
		return $wrapper;
	}

	public function toPropertyKey(mixed $value) : string|JsSymbol{
		if(is_string($value)){
			return $value;
		}
		if(is_int($value)){
			return (string) $value;
		}
		if($value instanceof JsSymbol){
			return $value;
		}
		$primitive = $this->toPrimitive($value, "string");
		if($primitive instanceof JsSymbol){
			return $primitive;
		}
		return $this->toString($primitive);
	}

	public static function isIndex(string|int $key) : bool{
		if(is_int($key)){
			return $key >= 0;
		}
		return $key !== "" && ctype_digit($key) && ($key === "0" || $key[0] !== "0") && strlen($key) < 16;
	}

	public function strictEquals(mixed $a, mixed $b) : bool{
		if((is_int($a) || is_float($a)) && (is_int($b) || is_float($b))){
			return $a == $b;
		}
		return $a === $b;
	}

	public function sameValueZero(mixed $a, mixed $b) : bool{
		if(is_float($a) && is_float($b) && is_nan($a) && is_nan($b)){
			return true;
		}
		if((is_int($a) || is_float($a)) && is_float($b) && is_nan($b)){
			return is_float($a) && is_nan($a);
		}
		return $this->strictEquals($a, $b);
	}

	public function looseEquals(mixed $a, mixed $b) : bool{
		$aNullish = $a === null || $a instanceof JsNull;
		$bNullish = $b === null || $b instanceof JsNull;
		if($aNullish || $bNullish){
			return $aNullish && $bNullish;
		}
		$aNumber = is_int($a) || is_float($a);
		$bNumber = is_int($b) || is_float($b);
		if(($aNumber && $bNumber) || (is_string($a) && is_string($b)) || (is_bool($a) && is_bool($b))){
			return $this->strictEquals($a, $b);
		}
		if($a instanceof JsObject && $b instanceof JsObject){
			return $a === $b;
		}
		if($a instanceof JsSymbol || $b instanceof JsSymbol){
			return $a === $b;
		}
		if(is_bool($a)){
			return $this->looseEquals($a ? 1 : 0, $b);
		}
		if(is_bool($b)){
			return $this->looseEquals($a, $b ? 1 : 0);
		}
		if($aNumber && is_string($b)){
			return $this->strictEquals($a, self::stringToNumber($b));
		}
		if(is_string($a) && $bNumber){
			return $this->strictEquals(self::stringToNumber($a), $b);
		}
		if($a instanceof JsObject){
			return $this->looseEquals($this->toPrimitive($a), $b);
		}
		if($b instanceof JsObject){
			return $this->looseEquals($a, $this->toPrimitive($b));
		}
		return false;
	}

	public function get(mixed $base, string|int|JsSymbol $key) : mixed{
		if($base instanceof JsObject){
			if($key instanceof JsSymbol){
				return $this->getSymbol($base, $key);
			}
			return $this->getFrom($base, $key, $base);
		}
		if(is_string($base)){
			if($key === "length"){
				return mb_strlen($base, "UTF-8");
			}
			if(!$key instanceof JsSymbol && self::isIndex($key)){
				$char = mb_substr($base, (int) $key, 1, "UTF-8");
				return $char === "" ? null : $char;
			}
			return $key instanceof JsSymbol ? $this->getSymbol($this->stringPrototype, $key, $base) : $this->getFrom($this->stringPrototype, $key, $base);
		}
		if($base === null || $base instanceof JsNull){
			$this->throwError("TypeError", "Cannot read properties of " . ($base === null ? "undefined" : "null") . " (reading '" . ($key instanceof JsSymbol ? "Symbol(" . $key->description . ")" : $key) . "')");
		}
		$proto = match(true){
			is_bool($base) => $this->booleanPrototype,
			$base instanceof JsSymbol => $this->symbolPrototype,
			default => $this->numberPrototype
		};
		return $key instanceof JsSymbol ? $this->getSymbol($proto, $key, $base) : $this->getFrom($proto, $key, $base);
	}

	public function getSymbol(JsObject $object, JsSymbol $symbol, mixed $receiver = null) : mixed{
		$id = spl_object_id($symbol);
		for($current = $object; $current !== null; $current = $current->proto){
			if(isset($current->symbols[$id])){
				return $current->symbols[$id][1];
			}
		}
		return null;
	}

	public function getFrom(JsObject $object, string|int $key, mixed $receiver) : mixed{
		$current = $object;
		do{
			if($current instanceof JsArray){
				if($key === "length"){
					return count($current->items);
				}
				if(is_int($key) || self::isIndex($key)){
					$index = (int) $key;
					if($index < count($current->items)){
						return $current->items[$index];
					}
				}
			}
			if(array_key_exists($key, $current->props)){
				return $current->props[$key];
			}
			if(isset($current->accessors[$key])){
				$getter = $current->accessors[$key][0];
				return $getter === null ? null : $this->call($getter, $receiver, []);
			}
			if($current instanceof JsCallable){
				if($key === "name"){
					return $current->name;
				}
				if($key === "length"){
					return $current->length;
				}
				if($key === "prototype" && $current instanceof JsFunction && $current->needsPrototype){
					return $this->createPrototypeProperty($current);
				}
			}
			$current = $current->proto;
		}while($current !== null);
		for($current = $object; $current !== null; $current = $current->proto){
			if($current->miss !== null){
				return ($current->miss)($receiver instanceof JsObject ? $receiver : $object, (string) $key);
			}
		}
		return null;
	}

	private function createPrototypeProperty(JsFunction $function) : JsObject{
		$function->needsPrototype = false;
		$generator = $function->node !== null && $function->node["generator"];
		$prototype = new JsObject($generator ? $this->generatorPrototype : $this->objectPrototype);
		if(!$generator){
			$this->defineHidden($prototype, "constructor", $function);
		}
		$this->defineHidden($function, "prototype", $prototype);
		return $prototype;
	}

	public function set(mixed $base, string|int|JsSymbol $key, mixed $value) : void{
		if(!$base instanceof JsObject){
			if($base === null || $base instanceof JsNull){
				$this->throwError("TypeError", "Cannot set properties of " . ($base === null ? "undefined" : "null") . " (setting '" . ($key instanceof JsSymbol ? "Symbol()" : $key) . "')");
			}
			return;
		}
		if($key instanceof JsSymbol){
			if(!$base->extensible && !isset($base->symbols[spl_object_id($key)])){
				$this->throwError("TypeError", "Cannot add property, object is not extensible");
			}
			$base->symbols[spl_object_id($key)] = [$key, $value];
			return;
		}
		$this->setOn($base, $key, $value, $base);
	}

	public function setOn(JsObject $object, string|int $key, mixed $value, mixed $receiver) : void{
		if($object instanceof JsArray){
			if(is_int($key) || self::isIndex($key)){
				if($object->frozen){
					$this->throwError("TypeError", "Cannot assign to read only property '" . $key . "' of object");
				}
				$index = (int) $key;
				$count = count($object->items);
				if($index < $count){
					$object->items[$index] = $value;
				}elseif($index === $count){
					$object->items[] = $value;
				}else{
					if($index > 50_000_000){
						$this->throwError("RangeError", "Invalid array length");
					}
					for($i = $count; $i < $index; ++$i){
						$object->items[] = null;
					}
					$object->items[] = $value;
				}
				return;
			}
			if($key === "length"){
				if($object->frozen){
					$this->throwError("TypeError", "Cannot assign to read only property 'length' of object");
				}
				$length = $this->toNumber($value);
				if(!is_int($length) || $length < 0 || $length > 50_000_000){
					$this->throwError("RangeError", "Invalid array length");
				}
				$count = count($object->items);
				if($length < $count){
					$object->items = array_slice($object->items, 0, $length);
				}else{
					for($i = $count; $i < $length; ++$i){
						$object->items[] = null;
					}
				}
				return;
			}
		}
		for($current = $object; $current !== null; $current = $current->proto){
			if(isset($current->accessors[$key])){
				$setter = $current->accessors[$key][1];
				if($setter === null){
					$this->throwError("TypeError", "Cannot set property " . $key . " of " . $this->describeObject($object) . " which has only a getter");
				}
				$this->call($setter, $receiver, [$value]);
				return;
			}
			if(array_key_exists($key, $current->props)){
				if(isset($current->locked[$key])){
					$this->throwError("TypeError", "Cannot assign to read only property '" . $key . "' of " . $this->describeObject($object));
				}
				if($current === $object){
					$object->props[$key] = $value;
					return;
				}
				break;
			}
		}
		if(!$object->extensible){
			$this->throwError("TypeError", "Cannot add property " . $key . ", object is not extensible");
		}
		$object->props[$key] = $value;
	}

	public function describeObject(JsObject $object) : string{
		if($object instanceof JsCallable){
			return "function " . $object->name;
		}
		$constructor = null;
		for($current = $object; $current !== null; $current = $current->proto){
			if(array_key_exists("constructor", $current->props)){
				$constructor = $current->props["constructor"];
				break;
			}
		}
		if($constructor instanceof JsCallable && $constructor->name !== ""){
			return "#<" . $constructor->name . ">";
		}
		return "#<" . $object->className . ">";
	}

	public function hasProperty(JsObject $object, string|int|JsSymbol $key) : bool{
		if($key instanceof JsSymbol){
			$id = spl_object_id($key);
			for($current = $object; $current !== null; $current = $current->proto){
				if(isset($current->symbols[$id])){
					return true;
				}
			}
			return false;
		}
		for($current = $object; $current !== null; $current = $current->proto){
			if($this->hasOwnProperty($current, $key)){
				return true;
			}
		}
		return false;
	}

	public function hasOwnProperty(JsObject $object, string|int|JsSymbol $key) : bool{
		if($key instanceof JsSymbol){
			return isset($object->symbols[spl_object_id($key)]);
		}
		if($object instanceof JsArray){
			if($key === "length"){
				return true;
			}
			if(self::isIndex($key)){
				return (int) $key < count($object->items);
			}
		}
		if(array_key_exists($key, $object->props) || isset($object->accessors[$key])){
			return true;
		}
		if($object instanceof JsCallable && ($key === "name" || $key === "length")){
			return true;
		}
		return $object instanceof JsFunction && $key === "prototype" && $object->needsPrototype;
	}

	public function deleteProperty(JsObject $object, string|int|JsSymbol $key) : bool{
		if($key instanceof JsSymbol){
			unset($object->symbols[spl_object_id($key)]);
			return true;
		}
		if($object instanceof JsArray && self::isIndex($key)){
			if($object->frozen){
				$this->throwError("TypeError", "Cannot delete property '" . $key . "' of " . $this->describeObject($object));
			}
			$index = (int) $key;
			if($index < count($object->items)){
				$object->items[$index] = null;
			}
			return true;
		}
		if(isset($object->locked[$key])){
			$this->throwError("TypeError", "Cannot delete property '" . $key . "' of " . $this->describeObject($object));
		}
		unset($object->props[$key], $object->accessors[$key], $object->hidden[$key]);
		return true;
	}

	/**
	 * Returns the own string keys of an object: integer keys first, in
	 * ascending order, then the other keys in insertion order.
	 *
	 * @return list<string>
	 */
	public function ownKeys(JsObject $object, bool $includeHidden = false) : array{
		$integers = [];
		$strings = [];
		if($object instanceof JsArray){
			for($i = 0, $count = count($object->items); $i < $count; ++$i){
				$integers[] = $i;
			}
		}
		foreach([$object->props, $object->accessors] as $map){
			foreach($map as $key => $unused){
				if(!$includeHidden && isset($object->hidden[$key])){
					continue;
				}
				if(is_int($key)){
					$integers[] = $key;
				}else{
					$strings[] = $key;
				}
			}
		}
		if(!$object instanceof JsArray && count($integers) > 1){
			sort($integers);
		}
		$keys = [];
		foreach($integers as $integer){
			$keys[] = (string) $integer;
		}
		return count($strings) === 0 ? $keys : array_merge($keys, $strings);
	}

	/**
	 * @return list<JsSymbol>
	 */
	public function ownSymbols(JsObject $object) : array{
		$symbols = [];
		foreach($object->symbols as [$symbol, $unused]){
			$symbols[] = $symbol;
		}
		return $symbols;
	}

	public function isEnumerable(JsObject $object, string $key) : bool{
		if($object instanceof JsArray && self::isIndex($key)){
			return true;
		}
		return !isset($object->hidden[$key]);
	}

	/**
	 * Copies the own enumerable properties of $source onto $target.
	 *
	 * @param array<string, true> $excluded
	 */
	public function copyDataProperties(JsObject $target, mixed $source, array $excluded = []) : void{
		if($source === null || $source instanceof JsNull){
			return;
		}
		if(is_string($source)){
			foreach(mb_str_split($source, 1, "UTF-8") as $index => $char){
				if(!isset($excluded[(string) $index])){
					$this->set($target, (string) $index, $char);
				}
			}
			return;
		}
		if(!$source instanceof JsObject){
			return;
		}
		foreach($this->ownKeys($source) as $key){
			if(!isset($excluded[$key])){
				$this->createDataProperty($target, $key, $this->getFrom($source, $key, $source));
			}
		}
		foreach($source->symbols as [$symbol, $value]){
			$target->symbols[spl_object_id($symbol)] = [$symbol, $value];
		}
	}

	public function createDataProperty(JsObject $object, string|int $key, mixed $value) : void{
		if($object instanceof JsArray && self::isIndex($key)){
			$this->setOn($object, $key, $value, $object);
			return;
		}
		unset($object->accessors[$key], $object->hidden[$key]);
		$object->props[$key] = $value;
	}

	public function isCallable(mixed $value) : bool{
		return $value instanceof JsCallable;
	}

	public function isConstructor(mixed $value) : bool{
		if($value instanceof NativeFunction){
			return $value->construct !== null;
		}
		if($value instanceof JsFunction){
			$node = $value->node;
			return $node === null || (!$node["arrow"] && !$node["method"] && !$node["async"] && !$node["generator"]) || $value->isClass;
		}
		return false;
	}

	/**
	 * @param list<mixed> $args
	 */
	public function call(mixed $function, mixed $thisValue, array $args) : mixed{
		if($function instanceof NativeFunction){
			return $this->callNative($function, $thisValue, $args);
		}
		if($function instanceof JsFunction){
			if($function->isClass){
				$this->throwError("TypeError", "Class constructor " . $function->name . " cannot be invoked without 'new'");
			}
			return $this->callFunction($function, $thisValue, $args, null);
		}
		$this->throwError("TypeError", $this->describeValue($function) . " is not a function");
	}

	/**
	 * @param list<mixed> $args
	 */
	private function callNative(NativeFunction $function, mixed $thisValue, array $args) : mixed{
		try{
			return ($function->call)($thisValue, $args);
		}catch(JsThrow|ScriptTimeoutException|GeneratorReturn $e){
			throw $e;
		}catch(SyntaxErrorException $e){
			throw new JsThrow($this->makeError("SyntaxError", $e->getMessage()));
		}catch(RuntimeException $e){
			throw new JsThrow($this->makeError("Error", $e->getMessage()));
		}catch(Error $e){
			if($e instanceof \FiberError){
				throw $e;
			}
			throw new JsThrow($this->makeError("Error", "Internal error in " . $function->name . ": " . $e->getMessage()));
		}
	}

	public function describeValue(mixed $value) : string{
		if($value === null){
			return "undefined";
		}
		if($value instanceof JsNull){
			return "null";
		}
		if(is_string($value)){
			return "\"" . $value . "\"";
		}
		if($value instanceof JsObject){
			return $this->describeObject($value);
		}
		return $this->toStringSafe($value);
	}

	/**
	 * @param list<mixed> $args
	 */
	public function construct(mixed $function, array $args, ?JsObject $newTarget = null) : JsObject{
		if(!$this->isConstructor($function)){
			$this->throwError("TypeError", $this->describeValue($function) . " is not a constructor");
		}
		$newTarget ??= $function;
		if($function instanceof NativeFunction){
			try{
				return ($function->construct)($args, $newTarget);
			}catch(JsThrow|ScriptTimeoutException|GeneratorReturn $e){
				throw $e;
			}catch(RuntimeException $e){
				throw new JsThrow($this->makeError("Error", $e->getMessage()));
			}
		}
		if(!$function instanceof JsFunction){
			$this->throwError("TypeError", "Not a constructor");
		}
		if(!$function->derived){
			$proto = $this->get($newTarget, "prototype");
			$object = new JsObject($proto instanceof JsObject ? $proto : $this->objectPrototype);
			$this->initializeFields($function, $object);
			if($function->node === null){
				return $object;
			}
			$result = $this->invoke($function, $object, $args, $newTarget)[0];
			return $result instanceof JsObject ? $result : $object;
		}
		$parent = $function->proto;
		if($function->node === null){
			$object = $this->construct($parent, $args, $newTarget);
			$this->initializeFields($function, $object);
			return $object;
		}
		[$result, $scope] = $this->invoke($function, Tdz::get(), $args, $newTarget);
		if($result instanceof JsObject){
			return $result;
		}
		$thisValue = $scope->vars["this"] ?? null;
		if(!$thisValue instanceof JsObject){
			$this->throwError("ReferenceError", "Must call super constructor in derived class before accessing 'this' or returning from derived constructor");
		}
		return $thisValue;
	}

	private function initializeFields(JsFunction $class, JsObject $object) : void{
		foreach($class->fields as [$key, $valueNode, $private]){
			$value = null;
			if($valueNode !== null){
				$scope = new Scope($class->scope, true);
				$scope->vars["this"] = $object;
				$scope->vars["%fn"] = $class;
				$scope->vars["%nt"] = null;
				$value = $this->evaluateNamed($valueNode, $scope, is_string($key) ? $key : "");
			}
			if($key instanceof JsSymbol){
				$object->symbols[spl_object_id($key)] = [$key, $value];
			}elseif($private){
				$this->defineHidden($object, $key, $value);
			}else{
				$this->createDataProperty($object, $key, $value);
			}
		}
	}

	/**
	 * @param list<mixed> $args
	 */
	private function callFunction(JsFunction $function, mixed $thisValue, array $args, ?JsObject $newTarget) : mixed{
		$node = $function->node;
		if($node === null){
			return null;
		}
		if($node["generator"]){
			if($node["async"]){
				$this->throwError("TypeError", "Async generators are not supported");
			}
			return $this->makeGenerator($function, $thisValue, $args);
		}
		if($node["async"]){
			return $this->runAsync($function, $thisValue, $args);
		}
		return $this->invoke($function, $thisValue, $args, $newTarget)[0];
	}

	/**
	 * Runs the body of a function and returns its result and scope.
	 *
	 * @param list<mixed> $args
	 * @return array{0: mixed, 1: Scope}
	 */
	private function invoke(JsFunction $function, mixed $thisValue, array $args, ?JsObject $newTarget) : array{
		if($this->depth >= self::MAX_DEPTH){
			$this->throwError("RangeError", "Maximum call stack size exceeded");
		}
		$this->tick();
		$node = $function->node;
		$scope = new Scope($function->scope, true);
		if(!$node["arrow"]){
			$scope->vars["this"] = $thisValue;
			$scope->vars["%fn"] = $function;
			$scope->vars["%nt"] = $newTarget;
			if($node["args"]){
				$arguments = new JsArray($this->arrayPrototype, $args);
				$arguments->className = "Arguments";
				$scope->vars["arguments"] = $arguments;
			}
		}
		$savedFile = $this->file;
		$savedLine = $this->line;
		$savedDepth = $this->depth;
		$frameCount = count($this->frames);
		$this->frames[] = [$function->name, $function->file, $savedLine];
		$this->depth++;
		$this->file = $function->file;
		try{
			$this->bindParameters($node["params"], $args, $scope);
			foreach($node["vars"] as $name){
				if(!array_key_exists($name, $scope->vars)){
					$scope->vars[$name] = null;
				}
			}
			$value = null;
			if($node["expression"]){
				$value = $this->evaluate($node["body"], $scope);
			}else{
				$body = $node["body"];
				$this->hoistLexical($body, $scope);
				foreach($body["body"] as $statement){
					$completion = $this->execute($statement, $scope);
					if($completion !== null){
						if($completion->type === Completion::RETURN){
							$value = $completion->value;
						}
						break;
					}
				}
			}
		}catch(Throwable $e){
			$this->frames = array_slice($this->frames, 0, $frameCount);
			$this->depth = $savedDepth;
			$this->file = $savedFile;
			throw $e;
		}
		$this->frames = array_slice($this->frames, 0, $frameCount);
		$this->depth = $savedDepth;
		$this->file = $savedFile;
		$this->line = $savedLine;
		return [$value, $scope];
	}

	/**
	 * @param list<array<string, mixed>> $params
	 * @param list<mixed>                $args
	 */
	private function bindParameters(array $params, array $args, Scope $scope) : void{
		foreach($params as $index => $param){
			if($param["type"] === "Identifier"){
				$scope->vars[$param["name"]] = $args[$index] ?? null;
				continue;
			}
			if($param["type"] === "RestElement"){
				$this->bindPattern($param["argument"], $this->newArray(array_slice($args, $index)), $scope, "let");
				break;
			}
			$this->bindPattern($param, $args[$index] ?? null, $scope, "let");
		}
	}

	/**
	 * @param list<mixed> $args
	 */
	private function runAsync(JsFunction $function, mixed $thisValue, array $args) : JsPromise{
		$promise = $this->newPromise();
		$fiber = new Fiber(function() use ($function, $thisValue, $args) : mixed{
			return $this->invoke($function, $thisValue, $args, null)[0];
		});
		$this->asyncFibers[spl_object_id($fiber)] = true;
		$this->stepAsync($fiber, $promise, 0, null);
		return $promise;
	}

	private function stepAsync(Fiber $fiber, JsPromise $promise, int $mode, mixed $value) : void{
		$state = $this->saveState();
		try{
			if($mode === 0){
				$request = $fiber->start();
			}elseif($mode === 1){
				$request = $fiber->resume($value);
			}else{
				$request = $fiber->throw(new JsThrow($value));
			}
		}catch(JsThrow $e){
			$this->restoreState($state);
			unset($this->asyncFibers[spl_object_id($fiber)]);
			$this->rejectPromise($promise, $e->value);
			return;
		}catch(Throwable $e){
			$this->restoreState($state);
			unset($this->asyncFibers[spl_object_id($fiber)]);
			throw $e;
		}
		$this->restoreState($state);
		if($fiber->isTerminated()){
			unset($this->asyncFibers[spl_object_id($fiber)]);
			$this->resolvePromise($promise, $fiber->getReturn());
			return;
		}
		$awaited = $this->promiseResolve($request[1] ?? null);
		$this->onSettled($awaited, function(int $settledState, mixed $settledValue) use ($fiber, $promise) : void{
			$this->stepAsync($fiber, $promise, $settledState === JsPromise::FULFILLED ? 1 : 2, $settledValue);
		});
	}

	public function await(mixed $value) : mixed{
		$fiber = Fiber::getCurrent();
		if($fiber === null || !isset($this->asyncFibers[spl_object_id($fiber)])){
			$promise = $this->promiseResolve($value);
			$promise->handled = true;
			unset($this->unhandled[spl_object_id($promise)]);
			$this->drainJobs();
			if($promise->state === JsPromise::PENDING){
				$this->throwError("Error", "Top-level await on a promise that does not settle during loading");
			}
			if($promise->state === JsPromise::REJECTED){
				throw new JsThrow($promise->value);
			}
			return $promise->value;
		}
		$state = $this->saveState();
		try{
			$result = Fiber::suspend(["await", $value]);
		}catch(Throwable $e){
			$this->restoreState($state);
			throw $e;
		}
		$this->restoreState($state);
		return $result;
	}

	/**
	 * @param list<mixed> $args
	 */
	private function makeGenerator(JsFunction $function, mixed $thisValue, array $args) : JsObject{
		$proto = $this->get($function, "prototype");
		$generator = new JsObject($proto instanceof JsObject ? $proto : $this->generatorPrototype);
		$generator->className = "Generator";
		$generator->host = new GeneratorState(new Fiber(function() use ($function, $thisValue, $args) : mixed{
			return $this->invoke($function, $thisValue, $args, null)[0];
		}));
		return $generator;
	}

	/**
	 * Resumes a generator: $mode is 0 for next, 1 for return, 2 for throw.
	 */
	public function generatorResume(mixed $generator, int $mode, mixed $value) : JsObject{
		if(!$generator instanceof JsObject || !$generator->host instanceof GeneratorState){
			$this->throwError("TypeError", "next method called on incompatible receiver");
		}
		$state = $generator->host;
		if($state->running){
			$this->throwError("TypeError", "Generator is already running");
		}
		if(!$state->started && $mode !== 0){
			$state->done = true;
		}
		if($state->done){
			if($mode === 2){
				throw new JsThrow($value);
			}
			return $this->iterResult($mode === 1 ? $value : null, true);
		}
		$saved = $this->saveState();
		$state->running = true;
		try{
			if(!$state->started){
				$state->started = true;
				$request = $state->fiber->start();
			}elseif($mode === 0){
				$request = $state->fiber->resume($value);
			}elseif($mode === 1){
				$request = $state->fiber->throw(new GeneratorReturn($value));
			}else{
				$request = $state->fiber->throw(new JsThrow($value));
			}
		}catch(GeneratorReturn $e){
			$state->running = false;
			$state->done = true;
			$this->restoreState($saved);
			return $this->iterResult($e->value, true);
		}catch(Throwable $e){
			$state->running = false;
			$state->done = true;
			$this->restoreState($saved);
			throw $e;
		}
		$state->running = false;
		$this->restoreState($saved);
		if($state->fiber->isTerminated()){
			$state->done = true;
			return $this->iterResult($state->fiber->getReturn(), true);
		}
		return $this->iterResult($request[1] ?? null, false);
	}

	private function yieldValue(mixed $value) : mixed{
		if(Fiber::getCurrent() === null){
			$this->throwError("SyntaxError", "yield outside of a generator");
		}
		$state = $this->saveState();
		try{
			$sent = Fiber::suspend(["yield", $value]);
		}catch(Throwable $e){
			$this->restoreState($state);
			throw $e;
		}
		$this->restoreState($state);
		return $sent;
	}

	private function yieldDelegate(mixed $iterable) : mixed{
		$method = $iterable instanceof JsObject || is_string($iterable) ? $this->get($iterable, $this->symIterator) : null;
		if(!$method instanceof JsCallable){
			$this->throwError("TypeError", $this->describeValue($iterable) . " is not iterable");
		}
		$iterator = $this->call($method, $iterable, []);
		$next = $this->get($iterator, "next");
		$sent = null;
		while(true){
			$result = $this->call($next, $iterator, [$sent]);
			if(!$result instanceof JsObject){
				$this->throwError("TypeError", "Iterator result is not an object");
			}
			if($this->toBoolean($this->get($result, "done"))){
				return $this->get($result, "value");
			}
			$sent = $this->yieldValue($this->get($result, "value"));
		}
	}

	public function newPromise() : JsPromise{
		return new JsPromise($this->promisePrototype);
	}

	public function promiseResolve(mixed $value) : JsPromise{
		if($value instanceof JsPromise){
			return $value;
		}
		$promise = $this->newPromise();
		$this->resolvePromise($promise, $value);
		return $promise;
	}

	public function resolvePromise(JsPromise $promise, mixed $value) : void{
		if($promise->resolving){
			return;
		}
		$promise->resolving = true;
		$this->resolveInner($promise, $value);
	}

	public function rejectPromise(JsPromise $promise, mixed $reason) : void{
		if($promise->resolving){
			return;
		}
		$promise->resolving = true;
		$this->settle($promise, JsPromise::REJECTED, $reason);
	}

	private function resolveInner(JsPromise $promise, mixed $value) : void{
		if($value === $promise){
			$this->settle($promise, JsPromise::REJECTED, $this->makeError("TypeError", "Chaining cycle detected for promise"));
			return;
		}
		if($value instanceof JsPromise){
			$this->onSettled($value, function(int $state, mixed $settled) use ($promise) : void{
				$this->settle($promise, $state, $settled);
			});
			return;
		}
		if($value instanceof JsObject){
			try{
				$then = $this->get($value, "then");
			}catch(JsThrow $e){
				$this->settle($promise, JsPromise::REJECTED, $e->value);
				return;
			}
			if($then instanceof JsCallable){
				$this->enqueueJob(function() use ($promise, $value, $then) : void{
					$called = false;
					$resolve = $this->native("", 1, function(mixed $thisValue, array $args) use ($promise, &$called) : mixed{
						if(!$called){
							$called = true;
							$this->resolveInner($promise, $args[0] ?? null);
						}
						return null;
					});
					$reject = $this->native("", 1, function(mixed $thisValue, array $args) use ($promise, &$called) : mixed{
						if(!$called){
							$called = true;
							$this->settle($promise, JsPromise::REJECTED, $args[0] ?? null);
						}
						return null;
					});
					try{
						$this->call($then, $value, [$resolve, $reject]);
					}catch(JsThrow $e){
						if(!$called){
							$called = true;
							$this->settle($promise, JsPromise::REJECTED, $e->value);
						}
					}
				});
				return;
			}
		}
		$this->settle($promise, JsPromise::FULFILLED, $value);
	}

	private function settle(JsPromise $promise, int $state, mixed $value) : void{
		if($promise->state !== JsPromise::PENDING){
			return;
		}
		$promise->state = $state;
		$promise->value = $value;
		$reactions = $promise->reactions;
		$promise->reactions = [];
		foreach($reactions as $reaction){
			$this->enqueueJob(function() use ($reaction, $state, $value) : void{
				$reaction($state, $value);
			});
		}
		if($state === JsPromise::REJECTED && !$promise->handled){
			$this->unhandled[spl_object_id($promise)] = $promise;
		}
	}

	/**
	 * Calls $callback with the state and value of a promise once it is
	 * settled, as a microtask.
	 *
	 * @param Closure(int, mixed) : void $callback
	 */
	public function onSettled(JsPromise $promise, Closure $callback) : void{
		$promise->handled = true;
		unset($this->unhandled[spl_object_id($promise)]);
		if($promise->state === JsPromise::PENDING){
			$promise->reactions[] = $callback;
			return;
		}
		$state = $promise->state;
		$value = $promise->value;
		$this->enqueueJob(function() use ($callback, $state, $value) : void{
			$callback($state, $value);
		});
	}

	public function promiseThen(JsPromise $promise, mixed $onFulfilled, mixed $onRejected) : JsPromise{
		$derived = $this->newPromise();
		$this->onSettled($promise, function(int $state, mixed $value) use ($derived, $onFulfilled, $onRejected) : void{
			$handler = $state === JsPromise::FULFILLED ? $onFulfilled : $onRejected;
			if(!$handler instanceof JsCallable){
				if($state === JsPromise::FULFILLED){
					$this->resolvePromise($derived, $value);
				}else{
					$this->rejectPromise($derived, $value);
				}
				return;
			}
			try{
				$this->resolvePromise($derived, $this->call($handler, null, [$value]));
			}catch(JsThrow $e){
				$this->rejectPromise($derived, $e->value);
			}
		});
		return $derived;
	}

	public function enqueueJob(Closure $job) : void{
		$this->jobs->enqueue([$job, $this->context]);
	}

	public function hasJobs() : bool{
		return !$this->jobs->isEmpty();
	}

	/**
	 * Runs every pending microtask. Uncaught errors are reported.
	 */
	public function drainJobs() : void{
		$context = $this->context;
		while(!$this->jobs->isEmpty()){
			[$job, $jobContext] = $this->jobs->dequeue();
			$this->context = $jobContext;
			$this->tick();
			try{
				$job();
			}catch(JsThrow $e){
				$this->reportError($e->value);
			}
		}
		$this->context = $context;
	}

	/**
	 * Reports the promises rejected without a handler since the last call.
	 */
	public function reportUnhandledRejections() : void{
		$unhandled = $this->unhandled;
		$this->unhandled = [];
		foreach($unhandled as $promise){
			if(!$promise->handled){
				$this->print("error", "Uncaught (in promise) " . $this->describeError($promise->value));
			}
		}
	}

	/**
	 * Removes the queued microtasks of a context, used when a pack is
	 * disabled.
	 */
	public function removeJobs(?string $context) : void{
		$kept = new SplQueue();
		while(!$this->jobs->isEmpty()){
			$job = $this->jobs->dequeue();
			if($job[1] !== $context){
				$kept->enqueue($job);
			}
		}
		$this->jobs = $kept;
	}

	/**
	 * Iterates a value with the iteration protocol. Breaking out of the loop
	 * closes the iterator.
	 */
	public function iterate(mixed $iterable) : Generator{
		if($iterable instanceof JsArray && $iterable->className === "Array" && !isset($iterable->symbols[spl_object_id($this->symIterator)]) && $this->arrayIteratorIsNative()){
			for($i = 0; $i < count($iterable->items); ++$i){
				yield $iterable->items[$i];
			}
			return;
		}
		if(is_string($iterable)){
			foreach(mb_str_split($iterable, 1, "UTF-8") as $char){
				yield $char;
			}
			return;
		}
		$method = $iterable instanceof JsObject ? $this->getSymbol($iterable, $this->symIterator) : null;
		if(!$method instanceof JsCallable){
			$this->throwError("TypeError", $this->describeValue($iterable) . " is not iterable");
		}
		$iterator = $this->call($method, $iterable, []);
		if(!$iterator instanceof JsObject){
			$this->throwError("TypeError", "Result of the Symbol.iterator method is not an object");
		}
		$next = $this->get($iterator, "next");
		$done = false;
		try{
			while(true){
				$result = $this->call($next, $iterator, []);
				if(!$result instanceof JsObject){
					$this->throwError("TypeError", "Iterator result is not an object");
				}
				if($this->toBoolean($this->get($result, "done"))){
					$done = true;
					return;
				}
				yield $this->get($result, "value");
			}
		}finally{
			if(!$done){
				$return = $this->get($iterator, "return");
				if($return instanceof JsCallable){
					$this->call($return, $iterator, []);
				}
			}
		}
	}

	private ?NativeFunction $nativeArrayIterator = null;

	public function setNativeArrayIterator(NativeFunction $iterator) : void{
		$this->nativeArrayIterator = $iterator;
	}

	private function arrayIteratorIsNative() : bool{
		$current = $this->arrayPrototype->symbols[spl_object_id($this->symIterator)][1] ?? null;
		return $current === $this->nativeArrayIterator;
	}

	/**
	 * @return list<mixed>
	 */
	public function iterableToList(mixed $iterable) : array{
		if($iterable instanceof JsArray && $iterable->className === "Array" && $this->arrayIteratorIsNative()){
			return $iterable->items;
		}
		$list = [];
		foreach($this->iterate($iterable) as $value){
			$list[] = $value;
		}
		return $list;
	}

	public function lookup(string $name, Scope $scope) : mixed{
		for($current = $scope; $current !== null; $current = $current->parent){
			if(array_key_exists($name, $current->vars)){
				$value = $current->vars[$name];
				if($value instanceof Tdz){
					$this->throwError("ReferenceError", "Cannot access '" . $name . "' before initialization");
				}
				return $value;
			}
			if(isset($current->imports[$name])){
				return $this->readExport($current->imports[$name][0], $current->imports[$name][1]);
			}
		}
		if(array_key_exists($name, $this->global->props)){
			return $this->global->props[$name];
		}
		if(isset($this->global->accessors[$name])){
			return $this->getFrom($this->global, $name, $this->global);
		}
		$this->throwError("ReferenceError", $name . " is not defined");
	}

	private function hasBinding(string $name, Scope $scope) : bool{
		for($current = $scope; $current !== null; $current = $current->parent){
			if(array_key_exists($name, $current->vars) || isset($current->imports[$name])){
				return true;
			}
		}
		return array_key_exists($name, $this->global->props) || isset($this->global->accessors[$name]);
	}

	public function assignVariable(string $name, mixed $value, Scope $scope) : void{
		for($current = $scope; $current !== null; $current = $current->parent){
			if(array_key_exists($name, $current->vars)){
				if(isset($current->consts[$name])){
					$this->throwError("TypeError", "Assignment to constant variable.");
				}
				if($current->vars[$name] instanceof Tdz){
					$this->throwError("ReferenceError", "Cannot access '" . $name . "' before initialization");
				}
				$current->vars[$name] = $value;
				return;
			}
			if(isset($current->imports[$name])){
				$this->throwError("TypeError", "Assignment to constant variable.");
			}
		}
		if(array_key_exists($name, $this->global->props) || isset($this->global->accessors[$name])){
			$this->setOn($this->global, $name, $value, $this->global);
			return;
		}
		$this->throwError("ReferenceError", $name . " is not defined");
	}

	public function readExport(Module $module, string $name) : mixed{
		$binding = $module->resolve($name);
		if($binding === null){
			if($module->missing !== null){
				return ($module->missing)($name);
			}
			$this->throwError("SyntaxError", "The requested module '" . $module->path . "' does not provide an export named '" . $name . "'");
		}
		switch($binding[0]){
			case "local":
				$value = $binding[1]->vars[$binding[2]] ?? null;
				if($value instanceof Tdz){
					$this->throwError("ReferenceError", "Cannot access '" . $name . "' before initialization");
				}
				return $value;
			case "value":
				return $binding[1];
			case "namespace":
				return $this->moduleNamespace($binding[1]);
		}
		return null;
	}

	public function moduleNamespace(Module $module) : JsObject{
		if($module->namespace !== null){
			return $module->namespace;
		}
		$namespace = new JsObject(null);
		$namespace->className = "Module";
		foreach($module->exportNames() as $name){
			$getter = $this->native($name, 0, function(mixed $thisValue, array $args) use ($module, $name) : mixed{
				return $this->readExport($module, $name);
			});
			$namespace->accessors[$name] = [$getter, null];
		}
		$namespace->extensible = false;
		if($module->missing !== null){
			$missing = $module->missing;
			$namespace->miss = function(JsObject $receiver, string $key) use ($missing) : mixed{
				return $missing($key);
			};
		}
		$module->namespace = $namespace;
		return $namespace;
	}

	/**
	 * Declares the lexical bindings and function declarations of a block.
	 *
	 * @param array<string, mixed> $node
	 */
	public function hoistLexical(array $node, Scope $scope) : void{
		foreach($node["lex"] as [$name, $kind]){
			$scope->vars[$name] = Tdz::get();
			if($kind === "const"){
				$scope->consts[$name] = true;
			}
		}
		foreach($node["funcs"] as $declaration){
			$fn = $declaration["fn"];
			$scope->vars[$fn["id"]] = $this->createFunction($fn, $scope, $fn["id"]);
		}
	}

	/**
	 * @param array<string, mixed> $node
	 */
	public function createFunction(array $node, Scope $scope, ?string $name = null, ?JsObject $home = null) : JsFunction{
		$function = new JsFunction($this->functionPrototype, $node, $scope);
		$function->name = $name ?? ($node["id"] ?? "");
		$function->home = $home;
		$function->file = $this->file;
		$length = 0;
		foreach($node["params"] as $param){
			if($param["type"] === "AssignmentPattern" || $param["type"] === "RestElement"){
				break;
			}
			$length++;
		}
		$function->length = $length;
		$function->needsPrototype = !$node["arrow"] && !$node["method"] && (!$node["async"] || $node["generator"]);
		return $function;
	}

	/**
	 * @param array<string, mixed> $node
	 * @param list<string>         $labels
	 */
	public function execute(array $node, Scope $scope, array $labels = []) : ?Completion{
		$this->line = $node["line"] ?? $this->line;
		switch($node["type"]){
			case "ExpressionStatement":
				$this->evaluate($node["expression"], $scope);
				return null;
			case "VariableDeclaration":
				$this->declareVariables($node, $scope);
				return null;
			case "ReturnStatement":
				return new Completion(Completion::RETURN, $node["argument"] === null ? null : $this->evaluate($node["argument"], $scope));
			case "IfStatement":
				if($this->toBoolean($this->evaluate($node["test"], $scope))){
					return $this->execute($node["consequent"], $scope);
				}
				return $node["alternate"] === null ? null : $this->execute($node["alternate"], $scope);
			case "BlockStatement":
				return $this->executeBlock($node, $scope);
			case "ForStatement":
				return $this->executeFor($node, $scope, $labels);
			case "ForOfStatement":
				return $this->executeForOf($node, $scope, $labels);
			case "ForInStatement":
				return $this->executeForIn($node, $scope, $labels);
			case "WhileStatement":
				while($this->toBoolean($this->evaluate($node["test"], $scope))){
					$this->tick();
					$completion = $this->execute($node["body"], $scope);
					if($completion !== null){
						if($completion->type === Completion::BREAK && ($completion->label === null || in_array($completion->label, $labels, true))){
							break;
						}
						if($completion->type === Completion::CONTINUE && ($completion->label === null || in_array($completion->label, $labels, true))){
							continue;
						}
						return $completion;
					}
				}
				return null;
			case "DoWhileStatement":
				do{
					$this->tick();
					$completion = $this->execute($node["body"], $scope);
					if($completion !== null){
						if($completion->type === Completion::BREAK && ($completion->label === null || in_array($completion->label, $labels, true))){
							break;
						}
						if(!($completion->type === Completion::CONTINUE && ($completion->label === null || in_array($completion->label, $labels, true)))){
							return $completion;
						}
					}
				}while($this->toBoolean($this->evaluate($node["test"], $scope)));
				return null;
			case "BreakStatement":
				return new Completion(Completion::BREAK, null, $node["label"]);
			case "ContinueStatement":
				return new Completion(Completion::CONTINUE, null, $node["label"]);
			case "ThrowStatement":
				$value = $this->evaluate($node["argument"], $scope);
				throw new JsThrow($value, "Uncaught " . $this->describeValue($value));
			case "TryStatement":
				return $this->executeTry($node, $scope);
			case "SwitchStatement":
				return $this->executeSwitch($node, $scope, $labels);
			case "LabeledStatement":
				$labels[] = $node["label"];
				$completion = $this->execute($node["body"], $scope, $labels);
				if($completion !== null && $completion->type === Completion::BREAK && $completion->label === $node["label"]){
					return null;
				}
				return $completion;
			case "FunctionDeclaration":
			case "EmptyStatement":
			case "ImportDeclaration":
				return null;
			case "ClassDeclaration":
				$class = $this->evaluateClass($node["class"], $scope, null);
				$scope->vars[$node["class"]["id"]] = $class;
				return null;
			case "ExportNamedDeclaration":
				if($node["declaration"] !== null){
					return $this->execute($node["declaration"], $scope);
				}
				return null;
			case "ExportDefaultDeclaration":
				if($node["declaration"] !== null){
					return $this->execute($node["declaration"], $scope);
				}
				$scope->vars["*default*"] = $this->evaluateNamed($node["expression"], $scope, "default");
				return null;
			case "ExportAllDeclaration":
				return null;
		}
		$this->throwError("SyntaxError", "Unsupported statement " . $node["type"]);
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private function declareVariables(array $node, Scope $scope) : void{
		$kind = $node["kind"];
		foreach($node["declarations"] as $declarator){
			$id = $declarator["id"];
			if($declarator["init"] === null){
				if($kind === "var"){
					continue;
				}
				$this->bindPattern($id, null, $scope, $kind);
				continue;
			}
			$name = $id["type"] === "Identifier" ? $id["name"] : "";
			$value = $this->evaluateNamed($declarator["init"], $scope, $name);
			$this->bindPattern($id, $value, $scope, $kind);
		}
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private function executeBlock(array $node, Scope $scope) : ?Completion{
		if(count($node["lex"]) > 0 || count($node["funcs"]) > 0){
			$scope = new Scope($scope);
			$this->hoistLexical($node, $scope);
		}
		foreach($node["body"] as $statement){
			$completion = $this->execute($statement, $scope);
			if($completion !== null){
				return $completion;
			}
		}
		return null;
	}

	private static function copyScope(Scope $scope) : Scope{
		$copy = new Scope($scope->parent);
		$copy->vars = $scope->vars;
		$copy->consts = $scope->consts;
		return $copy;
	}

	/**
	 * @param array<string, mixed> $node
	 * @param list<string>         $labels
	 */
	private function executeFor(array $node, Scope $scope, array $labels) : ?Completion{
		$init = $node["init"];
		$loopScope = $scope;
		$perIteration = false;
		if($init !== null){
			if($init["type"] === "VariableDeclaration"){
				if($init["kind"] !== "var"){
					$loopScope = new Scope($scope);
					foreach($init["declarations"] as $declarator){
						foreach(Parser::patternNames($declarator["id"]) as $name){
							$loopScope->vars[$name] = Tdz::get();
						}
					}
					$perIteration = true;
				}
				$this->declareVariables($init, $loopScope);
			}else{
				$this->evaluate($init, $scope);
			}
		}
		$iterationScope = $perIteration ? self::copyScope($loopScope) : $loopScope;
		while(true){
			$this->tick();
			if($node["test"] !== null && !$this->toBoolean($this->evaluate($node["test"], $iterationScope))){
				break;
			}
			$completion = $this->execute($node["body"], $iterationScope);
			if($completion !== null){
				if($completion->type === Completion::BREAK && ($completion->label === null || in_array($completion->label, $labels, true))){
					break;
				}
				if(!($completion->type === Completion::CONTINUE && ($completion->label === null || in_array($completion->label, $labels, true)))){
					return $completion;
				}
			}
			if($perIteration){
				$iterationScope = self::copyScope($iterationScope);
			}
			if($node["update"] !== null){
				$this->evaluate($node["update"], $iterationScope);
			}
		}
		return null;
	}

	/**
	 * @param array<string, mixed> $left
	 */
	private function bindForTarget(array $left, mixed $value, Scope $scope, Scope $iterationScope) : void{
		if($left["type"] === "VariableDeclaration"){
			$id = $left["declarations"][0]["id"];
			if($left["kind"] === "var"){
				$this->bindPattern($id, $value, $scope, "var");
			}else{
				$this->bindPattern($id, $value, $iterationScope, $left["kind"]);
			}
			return;
		}
		$this->bindPattern($left, $value, $scope, null);
	}

	/**
	 * @param array<string, mixed> $node
	 * @param list<string>         $labels
	 */
	private function executeForOf(array $node, Scope $scope, array $labels) : ?Completion{
		$iterable = $this->evaluate($node["right"], $scope);
		foreach($this->iterate($iterable) as $value){
			$this->tick();
			if($node["await"]){
				$value = $this->await($value);
			}
			$iterationScope = new Scope($scope);
			$this->bindForTarget($node["left"], $value, $scope, $iterationScope);
			$completion = $this->execute($node["body"], $iterationScope);
			if($completion !== null){
				if($completion->type === Completion::BREAK && ($completion->label === null || in_array($completion->label, $labels, true))){
					break;
				}
				if(!($completion->type === Completion::CONTINUE && ($completion->label === null || in_array($completion->label, $labels, true)))){
					return $completion;
				}
			}
		}
		return null;
	}

	/**
	 * @return list<string>
	 */
	public function forInKeys(mixed $value) : array{
		if($value === null || $value instanceof JsNull){
			return [];
		}
		if(is_string($value)){
			$keys = [];
			for($i = 0, $length = mb_strlen($value, "UTF-8"); $i < $length; ++$i){
				$keys[] = (string) $i;
			}
			return $keys;
		}
		if(!$value instanceof JsObject){
			return [];
		}
		$keys = [];
		$seen = [];
		for($current = $value; $current !== null; $current = $current->proto){
			foreach($this->ownKeys($current, true) as $key){
				if(isset($seen[$key])){
					continue;
				}
				$seen[$key] = true;
				if($this->isEnumerable($current, $key)){
					$keys[] = $key;
				}
			}
		}
		return $keys;
	}

	/**
	 * @param array<string, mixed> $node
	 * @param list<string>         $labels
	 */
	private function executeForIn(array $node, Scope $scope, array $labels) : ?Completion{
		$object = $this->evaluate($node["right"], $scope);
		foreach($this->forInKeys($object) as $key){
			$this->tick();
			if($object instanceof JsObject && !$this->hasProperty($object, $key)){
				continue;
			}
			$iterationScope = new Scope($scope);
			$this->bindForTarget($node["left"], $key, $scope, $iterationScope);
			$completion = $this->execute($node["body"], $iterationScope);
			if($completion !== null){
				if($completion->type === Completion::BREAK && ($completion->label === null || in_array($completion->label, $labels, true))){
					break;
				}
				if(!($completion->type === Completion::CONTINUE && ($completion->label === null || in_array($completion->label, $labels, true)))){
					return $completion;
				}
			}
		}
		return null;
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private function executeTry(array $node, Scope $scope) : ?Completion{
		$finalizer = $node["finalizer"];
		try{
			try{
				$completion = $this->executeBlock($node["block"], $scope);
			}catch(JsThrow $e){
				if($node["handler"] === null){
					throw $e;
				}
				$catchScope = new Scope($scope);
				if($node["param"] !== null){
					$this->bindPattern($node["param"], $e->value, $catchScope, "let");
				}
				$completion = $this->executeBlock($node["handler"], $catchScope);
			}
		}catch(JsThrow|GeneratorReturn $e){
			if($finalizer !== null){
				$override = $this->executeBlock($finalizer, $scope);
				if($override !== null){
					return $override;
				}
			}
			throw $e;
		}
		if($finalizer !== null){
			$override = $this->executeBlock($finalizer, $scope);
			if($override !== null){
				return $override;
			}
		}
		return $completion;
	}

	/**
	 * @param array<string, mixed> $node
	 * @param list<string>         $labels
	 */
	private function executeSwitch(array $node, Scope $scope, array $labels) : ?Completion{
		$discriminant = $this->evaluate($node["discriminant"], $scope);
		if(count($node["lex"]) > 0 || count($node["funcs"]) > 0){
			$scope = new Scope($scope);
			$this->hoistLexical($node, $scope);
		}
		$start = null;
		$default = null;
		foreach($node["cases"] as $index => $case){
			if($case["test"] === null){
				$default = $index;
				continue;
			}
			if($this->strictEquals($discriminant, $this->evaluate($case["test"], $scope))){
				$start = $index;
				break;
			}
		}
		$start ??= $default;
		if($start === null){
			return null;
		}
		$cases = $node["cases"];
		for($i = $start, $count = count($cases); $i < $count; ++$i){
			foreach($cases[$i]["consequent"] as $statement){
				$completion = $this->execute($statement, $scope);
				if($completion !== null){
					if($completion->type === Completion::BREAK && ($completion->label === null || in_array($completion->label, $labels, true))){
						return null;
					}
					return $completion;
				}
			}
		}
		return null;
	}

	/**
	 * Binds a pattern: $kind is "var", "let" or "const" for declarations and
	 * null for assignments.
	 *
	 * @param array<string, mixed> $pattern
	 */
	public function bindPattern(array $pattern, mixed $value, Scope $scope, ?string $kind) : void{
		switch($pattern["type"]){
			case "Identifier":
				$name = $pattern["name"];
				if($kind === null || $kind === "var"){
					$this->assignVariable($name, $value, $scope);
				}else{
					$scope->vars[$name] = $value;
					if($kind === "const"){
						$scope->consts[$name] = true;
					}
				}
				return;
			case "MemberExpression":
				$object = $this->evaluate($pattern["object"], $scope);
				$this->set($object, $this->memberKey($pattern, $scope), $value);
				return;
			case "AssignmentPattern":
				if($value === null){
					$left = $pattern["left"];
					$value = $this->evaluateNamed($pattern["right"], $scope, $left["type"] === "Identifier" ? $left["name"] : "");
				}
				$this->bindPattern($pattern["left"], $value, $scope, $kind);
				return;
			case "ArrayPattern":
				if($value === null || $value instanceof JsNull){
					$this->throwError("TypeError", $this->describeValue($value) . " is not iterable");
				}
				$iterator = $this->iterate($value);
				$first = true;
				foreach($pattern["elements"] as $element){
					if(!$first){
						$iterator->next();
					}
					$first = false;
					if($element !== null && $element["type"] === "RestElement"){
						$rest = [];
						while($iterator->valid()){
							$rest[] = $iterator->current();
							$iterator->next();
						}
						$this->bindPattern($element["argument"], $this->newArray($rest), $scope, $kind);
						return;
					}
					$item = $iterator->valid() ? $iterator->current() : null;
					if($element !== null){
						$this->bindPattern($element, $item, $scope, $kind);
					}
				}
				return;
			case "ObjectPattern":
				if($value === null || $value instanceof JsNull){
					$this->throwError("TypeError", "Cannot destructure '" . $this->describeValue($value) . "' as it is " . ($value === null ? "undefined" : "null") . ".");
				}
				$used = [];
				foreach($pattern["properties"] as $property){
					if($property["type"] === "RestElement"){
						$rest = $this->newObject();
						if($value instanceof JsObject){
							$this->copyDataProperties($rest, $value, $used);
						}
						$this->bindPattern($property["argument"], $rest, $scope, $kind);
						continue;
					}
					$key = $property["computed"] ? $this->toPropertyKey($this->evaluate($property["key"], $scope)) : $this->literalKey($property["key"]);
					if(is_string($key)){
						$used[$key] = true;
					}
					$this->bindPattern($property["value"], $this->get($value, $key), $scope, $kind);
				}
				return;
		}
		$this->throwError("SyntaxError", "Invalid destructuring target");
	}

	/**
	 * @param array<string, mixed> $key
	 */
	private function literalKey(array $key) : string{
		$value = $key["value"];
		if(is_string($value)){
			return $value;
		}
		return $this->toString($value);
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private static function isAnonymousFunction(array $node) : bool{
		return match($node["type"]){
			"ArrowFunctionExpression" => true,
			"FunctionExpression" => $node["fn"]["id"] === null,
			"ClassExpression" => $node["class"]["id"] === null,
			default => false
		};
	}

	/**
	 * Evaluates an expression, naming it when it is an anonymous function or
	 * class.
	 *
	 * @param array<string, mixed> $node
	 */
	public function evaluateNamed(array $node, Scope $scope, string $name) : mixed{
		if($name !== "" && self::isAnonymousFunction($node)){
			if($node["type"] === "ClassExpression"){
				return $this->evaluateClass($node["class"], $scope, $name);
			}
			return $this->createFunction($node["fn"], $scope, $name);
		}
		return $this->evaluate($node, $scope);
	}

	/**
	 * @param array<string, mixed> $member
	 */
	private function memberKey(array $member, Scope $scope) : string|JsSymbol{
		if(!$member["computed"]){
			return $member["property"]["value"];
		}
		return $this->toPropertyKey($this->evaluate($member["property"], $scope));
	}

	/**
	 * @param list<array<string, mixed>> $nodes
	 * @return list<mixed>
	 */
	private function evaluateArguments(array $nodes, Scope $scope) : array{
		$args = [];
		foreach($nodes as $node){
			if($node["type"] === "SpreadElement"){
				foreach($this->iterableToList($this->evaluate($node["argument"], $scope)) as $value){
					$args[] = $value;
				}
			}else{
				$args[] = $this->evaluate($node, $scope);
			}
		}
		return $args;
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private function describeNode(array $node) : string{
		return match($node["type"]){
			"Identifier" => $node["name"],
			"MemberExpression" => $this->describeNode($node["object"]) . ($node["computed"] ? "[...]" : "." . $node["property"]["value"]),
			"ThisExpression" => "this",
			"Super" => "super",
			"CallExpression" => $this->describeNode($node["callee"]) . "(...)",
			"ChainExpression" => $this->describeNode($node["expression"]),
			default => "expression"
		};
	}

	private function lookupThis(Scope $scope) : mixed{
		for($current = $scope; $current !== null; $current = $current->parent){
			if(array_key_exists("this", $current->vars)){
				$value = $current->vars["this"];
				if($value instanceof Tdz){
					$this->throwError("ReferenceError", "Must call super constructor in derived class before accessing 'this' or returning from derived constructor");
				}
				return $value;
			}
		}
		return null;
	}

	private function functionScope(Scope $scope) : ?Scope{
		for($current = $scope; $current !== null; $current = $current->parent){
			if(array_key_exists("%fn", $current->vars)){
				return $current;
			}
		}
		return null;
	}

	private function superGet(string|JsSymbol $key, Scope $scope) : mixed{
		$functionScope = $this->functionScope($scope);
		$function = $functionScope?->vars["%fn"] ?? null;
		if(!$function instanceof JsFunction || $function->home === null){
			$this->throwError("SyntaxError", "'super' keyword unexpected here");
		}
		$parent = $function->home->proto;
		if($parent === null){
			return null;
		}
		$thisValue = $this->lookupThis($scope);
		if($key instanceof JsSymbol){
			return $this->getSymbol($parent, $key);
		}
		return $this->getFrom($parent, $key, $thisValue);
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private function superCall(array $node, Scope $scope) : mixed{
		$functionScope = $this->functionScope($scope);
		$function = $functionScope?->vars["%fn"] ?? null;
		if(!$function instanceof JsFunction || !$function->derived){
			$this->throwError("SyntaxError", "'super' keyword unexpected here");
		}
		$parent = $function->proto;
		if(!$this->isConstructor($parent)){
			$this->throwError("TypeError", "Super constructor is not a constructor");
		}
		$args = $this->evaluateArguments($node["arguments"], $scope);
		$newTarget = $functionScope->vars["%nt"] ?? $function;
		$object = $this->construct($parent, $args, $newTarget instanceof JsObject ? $newTarget : $function);
		if(!($functionScope->vars["this"] ?? null) instanceof Tdz){
			$this->throwError("ReferenceError", "Super constructor may only be called once");
		}
		$functionScope->vars["this"] = $object;
		$this->initializeFields($function, $object);
		return null;
	}

	/**
	 * @param array<string, mixed> $node
	 */
	public function evaluate(array $node, Scope $scope) : mixed{
		switch($node["type"]){
			case "Identifier":
				$name = $node["name"];
				for($current = $scope; $current !== null; $current = $current->parent){
					if(array_key_exists($name, $current->vars)){
						$value = $current->vars[$name];
						if($value instanceof Tdz){
							$this->throwError("ReferenceError", "Cannot access '" . $name . "' before initialization");
						}
						return $value;
					}
					if(isset($current->imports[$name])){
						return $this->readExport($current->imports[$name][0], $current->imports[$name][1]);
					}
				}
				return $this->lookup($name, $scope);
			case "Literal":
				return isset($node["null"]) ? JsNull::get() : $node["value"];
			case "MemberExpression":
				return $this->evaluateMember($node, $scope);
			case "CallExpression":
				return $this->evaluateCall($node, $scope);
			case "ThisExpression":
				return $this->lookupThis($scope);
			case "TemplateLiteral":
				$result = "";
				foreach($node["cooked"] as $index => $chunk){
					$result .= $chunk ?? "";
					if(isset($node["expressions"][$index])){
						$value = $this->evaluate($node["expressions"][$index], $scope);
						$result .= is_string($value) ? $value : $this->toString($value);
					}
				}
				return $result;
			case "BinaryExpression":
				return $this->binary($node["operator"], $this->evaluate($node["left"], $scope), $this->evaluate($node["right"], $scope));
			case "LogicalExpression":
				$left = $this->evaluate($node["left"], $scope);
				switch($node["operator"]){
					case "&&":
						return $this->toBoolean($left) ? $this->evaluate($node["right"], $scope) : $left;
					case "||":
						return $this->toBoolean($left) ? $left : $this->evaluate($node["right"], $scope);
					default:
						return ($left === null || $left instanceof JsNull) ? $this->evaluate($node["right"], $scope) : $left;
				}
			case "AssignmentExpression":
				return $this->evaluateAssignment($node, $scope);
			case "UnaryExpression":
				return $this->evaluateUnary($node, $scope);
			case "UpdateExpression":
				return $this->evaluateUpdate($node, $scope);
			case "ConditionalExpression":
				return $this->toBoolean($this->evaluate($node["test"], $scope)) ? $this->evaluate($node["consequent"], $scope) : $this->evaluate($node["alternate"], $scope);
			case "ArrayExpression":
				$items = [];
				foreach($node["elements"] as $element){
					if($element === null){
						$items[] = null;
					}elseif($element["type"] === "SpreadElement"){
						foreach($this->iterableToList($this->evaluate($element["argument"], $scope)) as $value){
							$items[] = $value;
						}
					}else{
						$items[] = $this->evaluate($element, $scope);
					}
				}
				return $this->newArray($items);
			case "ObjectExpression":
				return $this->evaluateObject($node, $scope);
			case "FunctionExpression":
				$fn = $node["fn"];
				if($fn["id"] !== null){
					$functionScope = new Scope($scope);
					$function = $this->createFunction($fn, $functionScope);
					$functionScope->vars[$fn["id"]] = $function;
					$functionScope->consts[$fn["id"]] = true;
					return $function;
				}
				return $this->createFunction($fn, $scope, "");
			case "ArrowFunctionExpression":
				return $this->createFunction($node["fn"], $scope, "");
			case "ClassExpression":
				return $this->evaluateClass($node["class"], $scope, null);
			case "NewExpression":
				$callee = $this->evaluate($node["callee"], $scope);
				$args = $this->evaluateArguments($node["arguments"], $scope);
				$this->line = $node["line"];
				if(!$this->isConstructor($callee)){
					$this->throwError("TypeError", $this->describeNode($node["callee"]) . " is not a constructor");
				}
				return $this->construct($callee, $args);
			case "SequenceExpression":
				$value = null;
				foreach($node["expressions"] as $expression){
					$value = $this->evaluate($expression, $scope);
				}
				return $value;
			case "ChainExpression":
				$value = $this->evaluate($node["expression"], $scope);
				return $value === self::$short ? null : $value;
			case "AwaitExpression":
				return $this->await($this->evaluate($node["argument"], $scope));
			case "YieldExpression":
				if($node["delegate"]){
					return $this->yieldDelegate($this->evaluate($node["argument"], $scope));
				}
				return $this->yieldValue($node["argument"] === null ? null : $this->evaluate($node["argument"], $scope));
			case "TaggedTemplateExpression":
				return $this->evaluateTaggedTemplate($node, $scope);
			case "RegExpLiteral":
				return $this->strings->createRegExp($node["pattern"], $node["flags"]);
			case "NewTarget":
				$functionScope = $this->functionScope($scope);
				return $functionScope?->vars["%nt"] ?? null;
			case "ImportExpression":
				$specifier = $this->toString($this->evaluate($node["source"], $scope));
				if($this->dynamicImport === null){
					$this->throwError("Error", "Dynamic import is not available");
				}
				return ($this->dynamicImport)($this->file, $specifier);
			case "ImportMeta":
				$meta = $this->newObject();
				$meta->props["url"] = "file://" . $this->file;
				return $meta;
			case "PrivateIn":
				$object = $this->evaluate($node["right"], $scope);
				if(!$object instanceof JsObject){
					$this->throwError("TypeError", "Cannot use 'in' operator to search for a private field in a non-object");
				}
				return $this->hasOwnProperty($object, $node["name"]);
			case "Super":
				$this->throwError("SyntaxError", "'super' keyword unexpected here");
		}
		$this->throwError("SyntaxError", "Unsupported expression " . $node["type"]);
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private function evaluateMember(array $node, Scope $scope) : mixed{
		if($node["object"]["type"] === "Super"){
			return $this->superGet($this->memberKey($node, $scope), $scope);
		}
		$object = $this->evaluate($node["object"], $scope);
		if($object === self::$short){
			return $object;
		}
		if($node["optional"] && ($object === null || $object instanceof JsNull)){
			return self::$short;
		}
		if($node["computed"]){
			$keyValue = $this->evaluate($node["property"], $scope);
			if($object instanceof JsArray && is_int($keyValue) && $keyValue >= 0){
				if($keyValue < count($object->items)){
					return $object->items[$keyValue];
				}
				return $this->getFrom($object, $keyValue, $object);
			}
			$key = $this->toPropertyKey($keyValue);
		}else{
			$key = $node["property"]["value"];
		}
		if($object instanceof JsObject && is_string($key)){
			return $this->getFrom($object, $key, $object);
		}
		if($object === null || $object instanceof JsNull){
			$this->line = $node["line"] ?? $this->line;
			$this->throwError("TypeError", "Cannot read properties of " . ($object === null ? "undefined" : "null") . " (reading '" . ($key instanceof JsSymbol ? "Symbol(" . $key->description . ")" : $key) . "')");
		}
		return $this->get($object, $key);
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private function evaluateCall(array $node, Scope $scope) : mixed{
		$callee = $node["callee"];
		if($callee["type"] === "Super"){
			return $this->superCall($node, $scope);
		}
		if($callee["type"] === "MemberExpression"){
			if($callee["object"]["type"] === "Super"){
				$thisValue = $this->lookupThis($scope);
				$function = $this->superGet($this->memberKey($callee, $scope), $scope);
			}else{
				$thisValue = $this->evaluate($callee["object"], $scope);
				if($thisValue === self::$short){
					return $thisValue;
				}
				if($callee["optional"] && ($thisValue === null || $thisValue instanceof JsNull)){
					return self::$short;
				}
				$key = $this->memberKey($callee, $scope);
				if($thisValue === null || $thisValue instanceof JsNull){
					$this->line = $node["line"];
					$this->throwError("TypeError", "Cannot read properties of " . ($thisValue === null ? "undefined" : "null") . " (reading '" . ($key instanceof JsSymbol ? "Symbol(" . $key->description . ")" : $key) . "')");
				}
				$function = $this->get($thisValue, $key);
			}
		}else{
			$thisValue = null;
			$function = $this->evaluate($callee, $scope);
			if($function === self::$short){
				return $function;
			}
		}
		if($node["optional"] && ($function === null || $function instanceof JsNull)){
			return self::$short;
		}
		$args = $this->evaluateArguments($node["arguments"], $scope);
		$this->line = $node["line"];
		if(!$function instanceof JsCallable){
			$this->throwError("TypeError", $this->describeNode($callee) . " is not a function");
		}
		return $this->call($function, $thisValue, $args);
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private function evaluateObject(array $node, Scope $scope) : JsObject{
		$object = new JsObject($this->objectPrototype);
		foreach($node["properties"] as $property){
			if($property["kind"] === "spread"){
				$this->copyDataProperties($object, $this->evaluate($property["value"], $scope));
				continue;
			}
			$key = $property["computed"] ? $this->toPropertyKey($this->evaluate($property["key"], $scope)) : $this->literalKey($property["key"]);
			$name = $key instanceof JsSymbol ? "[" . $key->description . "]" : $key;
			if($property["kind"] === "get" || $property["kind"] === "set"){
				$function = $this->createFunction($property["value"]["fn"], $scope, $property["kind"] . " " . $name, $object);
				if($key instanceof JsSymbol){
					$this->throwError("SyntaxError", "Symbol accessors are not supported");
				}
				unset($object->props[$key]);
				$pair = $object->accessors[$key] ?? [null, null];
				$pair[$property["kind"] === "get" ? 0 : 1] = $function;
				$object->accessors[$key] = $pair;
				continue;
			}
			if(!empty($property["method"])){
				$value = $this->createFunction($property["value"]["fn"], $scope, $name, $object);
			}else{
				$value = $this->evaluateNamed($property["value"], $scope, $name);
			}
			if($key === "__proto__" && !$property["computed"] && empty($property["shorthand"]) && empty($property["method"])){
				if($value instanceof JsObject){
					$object->proto = $value;
				}elseif($value instanceof JsNull){
					$object->proto = null;
				}
				continue;
			}
			if($key instanceof JsSymbol){
				$object->symbols[spl_object_id($key)] = [$key, $value];
			}else{
				unset($object->accessors[$key]);
				$object->props[$key] = $value;
			}
		}
		return $object;
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private function evaluateTaggedTemplate(array $node, Scope $scope) : mixed{
		$quasi = $node["quasi"];
		$strings = $this->newArray($quasi["cooked"]);
		$this->defineHidden($strings, "raw", $this->newArray($quasi["raw"]));
		$args = [$strings];
		foreach($quasi["expressions"] as $expression){
			$args[] = $this->evaluate($expression, $scope);
		}
		$tag = $node["tag"];
		$thisValue = null;
		if($tag["type"] === "MemberExpression" && $tag["object"]["type"] !== "Super"){
			$thisValue = $this->evaluate($tag["object"], $scope);
			$function = $this->get($thisValue, $this->memberKey($tag, $scope));
		}else{
			$function = $this->evaluate($tag, $scope);
		}
		return $this->call($function, $thisValue, $args);
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private function evaluateAssignment(array $node, Scope $scope) : mixed{
		$operator = $node["operator"];
		$left = $node["left"];
		if($operator === "="){
			if($left["type"] === "Identifier"){
				$value = $this->evaluateNamed($node["right"], $scope, $left["name"]);
				$this->assignVariable($left["name"], $value, $scope);
				return $value;
			}
			if($left["type"] === "MemberExpression"){
				if($left["object"]["type"] === "Super"){
					$key = $this->memberKey($left, $scope);
					$value = $this->evaluate($node["right"], $scope);
					$this->set($this->lookupThis($scope), $key, $value);
					return $value;
				}
				$object = $this->evaluate($left["object"], $scope);
				if($left["computed"]){
					$keyValue = $this->evaluate($left["property"], $scope);
					$value = $this->evaluate($node["right"], $scope);
					if($object instanceof JsArray && is_int($keyValue) && $keyValue >= 0 && !$object->frozen && $keyValue < count($object->items)){
						$object->items[$keyValue] = $value;
						return $value;
					}
					$this->set($object, $this->toPropertyKey($keyValue), $value);
					return $value;
				}
				$value = $this->evaluate($node["right"], $scope);
				$this->line = $node["line"];
				$this->set($object, $left["property"]["value"], $value);
				return $value;
			}
			$value = $this->evaluate($node["right"], $scope);
			$this->bindPattern($left, $value, $scope, null);
			return $value;
		}
		if($left["type"] === "Identifier"){
			$current = $this->lookup($left["name"], $scope);
			$value = $this->compoundValue($operator, $current, $node["right"], $scope, $left["name"]);
			if($value === self::$short){
				return $current;
			}
			$this->assignVariable($left["name"], $value, $scope);
			return $value;
		}
		if($left["object"]["type"] === "Super"){
			$this->throwError("SyntaxError", "Compound assignment to super properties is not supported");
		}
		$object = $this->evaluate($left["object"], $scope);
		$key = $this->memberKey($left, $scope);
		$current = $this->get($object, $key);
		$value = $this->compoundValue($operator, $current, $node["right"], $scope, "");
		if($value === self::$short){
			return $current;
		}
		$this->set($object, $key, $value);
		return $value;
	}

	/**
	 * Computes the value of a compound assignment, or returns the short
	 * marker when a logical assignment does not assign.
	 *
	 * @param array<string, mixed> $right
	 */
	private function compoundValue(string $operator, mixed $current, array $right, Scope $scope, string $name) : mixed{
		switch($operator){
			case "&&=":
				return $this->toBoolean($current) ? $this->evaluateNamed($right, $scope, $name) : self::$short;
			case "||=":
				return $this->toBoolean($current) ? self::$short : $this->evaluateNamed($right, $scope, $name);
			case "??=":
				return ($current === null || $current instanceof JsNull) ? $this->evaluateNamed($right, $scope, $name) : self::$short;
		}
		return $this->binary(substr($operator, 0, -1), $current, $this->evaluate($right, $scope));
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private function evaluateUnary(array $node, Scope $scope) : mixed{
		$operator = $node["operator"];
		$argument = $node["argument"];
		switch($operator){
			case "typeof":
				if($argument["type"] === "Identifier" && !$this->hasBinding($argument["name"], $scope)){
					return "undefined";
				}
				return $this->typeOf($this->evaluate($argument, $scope));
			case "delete":
				if($argument["type"] === "MemberExpression"){
					$object = $this->evaluate($argument["object"], $scope);
					$key = $this->memberKey($argument, $scope);
					if($object instanceof JsObject){
						return $this->deleteProperty($object, $key);
					}
					return true;
				}
				if($argument["type"] === "ChainExpression"){
					return true;
				}
				$this->evaluate($argument, $scope);
				return true;
			case "void":
				$this->evaluate($argument, $scope);
				return null;
			case "!":
				return !$this->toBoolean($this->evaluate($argument, $scope));
			case "-":
				$value = $this->evaluate($argument, $scope);
				if(is_int($value) && $value !== 0){
					return -$value;
				}
				if($value === 0){
					return -0.0;
				}
				return -$this->toNumber($value);
			case "+":
				return $this->toNumber($this->evaluate($argument, $scope));
			case "~":
				return self::wrapInt32(~$this->toInt32($this->evaluate($argument, $scope)));
		}
		$this->throwError("SyntaxError", "Unknown unary operator " . $operator);
	}

	/**
	 * @param array<string, mixed> $node
	 */
	private function evaluateUpdate(array $node, Scope $scope) : mixed{
		$argument = $node["argument"];
		$delta = $node["operator"] === "++" ? 1 : -1;
		if($argument["type"] === "Identifier"){
			$old = $this->toNumber($this->lookup($argument["name"], $scope));
			$new = $old + $delta;
			$this->assignVariable($argument["name"], $new, $scope);
			return $node["prefix"] ? $new : $old;
		}
		$object = $this->evaluate($argument["object"], $scope);
		$key = $this->memberKey($argument, $scope);
		$old = $this->toNumber($this->get($object, $key));
		$new = $old + $delta;
		$this->set($object, $key, $new);
		return $node["prefix"] ? $new : $old;
	}

	public function binary(string $operator, mixed $a, mixed $b) : mixed{
		switch($operator){
			case "+":
				if(is_int($a) && is_int($b)){
					return $a + $b;
				}
				if((is_int($a) || is_float($a)) && (is_int($b) || is_float($b))){
					return $a + $b;
				}
				if(is_string($a) && is_string($b)){
					return $a . $b;
				}
				$a = $this->toPrimitive($a);
				$b = $this->toPrimitive($b);
				if(is_string($a) || is_string($b)){
					return $this->toString($a) . $this->toString($b);
				}
				return $this->toNumber($a) + $this->toNumber($b);
			case "-":
				return $this->toNumber($a) - $this->toNumber($b);
			case "*":
				return $this->toNumber($a) * $this->toNumber($b);
			case "/":
				$x = $this->toNumber($a);
				$y = $this->toNumber($b);
				if(is_int($x) && is_int($y) && $y !== 0){
					return $x % $y === 0 ? intdiv($x, $y) : $x / $y;
				}
				return fdiv((float) $x, (float) $y);
			case "%":
				$x = $this->toNumber($a);
				$y = $this->toNumber($b);
				if(is_int($x) && is_int($y) && $y !== 0 && !($x === PHP_INT_MIN && $y === -1)){
					return $x % $y;
				}
				if(is_float($y) && is_infinite($y) && (is_int($x) || is_finite($x))){
					return $x;
				}
				return fmod((float) $x, (float) $y);
			case "**":
				$x = $this->toNumber($a);
				$y = $this->toNumber($b);
				if(is_float($y) && is_nan($y)){
					return NAN;
				}
				if(is_int($x) && is_int($y) && $x === 0 && $y < 0){
					return INF;
				}
				return pow($x, $y);
			case "==":
				return $this->looseEquals($a, $b);
			case "!=":
				return !$this->looseEquals($a, $b);
			case "===":
				return $this->strictEquals($a, $b);
			case "!==":
				return !$this->strictEquals($a, $b);
			case "<":
				return $this->compare($a, $b, false) === 1;
			case ">":
				return $this->compare($b, $a, true) === 1;
			case "<=":
				return $this->compare($b, $a, true) === 0;
			case ">=":
				return $this->compare($a, $b, false) === 0;
			case "&":
				return $this->toInt32($a) & $this->toInt32($b);
			case "|":
				return $this->toInt32($a) | $this->toInt32($b);
			case "^":
				return $this->toInt32($a) ^ $this->toInt32($b);
			case "<<":
				return self::wrapInt32($this->toInt32($a) << ($this->toUint32($b) & 31));
			case ">>":
				return $this->toInt32($a) >> ($this->toUint32($b) & 31);
			case ">>>":
				return $this->toUint32($a) >> ($this->toUint32($b) & 31);
			case "instanceof":
				return $this->instanceOf($a, $b);
			case "in":
				if(!$b instanceof JsObject){
					$this->throwError("TypeError", "Cannot use 'in' operator to search for '" . $this->toStringSafe($a) . "' in " . $this->describeValue($b));
				}
				return $this->hasProperty($b, $this->toPropertyKey($a));
		}
		$this->throwError("SyntaxError", "Unknown operator " . $operator);
	}

	/**
	 * Abstract relational comparison: returns 1 when $a < $b, 0 when not,
	 * and -1 when undefined (NaN). $leftFirst tells which operand is
	 * converted first.
	 */
	private function compare(mixed $a, mixed $b, bool $leftFirst) : int{
		if(is_int($a) && is_int($b)){
			return $a < $b ? 1 : 0;
		}
		if($leftFirst){
			$y = $this->toPrimitive($b, "number");
			$x = $this->toPrimitive($a, "number");
		}else{
			$x = $this->toPrimitive($a, "number");
			$y = $this->toPrimitive($b, "number");
		}
		if(is_string($x) && is_string($y)){
			return strcmp($x, $y) < 0 ? 1 : 0;
		}
		$x = $this->toNumber($x);
		$y = $this->toNumber($y);
		if((is_float($x) && is_nan($x)) || (is_float($y) && is_nan($y))){
			return -1;
		}
		return $x < $y ? 1 : 0;
	}

	public function instanceOf(mixed $value, mixed $constructor) : bool{
		if(!$constructor instanceof JsObject){
			$this->throwError("TypeError", "Right-hand side of 'instanceof' is not callable");
		}
		$hasInstance = $this->getSymbol($constructor, $this->symHasInstance);
		if($hasInstance instanceof JsCallable){
			return $this->toBoolean($this->call($hasInstance, $constructor, [$value]));
		}
		if(!$constructor instanceof JsCallable){
			$this->throwError("TypeError", "Right-hand side of 'instanceof' is not callable");
		}
		if(!$value instanceof JsObject){
			return false;
		}
		$prototype = $this->get($constructor, "prototype");
		if(!$prototype instanceof JsObject){
			return false;
		}
		for($current = $value->proto; $current !== null; $current = $current->proto){
			if($current === $prototype){
				return true;
			}
		}
		return false;
	}

	/**
	 * @param array<string, mixed> $class
	 */
	public function evaluateClass(array $class, Scope $scope, ?string $name) : JsFunction{
		$name = $class["id"] ?? $name ?? "";
		$parent = null;
		$protoParent = $this->objectPrototype;
		$constructorParent = $this->functionPrototype;
		if($class["superClass"] !== null){
			$parent = $this->evaluate($class["superClass"], $scope);
			if($parent instanceof JsNull){
				$protoParent = null;
			}elseif(!$this->isConstructor($parent)){
				$this->throwError("TypeError", "Class extends value " . $this->describeValue($parent) . " is not a constructor or null");
			}else{
				$parentPrototype = $this->get($parent, "prototype");
				if(!$parentPrototype instanceof JsObject && !$parentPrototype instanceof JsNull){
					$this->throwError("TypeError", "Class extends value does not have valid prototype property");
				}
				$protoParent = $parentPrototype instanceof JsObject ? $parentPrototype : null;
				$constructorParent = $parent;
			}
		}
		$classScope = new Scope($scope);
		if($class["id"] !== null){
			$classScope->vars[$class["id"]] = Tdz::get();
			$classScope->consts[$class["id"]] = true;
		}
		$prototype = new JsObject($protoParent);
		$constructorNode = null;
		foreach($class["members"] as $member){
			if($member["kind"] === "constructor"){
				$constructorNode = $member["value"];
			}
		}
		$constructor = new JsFunction($constructorParent, $constructorNode, $classScope);
		$constructor->isClass = true;
		$constructor->derived = $parent instanceof JsCallable;
		$constructor->home = $prototype;
		$constructor->name = $name;
		$constructor->file = $this->file;
		$constructor->length = $constructorNode === null ? 0 : count($constructorNode["params"]);
		$this->defineHidden($constructor, "prototype", $prototype);
		$constructor->locked["prototype"] = true;
		$this->defineHidden($prototype, "constructor", $constructor);

		$staticInitializers = [];
		foreach($class["members"] as $member){
			$kind = $member["kind"];
			if($kind === "constructor"){
				continue;
			}
			$target = $member["static"] ? $constructor : $prototype;
			if($kind === "block"){
				$staticInitializers[] = ["block", $member["value"]];
				continue;
			}
			$key = $member["computed"] ? $this->toPropertyKey($this->evaluate($member["key"], $classScope)) : $this->literalKey($member["key"]);
			$private = !empty($member["key"]["private"]);
			if($kind === "field"){
				if($member["static"]){
					$staticInitializers[] = ["field", $key, $member["value"], $private];
				}else{
					$constructor->fields[] = [$key, $member["value"], $private];
				}
				continue;
			}
			$label = $key instanceof JsSymbol ? "[" . $key->description . "]" : $key;
			if($kind === "get" || $kind === "set"){
				if($key instanceof JsSymbol){
					$this->throwError("SyntaxError", "Symbol accessors are not supported");
				}
				$function = $this->createFunction($member["value"], $classScope, $kind . " " . $label, $target);
				$pair = $target->accessors[$key] ?? [null, null];
				$pair[$kind === "get" ? 0 : 1] = $function;
				unset($target->props[$key]);
				$target->accessors[$key] = $pair;
				$target->hidden[$key] = true;
				continue;
			}
			$function = $this->createFunction($member["value"], $classScope, $label, $target);
			$this->defineHidden($target, $key, $function);
		}
		if($class["id"] !== null){
			$classScope->vars[$class["id"]] = $constructor;
		}
		foreach($staticInitializers as $initializer){
			$initScope = new Scope($classScope, true);
			$initScope->vars["this"] = $constructor;
			$initScope->vars["%fn"] = $this->staticHome($constructor, $classScope);
			$initScope->vars["%nt"] = null;
			if($initializer[0] === "block"){
				$block = $initializer[1];
				foreach($block["vars"] as $varName){
					$initScope->vars[$varName] = null;
				}
				$this->hoistLexical($block["body"], $initScope);
				foreach($block["body"]["body"] as $statement){
					$this->execute($statement, $initScope);
				}
				continue;
			}
			[, $key, $valueNode, $private] = $initializer;
			$value = $valueNode === null ? null : $this->evaluateNamed($valueNode, $initScope, is_string($key) ? $key : "");
			if($key instanceof JsSymbol){
				$constructor->symbols[spl_object_id($key)] = [$key, $value];
			}elseif($private){
				$this->defineHidden($constructor, $key, $value);
			}else{
				$this->createDataProperty($constructor, $key, $value);
			}
		}
		return $constructor;
	}

	private function staticHome(JsFunction $constructor, Scope $scope) : JsFunction{
		$holder = new JsFunction($this->functionPrototype, null, $scope);
		$holder->home = $constructor;
		return $holder;
	}
}
<?php

declare(strict_types=1);

namespace behaviorpack\script\js;

/**
 * A JavaScript symbol, compared by identity.
 */
final class JsSymbol{

	public function __construct(
		public ?string $description = null
	){}
}
<?php

declare(strict_types=1);

namespace behaviorpack;

use RuntimeException;

final class BehaviorPackException extends RuntimeException{
}
<?php

declare(strict_types=1);

namespace behaviorpack;

use function is_bool;
use function is_int;
use function is_string;
use function max;

/**
 * The "behavior-packs" section of config.yml.
 */
final class BehaviorPackSettings{

	public function __construct(
		public readonly bool $enabled,
		public readonly string $folder,
		public readonly bool $itemsAndBlocks,
		public readonly bool $entities,
		public readonly bool $recipes,
		public readonly bool $lootTables,
		public readonly bool $scripts,
		public readonly int $scriptTimeoutMs
	){}

	/**
	 * @param array<mixed> $section
	 */
	public static function fromConfig(array $section) : self{
		$timeout = $section["script-timeout-ms"] ?? null;
		$folder = $section["folder"] ?? null;
		return new self(
			self::flag($section, "enabled"),
			is_string($folder) && $folder !== "" ? $folder : "behavior_packs",
			self::flag($section, "items-and-blocks"),
			self::flag($section, "entities"),
			self::flag($section, "recipes"),
			self::flag($section, "loot-tables"),
			self::flag($section, "scripts"),
			is_int($timeout) ? max(100, $timeout) : 2000
		);
	}

	/**
	 * @param array<mixed> $section
	 */
	private static function flag(array $section, string $key) : bool{
		$value = $section[$key] ?? null;
		return is_bool($value) ? $value : true;
	}
}
<?php

declare(strict_types=1);

namespace vanillaaltay;

use behaviorpack\BehaviorPackModule;
use behaviorpack\BehaviorPackSettings;
use dimension\DimensionModule;
use dimension\DimensionSettings;
use pocketmine\plugin\PluginBase;
use redstone\RedstoneModule;
use function is_array;

final class VanillaAltay extends PluginBase{

	private ?RedstoneModule $redstone = null;
	private ?DimensionModule $dimensions = null;
	private ?BehaviorPackModule $behaviorPacks = null;

	protected function onLoad() : void{
		$this->saveDefaultConfig();
		$config = $this->getConfig();

		$redstone = $config->get("redstone");
		if(!is_array($redstone) || ($redstone["enabled"] ?? true) === true){
			$this->redstone = new RedstoneModule($this);
			$this->redstone->load();
		}

		$dimensions = $config->get("dimensions");
		$dimensionSettings = DimensionSettings::fromConfig(is_array($dimensions) ? $dimensions : []);
		if($dimensionSettings->enabled){
			$this->dimensions = new DimensionModule($this, $dimensionSettings);
			$this->dimensions->load();
		}

		$section = $config->get("behavior-packs");
		$settings = BehaviorPackSettings::fromConfig(is_array($section) ? $section : []);
		if($settings->enabled){
			$this->behaviorPacks = new BehaviorPackModule($this, $settings);
		}
	}

	protected function onEnable() : void{
		$this->redstone?->enable();
		$this->dimensions?->enable();
		$this->behaviorPacks?->enable();
	}

	protected function onDisable() : void{
		$this->behaviorPacks?->disable();
	}
}
<?php

declare(strict_types=1);

namespace dimension;

use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\Server;
use pocketmine\world\World;

/**
 * Maps worlds to Bedrock dimensions. The overworld is the default world; the
 * nether and the end are the worlds named in config.yml. Any other world is
 * treated as an overworld.
 */
final class Dimensions{

	/**
	 * @param array<int, string> $worldNames dimension id => world folder name
	 */
	public function __construct(
		private Server $server,
		private array $worldNames
	){}

	public function getDimension(World $world) : int{
		$name = $world->getFolderName();
		foreach($this->worldNames as $dimension => $worldName){
			if($worldName === $name){
				return $dimension;
			}
		}
		return DimensionIds::OVERWORLD;
	}

	public function isEnabled(int $dimension) : bool{
		return $dimension === DimensionIds::OVERWORLD || isset($this->worldNames[$dimension]);
	}

	public function getWorldName(int $dimension) : ?string{
		return $this->worldNames[$dimension] ?? null;
	}

	/**
	 * Returns the loaded world of a dimension, the default world for the
	 * overworld, or null when the dimension is disabled or its world is not
	 * loaded.
	 */
	public function getWorld(int $dimension) : ?World{
		$manager = $this->server->getWorldManager();
		if($dimension === DimensionIds::OVERWORLD){
			return $manager->getDefaultWorld();
		}
		$name = $this->worldNames[$dimension] ?? null;
		return $name === null ? null : $manager->getWorldByName($name);
	}

	/**
	 * Lowest block Y the client of this dimension can see.
	 */
	public static function getMinY(int $dimension) : int{
		return $dimension === DimensionIds::OVERWORLD ? World::Y_MIN : 0;
	}

	/**
	 * Exclusive upper block Y the client of this dimension can see.
	 */
	public static function getMaxY(int $dimension) : int{
		return match($dimension){
			DimensionIds::NETHER => 128,
			DimensionIds::THE_END => 256,
			default => World::Y_MAX
		};
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator;

use pocketmine\world\format\Chunk;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;

/**
 * Writes column biomes into every subchunk of a chunk, over the whole world
 * height.
 */
final class BiomeFiller{

	private function __construct(){
	}

	/**
	 * @param int[] $biomes BiomeIds id per column, indexed x * 16 + z
	 */
	public static function fillColumns(Chunk $chunk, array $biomes) : void{
		$template = new PalettedBlockArray($biomes[0]);
		for($x = 0; $x < 16; $x++){
			for($z = 0; $z < 16; $z++){
				$id = $biomes[$x * 16 + $z];
				for($y = 0; $y < 16; $y++){
					$template->set($x, $y, $z, $id);
				}
			}
		}
		self::apply($chunk, $template);
	}

	public static function fill(Chunk $chunk, int $biomeId) : void{
		self::apply($chunk, new PalettedBlockArray($biomeId));
	}

	private static function apply(Chunk $chunk, PalettedBlockArray $template) : void{
		for($index = Chunk::MIN_SUBCHUNK_INDEX; $index <= Chunk::MAX_SUBCHUNK_INDEX; $index++){
			$old = $chunk->getSubChunk($index);
			$chunk->setSubChunk($index, new SubChunk($old->getEmptyBlockId(), $old->getBlockLayers(), clone $template));
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure;

use dimension\generator\ChunkHash;
use dimension\generator\math\Int64;
use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use dimension\generator\random\Xoroshiro128;
use dimension\generator\structure\endcity\EndCityLayout;
use dimension\generator\structure\placement\StructurePlacement;
use dimension\generator\structure\template\StateBlocks;
use pocketmine\world\ChunkManager;
use function abs;
use function array_key_exists;
use function array_key_first;
use function count;
use function min;
use const PHP_INT_MAX;

/**
 * End cities. The outer islands are cut into regions of 20 by 20 chunks,
 * each with one candidate chunk; a city is built there when the chunk lies
 * more than 64 chunks from the world centre and the ground under the city
 * centre is at Y 60 or higher.
 *
 * The city stands on the lowest end stone surface of the 5 by 5 columns at
 * the start chunk centre, so a city can only be planned while its start
 * chunk is part of the population area; every chunk it then crosses writes
 * its own part of the same plan.
 */
final class EndCityPopulator implements Populator{

	private const SPACING = 20;
	private const SEPARATION = 11;
	private const MIN_HEIGHT = 60;
	private const SEARCH_RADIUS_CHUNKS = 8;
	private const CACHE_SIZE = 4;

	/** @var array<string, StructureBuffer|null> */
	private static array $plans = [];

	private Xoroshiro128 $random;

	public function __construct(
		private int $seed,
		private string $resourceFolder
	){
		$this->random = new Xoroshiro128(0);
	}

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$minRegionX = StructurePlacement::floorDiv($chunkX - self::SEARCH_RADIUS_CHUNKS, self::SPACING);
		$maxRegionX = StructurePlacement::floorDiv($chunkX + self::SEARCH_RADIUS_CHUNKS, self::SPACING);
		$minRegionZ = StructurePlacement::floorDiv($chunkZ - self::SEARCH_RADIUS_CHUNKS, self::SPACING);
		$maxRegionZ = StructurePlacement::floorDiv($chunkZ + self::SEARCH_RADIUS_CHUNKS, self::SPACING);
		for($regionX = $minRegionX; $regionX <= $maxRegionX; ++$regionX){
			for($regionZ = $minRegionZ; $regionZ <= $maxRegionZ; ++$regionZ){
				$this->random->setSeed($seed ^ ChunkHash::hash($regionX, $regionZ));
				$startX = $regionX * self::SPACING + $this->random->nextBoundedInt(self::SPACING - self::SEPARATION);
				$startZ = $regionZ * self::SPACING + $this->random->nextBoundedInt(self::SPACING - self::SEPARATION);
				if(abs($startX - $chunkX) > self::SEARCH_RADIUS_CHUNKS || abs($startZ - $chunkZ) > self::SEARCH_RADIUS_CHUNKS){
					continue;
				}
				if($startX * $startX + $startZ * $startZ <= 4096){
					continue;
				}
				$rotation = $this->random->nextInt(4);
				$plan = $this->plan($world, $seed, $startX, $startZ, $rotation);
				if($plan !== null){
					$plan->emit($root, $chunkX, $chunkZ);
				}
			}
		}
	}

	private function plan(ChunkManager $world, int $seed, int $startX, int $startZ, int $rotation) : ?StructureBuffer{
		$key = $seed . ":" . $startX . ":" . $startZ;
		if(array_key_exists($key, self::$plans)){
			return self::$plans[$key];
		}
		$startY = $this->startHeight($world, $startX, $startZ);
		if($startY === null){
			return null;
		}
		if(count(self::$plans) >= self::CACHE_SIZE){
			unset(self::$plans[array_key_first(self::$plans)]);
		}
		return self::$plans[$key] = $this->buildPlan($seed, $startX, $startY, $startZ, $rotation);
	}

	private function buildPlan(int $seed, int $startX, int $startY, int $startZ, int $rotation) : ?StructureBuffer{
		if($startY < self::MIN_HEIGHT){
			return null;
		}
		$pieceRandom = new Xoroshiro128($seed);
		$first = Int64::mul($startX, $pieceRandom->nextInt());
		$second = Int64::mul($startZ, $pieceRandom->nextInt());
		$pieceRandom->setSeed($first ^ $second ^ $seed);

		$buffer = new StructureBuffer();
		$layout = new EndCityLayout($this->resourceFolder);
		if(!$layout->place($buffer, ($startX << 4) + 8, $startY, ($startZ << 4) + 8, $rotation, $pieceRandom)){
			return null;
		}
		return $buffer->isEmpty() ? null : $buffer;
	}

	/**
	 * Lowest first free Y above the end stone over the 5 by 5 columns at
	 * local 7 to 11 of the start chunk, null when that chunk is not
	 * available.
	 */
	private function startHeight(ChunkManager $world, int $startX, int $startZ) : ?int{
		$chunk = $world->getChunk($startX, $startZ);
		if($chunk === null){
			return null;
		}
		$endStone = StateBlocks::state("minecraft:end_stone");
		$endStoneBlock = $endStone === null ? null : StateBlocks::toBlock($endStone);
		if($endStoneBlock === null){
			return null;
		}
		$endStoneId = $endStoneBlock->getStateId();
		$minY = $world->getMinY();
		$maxY = $world->getMaxY();
		$lowest = PHP_INT_MAX;
		for($x = 7; $x <= 11; ++$x){
			for($z = 7; $z <= 11; ++$z){
				$height = $minY;
				for($y = $maxY - 1; $y >= $minY; --$y){
					if($chunk->getBlockStateId($x, $y, $z) === $endStoneId){
						$height = $y + 1;
						break;
					}
				}
				$lowest = min($lowest, $height);
			}
		}
		return $lowest;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure;

use pocketmine\math\Facing;
use function max;
use function min;
use const PHP_INT_MAX;
use const PHP_INT_MIN;

/**
 * Inclusive box of block coordinates.
 */
final class BoundingBox{

	public function __construct(
		public int $x0,
		public int $y0,
		public int $z0,
		public int $x1,
		public int $y1,
		public int $z1
	){}

	/**
	 * Box spanning the full build height over the given columns.
	 */
	public static function columns(int $x0, int $z0, int $x1, int $z1) : BoundingBox{
		return new BoundingBox($x0, -63, $z0, $x1, 512, $z1);
	}

	/**
	 * Box of a single chunk over the full build height.
	 */
	public static function chunk(int $chunkX, int $chunkZ) : BoundingBox{
		$x = $chunkX << 4;
		$z = $chunkZ << 4;
		return self::columns($x, $z, $x + 15, $z + 15);
	}

	/**
	 * Empty box that grows to the first box it is expanded with.
	 */
	public static function unknown() : BoundingBox{
		return new BoundingBox(PHP_INT_MAX, PHP_INT_MAX, PHP_INT_MAX, PHP_INT_MIN, PHP_INT_MIN, PHP_INT_MIN);
	}

	/**
	 * Box of a piece of the given size placed at a point, oriented towards
	 * one of the horizontal Facing values.
	 */
	public static function orient(int $x, int $y, int $z, int $xOffset, int $yOffset, int $zOffset, int $xLength, int $yLength, int $zLength, int $orientation) : BoundingBox{
		return match($orientation){
			Facing::NORTH => new BoundingBox($x + $xOffset, $y + $yOffset, $z - $zLength + 1 + $zOffset, $x + $xLength - 1 + $xOffset, $y + $yLength - 1 + $yOffset, $z + $zOffset),
			Facing::WEST => new BoundingBox($x - $zLength + 1 + $zOffset, $y + $yOffset, $z + $xOffset, $x + $zOffset, $y + $yLength - 1 + $yOffset, $z + $xLength - 1 + $xOffset),
			Facing::EAST => new BoundingBox($x + $zOffset, $y + $yOffset, $z + $xOffset, $x + $zLength - 1 + $zOffset, $y + $yLength - 1 + $yOffset, $z + $xLength - 1 + $xOffset),
			default => new BoundingBox($x + $xOffset, $y + $yOffset, $z + $zOffset, $x + $xLength - 1 + $xOffset, $y + $yLength - 1 + $yOffset, $z + $zLength - 1 + $zOffset)
		};
	}

	public function copy() : BoundingBox{
		return new BoundingBox($this->x0, $this->y0, $this->z0, $this->x1, $this->y1, $this->z1);
	}

	public function intersects(BoundingBox $other) : bool{
		return $this->x1 >= $other->x0 && $this->x0 <= $other->x1
			&& $this->z1 >= $other->z0 && $this->z0 <= $other->z1
			&& $this->y1 >= $other->y0 && $this->y0 <= $other->y1;
	}

	public function intersectsColumns(int $x0, int $z0, int $x1, int $z1) : bool{
		return $this->x1 >= $x0 && $this->x0 <= $x1 && $this->z1 >= $z0 && $this->z0 <= $z1;
	}

	public function expand(BoundingBox $other) : void{
		$this->x0 = min($this->x0, $other->x0);
		$this->y0 = min($this->y0, $other->y0);
		$this->z0 = min($this->z0, $other->z0);
		$this->x1 = max($this->x1, $other->x1);
		$this->y1 = max($this->y1, $other->y1);
		$this->z1 = max($this->z1, $other->z1);
	}

	public function move(int $x, int $y, int $z) : void{
		$this->x0 += $x;
		$this->y0 += $y;
		$this->z0 += $z;
		$this->x1 += $x;
		$this->y1 += $y;
		$this->z1 += $z;
	}

	public function isInside(int $x, int $y, int $z) : bool{
		return $x >= $this->x0 && $x <= $this->x1 && $z >= $this->z0 && $z <= $this->z1 && $y >= $this->y0 && $y <= $this->y1;
	}

	public function getXSpan() : int{
		return $this->x1 - $this->x0 + 1;
	}

	public function getYSpan() : int{
		return $this->y1 - $this->y0 + 1;
	}

	public function getZSpan() : int{
		return $this->z1 - $this->z0 + 1;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\template\BlockStateTransform;
use dimension\generator\structure\template\StateBlocks;
use pocketmine\block\Block;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\math\Facing;
use function array_key_exists;

/**
 * A box-shaped part of a structure built from code. Blocks are given in
 * piece coordinates and mapped to the world through the piece orientation:
 * a piece facing south is the north-facing layout mirrored along Z, a piece
 * facing east is turned a quarter clockwise, and a piece facing west is
 * both mirrored and turned. Block states follow the same mapping.
 *
 * Every write is clipped to the box passed to postProcess(), which is the
 * chunk being populated.
 */
abstract class StructurePiece{

	public const AIR = "minecraft:air";

	/** @var array<string, Block|null> */
	private static array $blocks = [];

	protected BoundingBox $boundingBox;
	private ?int $orientation = null;

	public function __construct(
		protected int $genDepth
	){}

	/**
	 * @param list<StructurePiece> $pieces
	 */
	public static function findCollisionPiece(array $pieces, BoundingBox $boundingBox) : ?StructurePiece{
		foreach($pieces as $piece){
			if($piece->getBoundingBox()->intersects($boundingBox)){
				return $piece;
			}
		}
		return null;
	}

	/**
	 * Writes the part of the piece inside $boundingBox. Returns false when
	 * the piece must be dropped from its structure.
	 */
	abstract public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool;

	public function getBoundingBox() : BoundingBox{
		return $this->boundingBox;
	}

	public function getGenDepth() : int{
		return $this->genDepth;
	}

	public function getOrientation() : ?int{
		return $this->orientation;
	}

	public function setOrientation(?int $orientation) : void{
		$this->orientation = $orientation;
	}

	public function move(int $x, int $y, int $z) : void{
		$this->boundingBox->move($x, $y, $z);
	}

	protected function getWorldX(int $x, int $z) : int{
		return match($this->orientation){
			Facing::NORTH, Facing::SOUTH => $this->boundingBox->x0 + $x,
			Facing::WEST => $this->boundingBox->x1 - $z,
			Facing::EAST => $this->boundingBox->x0 + $z,
			default => $x
		};
	}

	protected function getWorldY(int $y) : int{
		return $this->orientation === null ? $y : $y + $this->boundingBox->y0;
	}

	protected function getWorldZ(int $x, int $z) : int{
		return match($this->orientation){
			Facing::NORTH => $this->boundingBox->z1 - $z,
			Facing::SOUTH => $this->boundingBox->z0 + $z,
			Facing::WEST, Facing::EAST => $this->boundingBox->z0 + $x,
			default => $z
		};
	}

	/**
	 * Block of $state as seen in the world for this piece orientation.
	 */
	protected function orientedBlock(BlockStateData $state) : ?Block{
		$orientation = $this->orientation ?? Facing::NORTH;
		$key = $orientation . StateBlocks::key($state);
		if(!array_key_exists($key, self::$blocks)){
			$oriented = match($orientation){
				Facing::SOUTH => BlockStateTransform::mirrorZ($state),
				Facing::WEST => BlockStateTransform::clockwise90(BlockStateTransform::mirrorZ($state)),
				Facing::EAST => BlockStateTransform::clockwise90($state),
				default => $state
			};
			self::$blocks[$key] = StateBlocks::toBlock($oriented);
		}
		return self::$blocks[$key];
	}

	protected function placeBlock(BlockManager $level, BlockStateData $state, int $x, int $y, int $z, BoundingBox $boundingBox) : void{
		$worldX = $this->getWorldX($x, $z);
		$worldY = $this->getWorldY($y);
		$worldZ = $this->getWorldZ($x, $z);
		if(!$boundingBox->isInside($worldX, $worldY, $worldZ)){
			return;
		}
		$block = $this->orientedBlock($state);
		if($block !== null){
			$level->setBlockStateAt($worldX, $worldY, $worldZ, $block);
		}
	}

	protected function getBlockId(BlockManager $level, int $x, int $y, int $z, BoundingBox $boundingBox) : string{
		$worldX = $this->getWorldX($x, $z);
		$worldY = $this->getWorldY($y);
		$worldZ = $this->getWorldZ($x, $z);
		if(!$boundingBox->isInside($worldX, $worldY, $worldZ)){
			return self::AIR;
		}
		return $level->getBlockIdAt($worldX, $worldY, $worldZ);
	}

	protected function generateBox(BlockManager $level, BoundingBox $boundingBox, int $x1, int $y1, int $z1, int $x2, int $y2, int $z2, BlockStateData $outsideBlock, BlockStateData $insideBlock, bool $skipAir) : void{
		for($y = $y1; $y <= $y2; ++$y){
			for($x = $x1; $x <= $x2; ++$x){
				for($z = $z1; $z <= $z2; ++$z){
					if($skipAir){
						$id = $this->getBlockId($level, $x, $y, $z, $boundingBox);
						if($id === self::AIR || $id === "minecraft:water"){
							continue;
						}
					}
					if($y !== $y1 && $y !== $y2 && $x !== $x1 && $x !== $x2 && $z !== $z1 && $z !== $z2){
						$this->placeBlock($level, $insideBlock, $x, $y, $z, $boundingBox);
					}else{
						$this->placeBlock($level, $outsideBlock, $x, $y, $z, $boundingBox);
					}
				}
			}
		}
	}

	/**
	 * Fills the column downwards from the given point with $state while it
	 * meets air or liquid.
	 */
	protected function fillColumnDown(BlockManager $level, BlockStateData $state, int $x, int $y, int $z, BoundingBox $boundingBox) : void{
		$worldX = $this->getWorldX($x, $z);
		$worldY = $this->getWorldY($y);
		$worldZ = $this->getWorldZ($x, $z);
		if(!$boundingBox->isInside($worldX, $worldY, $worldZ)){
			return;
		}
		$block = $this->orientedBlock($state);
		if($block === null){
			return;
		}
		$minY = $level->getMinHeight();
		while($worldY > 1 && $worldY >= $minY && self::isAirOrLiquid($level->getBlockIdAt($worldX, $worldY, $worldZ))){
			$level->setBlockStateAt($worldX, $worldY, $worldZ, $block);
			--$worldY;
		}
	}

	private static function isAirOrLiquid(string $id) : bool{
		return $id === self::AIR
			|| $id === "minecraft:water"
			|| $id === "minecraft:flowing_water"
			|| $id === "minecraft:lava"
			|| $id === "minecraft:flowing_lava";
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\template;

use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\nbt\BigEndianNbtSerializer;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\IntTag;
use pocketmine\nbt\tag\ListTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use function count;
use function file_exists;
use function file_get_contents;
use function is_string;
use function rtrim;
use function zlib_decode;

/**
 * Reads the structure templates shipped in
 * dimension/structures/structures.nbt of the plugin resources. The file is
 * a gzip big-endian compound mapping a template name such as
 * "bastion/units/air_base" to its size, palette of block states, block
 * indices and jigsaw blocks. Palette states are upgraded to the running
 * block state version on load.
 *
 * Templates are read once per thread and never modified afterwards. A
 * missing or unreadable file yields no templates.
 */
final class StructureTemplates{

	public const FILE = "dimension/structures/structures.nbt";

	/** @var array<string, array<string, CompoundTag>> */
	private static array $sources = [];
	/** @var array<string, array<string, StructureTemplate|null>> */
	private static array $templates = [];

	private function __construct(){
	}

	public static function get(string $resourceFolder, string $name) : ?StructureTemplate{
		if(!isset(self::$sources[$resourceFolder])){
			self::$sources[$resourceFolder] = self::readFile($resourceFolder);
			self::$templates[$resourceFolder] = [];
		}
		if(!isset(self::$templates[$resourceFolder][$name]) && !isset(self::$sources[$resourceFolder][$name])){
			return null;
		}
		if(!isset(self::$templates[$resourceFolder][$name])){
			self::$templates[$resourceFolder][$name] = self::build(self::$sources[$resourceFolder][$name]);
			unset(self::$sources[$resourceFolder][$name]);
		}
		return self::$templates[$resourceFolder][$name];
	}

	/**
	 * @return array<string, CompoundTag>
	 */
	private static function readFile(string $resourceFolder) : array{
		if($resourceFolder === ""){
			return [];
		}
		$path = rtrim($resourceFolder, "/\\") . "/" . self::FILE;
		if(!file_exists($path)){
			return [];
		}
		$raw = @file_get_contents($path);
		if(!is_string($raw)){
			return [];
		}
		$decoded = @zlib_decode($raw);
		if(!is_string($decoded)){
			return [];
		}
		try{
			$root = (new BigEndianNbtSerializer())->read($decoded)->mustGetCompoundTag();
		}catch(\Exception){
			return [];
		}
		$sources = [];
		foreach($root->getValue() as $name => $tag){
			if($tag instanceof CompoundTag){
				$sources[$name] = $tag;
			}
		}
		return $sources;
	}

	private static function build(CompoundTag $tag) : ?StructureTemplate{
		$size = $tag->getIntArray("size", []);
		if(count($size) !== 3){
			return null;
		}
		$palette = [];
		$paletteTag = $tag->getListTag("palette");
		if($paletteTag !== null){
			foreach($paletteTag as $entry){
				$palette[] = $entry instanceof CompoundTag ? self::readState($entry) : null;
			}
		}
		$jigsaws = [];
		$jigsawTag = $tag->getListTag("jigsaw");
		if($jigsawTag instanceof ListTag){
			foreach($jigsawTag as $entry){
				if(!$entry instanceof CompoundTag){
					continue;
				}
				$pos = $entry->getIntArray("pos", []);
				if(count($pos) !== 3){
					continue;
				}
				$finalState = $entry->getTag("final_state");
				$jigsaws[] = new JigsawConnector(
					$pos[0],
					$pos[1],
					$pos[2],
					$finalState instanceof CompoundTag ? self::readState($finalState, true) : null,
					self::readString($entry, "name"),
					self::readString($entry, "joint"),
					self::readString($entry, "pool"),
					self::readString($entry, "target"),
					self::readInt($entry, "placement_priority"),
					self::readInt($entry, "selection_priority")
				);
			}
		}
		$blocks = $tag->getByteArray("blocks", "");
		return new StructureTemplate($size[0], $size[1], $size[2], $palette, $blocks, $jigsaws);
	}

	private static function readState(CompoundTag $tag, bool $keepVoid = false) : ?BlockStateData{
		try{
			$state = GlobalBlockStateHandlers::getUpgrader()->getBlockStateUpgrader()->upgrade(BlockStateData::fromNbt($tag));
		}catch(\Exception){
			return null;
		}
		if(!$keepVoid && $state->getName() === "minecraft:structure_void"){
			return null;
		}
		return $state;
	}

	private static function readString(CompoundTag $tag, string $name) : string{
		$value = $tag->getTag($name);
		return $value instanceof StringTag ? $value->getValue() : "";
	}

	private static function readInt(CompoundTag $tag, string $name) : int{
		$value = $tag->getTag($name);
		return $value instanceof IntTag ? $value->getValue() : 0;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\template;

use pocketmine\data\bedrock\block\BlockStateData;
use function chr;
use function count;
use function ord;
use function str_repeat;
use function strlen;

/**
 * A block template: a box of palette indices (0 is structure void, which
 * leaves the world untouched) plus the jigsaw blocks it contains. Indices
 * run X first, then Y, then Z.
 */
final class StructureTemplate{

	public const AIR = "minecraft:air";

	/** @var array<int, StructureTemplate> */
	private array $rotated = [];

	/**
	 * @param list<BlockStateData|null> $palette null entries are structure void
	 * @param list<JigsawConnector>     $jigsaws
	 */
	public function __construct(
		private int $sizeX,
		private int $sizeY,
		private int $sizeZ,
		private array $palette,
		private string $blocks,
		private array $jigsaws
	){}

	public function getSizeX() : int{
		return $this->sizeX;
	}

	public function getSizeY() : int{
		return $this->sizeY;
	}

	public function getSizeZ() : int{
		return $this->sizeZ;
	}

	public function getVolume() : int{
		return strlen($this->blocks);
	}

	/**
	 * @return list<JigsawConnector>
	 */
	public function getJigsaws() : array{
		return $this->jigsaws;
	}

	public function index(int $x, int $y, int $z) : int{
		return $x + ($y * $this->sizeX) + ($z * $this->sizeX * $this->sizeY);
	}

	/**
	 * State stored at a flat index, null for structure void.
	 */
	public function stateAt(int $index) : ?BlockStateData{
		$paletteIndex = ord($this->blocks[$index]) - 1;
		if($paletteIndex < 0){
			return null;
		}
		return $this->palette[$paletteIndex] ?? null;
	}

	public function xOf(int $index) : int{
		return $index % $this->sizeX;
	}

	public function yOf(int $index) : int{
		return (int) ($index / $this->sizeX) % $this->sizeY;
	}

	public function zOf(int $index) : int{
		return (int) ($index / ($this->sizeX * $this->sizeY));
	}

	/**
	 * Copy of this template with one block replaced.
	 */
	public function withBlock(int $x, int $y, int $z, BlockStateData $state) : StructureTemplate{
		$palette = $this->palette;
		$paletteIndex = -1;
		foreach($palette as $i => $entry){
			if($entry !== null && $entry->equals($state)){
				$paletteIndex = $i;
				break;
			}
		}
		if($paletteIndex === -1){
			$paletteIndex = count($palette);
			$palette[] = $state;
		}
		$blocks = $this->blocks;
		$blocks[$this->index($x, $y, $z)] = chr($paletteIndex + 1);
		return new StructureTemplate($this->sizeX, $this->sizeY, $this->sizeZ, $palette, $blocks, $this->jigsaws);
	}

	/**
	 * Turns the block grid by $geometryRotation and the block states by
	 * $stateRotation.
	 */
	public function rotate(int $geometryRotation, int $stateRotation) : StructureTemplate{
		if($geometryRotation === Rotation::NONE && $stateRotation === Rotation::NONE){
			return $this;
		}
		$key = ($geometryRotation << 2) | $stateRotation;
		return $this->rotated[$key] ??= $this->buildRotated($geometryRotation, $stateRotation);
	}

	/**
	 * Turns the grid and turns the states the opposite way.
	 */
	public function rotateGrid(int $rotation) : StructureTemplate{
		return $this->rotate($rotation, Rotation::inverse($rotation));
	}

	private function buildRotated(int $geometryRotation, int $stateRotation) : StructureTemplate{
		$swap = $geometryRotation === Rotation::ROTATE_90 || $geometryRotation === Rotation::ROTATE_270;
		$newSizeX = $swap ? $this->sizeZ : $this->sizeX;
		$newSizeZ = $swap ? $this->sizeX : $this->sizeZ;

		$palette = [];
		foreach($this->palette as $state){
			$palette[] = $state === null ? null : BlockStateTransform::rotate($state, $stateRotation);
		}

		$length = strlen($this->blocks);
		$blocks = str_repeat("\0", $length);
		for($index = 0; $index < $length; ++$index){
			$byte = $this->blocks[$index];
			if($byte === "\0"){
				continue;
			}
			$x = $index % $this->sizeX;
			$y = (int) ($index / $this->sizeX) % $this->sizeY;
			$z = (int) ($index / ($this->sizeX * $this->sizeY));
			$rx = self::rotateX($this->sizeX, $this->sizeZ, $x, $z, $geometryRotation);
			$rz = self::rotateZ($this->sizeX, $this->sizeZ, $x, $z, $geometryRotation);
			$blocks[$rx + ($y * $newSizeX) + ($rz * $newSizeX * $this->sizeY)] = $byte;
		}

		$jigsaws = [];
		foreach($this->jigsaws as $jigsaw){
			$rx = self::rotateX($this->sizeX, $this->sizeZ, $jigsaw->x, $jigsaw->z, $geometryRotation);
			$rz = self::rotateZ($this->sizeX, $this->sizeZ, $jigsaw->x, $jigsaw->z, $geometryRotation);
			$finalState = $jigsaw->finalState === null ? null : BlockStateTransform::rotate($jigsaw->finalState, $stateRotation);
			$jigsaws[] = $jigsaw->withPosition($rx, $jigsaw->y, $rz)->withFinalState($finalState);
		}

		return new StructureTemplate($newSizeX, $this->sizeY, $newSizeZ, $palette, $blocks, $jigsaws);
	}

	private static function rotateX(int $sizeX, int $sizeZ, int $x, int $z, int $rotation) : int{
		return match($rotation){
			Rotation::ROTATE_90 => $z,
			Rotation::ROTATE_180 => $sizeX - 1 - $x,
			Rotation::ROTATE_270 => $sizeZ - 1 - $z,
			default => $x
		};
	}

	private static function rotateZ(int $sizeX, int $sizeZ, int $x, int $z, int $rotation) : int{
		return match($rotation){
			Rotation::ROTATE_90 => $sizeX - 1 - $x,
			Rotation::ROTATE_180 => $sizeZ - 1 - $z,
			Rotation::ROTATE_270 => $x,
			default => $z
		};
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\template;

use pocketmine\data\bedrock\block\BlockStateData;

/**
 * A jigsaw block of a structure template: where it sits, what it turns into
 * once the structure is assembled, and which pieces it may connect to.
 */
final class JigsawConnector{

	public function __construct(
		public readonly int $x,
		public readonly int $y,
		public readonly int $z,
		public readonly ?BlockStateData $finalState,
		public readonly string $name,
		public readonly string $joint,
		public readonly string $pool,
		public readonly string $target,
		public readonly int $placementPriority,
		public readonly int $selectionPriority
	){}

	public function withPosition(int $x, int $y, int $z) : JigsawConnector{
		return new JigsawConnector($x, $y, $z, $this->finalState, $this->name, $this->joint, $this->pool, $this->target, $this->placementPriority, $this->selectionPriority);
	}

	public function withFinalState(?BlockStateData $finalState) : JigsawConnector{
		return new JigsawConnector($this->x, $this->y, $this->z, $finalState, $this->name, $this->joint, $this->pool, $this->target, $this->placementPriority, $this->selectionPriority);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\template;

use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\nbt\tag\ByteTag;
use pocketmine\nbt\tag\IntTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\nbt\tag\Tag;
use function str_ends_with;
use function str_replace;

/**
 * Turns and mirrors the orientation states of a Bedrock block state so that
 * a block keeps facing the same way relative to a rotated or mirrored
 * structure.
 */
final class BlockStateTransform{

	private const WALL_EAST = "wall_connection_type_east";
	private const WALL_NORTH = "wall_connection_type_north";
	private const WALL_SOUTH = "wall_connection_type_south";
	private const WALL_WEST = "wall_connection_type_west";
	private const CONNECTION_EAST = "minecraft:connection_east";
	private const CONNECTION_NORTH = "minecraft:connection_north";
	private const CONNECTION_SOUTH = "minecraft:connection_south";
	private const CONNECTION_WEST = "minecraft:connection_west";

	private const LEVER_DIRECTIONS = [
		"down_east_west",
		"east",
		"west",
		"south",
		"north",
		"up_north_south",
		"up_east_west",
		"down_north_south"
	];

	private function __construct(){
	}

	public static function rotate(BlockStateData $state, int $rotation) : BlockStateData{
		return match($rotation & 3){
			Rotation::ROTATE_90 => self::clockwise90($state),
			Rotation::ROTATE_180 => self::clockwise180($state),
			Rotation::ROTATE_270 => self::counterclockwise90($state),
			default => $state
		};
	}

	public static function clockwise180(BlockStateData $state) : BlockStateData{
		return self::clockwise90(self::clockwise90($state));
	}

	public static function counterclockwise90(BlockStateData $state) : BlockStateData{
		return self::clockwise90(self::clockwise90(self::clockwise90($state)));
	}

	/**
	 * Mirror flipping north and south, east and west unchanged.
	 */
	public static function mirrorZ(BlockStateData $state) : BlockStateData{
		return self::clockwise180(self::mirrorX($state));
	}

	public static function clockwise90(BlockStateData $state) : BlockStateData{
		$name = $state->getName();
		$states = $state->getStates();
		if($states === []){
			return $state;
		}
		$isJigsaw = $name === "minecraft:jigsaw";
		$isTrapdoor = str_ends_with($name, "trapdoor");
		$result = $states;
		foreach($states as $key => $tag){
			$value = $tag->getValue();
			switch($key){
				case "torch_facing_direction":
				case "minecraft:facing_direction":
				case "minecraft:block_face":
				case "minecraft:cardinal_direction":
					$result[$key] = new StringTag(self::rotateFaceName((string) $value));
					break;
				case "rail_direction":
					$result[$key] = new IntTag(match((int) $value){
						0 => 1,
						2 => 5,
						3 => 4,
						4 => 2,
						5 => 3,
						6 => 7,
						7 => 8,
						8 => 9,
						9 => 6,
						default => 0
					});
					break;
				case "weirdo_direction":
					$result[$key] = new IntTag(match((int) $value){
						0 => 2,
						1 => 3,
						2 => 1,
						default => 0
					});
					break;
				case "direction":
					$result[$key] = new IntTag($isTrapdoor ? self::rotateTrapdoorDirection((int) $value) : ((int) $value + 1) % 4);
					break;
				case "ground_sign_direction":
					$result[$key] = new IntTag(((int) $value + 4) % 16);
					break;
				case "facing_direction":
					$result[$key] = new IntTag($isJigsaw ? self::rotateFacingCounterclockwise((int) $value) : self::rotateFacingClockwise((int) $value));
					break;
				case "rotation":
					if($isJigsaw){
						$result[$key] = new IntTag(((int) $value + 1) & 3);
					}
					break;
				case "lever_direction":
					$result[$key] = new StringTag(self::rotateLever((string) $value));
					break;
				case "pillar_axis":
					$result[$key] = new StringTag(match((string) $value){
						"x" => "z",
						"z" => "x",
						default => (string) $value
					});
					break;
				case "huge_mushroom_bits":
					$bits = (int) $value;
					$result[$key] = new IntTag($bits <= 10 ? ($bits * 3) % 10 : $bits);
					break;
				case "vine_direction_bits":
					$bits = (int) $value;
					$result[$key] = new IntTag((($bits << 1) | ($bits >> 3)) & 0xf);
					break;
			}
		}
		self::moveSides($states, $result, self::WALL_NORTH, self::WALL_EAST, self::WALL_SOUTH, self::WALL_WEST);
		self::moveSides($states, $result, self::CONNECTION_NORTH, self::CONNECTION_EAST, self::CONNECTION_SOUTH, self::CONNECTION_WEST);
		return BlockStateData::current($name, $result);
	}

	/**
	 * Mirror flipping east and west, north and south unchanged. Handed
	 * states such as door hinges and stair corners switch sides.
	 */
	public static function mirrorX(BlockStateData $state) : BlockStateData{
		$name = $state->getName();
		$states = $state->getStates();
		if($states === []){
			return $state;
		}
		$isJigsaw = $name === "minecraft:jigsaw";
		$isTrapdoor = str_ends_with($name, "trapdoor");
		$result = $states;
		foreach($states as $key => $tag){
			$value = $tag->getValue();
			switch($key){
				case "torch_facing_direction":
				case "minecraft:facing_direction":
				case "minecraft:block_face":
				case "minecraft:cardinal_direction":
					$result[$key] = new StringTag(match((string) $value){
						"east" => "west",
						"west" => "east",
						default => (string) $value
					});
					break;
				case "rail_direction":
					$result[$key] = new IntTag(match((int) $value){
						2 => 3,
						3 => 2,
						6 => 7,
						7 => 6,
						8 => 9,
						9 => 8,
						default => (int) $value
					});
					break;
				case "weirdo_direction":
					$result[$key] = new IntTag(match((int) $value){
						0 => 1,
						1 => 0,
						default => (int) $value
					});
					break;
				case "direction":
					if($isTrapdoor){
						$result[$key] = new IntTag(match((int) $value){
							0 => 1,
							1 => 0,
							default => (int) $value
						});
					}else{
						$result[$key] = new IntTag(match((int) $value){
							1 => 3,
							3 => 1,
							default => (int) $value
						});
					}
					break;
				case "ground_sign_direction":
					$result[$key] = new IntTag((16 - (int) $value) % 16);
					break;
				case "facing_direction":
					$facing = (int) $value;
					$extra = $facing & ~0x7;
					$result[$key] = new IntTag(match($facing & 0x7){
						4 => 5,
						5 => 4,
						default => $facing & 0x7
					} | $extra);
					break;
				case "rotation":
					if($isJigsaw){
						$result[$key] = new IntTag((4 - ((int) $value & 0x3)) & 0x3);
					}
					break;
				case "lever_direction":
					$result[$key] = new StringTag(match((string) $value){
						"east" => "west",
						"west" => "east",
						default => (string) $value
					});
					break;
				case "vine_direction_bits":
					$bits = (int) $value;
					$result[$key] = new IntTag(($bits & 0x1) | ($bits & 0x4) | (($bits & 0x2) << 2) | (($bits & 0x8) >> 2));
					break;
				case "door_hinge_bit":
					$result[$key] = new ByteTag((int) $value !== 0 ? 0 : 1);
					break;
				case "minecraft:corner":
					$result[$key] = new StringTag(self::swapHand((string) $value));
					break;
			}
		}
		self::swapSides($states, $result, self::WALL_EAST, self::WALL_WEST);
		self::swapSides($states, $result, self::CONNECTION_EAST, self::CONNECTION_WEST);
		return BlockStateData::current($name, $result);
	}

	private static function rotateFaceName(string $face) : string{
		return match($face){
			"north" => "east",
			"east" => "south",
			"south" => "west",
			"west" => "north",
			default => $face
		};
	}

	private static function swapHand(string $value) : string{
		return str_replace(["left", "right", "\0"], ["\0", "left", "right"], $value);
	}

	private static function rotateFacingClockwise(int $facing) : int{
		$extra = $facing & ~0x7;
		return match($facing & 0x7){
			2 => 5,
			5 => 3,
			3 => 4,
			4 => 2,
			default => $facing & 0x7
		} | $extra;
	}

	private static function rotateFacingCounterclockwise(int $facing) : int{
		$extra = $facing & ~0x7;
		return match($facing & 0x7){
			2 => 4,
			4 => 3,
			3 => 5,
			5 => 2,
			default => $facing & 0x7
		} | $extra;
	}

	/**
	 * Trapdoor directions are east, west, south, north for 0 to 3.
	 */
	private static function rotateTrapdoorDirection(int $direction) : int{
		return match($direction){
			0 => 2,
			2 => 1,
			1 => 3,
			3 => 0,
			default => $direction
		};
	}

	private static function rotateLever(string $direction) : string{
		$index = -1;
		foreach(self::LEVER_DIRECTIONS as $i => $name){
			if($name === $direction){
				$index = $i;
				break;
			}
		}
		if($index === -1){
			return $direction;
		}
		return self::LEVER_DIRECTIONS[match($index){
			1 => 3,
			2 => 4,
			3 => 2,
			4 => 1,
			5 => 6,
			6 => 5,
			7 => 0,
			default => 7
		}];
	}

	/**
	 * Each side takes the value of the side a quarter turn counterclockwise
	 * from it.
	 *
	 * @param array<string, Tag> $states
	 * @param array<string, Tag> $result
	 */
	private static function moveSides(array $states, array &$result, string $north, string $east, string $south, string $west) : void{
		if(!isset($states[$north], $states[$east], $states[$south], $states[$west])){
			return;
		}
		$result[$east] = $states[$north];
		$result[$south] = $states[$east];
		$result[$west] = $states[$south];
		$result[$north] = $states[$west];
	}

	/**
	 * @param array<string, Tag> $states
	 * @param array<string, Tag> $result
	 */
	private static function swapSides(array $states, array &$result, string $first, string $second) : void{
		if(!isset($states[$first], $states[$second])){
			return;
		}
		$result[$first] = $states[$second];
		$result[$second] = $states[$first];
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\template;

/**
 * Quarter turns around the Y axis, stored as 0 to 3.
 */
final class Rotation{

	public const NONE = 0;
	public const ROTATE_90 = 1;
	public const ROTATE_180 = 2;
	public const ROTATE_270 = 3;

	private function __construct(){
	}

	public static function add(int $base, int $other) : int{
		return ($base + $other) & 3;
	}

	public static function inverse(int $rotation) : int{
		return match($rotation){
			self::ROTATE_90 => self::ROTATE_270,
			self::ROTATE_270 => self::ROTATE_90,
			default => $rotation
		};
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\template;

use pocketmine\block\Block;
use pocketmine\block\RuntimeBlockStateRegistry;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\nbt\tag\Tag;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use function array_key_exists;
use function array_merge;
use function ksort;
use function serialize;

/**
 * Converts Bedrock block states to server blocks, and builds block states
 * from an identifier plus the states to override. Results are cached per
 * thread. States the server does not know convert to null.
 */
final class StateBlocks{

	/** @var array<string, int> */
	private static array $stateIds = [];
	/** @var array<string, BlockStateData|null> */
	private static array $defaults = [];

	private function __construct(){
	}

	public static function toBlock(BlockStateData $state) : ?Block{
		$key = self::key($state);
		if(!isset(self::$stateIds[$key])){
			self::$stateIds[$key] = self::resolve($state);
		}
		$stateId = self::$stateIds[$key];
		if($stateId === -1){
			return null;
		}
		return RuntimeBlockStateRegistry::getInstance()->fromStateId($stateId);
	}

	/**
	 * State of $identifier with its default values, overridden by $states.
	 *
	 * @param array<string, Tag> $states
	 */
	public static function state(string $identifier, array $states = []) : ?BlockStateData{
		if(!array_key_exists($identifier, self::$defaults)){
			self::$defaults[$identifier] = self::resolveDefault($identifier);
		}
		$default = self::$defaults[$identifier];
		if($default === null){
			return null;
		}
		if($states === []){
			return $default;
		}
		return BlockStateData::current($identifier, array_merge($default->getStates(), $states));
	}

	public static function key(BlockStateData $state) : string{
		$values = [];
		foreach($state->getStates() as $name => $tag){
			$values[$name] = $tag->getValue();
		}
		ksort($values);
		return $state->getName() . serialize($values);
	}

	private static function resolve(BlockStateData $state) : int{
		try{
			return GlobalBlockStateHandlers::getDeserializer()->deserialize($state);
		}catch(\Exception){
			return -1;
		}
	}

	private static function resolveDefault(string $identifier) : ?BlockStateData{
		$upgrader = GlobalBlockStateHandlers::getUpgrader();
		try{
			$default = $upgrader->getBlockStateUpgrader()->upgrade($upgrader->upgradeStringIdMeta($identifier, 0));
		}catch(\Exception){
			return null;
		}
		if($default->getName() !== $identifier){
			return null;
		}
		return $default;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\placement;

use dimension\generator\ChunkHash;
use dimension\generator\math\Int64;
use dimension\generator\random\RandomSource;

/**
 * Shared placement of nether fortresses and bastion remnants: one start per
 * region of 30 by 30 chunks, which is a bastion or a fortress depending on
 * a second roll made per region.
 */
final class NetherComplexPlacement{

	public const REGION_SIZE_CHUNKS = 30;
	public const EDGE_EXCLUSION_CHUNKS = 4;
	private const CANDIDATE_OFFSET_MAX = self::REGION_SIZE_CHUNKS - self::EDGE_EXCLUSION_CHUNKS - 1;
	private const PLACEMENT_SALT = 0x42415354494F4E;
	private const SELECTOR_SALT = 0x4E45544845524C;

	private function __construct(){
	}

	/**
	 * Start chunk of the region, as [chunkX, chunkZ].
	 *
	 * @return array{int, int}
	 */
	public static function regionStart(int $levelSeed, int $regionX, int $regionZ, RandomSource $random) : array{
		$random->setSeed(Int64::add($levelSeed ^ self::PLACEMENT_SALT, ChunkHash::hash($regionX, $regionZ)));
		$candidateX = $regionX * self::REGION_SIZE_CHUNKS + $random->nextBoundedInt(self::CANDIDATE_OFFSET_MAX);
		$candidateZ = $regionZ * self::REGION_SIZE_CHUNKS + $random->nextBoundedInt(self::CANDIDATE_OFFSET_MAX);
		return [$candidateX, $candidateZ];
	}

	public static function isNetherComplexStart(int $levelSeed, int $chunkX, int $chunkZ, RandomSource $random) : bool{
		$regionX = StructurePlacement::floorDiv($chunkX, self::REGION_SIZE_CHUNKS);
		$regionZ = StructurePlacement::floorDiv($chunkZ, self::REGION_SIZE_CHUNKS);
		[$candidateX, $candidateZ] = self::regionStart($levelSeed, $regionX, $regionZ, $random);
		return $candidateX === $chunkX && $candidateZ === $chunkZ;
	}

	public static function shouldGenerateBastion(int $levelSeed, int $chunkX, int $chunkZ, RandomSource $random) : bool{
		$regionX = StructurePlacement::floorDiv($chunkX, self::REGION_SIZE_CHUNKS);
		$regionZ = StructurePlacement::floorDiv($chunkZ, self::REGION_SIZE_CHUNKS);
		$random->setSeed(Int64::add($levelSeed ^ self::SELECTOR_SALT, ChunkHash::hash($regionX, $regionZ)));
		return $random->nextBoundedInt(2) !== 0;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\placement;

use dimension\generator\ChunkHash;
use dimension\generator\math\Int64;
use dimension\generator\random\RandomSource;
use function floor;

/**
 * Grid placement of a structure: the world is cut into square regions of
 * $maxDistance chunks and each region holds one candidate start chunk, at a
 * random offset below $maxDistance - $minDistance from the region corner.
 */
final class StructurePlacement{

	public const DEFAULT_SALT = 0x76694565C616765;

	/**
	 * @param \Closure(int) : bool|null $biomeFilter
	 */
	public function __construct(
		private int $salt,
		private int $minDistance,
		private int $maxDistance,
		private ?\Closure $biomeFilter = null
	){}

	public function isValidBiome(int $biome) : bool{
		return $this->biomeFilter === null || ($this->biomeFilter)($biome);
	}

	public function canGenerate(int $levelSeed, RandomSource $random, int $chunkX, int $chunkZ, int $biome) : bool{
		$regionX = self::floorDiv($chunkX, $this->maxDistance);
		$regionZ = self::floorDiv($chunkZ, $this->maxDistance);
		$random->setSeed(Int64::add($levelSeed ^ $this->salt, ChunkHash::hash($regionX, $regionZ)));
		return $this->isValidBiome($biome)
			&& $regionX * $this->maxDistance + $random->nextBoundedInt($this->maxDistance - $this->minDistance) === $chunkX
			&& $regionZ * $this->maxDistance + $random->nextBoundedInt($this->maxDistance - $this->minDistance) === $chunkZ;
	}

	public static function floorDiv(int $value, int $divisor) : int{
		return (int) floor($value / $divisor);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure;

use dimension\generator\ChunkHash;
use dimension\generator\math\GenerationMath;
use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use dimension\generator\random\RandomSource;
use dimension\generator\random\Xoroshiro128;
use dimension\generator\structure\placement\StructurePlacement;
use dimension\generator\structure\template\StateBlocks;
use dimension\generator\structure\template\StructureTemplates;
use pocketmine\block\Block;
use pocketmine\block\Water;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\world\ChunkManager;
use function in_array;

/**
 * Ruined portals, one candidate chunk per region of 40 by 40 chunks. The
 * biome at the chunk centre decides how the portal is set in the terrain:
 * buried in deserts, on the surface in jungles and swamps, on the sea floor
 * in oceans, in or on mountains, underground or on the surface elsewhere,
 * and at a random height in the nether, where the stone bricks become
 * blackstone. One portal in twenty-one is a giant portal. Obsidian turns
 * crying one time in five.
 */
final class RuinedPortalPopulator implements Populator{

	private const SEA_LEVEL = 63;

	private const ON_LAND_SURFACE = 0;
	private const PARTLY_BURIED = 1;
	private const ON_OCEAN_FLOOR = 2;
	private const IN_MOUNTAIN = 3;
	private const UNDERGROUND = 4;
	private const IN_NETHER = 5;

	private const PORTALS = [
		"ruined_portal/portal_1",
		"ruined_portal/portal_2",
		"ruined_portal/portal_3",
		"ruined_portal/portal_4",
		"ruined_portal/portal_5",
		"ruined_portal/portal_6",
		"ruined_portal/portal_7",
		"ruined_portal/portal_8",
		"ruined_portal/portal_9",
		"ruined_portal/portal_10"
	];
	private const GIANT_PORTALS = [
		"ruined_portal/giant_portal_1",
		"ruined_portal/giant_portal_2",
		"ruined_portal/giant_portal_3"
	];

	private const NETHER_BRICK_REPLACEMENTS = [
		"minecraft:chiseled_stone_bricks" => "minecraft:chiseled_polished_blackstone",
		"minecraft:cracked_stone_bricks" => "minecraft:cracked_polished_blackstone_bricks",
		"minecraft:stone_bricks" => "minecraft:polished_blackstone_bricks",
		"minecraft:stone_brick_slab" => "minecraft:polished_blackstone_brick_slab",
		"minecraft:stone_brick_stairs" => "minecraft:polished_blackstone_brick_stairs",
		"minecraft:stone_brick_wall" => "minecraft:polished_blackstone_wall"
	];

	private const DESERT = [BiomeIds::DESERT, BiomeIds::DESERT_HILLS, BiomeIds::DESERT_MUTATED];
	private const JUNGLE_OR_SWAMP = [
		BiomeIds::JUNGLE, BiomeIds::JUNGLE_HILLS, BiomeIds::JUNGLE_EDGE, BiomeIds::JUNGLE_MUTATED, BiomeIds::JUNGLE_EDGE_MUTATED,
		BiomeIds::BAMBOO_JUNGLE, BiomeIds::BAMBOO_JUNGLE_HILLS,
		BiomeIds::SWAMPLAND, BiomeIds::SWAMPLAND_MUTATED, BiomeIds::MANGROVE_SWAMP
	];
	private const MOUNTAINS = [
		BiomeIds::EXTREME_HILLS, BiomeIds::EXTREME_HILLS_EDGE, BiomeIds::EXTREME_HILLS_MUTATED,
		BiomeIds::EXTREME_HILLS_PLUS_TREES, BiomeIds::EXTREME_HILLS_PLUS_TREES_MUTATED,
		BiomeIds::ICE_MOUNTAINS, BiomeIds::JAGGED_PEAKS, BiomeIds::FROZEN_PEAKS, BiomeIds::STONY_PEAKS,
		BiomeIds::SNOWY_SLOPES, BiomeIds::GROVE, BiomeIds::MEADOW, BiomeIds::CHERRY_GROVE
	];
	private const OCEAN = [
		BiomeIds::OCEAN, BiomeIds::DEEP_OCEAN, BiomeIds::LEGACY_FROZEN_OCEAN, BiomeIds::WARM_OCEAN, BiomeIds::DEEP_WARM_OCEAN,
		BiomeIds::LUKEWARM_OCEAN, BiomeIds::DEEP_LUKEWARM_OCEAN, BiomeIds::COLD_OCEAN, BiomeIds::DEEP_COLD_OCEAN,
		BiomeIds::FROZEN_OCEAN, BiomeIds::DEEP_FROZEN_OCEAN
	];
	private const NETHER = [
		BiomeIds::HELL, BiomeIds::SOULSAND_VALLEY, BiomeIds::CRIMSON_FOREST, BiomeIds::WARPED_FOREST, BiomeIds::BASALT_DELTAS
	];

	private StructurePlacement $placement;
	private Xoroshiro128 $random;

	public function __construct(
		private int $seed,
		private string $resourceFolder
	){
		$this->random = new Xoroshiro128(0);
		$this->placement = new StructurePlacement(34222645, 15, 40);
	}

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$chunk = $world->getChunk($chunkX, $chunkZ);
		if($chunk === null){
			return;
		}
		$biome = $chunk->getBiomeId(7, self::SEA_LEVEL, 7);
		if(!$this->placement->canGenerate($seed, $this->random, $chunkX, $chunkZ, $biome)){
			return;
		}
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$x = ($chunkX << 4) + 7;
		$z = ($chunkZ << 4) + 7;

		$isNether = in_array($biome, self::NETHER, true);
		if(in_array($biome, self::DESERT, true)){
			$height = self::PARTLY_BURIED;
		}elseif(in_array($biome, self::JUNGLE_OR_SWAMP, true)){
			$height = self::ON_LAND_SURFACE;
		}elseif(in_array($biome, self::MOUNTAINS, true)){
			$height = $random->nextBoolean() ? self::ON_LAND_SURFACE : self::IN_MOUNTAIN;
		}elseif(in_array($biome, self::OCEAN, true)){
			$height = self::ON_OCEAN_FLOOR;
		}elseif($isNether){
			$height = self::IN_NETHER;
		}else{
			$height = $random->nextBoolean() ? self::ON_LAND_SURFACE : self::UNDERGROUND;
		}

		$big = $random->nextBoundedInt(20) === 0;
		$name = $big ? self::GIANT_PORTALS[$random->nextInt(3)] : self::PORTALS[$random->nextInt(10)];
		$airPocket = $height === self::IN_NETHER && $random->nextFloat() < 0.5;
		$structure = StructureTemplates::get($this->resourceFolder, $name);
		if($structure === null){
			return;
		}
		$y = $this->findSuitableY($random, $world, $x, $z, $height, $airPocket, $structure->getSizeX(), $structure->getSizeY(), $structure->getSizeZ());

		$manager = new BlockManager($world);
		$water = StateBlocks::state("minecraft:water");
		$netherrack = StateBlocks::state("minecraft:netherrack");
		$obsidian = StateBlocks::state("minecraft:obsidian");
		$cryingObsidian = StateBlocks::state("minecraft:crying_obsidian");

		$volume = $structure->getVolume();
		for($index = 0; $index < $volume; ++$index){
			$state = $structure->stateAt($index);
			if($state === null){
				continue;
			}
			$blockX = $x + $structure->xOf($index);
			$blockY = $y + $structure->yOf($index);
			$blockZ = $z + $structure->zOf($index);
			$id = $state->getName();
			if($id === "minecraft:air" && $water !== null && $this->shouldFillAirWithWater($world, $blockX, $blockY, $blockZ)){
				$this->set($manager, $blockX, $blockY, $blockZ, $water);
				continue;
			}
			$result = $state;
			if($id === "minecraft:jigsaw" && $netherrack !== null){
				$result = $netherrack;
			}
			if(($id === "minecraft:lava" || $id === "minecraft:flowing_lava") && $obsidian !== null && $this->isWater($world->getBlockAt($blockX, $blockY + 1, $blockZ))){
				$result = $obsidian;
			}
			if($id === "minecraft:obsidian" && $random->nextInt(5) === 0 && $cryingObsidian !== null){
				$result = $cryingObsidian;
			}
			if($isNether){
				if($id === "minecraft:mossy_stone_bricks"){
					continue;
				}
				$replacement = self::NETHER_BRICK_REPLACEMENTS[$id] ?? null;
				if($replacement !== null){
					$result = $this->replaceKeepingStates($state, $replacement) ?? $result;
				}
			}
			$this->set($manager, $blockX, $blockY, $blockZ, $result);
		}
		$root->merge($manager);
	}

	private function set(BlockManager $manager, int $x, int $y, int $z, BlockStateData $state) : void{
		$block = StateBlocks::toBlock($state);
		if($block !== null){
			$manager->setBlockStateAt($x, $y, $z, $block);
		}
	}

	/**
	 * State of $identifier carrying over the values of the states both
	 * blocks share.
	 */
	private function replaceKeepingStates(BlockStateData $state, string $identifier) : ?BlockStateData{
		$default = StateBlocks::state($identifier);
		if($default === null){
			return null;
		}
		$overrides = [];
		foreach($default->getStates() as $key => $tag){
			$value = $state->getState($key);
			if($value !== null && $value->getType() === $tag->getType()){
				$overrides[$key] = $value;
			}
		}
		return StateBlocks::state($identifier, $overrides);
	}

	private function isWater(Block $block) : bool{
		return $block instanceof Water;
	}

	private function shouldFillAirWithWater(ChunkManager $world, int $x, int $y, int $z) : bool{
		if($y >= self::SEA_LEVEL){
			return false;
		}
		if($this->isWater($world->getBlockAt($x, $y, $z))){
			return true;
		}
		foreach([[0, -1, 0], [0, 1, 0], [0, 0, -1], [0, 0, 1], [-1, 0, 0], [1, 0, 0]] as [$dx, $dy, $dz]){
			if($this->isWater($world->getBlockAt($x + $dx, $y + $dy, $z + $dz))){
				return true;
			}
		}
		return false;
	}

	private function findSuitableY(RandomSource $random, ChunkManager $world, int $worldX, int $worldZ, int $portalHeight, bool $airPocket, int $structureWidth, int $structureHeight, int $structureDepth) : int{
		$minY = $world->getMinY() + 15;
		$surfaceYAtCenter = $this->getSurfaceY($world, $worldX + ($structureWidth >> 1), $worldZ + ($structureDepth >> 1), $portalHeight);
		if($portalHeight === self::IN_NETHER){
			if($airPocket){
				$i = GenerationMath::randomRange($random, 32, 100);
			}elseif($random->nextBoolean()){
				$i = GenerationMath::randomRange($random, 27, 29);
			}else{
				$i = GenerationMath::randomRange($random, 29, 100);
			}
		}elseif($portalHeight === self::IN_MOUNTAIN){
			$i = self::getRandomWithinInterval($random, 70, $surfaceYAtCenter - $structureHeight);
		}elseif($portalHeight === self::UNDERGROUND){
			$i = self::getRandomWithinInterval($random, $minY, $surfaceYAtCenter - $structureHeight);
		}elseif($portalHeight === self::PARTLY_BURIED){
			$i = $surfaceYAtCenter - $structureHeight + GenerationMath::randomRange($random, 2, 8);
		}else{
			$i = $surfaceYAtCenter;
		}

		$maxCornerX = $worldX + $structureWidth - 1;
		$maxCornerZ = $worldZ + $structureDepth - 1;
		for($y = $i; $y > $minY; --$y){
			$cornersOnSolidGround = 0;
			if(self::isOpaqueGround($world->getBlockAt($worldX, $y, $worldZ))){
				++$cornersOnSolidGround;
			}
			if(self::isOpaqueGround($world->getBlockAt($maxCornerX, $y, $worldZ))){
				++$cornersOnSolidGround;
			}
			if(self::isOpaqueGround($world->getBlockAt($worldX, $y, $maxCornerZ))){
				++$cornersOnSolidGround;
			}
			if(self::isOpaqueGround($world->getBlockAt($maxCornerX, $y, $maxCornerZ))){
				++$cornersOnSolidGround;
			}
			if($cornersOnSolidGround >= 3){
				return $y;
			}
		}
		return $y;
	}

	/**
	 * First free Y above the highest block of the column, lowered through
	 * water for portals on the sea floor.
	 */
	private function getSurfaceY(ChunkManager $world, int $x, int $z, int $portalHeight) : int{
		$minY = $world->getMinY();
		$chunk = $world->getChunk($x >> 4, $z >> 4);
		$highest = $chunk?->getHighestBlockAt($x & 0x0f, $z & 0x0f);
		$y = $highest === null ? $minY : $highest + 1;
		if($portalHeight !== self::ON_OCEAN_FLOOR){
			return $y;
		}
		while($y > $minY && $this->isWater($world->getBlockAt($x, $y, $z))){
			--$y;
		}
		return $y;
	}

	private static function isOpaqueGround(Block $block) : bool{
		return $block->isSolid() && !$block->canBeReplaced() && !$block->isTransparent();
	}

	private static function getRandomWithinInterval(RandomSource $random, int $start, int $end) : int{
		return $start < $end ? GenerationMath::randomRange($random, $start, $end) : $end;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure;

use dimension\generator\ChunkHash;
use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use dimension\generator\random\Xoroshiro128;
use dimension\generator\structure\placement\StructurePlacement;
use dimension\generator\structure\template\StateBlocks;
use dimension\generator\structure\template\StructureTemplate;
use dimension\generator\structure\template\StructureTemplates;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;
use const PHP_INT_MAX;

/**
 * Bone fossils of soul sand valleys: one candidate chunk per region of 32
 * by 32 chunks. The fossil sits on the lowest soul sand or soul soil
 * surface found under its footprint, and one fossil in three gets a dried
 * ghast beside it. Air cells of the fossil templates are not placed.
 */
final class NetherFossilPopulator implements Populator{

	private const LAVA_LEVEL = 31;

	private StructurePlacement $placement;
	private Xoroshiro128 $random;

	public function __construct(
		private int $seed,
		private string $resourceFolder
	){
		$this->random = new Xoroshiro128(0);
		$this->placement = new StructurePlacement(
			StructurePlacement::DEFAULT_SALT,
			2,
			32,
			static fn(int $biome) : bool => $biome === BiomeIds::SOULSAND_VALLEY
		);
	}

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$chunk = $world->getChunk($chunkX, $chunkZ);
		if($chunk === null){
			return;
		}
		$biome = $chunk->getBiomeId(3, self::LAVA_LEVEL, 3);
		if(!$this->placement->canGenerate($seed, $this->random, $chunkX, $chunkZ, $biome)){
			return;
		}
		$this->random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$x = ($chunkX << 4) + 3;
		$z = ($chunkZ << 4) + 3;
		$manager = new BlockManager($world);
		$structure = StructureTemplates::get($this->resourceFolder, "nether_fossils/fossil_" . ($this->random->nextInt(14) + 1));
		if($structure === null){
			return;
		}

		$height = PHP_INT_MAX;
		for($bx = 0; $bx < $structure->getSizeX(); ++$bx){
			for($bz = 0; $bz < $structure->getSizeZ(); ++$bz){
				foreach($this->getHighestWorkableBlocks($manager, $x + $bx, $z + $bz) as $y){
					if($y < $height){
						$height = $y;
					}
				}
			}
		}
		if($height === PHP_INT_MAX){
			return;
		}

		$volume = $structure->getVolume();
		for($index = 0; $index < $volume; ++$index){
			$state = $structure->stateAt($index);
			if($state === null || $state->getName() === StructureTemplate::AIR){
				continue;
			}
			$block = StateBlocks::toBlock($state);
			if($block !== null){
				$manager->setBlockStateAt($x + $structure->xOf($index), $height + $structure->yOf($index), $z + $structure->zOf($index), $block);
			}
		}
		if($this->random->nextInt(3) === 0){
			$driedGhast = StateBlocks::state("minecraft:dried_ghast");
			$block = $driedGhast === null ? null : StateBlocks::toBlock($driedGhast);
			if($block !== null){
				$manager->setBlockStateAt($x - 1, $height, $z - 1, $block);
			}
		}
		$root->merge($manager);
	}

	/**
	 * Y just above every soul sand or soul soil block of the column that has
	 * a replaceable block on top, scanning down from Y 128.
	 *
	 * @return list<int>
	 */
	private function getHighestWorkableBlocks(BlockManager $level, int $x, int $z) : array{
		$blockYs = [];
		for($y = 128; $y > 0; --$y){
			$id = $level->getBlockIdAt($x, $y, $z);
			if(($id === "minecraft:soul_sand" || $id === "minecraft:soul_soil") && $level->getBlockAt($x, $y + 1, $z)->canBeReplaced()){
				$blockYs[] = $y + 1;
			}
		}
		return $blockYs;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure;

use dimension\generator\math\Int64;
use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use dimension\generator\random\LegacyRandom;
use dimension\generator\random\Xoroshiro128;
use dimension\generator\structure\fortress\FortressLayout;
use dimension\generator\structure\placement\NetherComplexPlacement;
use dimension\generator\structure\placement\StructurePlacement;
use pocketmine\world\ChunkManager;
use function abs;
use function array_key_first;
use function count;

/**
 * Nether fortresses. A fortress starts in the chosen chunk of each 30 by 30
 * chunk region that does not hold a bastion. Its layout depends on the
 * world seed only, so every chunk the fortress crosses rebuilds the same
 * layout and writes its own part, with the chunk random the whole fortress
 * would use for that chunk.
 */
final class NetherFortressPopulator implements Populator{

	private const SEARCH_RADIUS_CHUNKS = 10;
	private const CACHE_SIZE = 8;

	/** @var array<string, FortressLayout|null> */
	private static array $layouts = [];

	private Xoroshiro128 $random;

	public function __construct(
		private int $seed,
		private string $resourceFolder
	){
		$this->random = new Xoroshiro128(0);
	}

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$size = NetherComplexPlacement::REGION_SIZE_CHUNKS;
		$minRegionX = StructurePlacement::floorDiv($chunkX - self::SEARCH_RADIUS_CHUNKS, $size);
		$maxRegionX = StructurePlacement::floorDiv($chunkX + self::SEARCH_RADIUS_CHUNKS, $size);
		$minRegionZ = StructurePlacement::floorDiv($chunkZ - self::SEARCH_RADIUS_CHUNKS, $size);
		$maxRegionZ = StructurePlacement::floorDiv($chunkZ + self::SEARCH_RADIUS_CHUNKS, $size);
		for($regionX = $minRegionX; $regionX <= $maxRegionX; ++$regionX){
			for($regionZ = $minRegionZ; $regionZ <= $maxRegionZ; ++$regionZ){
				[$startX, $startZ] = NetherComplexPlacement::regionStart($seed, $regionX, $regionZ, $this->random);
				if(abs($startX - $chunkX) > self::SEARCH_RADIUS_CHUNKS || abs($startZ - $chunkZ) > self::SEARCH_RADIUS_CHUNKS){
					continue;
				}
				$this->populateFrom($root, $world, $seed, $startX, $startZ, $chunkX, $chunkZ);
			}
		}
	}

	private function populateFrom(BlockManager $root, ChunkManager $world, int $seed, int $startX, int $startZ, int $chunkX, int $chunkZ) : void{
		if(!NetherComplexPlacement::isNetherComplexStart($seed, $startX, $startZ, $this->random) || NetherComplexPlacement::shouldGenerateBastion($seed, $startX, $startZ, $this->random)){
			return;
		}
		$this->random->setSeed($seed);
		$r1 = $this->random->nextInt();
		$r2 = $this->random->nextInt();

		$layout = $this->layout($seed, $startX, $startZ);
		if($layout === null || !$layout->isValid()){
			return;
		}
		$box = $layout->getBoundingBox();
		if($chunkX < ($box->x0 >> 4) || $chunkX > ($box->x1 >> 4) || $chunkZ < ($box->z0 >> 4) || $chunkZ > ($box->z1 >> 4)){
			return;
		}
		$chunkRandom = new LegacyRandom(Int64::mul($chunkX, $r1) ^ Int64::mul($chunkZ, $r2) ^ $seed);
		$manager = new BlockManager($world);
		$layout->postProcess($manager, $chunkRandom, BoundingBox::chunk($chunkX, $chunkZ), $chunkX, $chunkZ);
		$root->merge($manager);
	}

	private function layout(int $seed, int $startX, int $startZ) : ?FortressLayout{
		$key = $seed . ":" . $startX . ":" . $startZ;
		if(!isset(self::$layouts[$key])){
			if(count(self::$layouts) >= self::CACHE_SIZE){
				unset(self::$layouts[array_key_first(self::$layouts)]);
			}
			self::$layouts[$key] = new FortressLayout($seed, $startX, $startZ);
		}
		return self::$layouts[$key];
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure;

use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use dimension\generator\random\Xoroshiro128;
use dimension\generator\structure\jigsaw\BastionLayout;
use dimension\generator\structure\placement\NetherComplexPlacement;
use dimension\generator\structure\placement\StructurePlacement;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;
use function abs;
use function array_key_exists;
use function array_key_first;
use function count;

/**
 * Bastion remnants. A bastion starts in the chosen chunk of each 30 by 30
 * chunk region picked for bastions, unless that chunk lies in a basalt
 * delta, and is assembled from template pools with its lowest corner at
 * Y 33. The assembly depends on the world seed only, so every chunk the
 * bastion crosses rebuilds the same plan and writes its own part.
 *
 * The basalt delta check needs the biome of the start chunk, known only
 * when that chunk is part of the population area. Farther chunks assume
 * the start chunk is not a basalt delta.
 */
final class BastionRemnantPopulator implements Populator{

	private const ORIGIN_Y = 33;
	private const SEARCH_RADIUS_CHUNKS = 10;
	private const CACHE_SIZE = 4;

	/** @var array<string, StructureBuffer|null> */
	private static array $plans = [];

	private Xoroshiro128 $random;

	public function __construct(
		private int $seed,
		private string $resourceFolder
	){
		$this->random = new Xoroshiro128(0);
	}

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$size = NetherComplexPlacement::REGION_SIZE_CHUNKS;
		$minRegionX = StructurePlacement::floorDiv($chunkX - self::SEARCH_RADIUS_CHUNKS, $size);
		$maxRegionX = StructurePlacement::floorDiv($chunkX + self::SEARCH_RADIUS_CHUNKS, $size);
		$minRegionZ = StructurePlacement::floorDiv($chunkZ - self::SEARCH_RADIUS_CHUNKS, $size);
		$maxRegionZ = StructurePlacement::floorDiv($chunkZ + self::SEARCH_RADIUS_CHUNKS, $size);
		for($regionX = $minRegionX; $regionX <= $maxRegionX; ++$regionX){
			for($regionZ = $minRegionZ; $regionZ <= $maxRegionZ; ++$regionZ){
				[$startX, $startZ] = NetherComplexPlacement::regionStart($seed, $regionX, $regionZ, $this->random);
				if(abs($startX - $chunkX) > self::SEARCH_RADIUS_CHUNKS || abs($startZ - $chunkZ) > self::SEARCH_RADIUS_CHUNKS){
					continue;
				}
				if($this->startBiome($world, $startX, $startZ) === BiomeIds::BASALT_DELTAS){
					continue;
				}
				$plan = $this->plan($seed, $startX, $startZ);
				if($plan !== null){
					$plan->emit($root, $chunkX, $chunkZ);
				}
			}
		}
	}

	/**
	 * Biome sampled at the start chunk, -1 when that chunk is not available.
	 */
	private function startBiome(ChunkManager $world, int $startX, int $startZ) : int{
		$chunk = $world->getChunk($startX, $startZ);
		if($chunk === null){
			return -1;
		}
		return $chunk->getBiomeId(7, self::ORIGIN_Y, 7);
	}

	private function plan(int $seed, int $startX, int $startZ) : ?StructureBuffer{
		$key = $seed . ":" . $startX . ":" . $startZ;
		if(array_key_exists($key, self::$plans)){
			return self::$plans[$key];
		}
		if(count(self::$plans) >= self::CACHE_SIZE){
			unset(self::$plans[array_key_first(self::$plans)]);
		}
		return self::$plans[$key] = $this->buildPlan($seed, $startX, $startZ);
	}

	private function buildPlan(int $seed, int $startX, int $startZ) : ?StructureBuffer{
		if(!NetherComplexPlacement::isNetherComplexStart($seed, $startX, $startZ, $this->random) || !NetherComplexPlacement::shouldGenerateBastion($seed, $startX, $startZ, $this->random)){
			return null;
		}
		$buffer = new StructureBuffer();
		(new BastionLayout())->assemble($buffer, $startX << 4, self::ORIGIN_Y, $startZ << 4, $this->resourceFolder, $this->random->fork());
		return $buffer->isEmpty() ? null : $buffer;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\jigsaw;

use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\template\JigsawConnector;
use dimension\generator\structure\template\StructureTemplate;

/**
 * A template placed during jigsaw assembly, at a position relative to the
 * structure origin.
 */
final class JigsawPiece{

	/**
	 * @param list<JigsawConnector> $sourceJigsaws jigsaws of the unrotated template
	 * @param list<JigsawConnector> $placedJigsaws the same jigsaws after rotation, relative to the piece
	 */
	public function __construct(
		public readonly string $structureName,
		public readonly StructureTemplate $source,
		public readonly int $x,
		public readonly int $y,
		public readonly int $z,
		public readonly int $rotation,
		public readonly BoundingBox $boundingBox,
		public readonly array $sourceJigsaws,
		public readonly array $placedJigsaws
	){}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\jigsaw;

use dimension\generator\structure\BoundingBox;

/**
 * Template pools of the bastion remnant: four layouts (units, hoglin
 * stable, treasure room, bridge) drawn from the start pool, assembled six
 * pieces deep. Pieces may overlap one another.
 */
final class BastionLayout extends JigsawAssembler{

	private const START = "bastion/starts";

	/** @var array<string, StructurePool>|null */
	private static ?array $pools = null;

	protected function getEntryPool() : string{
		return self::START;
	}

	protected function getMaxDepth() : int{
		return 6;
	}

	protected function strictlyIntersects(BoundingBox $first, BoundingBox $second) : bool{
		return false;
	}

	protected function getPools() : array{
		return self::$pools ??= self::buildPools();
	}

	/**
	 * @param array<string, int> $entries template name => weight
	 */
	private static function pool(string $name, array $entries) : StructurePool{
		$list = [];
		foreach($entries as $structureName => $weight){
			$list[] = new PoolEntry($structureName, $weight);
		}
		return new StructurePool($name, $list);
	}

	/**
	 * @param list<array{string, int}> $entries
	 */
	private static function poolList(string $name, array $entries) : StructurePool{
		$list = [];
		foreach($entries as [$structureName, $weight]){
			$list[] = new PoolEntry($structureName, $weight);
		}
		return new StructurePool($name, $list);
	}

	/**
	 * @return array<string, StructurePool>
	 */
	private static function buildPools() : array{
		$definitions = [
			self::START => [
				"bastion/units/air_base" => 1,
				"bastion/hoglin_stable/air_base" => 1,
				"bastion/treasure/big_air_full" => 1,
				"bastion/bridge/starting_pieces/entrance_base" => 1
			],
			"bastion/units/center_pieces" => [
				"bastion/units/center_pieces/center_0" => 1,
				"bastion/units/center_pieces/center_1" => 1,
				"bastion/units/center_pieces/center_2" => 1
			],
			"bastion/units/pathways" => [
				"bastion/units/pathways/pathway_0" => 1,
				"bastion/units/pathways/pathway_wall_0" => 1
			],
			"bastion/units/walls/wall_bases" => [
				"bastion/units/walls/wall_base" => 1,
				"bastion/units/walls/connected_wall" => 1
			],
			"bastion/units/stages/stage_0" => [
				"bastion/units/stages/stage_0_0" => 1,
				"bastion/units/stages/stage_0_1" => 1,
				"bastion/units/stages/stage_0_2" => 1,
				"bastion/units/stages/stage_0_3" => 1
			],
			"bastion/units/stages/stage_1" => [
				"bastion/units/stages/stage_1_0" => 1,
				"bastion/units/stages/stage_1_1" => 1,
				"bastion/units/stages/stage_1_2" => 1,
				"bastion/units/stages/stage_1_3" => 1
			],
			"bastion/units/stages/rot/stage_1" => [
				"bastion/units/stages/rot/stage_1_0" => 1
			],
			"bastion/units/stages/stage_2" => [
				"bastion/units/stages/stage_2_0" => 1,
				"bastion/units/stages/stage_2_1" => 1
			],
			"bastion/units/stages/stage_3" => [
				"bastion/units/stages/stage_3_0" => 1,
				"bastion/units/stages/stage_3_1" => 1,
				"bastion/units/stages/stage_3_2" => 1,
				"bastion/units/stages/stage_3_3" => 1
			],
			"bastion/units/fillers/stage_0" => [
				"bastion/units/fillers/stage_0" => 1
			],
			"bastion/units/edges" => [
				"bastion/units/edges/edge_0" => 1
			],
			"bastion/units/wall_units" => [
				"bastion/units/wall_units/unit_0" => 1
			],
			"bastion/units/edge_wall_units" => [
				"bastion/units/wall_units/edge_0_large" => 1
			],
			"bastion/units/ramparts" => [
				"bastion/units/ramparts/ramparts_0" => 1,
				"bastion/units/ramparts/ramparts_1" => 1,
				"bastion/units/ramparts/ramparts_2" => 1
			],
			"bastion/units/large_ramparts" => [
				"bastion/units/ramparts/ramparts_0" => 1
			],
			"bastion/units/rampart_plates" => [
				"bastion/units/rampart_plates/plate_0" => 1
			],
			"bastion/hoglin_stable/starting_pieces" => [
				"bastion/hoglin_stable/starting_pieces/starting_stairs_0" => 1,
				"bastion/hoglin_stable/starting_pieces/starting_stairs_1" => 1,
				"bastion/hoglin_stable/starting_pieces/starting_stairs_2" => 1,
				"bastion/hoglin_stable/starting_pieces/starting_stairs_3" => 1,
				"bastion/hoglin_stable/starting_pieces/starting_stairs_4" => 1
			],
			"bastion/hoglin_stable/mirrored_starting_pieces" => [
				"bastion/hoglin_stable/starting_pieces/stairs_0_mirrored" => 1,
				"bastion/hoglin_stable/starting_pieces/stairs_1_mirrored" => 1,
				"bastion/hoglin_stable/starting_pieces/stairs_2_mirrored" => 1,
				"bastion/hoglin_stable/starting_pieces/stairs_3_mirrored" => 1,
				"bastion/hoglin_stable/starting_pieces/stairs_4_mirrored" => 1
			],
			"bastion/hoglin_stable/wall_bases" => [
				"bastion/hoglin_stable/walls/wall_base" => 1
			],
			"bastion/hoglin_stable/walls" => [
				"bastion/hoglin_stable/walls/side_wall_0" => 1,
				"bastion/hoglin_stable/walls/side_wall_1" => 1
			],
			"bastion/hoglin_stable/stairs" => [
				"bastion/hoglin_stable/stairs/stairs_1_0" => 1,
				"bastion/hoglin_stable/stairs/stairs_1_1" => 1,
				"bastion/hoglin_stable/stairs/stairs_1_2" => 1,
				"bastion/hoglin_stable/stairs/stairs_1_3" => 1,
				"bastion/hoglin_stable/stairs/stairs_1_4" => 1,
				"bastion/hoglin_stable/stairs/stairs_2_0" => 1,
				"bastion/hoglin_stable/stairs/stairs_2_1" => 1,
				"bastion/hoglin_stable/stairs/stairs_2_2" => 1,
				"bastion/hoglin_stable/stairs/stairs_2_3" => 1,
				"bastion/hoglin_stable/stairs/stairs_2_4" => 1,
				"bastion/hoglin_stable/stairs/stairs_3_0" => 1,
				"bastion/hoglin_stable/stairs/stairs_3_1" => 1,
				"bastion/hoglin_stable/stairs/stairs_3_2" => 1,
				"bastion/hoglin_stable/stairs/stairs_3_3" => 1,
				"bastion/hoglin_stable/stairs/stairs_3_4" => 1
			],
			"bastion/hoglin_stable/small_stables/inner" => [
				"bastion/hoglin_stable/small_stables/inner_0" => 1,
				"bastion/hoglin_stable/small_stables/inner_1" => 1,
				"bastion/hoglin_stable/small_stables/inner_2" => 1,
				"bastion/hoglin_stable/small_stables/inner_3" => 1
			],
			"bastion/hoglin_stable/small_stables/outer" => [
				"bastion/hoglin_stable/small_stables/outer_0" => 1,
				"bastion/hoglin_stable/small_stables/outer_1" => 1,
				"bastion/hoglin_stable/small_stables/outer_2" => 1,
				"bastion/hoglin_stable/small_stables/outer_3" => 1
			],
			"bastion/hoglin_stable/large_stables/inner" => [
				"bastion/hoglin_stable/large_stables/inner_0" => 1,
				"bastion/hoglin_stable/large_stables/inner_1" => 1,
				"bastion/hoglin_stable/large_stables/inner_2" => 1,
				"bastion/hoglin_stable/large_stables/inner_3" => 1,
				"bastion/hoglin_stable/large_stables/inner_4" => 1
			],
			"bastion/hoglin_stable/large_stables/outer" => [
				"bastion/hoglin_stable/large_stables/outer_0" => 1,
				"bastion/hoglin_stable/large_stables/outer_1" => 1,
				"bastion/hoglin_stable/large_stables/outer_2" => 1,
				"bastion/hoglin_stable/large_stables/outer_3" => 1,
				"bastion/hoglin_stable/large_stables/outer_4" => 1
			],
			"bastion/hoglin_stable/posts" => [
				"bastion/hoglin_stable/posts/stair_post" => 1,
				"bastion/hoglin_stable/posts/end_post" => 1
			],
			"bastion/hoglin_stable/ramparts" => [
				"bastion/hoglin_stable/ramparts/ramparts_1" => 1,
				"bastion/hoglin_stable/ramparts/ramparts_2" => 1,
				"bastion/hoglin_stable/ramparts/ramparts_3" => 1
			],
			"bastion/hoglin_stable/rampart_plates" => [
				"bastion/hoglin_stable/rampart_plates/rampart_plate_1" => 1
			],
			"bastion/hoglin_stable/connectors" => [
				"bastion/hoglin_stable/connectors/end_post_connector" => 1
			],
			"bastion/treasure/bases" => [
				"bastion/treasure/bases/lava_basin" => 1
			],
			"bastion/treasure/stairs" => [
				"bastion/treasure/stairs/lower_stairs" => 1
			],
			"bastion/treasure/bases/centers" => [
				"bastion/treasure/bases/centers/center_0" => 1,
				"bastion/treasure/bases/centers/center_1" => 1,
				"bastion/treasure/bases/centers/center_2" => 1,
				"bastion/treasure/bases/centers/center_3" => 1
			],
			"bastion/treasure/brains" => [
				"bastion/treasure/brains/center_brain" => 1
			],
			"bastion/treasure/walls" => [
				"bastion/treasure/walls/lava_wall" => 1,
				"bastion/treasure/walls/entrance_wall" => 1
			],
			"bastion/treasure/walls/outer" => [
				"bastion/treasure/walls/outer/top_corner" => 1,
				"bastion/treasure/walls/outer/mid_corner" => 1,
				"bastion/treasure/walls/outer/bottom_corner" => 1,
				"bastion/treasure/walls/outer/outer_wall" => 1,
				"bastion/treasure/walls/outer/medium_outer_wall" => 1,
				"bastion/treasure/walls/outer/tall_outer_wall" => 1
			],
			"bastion/treasure/walls/bottom" => [
				"bastion/treasure/walls/bottom/wall_0" => 1,
				"bastion/treasure/walls/bottom/wall_1" => 1,
				"bastion/treasure/walls/bottom/wall_2" => 1,
				"bastion/treasure/walls/bottom/wall_3" => 1
			],
			"bastion/treasure/walls/mid" => [
				"bastion/treasure/walls/mid/wall_0" => 1,
				"bastion/treasure/walls/mid/wall_1" => 1,
				"bastion/treasure/walls/mid/wall_2" => 1
			],
			"bastion/treasure/walls/top" => [
				"bastion/treasure/walls/top/main_entrance" => 1,
				"bastion/treasure/walls/top/wall_0" => 1,
				"bastion/treasure/walls/top/wall_1" => 1
			],
			"bastion/treasure/connectors" => [
				"bastion/treasure/connectors/center_to_wall_middle" => 1,
				"bastion/treasure/connectors/center_to_wall_top" => 1,
				"bastion/treasure/connectors/center_to_wall_top_entrance" => 1
			],
			"bastion/treasure/entrances" => [
				"bastion/treasure/entrances/entrance_0" => 1
			],
			"bastion/treasure/ramparts" => [
				"bastion/treasure/ramparts/mid_wall_main" => 1,
				"bastion/treasure/ramparts/mid_wall_side" => 1,
				"bastion/treasure/ramparts/bottom_wall_0" => 1,
				"bastion/treasure/ramparts/top_wall" => 1,
				"bastion/treasure/ramparts/lava_basin_side" => 1,
				"bastion/treasure/ramparts/lava_basin_main" => 1
			],
			"bastion/treasure/corners/bottom" => [
				"bastion/treasure/corners/bottom/corner_0" => 1,
				"bastion/treasure/corners/bottom/corner_1" => 1
			],
			"bastion/treasure/corners/edges" => [
				"bastion/treasure/corners/edges/bottom" => 1,
				"bastion/treasure/corners/edges/middle" => 1,
				"bastion/treasure/corners/edges/top" => 1
			],
			"bastion/treasure/corners/middle" => [
				"bastion/treasure/corners/middle/corner_0" => 1,
				"bastion/treasure/corners/middle/corner_1" => 1
			],
			"bastion/treasure/corners/top" => [
				"bastion/treasure/corners/top/corner_0" => 1,
				"bastion/treasure/corners/top/corner_1" => 1
			],
			"bastion/treasure/extensions/houses" => [
				"bastion/treasure/extensions/house_0" => 1,
				"bastion/treasure/extensions/house_1" => 1
			],
			"bastion/treasure/roofs" => [
				"bastion/treasure/roofs/wall_roof" => 1,
				"bastion/treasure/roofs/corner_roof" => 1,
				"bastion/treasure/roofs/center_roof" => 1
			],
			"bastion/bridge/starting_pieces" => [
				"bastion/bridge/starting_pieces/entrance" => 1,
				"bastion/bridge/starting_pieces/entrance_face" => 1
			],
			"bastion/bridge/bridge_pieces" => [
				"bastion/bridge/bridge_pieces/bridge" => 1
			],
			"bastion/bridge/legs" => [
				"bastion/bridge/legs/leg_0" => 1,
				"bastion/bridge/legs/leg_1" => 1
			],
			"bastion/bridge/walls" => [
				"bastion/bridge/walls/wall_base_0" => 1,
				"bastion/bridge/walls/wall_base_1" => 1
			],
			"bastion/bridge/ramparts" => [
				"bastion/bridge/ramparts/rampart_0" => 1,
				"bastion/bridge/ramparts/rampart_1" => 1
			],
			"bastion/bridge/rampart_plates" => [
				"bastion/bridge/rampart_plates/plate_0" => 1
			],
			"bastion/bridge/connectors" => [
				"bastion/bridge/connectors/back_bridge_top" => 1,
				"bastion/bridge/connectors/back_bridge_bottom" => 1
			],
			"bastion/mobs/piglin" => [
				"bastion/mobs/melee_piglin" => 1,
				"bastion/mobs/sword_piglin" => 4,
				"bastion/mobs/crossbow_piglin" => 4,
				"bastion/mobs/empty" => 1
			],
			"bastion/mobs/hoglin" => [
				"bastion/mobs/hoglin" => 2,
				"bastion/mobs/empty" => 1
			],
			"bastion/blocks/gold" => [
				"bastion/blocks/air" => 3,
				"bastion/blocks/gold" => 1
			],
			"bastion/mobs/piglin_melee" => [
				"bastion/mobs/melee_piglin_always" => 1,
				"bastion/mobs/melee_piglin" => 5,
				"bastion/mobs/sword_piglin" => 1
			]
		];
		$pools = [];
		foreach($definitions as $name => $entries){
			$pools[$name] = self::pool($name, $entries);
		}
		$pools["bastion/treasure/extensions/large_pool"] = self::poolList("bastion/treasure/extensions/large_pool", [
			["bastion/treasure/extensions/empty", 1],
			["bastion/treasure/extensions/empty", 1],
			["bastion/treasure/extensions/fire_room", 1],
			["bastion/treasure/extensions/large_bridge_0", 1],
			["bastion/treasure/extensions/large_bridge_1", 1],
			["bastion/treasure/extensions/large_bridge_2", 1],
			["bastion/treasure/extensions/large_bridge_3", 1],
			["bastion/treasure/extensions/roofed_bridge", 1],
			["bastion/treasure/extensions/empty", 1]
		]);
		$pools["bastion/treasure/extensions/small_pool"] = self::poolList("bastion/treasure/extensions/small_pool", [
			["bastion/treasure/extensions/empty", 1],
			["bastion/treasure/extensions/fire_room", 1],
			["bastion/treasure/extensions/empty", 1],
			["bastion/treasure/extensions/small_bridge_0", 1],
			["bastion/treasure/extensions/small_bridge_1", 1],
			["bastion/treasure/extensions/small_bridge_2", 1],
			["bastion/treasure/extensions/small_bridge_3", 1]
		]);
		return $pools;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\jigsaw;

use dimension\generator\random\RandomSource;

/**
 * Weighted list of templates a jigsaw block can pull a piece from.
 */
final class StructurePool{

	/**
	 * @param list<PoolEntry> $entries
	 */
	public function __construct(
		public readonly string $name,
		public readonly array $entries,
		public readonly ?string $fallback = null
	){}

	public function getRandomEntry(RandomSource $random) : PoolEntry{
		$totalWeight = 0;
		foreach($this->entries as $entry){
			$totalWeight += $entry->weight;
		}
		$target = $random->nextBoundedInt($totalWeight - 1);
		foreach($this->entries as $entry){
			if($target < $entry->weight){
				return $entry;
			}
			$target -= $entry->weight;
		}
		throw new \RuntimeException("Failed to select structure from pool '$this->name'");
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\jigsaw;

use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructureBuffer;
use dimension\generator\structure\template\JigsawConnector;
use dimension\generator\structure\template\Rotation;
use dimension\generator\structure\template\StateBlocks;
use dimension\generator\structure\template\StructureTemplate;
use dimension\generator\structure\template\StructureTemplates;
use pocketmine\math\Facing;
use function array_shift;
use function array_splice;
use function count;
use function strpos;
use function substr;
use function trim;
use function usort;

/**
 * Assembles a structure from template pools: a start piece is drawn from
 * the entry pool, then every jigsaw block of every placed piece pulls a
 * matching piece from its pool, breadth first by placement priority, until
 * the maximum depth is reached. Jigsaw blocks are then replaced by their
 * final state.
 */
abstract class JigsawAssembler{

	private const AIR = "minecraft:air";
	private const JIGSAW = "minecraft:jigsaw";
	private const STRUCTURE_VOID = "minecraft:structure_void";

	private string $resourceFolder = "";
	private int $originX = 0;
	private int $originY = 0;
	private int $originZ = 0;

	/**
	 * @return array<string, StructurePool>
	 */
	abstract protected function getPools() : array;

	abstract protected function getEntryPool() : string;

	abstract protected function getMaxDepth() : int;

	protected function strictlyIntersects(BoundingBox $first, BoundingBox $second) : bool{
		return $first->x1 >= $second->x0 && $first->x0 <= $second->x1
			&& $first->y1 >= $second->y0 && $first->y0 <= $second->y1
			&& $first->z1 >= $second->z0 && $first->z0 <= $second->z1;
	}

	/**
	 * Plans the whole structure into $buffer, the start piece having its
	 * minimum corner at the origin.
	 */
	final public function assemble(StructureBuffer $buffer, int $originX, int $originY, int $originZ, string $resourceFolder, RandomSource $random) : void{
		$this->resourceFolder = $resourceFolder;
		$this->originX = $originX;
		$this->originY = $originY;
		$this->originZ = $originZ;
		$pools = $this->getPools();

		$startPool = $pools[self::normalize($this->getEntryPool())] ?? null;
		if($startPool === null){
			return;
		}
		$rootPiece = $this->placeFromPool($this->getRandomRotation($random), $buffer, $startPool, $random);
		if($rootPiece === null){
			return;
		}

		$connected = [];
		$occupied = [$rootPiece->boundingBox];
		$pending = [[$rootPiece, 0, 0]];
		$maxDepth = $this->getMaxDepth();

		while(count($pending) > 0){
			[$piece, $depth] = array_shift($pending);
			if($depth >= $maxDepth){
				continue;
			}
			foreach($this->getOrderedJigsaws($piece->sourceJigsaws, $piece->placedJigsaws, $random) as [$sourceJigsaw, $placedJigsaw]){
				$parentKey = self::key($piece->x + $placedJigsaw->x, $piece->y + $placedJigsaw->y, $piece->z + $placedJigsaw->z);
				if(isset($connected[$parentKey])){
					continue;
				}
				$nextPool = $pools[self::normalize($sourceJigsaw->pool)] ?? null;
				if($nextPool === null){
					continue;
				}
				$candidate = $this->findCandidate($pools, $nextPool, $piece, $sourceJigsaw, $placedJigsaw, $random, $connected, $occupied);
				if($candidate === null){
					continue;
				}
				[$structureName, $childTemplate, $connection] = $candidate;
				[$childRotation, $childX, $childY, $childZ, $jigsawX, $jigsawY, $jigsawZ] = $connection;
				$childPiece = $this->placePiece($childX, $childY, $childZ, $childRotation, $buffer, $structureName, $childTemplate, $occupied, $piece->boundingBox);
				if($childPiece === null){
					continue;
				}
				$connected[$parentKey] = true;
				$connected[self::key($jigsawX, $jigsawY, $jigsawZ)] = true;
				$occupied[] = $childPiece->boundingBox;
				if($depth + 1 < $maxDepth){
					$this->insertPendingPiece($pending, [$childPiece, $depth + 1, $sourceJigsaw->placementPriority]);
				}
			}
		}
	}

	private static function key(int $x, int $y, int $z) : string{
		return $x . ":" . $y . ":" . $z;
	}

	private static function normalize(string $key) : string{
		$separator = strpos($key, ":");
		return $separator === false ? $key : substr($key, $separator + 1);
	}

	private static function isEmptyPoolEntry(string $key) : bool{
		$normalized = self::normalize($key);
		return $normalized === "" || $normalized === "empty";
	}

	private function template(string $name) : ?StructureTemplate{
		return StructureTemplates::get($this->resourceFolder, $name);
	}

	private function placeFromPool(int $rotation, StructureBuffer $buffer, StructurePool $pool, RandomSource $random) : ?JigsawPiece{
		$entry = $pool->getRandomEntry($random);
		if(self::isEmptyPoolEntry($entry->structureName)){
			return null;
		}
		$structureName = self::normalize($entry->structureName);
		$template = $this->template($structureName);
		if($template === null){
			return null;
		}
		return $this->placePiece(0, 0, 0, $rotation, $buffer, $structureName, $template, null, null);
	}

	/**
	 * @param list<BoundingBox>|null $occupied
	 */
	private function placePiece(int $x, int $y, int $z, int $rotation, StructureBuffer $buffer, string $structureName, StructureTemplate $template, ?array $occupied, ?BoundingBox $ignored) : ?JigsawPiece{
		$placed = $rotation === Rotation::NONE ? $template : $template->rotateGrid($rotation);
		$box = self::createBoundingBox($x, $y, $z, $placed);
		if($occupied !== null && $this->overlapsExistingBox($occupied, $box, $ignored)){
			return null;
		}
		$worldX = $this->originX + $x;
		$worldY = $this->originY + $y;
		$worldZ = $this->originZ + $z;
		$volume = $placed->getVolume();
		for($index = 0; $index < $volume; ++$index){
			$state = $placed->stateAt($index);
			if($state === null){
				continue;
			}
			$buffer->set($worldX + $placed->xOf($index), $worldY + $placed->yOf($index), $worldZ + $placed->zOf($index), $state);
		}
		foreach($placed->getJigsaws() as $jigsaw){
			$finalState = $jigsaw->finalState;
			if($finalState !== null && $finalState->getName() === self::STRUCTURE_VOID){
				continue;
			}
			if($finalState === null || $finalState->getName() === self::JIGSAW){
				$finalState = StateBlocks::state(self::AIR);
				if($finalState === null){
					continue;
				}
			}
			$buffer->set($worldX + $jigsaw->x, $worldY + $jigsaw->y, $worldZ + $jigsaw->z, $finalState);
		}
		return new JigsawPiece($structureName, $template, $x, $y, $z, $rotation, $box, $template->getJigsaws(), $placed->getJigsaws());
	}

	/**
	 * @param array<string, StructurePool> $pools
	 * @param array<string, true>          $connected
	 * @param list<BoundingBox>            $occupied
	 *
	 * @return array{string, StructureTemplate, array{int, int, int, int, int, int, int}}|null
	 */
	private function findCandidate(array $pools, StructurePool $pool, JigsawPiece $parent, JigsawConnector $sourceJigsaw, JigsawConnector $placedJigsaw, RandomSource $random, array $connected, array $occupied) : ?array{
		foreach($this->getCandidateEntries($pools, $pool, $random) as $entry){
			if(self::isEmptyPoolEntry($entry->structureName)){
				return null;
			}
			$structureKey = self::normalize($entry->structureName);
			$childTemplate = $this->template($structureKey);
			if($childTemplate === null){
				continue;
			}
			foreach($this->getShuffledRotations($random) as $childRotation){
				$connection = $this->resolveConnection($parent, $sourceJigsaw, $placedJigsaw, $childTemplate, $childRotation, $random);
				if($connection === null){
					continue;
				}
				[$connection, $childBox] = $connection;
				if(isset($connected[self::key($connection[4], $connection[5], $connection[6])])){
					continue;
				}
				if($this->overlapsExistingBox($occupied, $childBox, $parent->boundingBox)){
					continue;
				}
				return [$structureKey, $childTemplate, $connection];
			}
		}
		return null;
	}

	/**
	 * @param array<string, StructurePool> $pools
	 *
	 * @return list<PoolEntry>
	 */
	private function getCandidateEntries(array $pools, StructurePool $pool, RandomSource $random) : array{
		$weighted = [];
		foreach($pool->entries as $entry){
			for($i = 0; $i < $entry->weight; ++$i){
				$weighted[] = $entry;
			}
		}
		if($pool->fallback !== null){
			$fallbackPool = $pools[self::normalize($pool->fallback)] ?? null;
			if($fallbackPool !== null){
				foreach($fallbackPool->entries as $entry){
					for($i = 0; $i < $entry->weight; ++$i){
						$weighted[] = $entry;
					}
				}
			}
		}
		for($i = count($weighted) - 1; $i > 0; --$i){
			$index = $random->nextBoundedInt($i);
			$value = $weighted[$i];
			$weighted[$i] = $weighted[$index];
			$weighted[$index] = $value;
		}
		return $weighted;
	}

	/**
	 * @return array{array{int, int, int, int, int, int, int}, BoundingBox}|null
	 */
	private function resolveConnection(JigsawPiece $parent, JigsawConnector $sourceJigsaw, JigsawConnector $placedJigsaw, StructureTemplate $childTemplate, int $childRotation, RandomSource $random) : ?array{
		$parentOrientation = $this->getJigsawOrientation($parent->source, $sourceJigsaw, $parent->rotation);
		if($parentOrientation === null){
			return null;
		}
		[$parentFront, $parentTop] = $parentOrientation;
		$parentJoint = $this->getJigsawJoint($sourceJigsaw, $parentFront);
		$parentWorldX = $parent->x + $placedJigsaw->x;
		$parentWorldY = $parent->y + $placedJigsaw->y;
		$parentWorldZ = $parent->z + $placedJigsaw->z;
		$rotatedChild = $childRotation === Rotation::NONE ? $childTemplate : $childTemplate->rotateGrid($childRotation);
		$target = self::normalize($sourceJigsaw->target);

		foreach($this->getOrderedJigsaws($childTemplate->getJigsaws(), $rotatedChild->getJigsaws(), $random) as [$childSource, $childPlaced]){
			if(self::normalize($childSource->name) !== $target){
				continue;
			}
			$childOrientation = $this->getJigsawOrientation($childTemplate, $childSource, $childRotation);
			if($childOrientation === null){
				continue;
			}
			[$childFront, $childTop] = $childOrientation;
			if($parentFront !== Facing::opposite($childFront)){
				continue;
			}
			if($parentJoint !== "rollable" && $parentTop !== $childTop){
				continue;
			}
			[$dx, $dy, $dz] = self::offset($parentFront);
			$childWorldX = $parentWorldX + $dx;
			$childWorldY = $parentWorldY + $dy;
			$childWorldZ = $parentWorldZ + $dz;
			$childX = $childWorldX - $childPlaced->x;
			$childY = $childWorldY - $childPlaced->y;
			$childZ = $childWorldZ - $childPlaced->z;
			return [
				[$childRotation, $childX, $childY, $childZ, $childWorldX, $childWorldY, $childWorldZ],
				self::createBoundingBox($childX, $childY, $childZ, $rotatedChild)
			];
		}
		return null;
	}

	/**
	 * Front and top faces of a jigsaw block once its template is turned by
	 * $appliedRotation, or null when the position holds no jigsaw block.
	 *
	 * @return array{int, int}|null
	 */
	private function getJigsawOrientation(StructureTemplate $template, JigsawConnector $jigsaw, int $appliedRotation) : ?array{
		$state = $template->stateAt($template->index($jigsaw->x, $jigsaw->y, $jigsaw->z));
		if($state === null || $state->getName() !== self::JIGSAW){
			return null;
		}
		$facing = $state->getState("facing_direction")?->getValue();
		$front = self::rotateFace((int) ($facing ?? 0) & 0x7, $appliedRotation);
		if(self::isHorizontal($front)){
			return [$front, Facing::UP];
		}
		$blockRotation = (int) ($state->getState("rotation")?->getValue() ?? 0);
		if($front === Facing::DOWN){
			$top = match($blockRotation){
				1 => Facing::WEST,
				2 => Facing::SOUTH,
				3 => Facing::EAST,
				default => Facing::NORTH
			};
		}else{
			$top = match($blockRotation){
				1 => Facing::EAST,
				2 => Facing::SOUTH,
				3 => Facing::WEST,
				default => Facing::NORTH
			};
		}
		return [$front, self::rotateFace($top, $appliedRotation)];
	}

	private function getJigsawJoint(JigsawConnector $jigsaw, int $front) : string{
		if(trim($jigsaw->joint) !== ""){
			return $jigsaw->joint;
		}
		return self::isHorizontal($front) ? "aligned" : "rollable";
	}

	private static function isHorizontal(int $face) : bool{
		return $face === Facing::NORTH || $face === Facing::SOUTH || $face === Facing::EAST || $face === Facing::WEST;
	}

	/**
	 * A grid turn by one step moves faces a quarter turn counterclockwise.
	 */
	private static function rotateFace(int $face, int $rotation) : int{
		return match($rotation){
			Rotation::ROTATE_90 => self::rotateCounterClockwise($face),
			Rotation::ROTATE_180 => self::rotateClockwise(self::rotateClockwise($face)),
			Rotation::ROTATE_270 => self::rotateClockwise($face),
			default => $face
		};
	}

	private static function rotateClockwise(int $face) : int{
		return match($face){
			Facing::NORTH => Facing::EAST,
			Facing::EAST => Facing::SOUTH,
			Facing::SOUTH => Facing::WEST,
			Facing::WEST => Facing::NORTH,
			default => $face
		};
	}

	private static function rotateCounterClockwise(int $face) : int{
		return match($face){
			Facing::NORTH => Facing::WEST,
			Facing::WEST => Facing::SOUTH,
			Facing::SOUTH => Facing::EAST,
			Facing::EAST => Facing::NORTH,
			default => $face
		};
	}

	/**
	 * @return array{int, int, int}
	 */
	private static function offset(int $face) : array{
		return match($face){
			Facing::DOWN => [0, -1, 0],
			Facing::UP => [0, 1, 0],
			Facing::NORTH => [0, 0, -1],
			Facing::SOUTH => [0, 0, 1],
			Facing::WEST => [-1, 0, 0],
			default => [1, 0, 0]
		};
	}

	/**
	 * @return list<int>
	 */
	private function getShuffledRotations(RandomSource $random) : array{
		$rotations = [Rotation::NONE, Rotation::ROTATE_90, Rotation::ROTATE_180, Rotation::ROTATE_270];
		for($i = count($rotations) - 1; $i > 0; --$i){
			$index = $random->nextBoundedInt($i);
			$value = $rotations[$i];
			$rotations[$i] = $rotations[$index];
			$rotations[$index] = $value;
		}
		return $rotations;
	}

	private function getRandomRotation(RandomSource $random) : int{
		$rotations = [Rotation::NONE, Rotation::ROTATE_90, Rotation::ROTATE_180, Rotation::ROTATE_270];
		return $rotations[$random->nextBoundedInt(count($rotations) - 1)];
	}

	/**
	 * Jigsaws in random order, then stably sorted by descending selection
	 * priority, each paired with its rotated counterpart.
	 *
	 * @param list<JigsawConnector> $sourceJigsaws
	 * @param list<JigsawConnector> $placedJigsaws
	 *
	 * @return list<array{JigsawConnector, JigsawConnector}>
	 */
	private function getOrderedJigsaws(array $sourceJigsaws, array $placedJigsaws, RandomSource $random) : array{
		$indices = [];
		for($i = 0, $count = count($sourceJigsaws); $i < $count; ++$i){
			$indices[] = $i;
		}
		for($i = count($indices) - 1; $i > 0; --$i){
			$index = $random->nextBoundedInt($i);
			$value = $indices[$i];
			$indices[$i] = $indices[$index];
			$indices[$index] = $value;
		}
		usort($indices, static function(int $a, int $b) use ($sourceJigsaws) : int{
			return $sourceJigsaws[$b]->selectionPriority <=> $sourceJigsaws[$a]->selectionPriority;
		});
		$pairs = [];
		foreach($indices as $index){
			$pairs[] = [$sourceJigsaws[$index], $placedJigsaws[$index]];
		}
		return $pairs;
	}

	/**
	 * @param list<array{JigsawPiece, int, int}> $queue
	 * @param array{JigsawPiece, int, int}       $pending
	 */
	private function insertPendingPiece(array &$queue, array $pending) : void{
		$index = count($queue);
		foreach($queue as $i => $queued){
			if($pending[2] > $queued[2]){
				$index = $i;
				break;
			}
		}
		array_splice($queue, $index, 0, [$pending]);
	}

	/**
	 * @param list<BoundingBox> $occupied
	 */
	private function overlapsExistingBox(array $occupied, BoundingBox $box, ?BoundingBox $ignored) : bool{
		foreach($occupied as $occupiedBox){
			if($occupiedBox === $ignored){
				continue;
			}
			if($this->strictlyIntersects($occupiedBox, $box)){
				return true;
			}
		}
		return false;
	}

	private static function createBoundingBox(int $x, int $y, int $z, StructureTemplate $template) : BoundingBox{
		return new BoundingBox($x, $y, $z, $x + $template->getSizeX() - 1, $y + $template->getSizeY() - 1, $z + $template->getSizeZ() - 1);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\jigsaw;

/**
 * A template name with its weight in a structure pool.
 */
final class PoolEntry{

	public function __construct(
		public readonly string $structureName,
		public readonly int $weight,
		public readonly string $projection = "rigid"
	){}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;
use pocketmine\math\Facing;

/**
 * T-shaped castle corridor with a balcony looking outwards.
 */
final class CastleCorridorTBalcony extends FortressPiece{

	public function __construct(int $genDepth, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -3, 0, 0, 9, 7, 9, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new CastleCorridorTBalcony($genDepth, $boundingBox, $orientation) : null;
	}

	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
		$horizontalOffset = 1;
		$orientation = $this->getOrientation();
		if($orientation === Facing::WEST || $orientation === Facing::NORTH){
			$horizontalOffset = 5;
		}
		$this->generateChildLeft($start, $pieces, $random, 0, $horizontalOffset, $random->nextBoundedInt(8) > 0);
		$this->generateChildRight($start, $pieces, $random, 0, $horizontalOffset, $random->nextBoundedInt(8) > 0);
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$fence = FortressBlocks::netherBrickFence();
		$air = FortressBlocks::air();
		$this->generateBox($level, $boundingBox, 0, 0, 0, 8, 1, 8, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 8, 5, 8, $air, $air, false);
		$this->generateBox($level, $boundingBox, 0, 6, 0, 8, 6, 5, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 2, 5, 0, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 6, 2, 0, 8, 5, 0, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 3, 0, 1, 4, 0, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 7, 3, 0, 7, 4, 0, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 0, 2, 4, 8, 2, 8, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 1, 4, 2, 2, 4, $air, $air, false);
		$this->generateBox($level, $boundingBox, 6, 1, 4, 7, 2, 4, $air, $air, false);
		$this->generateBox($level, $boundingBox, 1, 3, 8, 7, 3, 8, $fence, $fence, false);
		$this->placeBlock($level, $fence, 0, 3, 8, $boundingBox);
		$this->placeBlock($level, $fence, 8, 3, 8, $boundingBox);
		$this->generateBox($level, $boundingBox, 0, 3, 6, 0, 3, 7, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 8, 3, 6, 8, 3, 7, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 0, 3, 4, 0, 5, 5, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 8, 3, 4, 8, 5, 5, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 3, 5, 2, 5, 5, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 6, 3, 5, 7, 5, 5, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 4, 5, 1, 5, 5, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 7, 4, 5, 7, 5, 5, $fence, $fence, false);

		for($x = 0; $x <= 8; ++$x){
			for($z = 0; $z <= 5; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;
use pocketmine\math\Facing;
use function abs;

/**
 * A nether fortress piece. Pieces grow the fortress by asking for children
 * at their exits: bridge pieces lead to bridge pieces, castle pieces to
 * castle pieces, and a dead end is closed by a crumbling bridge end.
 */
abstract class FortressPiece extends StructurePiece{

	/**
	 * @param list<StructurePiece> $pieces
	 */
	abstract public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece;

	protected static function isOkBox(?BoundingBox $boundingBox) : bool{
		return $boundingBox !== null && $boundingBox->y0 > 10;
	}

	/**
	 * @param list<StructurePiece> $pieces
	 */
	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
	}

	/**
	 * @param list<FortressPieceWeight> $weights
	 */
	private function updatePieceWeight(array $weights) : int{
		$success = false;
		$total = 0;
		foreach($weights as $weight){
			if($weight->maxPlaceCount > 0 && $weight->placeCount < $weight->maxPlaceCount){
				$success = true;
			}
			$total += $weight->weight;
		}
		return $success ? $total : -1;
	}

	/**
	 * @param list<StructurePiece> $pieces
	 */
	private function generatePiece(FortressStartPiece $start, bool $isCastle, array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$weights = $isCastle ? $start->availableCastlePieces : $start->availableBridgePieces;
		$total = $this->updatePieceWeight($weights);
		if($total > 0 && $genDepth <= 30){
			for($i = 0; $i < 5; ++$i){
				$target = $random->nextBoundedInt($total);
				foreach($weights as $weight){
					$target -= $weight->weight;
					if($target < 0){
						if(!$weight->doPlace($genDepth) || ($weight === $start->previousPiece && !$weight->allowInRow)){
							break;
						}
						$type = $weight->pieceType;
						$piece = $type::createPiece($pieces, $random, $x, $y, $z, $orientation, $genDepth);
						if($piece !== null){
							++$weight->placeCount;
							$start->previousPiece = $weight;
							if(!$weight->isValid()){
								$start->removeWeight($weight, $isCastle);
							}
							return $piece;
						}
					}
				}
			}
		}
		return BridgeEndFiller::createPiece($pieces, $random, $x, $y, $z, $orientation, $genDepth);
	}

	/**
	 * @param list<StructurePiece> $pieces
	 */
	private function generateAndAddPiece(FortressStartPiece $start, array &$pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth, bool $isCastle) : ?StructurePiece{
		$startBox = $start->getBoundingBox();
		if(abs($x - $startBox->x0) > 112 || abs($z - $startBox->z0) > 112){
			return BridgeEndFiller::createPiece($pieces, $random, $x, $y, $z, $orientation, $genDepth);
		}
		$piece = $this->generatePiece($start, $isCastle, $pieces, $random, $x, $y, $z, $orientation, $genDepth + 1);
		if($piece !== null){
			$pieces[] = $piece;
			$start->pendingChildren[] = $piece;
		}
		return $piece;
	}

	/**
	 * @param list<StructurePiece> $pieces
	 */
	protected function generateChildForward(FortressStartPiece $start, array &$pieces, RandomSource $random, int $horizontalOffset, int $yOffset, bool $isCastle) : ?StructurePiece{
		$orientation = $this->getOrientation();
		if($orientation === null){
			return null;
		}
		$box = $this->boundingBox;
		return match($orientation){
			Facing::SOUTH => $this->generateAndAddPiece($start, $pieces, $random, $box->x0 + $horizontalOffset, $box->y0 + $yOffset, $box->z1 + 1, $orientation, $this->genDepth, $isCastle),
			Facing::WEST => $this->generateAndAddPiece($start, $pieces, $random, $box->x0 - 1, $box->y0 + $yOffset, $box->z0 + $horizontalOffset, $orientation, $this->genDepth, $isCastle),
			Facing::EAST => $this->generateAndAddPiece($start, $pieces, $random, $box->x1 + 1, $box->y0 + $yOffset, $box->z0 + $horizontalOffset, $orientation, $this->genDepth, $isCastle),
			default => $this->generateAndAddPiece($start, $pieces, $random, $box->x0 + $horizontalOffset, $box->y0 + $yOffset, $box->z0 - 1, $orientation, $this->genDepth, $isCastle)
		};
	}

	/**
	 * @param list<StructurePiece> $pieces
	 */
	protected function generateChildLeft(FortressStartPiece $start, array &$pieces, RandomSource $random, int $yOffset, int $horizontalOffset, bool $isCastle) : ?StructurePiece{
		$orientation = $this->getOrientation();
		if($orientation === null){
			return null;
		}
		$box = $this->boundingBox;
		return match($orientation){
			Facing::WEST, Facing::EAST => $this->generateAndAddPiece($start, $pieces, $random, $box->x0 + $horizontalOffset, $box->y0 + $yOffset, $box->z0 - 1, Facing::NORTH, $this->genDepth, $isCastle),
			default => $this->generateAndAddPiece($start, $pieces, $random, $box->x0 - 1, $box->y0 + $yOffset, $box->z0 + $horizontalOffset, Facing::WEST, $this->genDepth, $isCastle)
		};
	}

	/**
	 * @param list<StructurePiece> $pieces
	 */
	protected function generateChildRight(FortressStartPiece $start, array &$pieces, RandomSource $random, int $yOffset, int $horizontalOffset, bool $isCastle) : ?StructurePiece{
		$orientation = $this->getOrientation();
		if($orientation === null){
			return null;
		}
		$box = $this->boundingBox;
		return match($orientation){
			Facing::WEST, Facing::EAST => $this->generateAndAddPiece($start, $pieces, $random, $box->x0 + $horizontalOffset, $box->y0 + $yOffset, $box->z1 + 1, Facing::SOUTH, $this->genDepth, $isCastle),
			default => $this->generateAndAddPiece($start, $pieces, $random, $box->x1 + 1, $box->y0 + $yOffset, $box->z0 + $horizontalOffset, Facing::EAST, $this->genDepth, $isCastle)
		};
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;

/**
 * Castle room growing nether wart on soul sand, with a central staircase
 * to an upper exit.
 */
final class CastleStalkRoom extends FortressPiece{

	public function __construct(int $genDepth, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -5, -3, 0, 13, 14, 13, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new CastleStalkRoom($genDepth, $boundingBox, $orientation) : null;
	}

	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
		$this->generateChildForward($start, $pieces, $random, 5, 3, true);
		$this->generateChildForward($start, $pieces, $random, 5, 11, true);
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$fence = FortressBlocks::netherBrickFence();
		$air = FortressBlocks::air();
		$this->generateBox($level, $boundingBox, 0, 3, 0, 12, 4, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 5, 0, 12, 13, 12, $air, $air, false);
		$this->generateBox($level, $boundingBox, 0, 5, 0, 1, 12, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 11, 5, 0, 12, 12, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 5, 11, 4, 12, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 8, 5, 11, 10, 12, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 5, 9, 11, 7, 12, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 5, 0, 4, 12, 1, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 8, 5, 0, 10, 12, 1, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 5, 9, 0, 7, 12, 1, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 11, 2, 10, 12, 10, $bricks, $bricks, false);

		for($i = 1; $i <= 11; $i += 2){
			$this->generateBox($level, $boundingBox, $i, 10, 0, $i, 11, 0, $fence, $fence, false);
			$this->generateBox($level, $boundingBox, $i, 10, 12, $i, 11, 12, $fence, $fence, false);
			$this->generateBox($level, $boundingBox, 0, 10, $i, 0, 11, $i, $fence, $fence, false);
			$this->generateBox($level, $boundingBox, 12, 10, $i, 12, 11, $i, $fence, $fence, false);
			$this->placeBlock($level, $bricks, $i, 13, 0, $boundingBox);
			$this->placeBlock($level, $bricks, $i, 13, 12, $boundingBox);
			$this->placeBlock($level, $bricks, 0, 13, $i, $boundingBox);
			$this->placeBlock($level, $bricks, 12, 13, $i, $boundingBox);

			if($i !== 11){
				$this->placeBlock($level, $fence, $i + 1, 13, 0, $boundingBox);
				$this->placeBlock($level, $fence, $i + 1, 13, 12, $boundingBox);
				$this->placeBlock($level, $fence, 0, 13, $i + 1, $boundingBox);
				$this->placeBlock($level, $fence, 12, 13, $i + 1, $boundingBox);
			}
		}

		$this->placeBlock($level, $fence, 0, 13, 0, $boundingBox);
		$this->placeBlock($level, $fence, 0, 13, 12, $boundingBox);
		$this->placeBlock($level, $fence, 12, 13, 12, $boundingBox);
		$this->placeBlock($level, $fence, 12, 13, 0, $boundingBox);

		for($z = 3; $z <= 9; $z += 2){
			$this->generateBox($level, $boundingBox, 1, 7, $z, 1, 8, $z, $fence, $fence, false);
			$this->generateBox($level, $boundingBox, 11, 7, $z, 11, 8, $z, $fence, $fence, false);
		}

		$stairsNorth = FortressBlocks::netherBrickStairs(3);
		for($y = 0; $y <= 6; ++$y){
			$z = $y + 4;
			for($x = 5; $x <= 7; ++$x){
				$this->placeBlock($level, $stairsNorth, $x, 5 + $y, $z, $boundingBox);
			}
			if($z >= 5 && $z <= 8){
				$this->generateBox($level, $boundingBox, 5, 5, $z, 7, $y + 4, $z, $bricks, $bricks, false);
			}elseif($z >= 9 && $z <= 10){
				$this->generateBox($level, $boundingBox, 5, 8, $z, 7, $y + 4, $z, $bricks, $bricks, false);
			}
			if($y >= 1){
				$this->generateBox($level, $boundingBox, 5, 6 + $y, $z, 7, 9 + $y, $z, $air, $air, false);
			}
		}

		for($x = 5; $x <= 7; ++$x){
			$this->placeBlock($level, $stairsNorth, $x, 12, 11, $boundingBox);
		}

		$this->generateBox($level, $boundingBox, 5, 6, 7, 5, 7, 7, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 7, 6, 7, 7, 7, 7, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 5, 13, 12, 7, 13, 12, $air, $air, false);
		$this->generateBox($level, $boundingBox, 2, 5, 2, 3, 5, 3, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 5, 9, 3, 5, 10, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 5, 4, 2, 5, 8, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 9, 5, 2, 10, 5, 3, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 9, 5, 9, 10, 5, 10, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 10, 5, 4, 10, 5, 8, $bricks, $bricks, false);
		$stairsWest = FortressBlocks::netherBrickStairs(1);
		$this->placeBlock($level, $stairsWest, 4, 5, 2, $boundingBox);
		$this->placeBlock($level, $stairsWest, 4, 5, 3, $boundingBox);
		$this->placeBlock($level, $stairsWest, 4, 5, 9, $boundingBox);
		$this->placeBlock($level, $stairsWest, 4, 5, 10, $boundingBox);
		$stairsEast = FortressBlocks::netherBrickStairs(0);
		$this->placeBlock($level, $stairsEast, 8, 5, 2, $boundingBox);
		$this->placeBlock($level, $stairsEast, 8, 5, 3, $boundingBox);
		$this->placeBlock($level, $stairsEast, 8, 5, 9, $boundingBox);
		$this->placeBlock($level, $stairsEast, 8, 5, 10, $boundingBox);
		$soulSand = FortressBlocks::soulSand();
		$wart = FortressBlocks::netherWart();
		$this->generateBox($level, $boundingBox, 3, 4, 4, 4, 4, 8, $soulSand, $soulSand, false);
		$this->generateBox($level, $boundingBox, 8, 4, 4, 9, 4, 8, $soulSand, $soulSand, false);
		$this->generateBox($level, $boundingBox, 3, 5, 4, 4, 5, 8, $wart, $wart, false);
		$this->generateBox($level, $boundingBox, 8, 5, 4, 9, 5, 8, $wart, $wart, false);
		$this->generateBox($level, $boundingBox, 4, 2, 0, 8, 2, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 4, 12, 2, 8, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 4, 0, 0, 8, 1, 3, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 4, 0, 9, 8, 1, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 0, 4, 3, 1, 8, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 9, 0, 4, 12, 1, 8, $bricks, $bricks, false);

		for($x = 4; $x <= 8; ++$x){
			for($z = 0; $z <= 2; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
				$this->fillColumnDown($level, $bricks, $x, -1, 12 - $z, $boundingBox);
			}
		}

		for($x = 0; $x <= 2; ++$x){
			for($z = 4; $z <= 8; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
				$this->fillColumnDown($level, $bricks, 12 - $x, -1, $z, $boundingBox);
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

/**
 * Weight and placement limit of one fortress piece type while a fortress
 * is laid out. A limit of 0 means unlimited.
 */
final class FortressPieceWeight{

	public int $placeCount = 0;

	/**
	 * @param class-string<FortressPiece> $pieceType
	 */
	public function __construct(
		public readonly string $pieceType,
		public readonly int $weight,
		public readonly int $maxPlaceCount,
		public readonly bool $allowInRow = false
	){}

	public function doPlace(int $genDepth) : bool{
		return $this->maxPlaceCount === 0 || $this->placeCount < $this->maxPlaceCount;
	}

	public function isValid() : bool{
		return $this->maxPlaceCount === 0 || $this->placeCount < $this->maxPlaceCount;
	}

	/**
	 * @return list<FortressPieceWeight>
	 */
	public static function bridgePieces() : array{
		return [
			new FortressPieceWeight(BridgeStraight::class, 30, 0, true),
			new FortressPieceWeight(BridgeCrossing::class, 10, 4),
			new FortressPieceWeight(RoomCrossing::class, 10, 4),
			new FortressPieceWeight(StairsRoom::class, 10, 3),
			new FortressPieceWeight(MonsterThrone::class, 5, 2),
			new FortressPieceWeight(CastleEntrance::class, 5, 1)
		];
	}

	/**
	 * @return list<FortressPieceWeight>
	 */
	public static function castlePieces() : array{
		return [
			new FortressPieceWeight(CastleSmallCorridor::class, 25, 0, true),
			new FortressPieceWeight(CastleSmallCorridorCrossing::class, 15, 5),
			new FortressPieceWeight(CastleSmallCorridorRightTurn::class, 5, 10),
			new FortressPieceWeight(CastleSmallCorridorLeftTurn::class, 5, 10),
			new FortressPieceWeight(CastleCorridorStairs::class, 10, 3, true),
			new FortressPieceWeight(CastleCorridorTBalcony::class, 7, 2),
			new FortressPieceWeight(CastleStalkRoom::class, 5, 2)
		];
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\math\Int64;
use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\random\Xoroshiro128;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;
use function array_splice;
use function array_values;
use function count;

/**
 * The pieces of one nether fortress, laid out from its start chunk and the
 * world seed only, then lifted to a random height between 48 and 70.
 */
final class FortressLayout{

	/** @var list<StructurePiece> */
	private array $pieces = [];
	private BoundingBox $boundingBox;

	public function __construct(int $seed, int $chunkX, int $chunkZ){
		$random = new Xoroshiro128($seed);
		$first = Int64::mul($chunkX, $random->nextInt());
		$second = Int64::mul($chunkZ, $random->nextInt());
		$random->setSeed($first ^ $second ^ $seed);
		$this->boundingBox = BoundingBox::unknown();
		$this->generatePieces($random, $chunkX, $chunkZ);
	}

	private function generatePieces(RandomSource $random, int $chunkX, int $chunkZ) : void{
		$start = new FortressStartPiece($random, ($chunkX << 4) + 2, ($chunkZ << 4) + 2);
		$pieces = [$start];
		$start->addChildren($start, $pieces, $random);

		while(count($start->pendingChildren) > 0){
			$index = $random->nextBoundedInt(count($start->pendingChildren) - 1);
			$piece = $start->pendingChildren[$index];
			array_splice($start->pendingChildren, $index, 1);
			if($piece instanceof FortressPiece){
				$piece->addChildren($start, $pieces, $random);
			}
		}

		$this->pieces = $pieces;
		$this->calculateBoundingBox();
		$this->moveInsideHeights($random, 48, 70);
	}

	public function isValid() : bool{
		return count($this->pieces) > 0;
	}

	public function getBoundingBox() : BoundingBox{
		return $this->boundingBox;
	}

	/**
	 * @return list<StructurePiece>
	 */
	public function getPieces() : array{
		return $this->pieces;
	}

	/**
	 * Writes the pieces crossing $boundingBox, dropping the ones that ask to
	 * be removed.
	 */
	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : void{
		$kept = [];
		foreach($this->pieces as $piece){
			if($piece->getBoundingBox()->intersects($boundingBox) && !$piece->postProcess($level, $random, $boundingBox, $chunkX, $chunkZ)){
				continue;
			}
			$kept[] = $piece;
		}
		if(count($kept) !== count($this->pieces)){
			$this->pieces = array_values($kept);
			$this->calculateBoundingBox();
		}
	}

	private function calculateBoundingBox() : void{
		$this->boundingBox = BoundingBox::unknown();
		foreach($this->pieces as $piece){
			$this->boundingBox->expand($piece->getBoundingBox());
		}
	}

	private function moveInsideHeights(RandomSource $random, int $min, int $max) : void{
		$range = $max - $min + 1 - $this->boundingBox->getYSpan();
		if($range > 1){
			$y = $min + $random->nextBoundedInt($range);
		}else{
			$y = $min;
		}
		$offset = $y - $this->boundingBox->y0;
		$this->boundingBox->move(0, $offset, 0);
		foreach($this->pieces as $piece){
			$piece->move(0, $offset, 0);
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;

/**
 * Raised platform holding the blaze spawner.
 */
final class MonsterThrone extends FortressPiece{

	private bool $hasPlacedSpawner = false;

	public function __construct(int $genDepth, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -2, 0, 0, 7, 8, 9, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new MonsterThrone($genDepth, $boundingBox, $orientation) : null;
	}

	/**
	 * World position of the spawner, as [x, y, z].
	 *
	 * @return array{int, int, int}
	 */
	public function getSpawnerPosition() : array{
		return [$this->getWorldX(3, 5), $this->getWorldY(5), $this->getWorldZ(3, 5)];
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$fence = FortressBlocks::netherBrickFence();
		$air = FortressBlocks::air();
		$this->generateBox($level, $boundingBox, 0, 2, 0, 6, 7, 7, $air, $air, false);
		$this->generateBox($level, $boundingBox, 1, 0, 0, 5, 1, 7, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 2, 1, 5, 2, 7, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 3, 2, 5, 3, 7, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 4, 3, 5, 4, 7, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 2, 0, 1, 4, 2, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 5, 2, 0, 5, 4, 2, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 5, 2, 1, 5, 3, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 5, 5, 2, 5, 5, 3, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 5, 3, 0, 5, 8, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 6, 5, 3, 6, 5, 8, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 5, 8, 5, 5, 8, $bricks, $bricks, false);
		$this->placeBlock($level, $fence, 1, 6, 3, $boundingBox);
		$this->placeBlock($level, $fence, 5, 6, 3, $boundingBox);
		$this->placeBlock($level, $fence, 0, 6, 3, $boundingBox);
		$this->placeBlock($level, $fence, 6, 6, 3, $boundingBox);
		$this->generateBox($level, $boundingBox, 0, 6, 4, 0, 6, 7, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 6, 6, 4, 6, 6, 7, $fence, $fence, false);
		$this->placeBlock($level, $fence, 0, 6, 8, $boundingBox);
		$this->placeBlock($level, $fence, 6, 6, 8, $boundingBox);
		$this->generateBox($level, $boundingBox, 1, 6, 8, 5, 6, 8, $fence, $fence, false);
		$this->placeBlock($level, $fence, 1, 7, 8, $boundingBox);
		$this->generateBox($level, $boundingBox, 2, 7, 8, 4, 7, 8, $fence, $fence, false);
		$this->placeBlock($level, $fence, 5, 7, 8, $boundingBox);
		$this->placeBlock($level, $fence, 2, 8, 8, $boundingBox);
		$this->placeBlock($level, $fence, 3, 8, 8, $boundingBox);
		$this->placeBlock($level, $fence, 4, 8, 8, $boundingBox);

		if(!$this->hasPlacedSpawner){
			[$x, $y, $z] = $this->getSpawnerPosition();
			if($boundingBox->isInside($x, $y, $z)){
				$this->hasPlacedSpawner = true;
				$this->placeBlock($level, FortressBlocks::spawner(), 3, 5, 5, $boundingBox);
			}
		}

		for($x = 0; $x <= 6; ++$x){
			for($z = 0; $z <= 6; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;

/**
 * Castle corridor turning left, holding a chest one time in three.
 */
final class CastleSmallCorridorLeftTurn extends FortressPiece{

	private bool $isNeedingChest;
	private bool $hasChest;

	public function __construct(int $genDepth, RandomSource $random, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
		$this->isNeedingChest = $random->nextBoundedInt(3) === 0;
		$this->hasChest = $this->isNeedingChest;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -1, 0, 0, 5, 7, 5, $orientation);
		if(!self::isOkBox($boundingBox) || StructurePiece::findCollisionPiece($pieces, $boundingBox) !== null){
			return null;
		}
		return new CastleSmallCorridorLeftTurn($genDepth, $random, $boundingBox, $orientation);
	}

	/**
	 * World position of the chest as [x, y, z], null when the piece has none.
	 *
	 * @return array{int, int, int}|null
	 */
	public function getChestPosition() : ?array{
		return $this->hasChest ? [$this->getWorldX(3, 3), $this->getWorldY(2), $this->getWorldZ(3, 3)] : null;
	}

	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
		$this->generateChildLeft($start, $pieces, $random, 0, 1, true);
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$fence = FortressBlocks::netherBrickFence();
		$air = FortressBlocks::air();
		$this->generateBox($level, $boundingBox, 0, 0, 0, 4, 1, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 4, 5, 4, $air, $air, false);
		$this->generateBox($level, $boundingBox, 4, 2, 0, 4, 5, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 4, 3, 1, 4, 4, 1, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 4, 3, 3, 4, 4, 3, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 0, 5, 0, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 4, 3, 5, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 3, 4, 1, 4, 4, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 3, 3, 4, 3, 4, 4, $fence, $fence, false);

		if($this->isNeedingChest && $boundingBox->isInside($this->getWorldX(3, 3), $this->getWorldY(2), $this->getWorldZ(3, 3))){
			$this->isNeedingChest = false;
			$this->placeBlock($level, FortressBlocks::chest("west"), 3, 2, 3, $boundingBox);
		}

		$this->generateBox($level, $boundingBox, 0, 6, 0, 4, 6, 4, $bricks, $bricks, false);

		for($x = 0; $x <= 4; ++$x){
			for($z = 0; $z <= 4; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;

/**
 * Walled courtyard leading from the bridges into the castle, with a lava
 * well in its middle.
 */
final class CastleEntrance extends FortressPiece{

	public function __construct(int $genDepth, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -5, -3, 0, 13, 14, 13, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new CastleEntrance($genDepth, $boundingBox, $orientation) : null;
	}

	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
		$this->generateChildForward($start, $pieces, $random, 5, 3, true);
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$fence = FortressBlocks::netherBrickFence();
		$air = FortressBlocks::air();
		$this->generateBox($level, $boundingBox, 0, 3, 0, 12, 4, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 5, 0, 12, 13, 12, $air, $air, false);
		$this->generateBox($level, $boundingBox, 0, 5, 0, 1, 12, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 11, 5, 0, 12, 12, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 5, 11, 4, 12, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 8, 5, 11, 10, 12, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 5, 9, 11, 7, 12, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 5, 0, 4, 12, 1, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 8, 5, 0, 10, 12, 1, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 5, 9, 0, 7, 12, 1, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 11, 2, 10, 12, 10, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 5, 8, 0, 7, 8, 0, $fence, $fence, false);

		for($i = 1; $i <= 11; $i += 2){
			$this->generateBox($level, $boundingBox, $i, 10, 0, $i, 11, 0, $fence, $fence, false);
			$this->generateBox($level, $boundingBox, $i, 10, 12, $i, 11, 12, $fence, $fence, false);
			$this->generateBox($level, $boundingBox, 0, 10, $i, 0, 11, $i, $fence, $fence, false);
			$this->generateBox($level, $boundingBox, 12, 10, $i, 12, 11, $i, $fence, $fence, false);
			$this->placeBlock($level, $bricks, $i, 13, 0, $boundingBox);
			$this->placeBlock($level, $bricks, $i, 13, 12, $boundingBox);
			$this->placeBlock($level, $bricks, 0, 13, $i, $boundingBox);
			$this->placeBlock($level, $bricks, 12, 13, $i, $boundingBox);

			if($i !== 11){
				$this->placeBlock($level, $fence, $i + 1, 13, 0, $boundingBox);
				$this->placeBlock($level, $fence, $i + 1, 13, 12, $boundingBox);
				$this->placeBlock($level, $fence, 0, 13, $i + 1, $boundingBox);
				$this->placeBlock($level, $fence, 12, 13, $i + 1, $boundingBox);
			}
		}

		$this->placeBlock($level, $fence, 0, 13, 0, $boundingBox);
		$this->placeBlock($level, $fence, 0, 13, 12, $boundingBox);
		$this->placeBlock($level, $fence, 12, 13, 12, $boundingBox);
		$this->placeBlock($level, $fence, 12, 13, 0, $boundingBox);

		for($z = 3; $z <= 9; $z += 2){
			$this->generateBox($level, $boundingBox, 1, 7, $z, 1, 8, $z, $fence, $fence, false);
			$this->generateBox($level, $boundingBox, 11, 7, $z, 11, 8, $z, $fence, $fence, false);
		}

		$this->generateBox($level, $boundingBox, 4, 2, 0, 8, 2, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 4, 12, 2, 8, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 4, 0, 0, 8, 1, 3, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 4, 0, 9, 8, 1, 12, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 0, 4, 3, 1, 8, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 9, 0, 4, 12, 1, 8, $bricks, $bricks, false);

		for($x = 4; $x <= 8; ++$x){
			for($z = 0; $z <= 2; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
				$this->fillColumnDown($level, $bricks, $x, -1, 12 - $z, $boundingBox);
			}
		}

		for($x = 0; $x <= 2; ++$x){
			for($z = 4; $z <= 8; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
				$this->fillColumnDown($level, $bricks, 12 - $x, -1, $z, $boundingBox);
			}
		}

		$this->generateBox($level, $boundingBox, 5, 5, 5, 7, 5, 7, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 6, 1, 6, 6, 4, 6, $air, $air, false);
		$this->placeBlock($level, $bricks, 6, 0, 6, $boundingBox);
		$this->placeBlock($level, FortressBlocks::lava(), 6, 5, 6, $boundingBox);

		$x = $this->getWorldX(6, 6);
		$y = $this->getWorldY(5);
		$z = $this->getWorldZ(6, 6);
		if($boundingBox->isInside($x, $y, $z)){
			$this->pourLava($level, $x, $y - 1, $z);
		}
		return true;
	}

	/**
	 * Fills the air below the lava source with falling lava, as the source
	 * would once it flows.
	 */
	private function pourLava(BlockManager $level, int $x, int $y, int $z) : void{
		$falling = $this->orientedBlock(FortressBlocks::fallingLava());
		if($falling === null){
			return;
		}
		while($y > $level->getMinHeight() && $level->getBlockIdAt($x, $y, $z) === self::AIR){
			$level->setBlockStateAt($x, $y, $z, $falling);
			--$y;
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;
use function max;
use function min;

/**
 * Castle corridor stepping down seven blocks.
 */
final class CastleCorridorStairs extends FortressPiece{

	public function __construct(int $genDepth, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -1, -7, 0, 5, 14, 10, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new CastleCorridorStairs($genDepth, $boundingBox, $orientation) : null;
	}

	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
		$this->generateChildForward($start, $pieces, $random, 1, 0, true);
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$fence = FortressBlocks::netherBrickFence();
		$air = FortressBlocks::air();
		$stairsSouth = FortressBlocks::netherBrickStairs(2);
		for($i = 0; $i <= 9; ++$i){
			$maxY = max(1, 7 - $i);
			$minY = min(max($maxY + 5, 14 - $i), 13);

			$this->generateBox($level, $boundingBox, 0, 0, $i, 4, $maxY, $i, $bricks, $bricks, false);
			$this->generateBox($level, $boundingBox, 1, $maxY + 1, $i, 3, $minY - 1, $i, $air, $air, false);

			if($i <= 6){
				$this->placeBlock($level, $stairsSouth, 1, $maxY + 1, $i, $boundingBox);
				$this->placeBlock($level, $stairsSouth, 2, $maxY + 1, $i, $boundingBox);
				$this->placeBlock($level, $stairsSouth, 3, $maxY + 1, $i, $boundingBox);
			}

			$this->generateBox($level, $boundingBox, 0, $minY, $i, 4, $minY, $i, $bricks, $bricks, false);
			$this->generateBox($level, $boundingBox, 0, $maxY + 1, $i, 0, $minY - 1, $i, $bricks, $bricks, false);
			$this->generateBox($level, $boundingBox, 4, $maxY + 1, $i, 4, $minY - 1, $i, $bricks, $bricks, false);

			if(($i & 0x1) === 0){
				$this->generateBox($level, $boundingBox, 0, $maxY + 2, $i, 0, $maxY + 3, $i, $fence, $fence, false);
				$this->generateBox($level, $boundingBox, 4, $maxY + 2, $i, 4, $maxY + 3, $i, $fence, $fence, false);
			}

			for($x = 0; $x <= 4; ++$x){
				$this->fillColumnDown($level, $bricks, $x, -1, $i, $boundingBox);
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;

/**
 * Crossing of two bridges, 19 by 19, open on all four sides.
 */
class BridgeCrossing extends FortressPiece{

	public function __construct(int $genDepth, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -8, -3, 0, 19, 10, 19, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new BridgeCrossing($genDepth, $boundingBox, $orientation) : null;
	}

	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
		$this->generateChildForward($start, $pieces, $random, 8, 3, false);
		$this->generateChildLeft($start, $pieces, $random, 3, 8, false);
		$this->generateChildRight($start, $pieces, $random, 3, 8, false);
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$air = FortressBlocks::air();
		$this->generateBox($level, $boundingBox, 7, 3, 0, 11, 4, 18, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 3, 7, 18, 4, 11, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 8, 5, 0, 10, 7, 18, $air, $air, false);
		$this->generateBox($level, $boundingBox, 0, 5, 8, 18, 7, 10, $air, $air, false);
		$this->generateBox($level, $boundingBox, 7, 5, 0, 7, 5, 7, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 7, 5, 11, 7, 5, 18, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 11, 5, 0, 11, 5, 7, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 11, 5, 11, 11, 5, 18, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 5, 7, 7, 5, 7, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 11, 5, 7, 18, 5, 7, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 5, 11, 7, 5, 11, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 11, 5, 11, 18, 5, 11, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 7, 2, 0, 11, 2, 5, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 7, 2, 13, 11, 2, 18, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 7, 0, 0, 11, 1, 3, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 7, 0, 15, 11, 1, 18, $bricks, $bricks, false);

		for($x = 7; $x <= 11; ++$x){
			for($z = 0; $z <= 2; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
				$this->fillColumnDown($level, $bricks, $x, -1, 18 - $z, $boundingBox);
			}
		}

		$this->generateBox($level, $boundingBox, 0, 2, 7, 5, 2, 11, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 13, 2, 7, 18, 2, 11, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 0, 7, 3, 1, 11, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 15, 0, 7, 18, 1, 11, $bricks, $bricks, false);

		for($x = 0; $x <= 2; ++$x){
			for($z = 7; $z <= 11; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
				$this->fillColumnDown($level, $bricks, 18 - $x, -1, $z, $boundingBox);
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;

/**
 * Room with a staircase climbing to an upper exit on its right side.
 */
final class StairsRoom extends FortressPiece{

	public function __construct(int $genDepth, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -2, 0, 0, 7, 11, 7, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new StairsRoom($genDepth, $boundingBox, $orientation) : null;
	}

	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
		$this->generateChildRight($start, $pieces, $random, 6, 2, false);
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$fence = FortressBlocks::netherBrickFence();
		$air = FortressBlocks::air();
		$this->generateBox($level, $boundingBox, 0, 0, 0, 6, 1, 6, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 6, 10, 6, $air, $air, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 1, 8, 0, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 5, 2, 0, 6, 8, 0, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 1, 0, 8, 6, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 6, 2, 1, 6, 8, 6, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 2, 6, 5, 8, 6, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 3, 2, 0, 5, 4, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 6, 3, 2, 6, 5, 2, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 6, 3, 4, 6, 5, 4, $fence, $fence, false);
		$this->placeBlock($level, $bricks, 5, 2, 5, $boundingBox);
		$this->generateBox($level, $boundingBox, 4, 2, 5, 4, 3, 5, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 3, 2, 5, 3, 4, 5, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 2, 5, 2, 5, 5, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 2, 5, 1, 6, 5, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 7, 1, 5, 7, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 6, 8, 2, 6, 8, 4, $air, $air, false);
		$this->generateBox($level, $boundingBox, 2, 6, 0, 4, 8, 0, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 5, 0, 4, 5, 0, $fence, $fence, false);

		for($x = 0; $x <= 6; ++$x){
			for($z = 0; $z <= 6; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;

/**
 * Straight bridge segment, 19 blocks long, with fence railings and support
 * pillars at both ends.
 */
final class BridgeStraight extends FortressPiece{

	public function __construct(int $genDepth, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -1, -3, 0, 5, 10, 19, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new BridgeStraight($genDepth, $boundingBox, $orientation) : null;
	}

	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
		$this->generateChildForward($start, $pieces, $random, 1, 3, false);
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$fence = FortressBlocks::netherBrickFence();
		$air = FortressBlocks::air();
		$this->generateBox($level, $boundingBox, 0, 3, 0, 4, 4, 18, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 5, 0, 3, 7, 18, $air, $air, false);
		$this->generateBox($level, $boundingBox, 0, 5, 0, 0, 5, 18, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 4, 5, 0, 4, 5, 18, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 4, 2, 5, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 13, 4, 2, 18, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 0, 0, 4, 1, 3, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 0, 15, 4, 1, 18, $bricks, $bricks, false);

		for($x = 0; $x <= 4; ++$x){
			for($z = 0; $z <= 2; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
				$this->fillColumnDown($level, $bricks, $x, -1, 18 - $z, $boundingBox);
			}
		}

		$this->generateBox($level, $boundingBox, 0, 1, 1, 0, 4, 1, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 0, 3, 4, 0, 4, 4, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 0, 3, 14, 0, 4, 14, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 0, 1, 17, 0, 4, 17, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 4, 1, 1, 4, 4, 1, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 4, 3, 4, 4, 4, 4, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 4, 3, 14, 4, 4, 14, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 4, 1, 17, 4, 4, 17, $fence, $fence, false);
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\structure\template\StateBlocks;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\nbt\tag\IntTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\nbt\tag\Tag;

/**
 * Block states used by the nether fortress pieces, in piece coordinates.
 */
final class FortressBlocks{

	/** @var array<string, BlockStateData> */
	private static array $states = [];

	private function __construct(){
	}

	public static function air() : BlockStateData{
		return self::get("minecraft:air");
	}

	public static function netherBricks() : BlockStateData{
		return self::get("minecraft:nether_brick");
	}

	public static function netherBrickFence() : BlockStateData{
		return self::get("minecraft:nether_brick_fence");
	}

	public static function soulSand() : BlockStateData{
		return self::get("minecraft:soul_sand");
	}

	public static function netherWart() : BlockStateData{
		return self::get("minecraft:nether_wart", ["age" => new IntTag(3)]);
	}

	public static function lava() : BlockStateData{
		return self::get("minecraft:lava");
	}

	public static function fallingLava() : BlockStateData{
		return self::get("minecraft:flowing_lava", ["liquid_depth" => new IntTag(8)]);
	}

	public static function spawner() : BlockStateData{
		return self::get("minecraft:mob_spawner");
	}

	/**
	 * @param int $direction weirdo direction: 0 east, 1 west, 2 south, 3 north
	 */
	public static function netherBrickStairs(int $direction) : BlockStateData{
		return self::get("minecraft:nether_brick_stairs", ["weirdo_direction" => new IntTag($direction)]);
	}

	public static function chest(string $cardinal) : BlockStateData{
		return self::get("minecraft:chest", ["minecraft:cardinal_direction" => new StringTag($cardinal)]);
	}

	/**
	 * @param array<string, Tag> $states
	 */
	private static function get(string $name, array $states = []) : BlockStateData{
		$key = $name;
		foreach($states as $state => $tag){
			$key .= ";" . $state . "=" . $tag->getValue();
		}
		return self::$states[$key] ??= StateBlocks::state($name, $states) ?? BlockStateData::current($name, $states);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;

/**
 * Castle corridor turning right, holding a chest one time in three.
 */
final class CastleSmallCorridorRightTurn extends FortressPiece{

	private bool $isNeedingChest;
	private bool $hasChest;

	public function __construct(int $genDepth, RandomSource $random, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
		$this->isNeedingChest = $random->nextBoundedInt(3) === 0;
		$this->hasChest = $this->isNeedingChest;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -1, 0, 0, 5, 7, 5, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new CastleSmallCorridorRightTurn($genDepth, $random, $boundingBox, $orientation) : null;
	}

	/**
	 * World position of the chest as [x, y, z], null when the piece has none.
	 *
	 * @return array{int, int, int}|null
	 */
	public function getChestPosition() : ?array{
		return $this->hasChest ? [$this->getWorldX(1, 3), $this->getWorldY(2), $this->getWorldZ(1, 3)] : null;
	}

	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
		$this->generateChildRight($start, $pieces, $random, 0, 1, true);
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$fence = FortressBlocks::netherBrickFence();
		$air = FortressBlocks::air();
		$this->generateBox($level, $boundingBox, 0, 0, 0, 4, 1, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 4, 5, 4, $air, $air, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 0, 5, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 3, 1, 0, 4, 1, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 0, 3, 3, 0, 4, 3, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 4, 2, 0, 4, 5, 0, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 2, 4, 4, 5, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 1, 3, 4, 1, 4, 4, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 3, 3, 4, 3, 4, 4, $fence, $fence, false);

		if($this->isNeedingChest && $boundingBox->isInside($this->getWorldX(1, 3), $this->getWorldY(2), $this->getWorldZ(1, 3))){
			$this->isNeedingChest = false;
			$this->placeBlock($level, FortressBlocks::chest("east"), 1, 2, 3, $boundingBox);
		}

		$this->generateBox($level, $boundingBox, 0, 6, 0, 4, 6, 4, $bricks, $bricks, false);

		for($x = 0; $x <= 4; ++$x){
			for($z = 0; $z <= 4; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\random\Xoroshiro128;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;

/**
 * Crumbling end of a bridge. Its jagged outline comes from a seed drawn
 * when the piece is laid out, so the piece looks the same in every chunk
 * it crosses.
 */
final class BridgeEndFiller extends FortressPiece{

	private int $selfSeed;

	public function __construct(int $genDepth, RandomSource $random, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
		$this->selfSeed = $random->nextInt();
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -1, -3, 0, 5, 10, 8, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new BridgeEndFiller($genDepth, $random, $boundingBox, $orientation) : null;
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$rand = new Xoroshiro128($this->selfSeed);
		$bricks = FortressBlocks::netherBricks();

		for($x = 0; $x <= 4; ++$x){
			for($y = 3; $y <= 4; ++$y){
				$this->generateBox($level, $boundingBox, $x, $y, 0, $x, $y, $rand->nextBoundedInt(8), $bricks, $bricks, false);
			}
		}

		$this->generateBox($level, $boundingBox, 0, 5, 0, 0, 5, $rand->nextBoundedInt(8), $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 4, 5, 0, 4, 5, $rand->nextBoundedInt(8), $bricks, $bricks, false);

		for($x = 0; $x <= 4; ++$x){
			$this->generateBox($level, $boundingBox, $x, 2, 0, $x, 2, $rand->nextBoundedInt(5), $bricks, $bricks, false);
		}

		for($x = 0; $x <= 4; ++$x){
			for($y = 0; $y <= 1; ++$y){
				$this->generateBox($level, $boundingBox, $x, $y, 0, $x, $y, $rand->nextBoundedInt(3), $bricks, $bricks, false);
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;

/**
 * Small walled room with three exits.
 */
final class RoomCrossing extends FortressPiece{

	public function __construct(int $genDepth, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -2, 0, 0, 7, 9, 7, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new RoomCrossing($genDepth, $boundingBox, $orientation) : null;
	}

	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
		$this->generateChildForward($start, $pieces, $random, 2, 0, false);
		$this->generateChildLeft($start, $pieces, $random, 0, 2, false);
		$this->generateChildRight($start, $pieces, $random, 0, 2, false);
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$fence = FortressBlocks::netherBrickFence();
		$air = FortressBlocks::air();
		$this->generateBox($level, $boundingBox, 0, 0, 0, 6, 1, 6, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 6, 7, 6, $air, $air, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 1, 6, 0, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 6, 1, 6, 6, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 5, 2, 0, 6, 6, 0, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 5, 2, 6, 6, 6, 6, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 0, 6, 1, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 5, 0, 6, 6, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 6, 2, 0, 6, 6, 1, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 6, 2, 5, 6, 6, 6, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 6, 0, 4, 6, 0, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 5, 0, 4, 5, 0, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 2, 6, 6, 4, 6, 6, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 2, 5, 6, 4, 5, 6, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 0, 6, 2, 0, 6, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 5, 2, 0, 5, 4, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 6, 6, 2, 6, 6, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 6, 5, 2, 6, 5, 4, $fence, $fence, false);

		for($x = 0; $x <= 6; ++$x){
			for($z = 0; $z <= 6; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;
use pocketmine\math\Facing;
use function array_search;
use function array_splice;

/**
 * The bridge crossing a fortress grows from. It also holds the layout
 * state: pieces waiting to grow their children, the piece weights still
 * available and the last piece type placed.
 */
final class FortressStartPiece extends BridgeCrossing{

	private const HORIZONTAL = [Facing::NORTH, Facing::EAST, Facing::SOUTH, Facing::WEST];

	/** @var list<StructurePiece> */
	public array $pendingChildren = [];
	public ?FortressPieceWeight $previousPiece = null;
	/** @var list<FortressPieceWeight> */
	public array $availableBridgePieces;
	/** @var list<FortressPieceWeight> */
	public array $availableCastlePieces;

	public function __construct(RandomSource $random, int $x, int $z){
		$orientation = self::HORIZONTAL[$random->nextInt(4)];
		parent::__construct(0, new BoundingBox($x, 64, $z, $x + 19 - 1, 73, $z + 19 - 1), $orientation);
		$this->availableBridgePieces = FortressPieceWeight::bridgePieces();
		$this->availableCastlePieces = FortressPieceWeight::castlePieces();
	}

	public function removeWeight(FortressPieceWeight $weight, bool $isCastle) : void{
		if($isCastle){
			$index = array_search($weight, $this->availableCastlePieces, true);
			if($index !== false){
				array_splice($this->availableCastlePieces, $index, 1);
			}
		}else{
			$index = array_search($weight, $this->availableBridgePieces, true);
			if($index !== false){
				array_splice($this->availableBridgePieces, $index, 1);
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;

/**
 * Four-way crossing of castle corridors.
 */
final class CastleSmallCorridorCrossing extends FortressPiece{

	public function __construct(int $genDepth, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -1, 0, 0, 5, 7, 5, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new CastleSmallCorridorCrossing($genDepth, $boundingBox, $orientation) : null;
	}

	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
		$this->generateChildForward($start, $pieces, $random, 1, 0, true);
		$this->generateChildLeft($start, $pieces, $random, 0, 1, true);
		$this->generateChildRight($start, $pieces, $random, 0, 1, true);
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$air = FortressBlocks::air();
		$this->generateBox($level, $boundingBox, 0, 0, 0, 4, 1, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 4, 5, 4, $air, $air, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 0, 5, 0, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 4, 2, 0, 4, 5, 0, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 4, 0, 5, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 4, 2, 4, 4, 5, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 6, 0, 4, 6, 4, $bricks, $bricks, false);

		for($x = 0; $x <= 4; ++$x){
			for($z = 0; $z <= 4; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\fortress;

use dimension\generator\object\BlockManager;
use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructurePiece;

/**
 * Straight roofed castle corridor.
 */
final class CastleSmallCorridor extends FortressPiece{

	public function __construct(int $genDepth, BoundingBox $boundingBox, int $orientation){
		parent::__construct($genDepth);
		$this->setOrientation($orientation);
		$this->boundingBox = $boundingBox;
	}

	public static function createPiece(array $pieces, RandomSource $random, int $x, int $y, int $z, int $orientation, int $genDepth) : ?FortressPiece{
		$boundingBox = BoundingBox::orient($x, $y, $z, -1, 0, 0, 5, 7, 5, $orientation);
		return self::isOkBox($boundingBox) && StructurePiece::findCollisionPiece($pieces, $boundingBox) === null ? new CastleSmallCorridor($genDepth, $boundingBox, $orientation) : null;
	}

	public function addChildren(FortressStartPiece $start, array &$pieces, RandomSource $random) : void{
		$this->generateChildForward($start, $pieces, $random, 1, 0, true);
	}

	public function postProcess(BlockManager $level, RandomSource $random, BoundingBox $boundingBox, int $chunkX, int $chunkZ) : bool{
		$bricks = FortressBlocks::netherBricks();
		$fence = FortressBlocks::netherBrickFence();
		$air = FortressBlocks::air();
		$this->generateBox($level, $boundingBox, 0, 0, 0, 4, 1, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 4, 5, 4, $air, $air, false);
		$this->generateBox($level, $boundingBox, 0, 2, 0, 0, 5, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 4, 2, 0, 4, 5, 4, $bricks, $bricks, false);
		$this->generateBox($level, $boundingBox, 0, 3, 1, 0, 4, 1, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 0, 3, 3, 0, 4, 3, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 4, 3, 1, 4, 4, 1, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 4, 3, 3, 4, 4, 3, $fence, $fence, false);
		$this->generateBox($level, $boundingBox, 0, 6, 0, 4, 6, 4, $bricks, $bricks, false);

		for($x = 0; $x <= 4; ++$x){
			for($z = 0; $z <= 4; ++$z){
				$this->fillColumnDown($level, $bricks, $x, -1, $z, $boundingBox);
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\endcity;

use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructureBuffer;
use dimension\generator\structure\template\Rotation;
use dimension\generator\structure\template\StructureTemplate;

/**
 * One template of an end city, turned by a quarter-turn count and placed at
 * a template position (its minimum corner).
 */
final class EndCityPiece{

	private BoundingBox $boundingBox;
	private int $genDepth = 0;
	private StructureTemplate $template;
	private int $sizeX;
	private int $sizeZ;

	public function __construct(
		private string $templateName,
		StructureTemplate $baseTemplate,
		private int $x,
		private int $y,
		private int $z,
		private int $rotation,
		private bool $overwrite
	){
		$this->sizeX = $baseTemplate->getSizeX();
		$this->sizeZ = $baseTemplate->getSizeZ();
		$this->template = $rotation === Rotation::NONE ? $baseTemplate : $baseTemplate->rotate(Rotation::inverse($rotation), $rotation);
		$this->boundingBox = new BoundingBox(
			$x,
			$y,
			$z,
			$x + $this->template->getSizeX() - 1,
			$y + $this->template->getSizeY() - 1,
			$z + $this->template->getSizeZ() - 1
		);
	}

	public function getTemplateName() : string{
		return $this->templateName;
	}

	public function move(int $x, int $y, int $z) : void{
		$this->x += $x;
		$this->y += $y;
		$this->z += $z;
		$this->boundingBox->move($x, $y, $z);
	}

	/**
	 * Writes the template into $buffer. Pieces that do not overwrite leave
	 * the blocks under their air cells untouched.
	 */
	public function place(StructureBuffer $buffer) : void{
		$template = $this->template;
		$volume = $template->getVolume();
		for($index = 0; $index < $volume; ++$index){
			$state = $template->stateAt($index);
			if($state === null){
				continue;
			}
			if(!$this->overwrite && $state->getName() === StructureTemplate::AIR){
				continue;
			}
			$buffer->set($this->x + $template->xOf($index), $this->y + $template->yOf($index), $this->z + $template->zOf($index), $state);
		}
	}

	public function getRotation() : int{
		return $this->rotation;
	}

	public function getSizeX() : int{
		return $this->sizeX;
	}

	public function getSizeZ() : int{
		return $this->sizeZ;
	}

	public function getBoundingBox() : BoundingBox{
		return $this->boundingBox;
	}

	public function getX() : int{
		return $this->x;
	}

	public function getY() : int{
		return $this->y;
	}

	public function getZ() : int{
		return $this->z;
	}

	public function getGenDepth() : int{
		return $this->genDepth;
	}

	public function setGenDepth(int $genDepth) : void{
		$this->genDepth = $genDepth;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure\endcity;

use dimension\generator\random\RandomSource;
use dimension\generator\structure\BoundingBox;
use dimension\generator\structure\StructureBuffer;
use dimension\generator\structure\template\Rotation;
use dimension\generator\structure\template\StateBlocks;
use dimension\generator\structure\template\StructureTemplate;
use dimension\generator\structure\template\StructureTemplates;
use pocketmine\math\Facing;
use pocketmine\nbt\tag\IntTag;
use function array_merge;

/**
 * Lays out an end city: a base house topped by towers, which branch into
 * bridges leading to fat towers, further houses and at most one ship.
 * Branches that would collide with an earlier branch are dropped.
 */
final class EndCityLayout{

	private const MAX_GEN_DEPTH = 8;
	private const HOUSE_TOWER = 0;
	private const TOWER = 1;
	private const TOWER_BRIDGE = 2;
	private const FAT_TOWER = 3;

	private const TOWER_BRIDGES = [
		[Rotation::NONE, 1, -1, 0],
		[Rotation::ROTATE_90, 6, -1, 1],
		[Rotation::ROTATE_270, 0, -1, 5],
		[Rotation::ROTATE_180, 5, -1, 6]
	];
	private const FAT_TOWER_BRIDGES = [
		[Rotation::NONE, 4, -1, 0],
		[Rotation::ROTATE_90, 12, -1, 4],
		[Rotation::ROTATE_270, 0, -1, 8],
		[Rotation::ROTATE_180, 8, -1, 12]
	];

	private bool $shipCreated = false;
	private bool $missingTemplate = false;
	/** @var array<string, StructureTemplate|null> */
	private array $templates = [];

	public function __construct(
		private string $resourceFolder
	){}

	/**
	 * Plans the city into $buffer. Structure blocks marking shulker spots
	 * become air. Returns false, leaving $buffer untouched, when a template
	 * is missing.
	 */
	public function place(StructureBuffer $buffer, int $x, int $y, int $z, int $rotation, RandomSource $random) : bool{
		$pieces = $this->generate($x, $y, $z, $rotation, $random);
		if($pieces === null){
			return false;
		}
		foreach($pieces as $piece){
			$piece->place($buffer);
		}
		$air = StateBlocks::state(StructureTemplate::AIR);
		foreach($buffer->all() as [$blockX, $blockY, $blockZ, $state]){
			if($state->getName() === "minecraft:structure_block"){
				if($air === null){
					$buffer->remove($blockX, $blockY, $blockZ);
				}else{
					$buffer->set($blockX, $blockY, $blockZ, $air);
				}
			}
		}
		return true;
	}

	/**
	 * @return list<EndCityPiece>|null
	 */
	public function generate(int $x, int $y, int $z, int $rotation, RandomSource $random) : ?array{
		$this->shipCreated = false;
		$this->missingTemplate = false;
		$pieces = [];
		$base = $this->createPiece("base_floor", $x, $y, $z, $rotation, true);
		if($base === null){
			return null;
		}
		try{
			$lastPiece = $this->addHelper($pieces, $base);
			$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, -1, 0, -1, "second_floor_1", $rotation, false));
			$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, -1, 4, -1, "third_floor_1", $rotation, false));
			$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, -1, 8, -1, "third_roof", $rotation, true));
		}catch(\RuntimeException){
			return null;
		}
		$this->recursiveChildren(self::TOWER, 1, $lastPiece, null, $pieces, $random);
		return $this->missingTemplate ? null : $pieces;
	}

	private function template(string $name) : ?StructureTemplate{
		if(!isset($this->templates[$name])){
			$template = StructureTemplates::get($this->resourceFolder, "end_city/" . $name);
			if($template !== null && $name === "ship"){
				$frame = StateBlocks::state("minecraft:frame", ["facing_direction" => new IntTag(Facing::SOUTH)]);
				if($frame !== null){
					$template = $template->withBlock(6, 5, 7, $frame);
				}
			}
			$this->templates[$name] = $template;
		}
		return $this->templates[$name];
	}

	private function createPiece(string $name, int $x, int $y, int $z, int $rotation, bool $overwrite) : ?EndCityPiece{
		$template = $this->template($name);
		if($template === null){
			$this->missingTemplate = true;
			return null;
		}
		return new EndCityPiece($name, $template, $x, $y, $z, $rotation, $overwrite);
	}

	/**
	 * @param list<EndCityPiece> $pieces
	 */
	private function addHelper(array &$pieces, ?EndCityPiece $piece) : EndCityPiece{
		if($piece === null){
			throw new \RuntimeException("Missing end city template");
		}
		$pieces[] = $piece;
		return $piece;
	}

	private function addPiece(EndCityPiece $parent, int $offsetX, int $offsetY, int $offsetZ, string $templateName, int $rotation, bool $overwrite) : ?EndCityPiece{
		$child = $this->createPiece($templateName, $parent->getX(), $parent->getY(), $parent->getZ(), $rotation, $overwrite);
		if($child === null){
			return null;
		}
		[$parentX, $parentY, $parentZ] = self::transform($offsetX, $offsetY, $offsetZ, $parent->getSizeX(), $parent->getSizeZ(), $parent->getRotation());
		[$childX, $childY, $childZ] = self::transform(0, 0, 0, $child->getSizeX(), $child->getSizeZ(), $child->getRotation());
		$originX = $parent->getX() + $parentX - $childX;
		$originY = $parent->getY() + $parentY - $childY;
		$originZ = $parent->getZ() + $parentZ - $childZ;
		$child->move($originX - $child->getX(), $originY - $child->getY(), $originZ - $child->getZ());
		return $child;
	}

	/**
	 * @return array{int, int, int}
	 */
	private static function transform(int $x, int $y, int $z, int $sizeX, int $sizeZ, int $rotation) : array{
		return match($rotation){
			Rotation::ROTATE_90 => [$sizeZ - 1 - $z, $y, $x],
			Rotation::ROTATE_180 => [$sizeX - 1 - $x, $y, $sizeZ - 1 - $z],
			Rotation::ROTATE_270 => [$z, $y, $sizeX - 1 - $x],
			default => [$x, $y, $z]
		};
	}

	/**
	 * @param array{int, int, int}|null $offset
	 * @param list<EndCityPiece>        $pieces
	 */
	private function recursiveChildren(int $generator, int $genDepth, EndCityPiece $parent, ?array $offset, array &$pieces, RandomSource $random) : bool{
		if($genDepth > self::MAX_GEN_DEPTH){
			return false;
		}
		$childPieces = [];
		try{
			$generated = $this->generateSection($generator, $genDepth, $parent, $offset, $childPieces, $random);
		}catch(\RuntimeException){
			$this->missingTemplate = true;
			return false;
		}
		if($generated){
			$collision = false;
			$childTag = $random->nextInt();
			foreach($childPieces as $child){
				$child->setGenDepth($childTag);
				$collisionPiece = self::findCollisionPiece($pieces, $child->getBoundingBox());
				if($collisionPiece !== null && $collisionPiece->getGenDepth() !== $parent->getGenDepth()){
					$collision = true;
					break;
				}
			}
			if(!$collision){
				$pieces = array_merge($pieces, $childPieces);
				return true;
			}
		}
		return false;
	}

	/**
	 * @param list<EndCityPiece> $pieces
	 */
	private static function findCollisionPiece(array $pieces, BoundingBox $boundingBox) : ?EndCityPiece{
		foreach($pieces as $piece){
			if($piece->getBoundingBox()->intersects($boundingBox)){
				return $piece;
			}
		}
		return null;
	}

	/**
	 * @param array{int, int, int}|null $offset
	 * @param list<EndCityPiece>        $pieces
	 */
	private function generateSection(int $generator, int $genDepth, EndCityPiece $parent, ?array $offset, array &$pieces, RandomSource $random) : bool{
		return match($generator){
			self::HOUSE_TOWER => $this->houseTower($genDepth, $parent, $offset ?? [0, 0, 0], $pieces, $random),
			self::TOWER => $this->tower($genDepth, $parent, $pieces, $random),
			self::TOWER_BRIDGE => $this->towerBridge($genDepth, $parent, $pieces, $random),
			default => $this->fatTower($genDepth, $parent, $pieces, $random)
		};
	}

	/**
	 * @param array{int, int, int} $offset
	 * @param list<EndCityPiece>   $pieces
	 */
	private function houseTower(int $genDepth, EndCityPiece $parent, array $offset, array &$pieces, RandomSource $random) : bool{
		if($genDepth > self::MAX_GEN_DEPTH){
			return false;
		}
		$rotation = $parent->getRotation();
		$lastPiece = $this->addHelper($pieces, $this->addPiece($parent, $offset[0], $offset[1], $offset[2], "base_floor", $rotation, true));
		$numFloors = $random->nextInt(3);
		if($numFloors === 0){
			$this->addHelper($pieces, $this->addPiece($lastPiece, -1, 4, -1, "base_roof", $rotation, true));
		}elseif($numFloors === 1){
			$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, -1, 0, -1, "second_floor_2", $rotation, false));
			$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, -1, 8, -1, "second_roof", $rotation, false));
			$this->recursiveChildren(self::TOWER, $genDepth + 1, $lastPiece, null, $pieces, $random);
		}else{
			$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, -1, 0, -1, "second_floor_2", $rotation, false));
			$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, -1, 4, -1, "third_floor_2", $rotation, false));
			$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, -1, 8, -1, "third_roof", $rotation, true));
			$this->recursiveChildren(self::TOWER, $genDepth + 1, $lastPiece, null, $pieces, $random);
		}
		return true;
	}

	/**
	 * @param list<EndCityPiece> $pieces
	 */
	private function tower(int $genDepth, EndCityPiece $parent, array &$pieces, RandomSource $random) : bool{
		$rotation = $parent->getRotation();
		$offsetX = 3 + $random->nextInt(2);
		$offsetZ = 3 + $random->nextInt(2);
		$lastPiece = $this->addHelper($pieces, $this->addPiece($parent, $offsetX, -3, $offsetZ, "tower_base", $rotation, true));
		$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, 0, 7, 0, "tower_piece", $rotation, true));
		$bridgePiece = $random->nextInt(3) === 0 ? $lastPiece : null;
		$towerHeight = 1 + $random->nextInt(3);

		for($i = 0; $i < $towerHeight; ++$i){
			$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, 0, 4, 0, "tower_piece", $rotation, true));
			if($i < $towerHeight - 1 && $random->nextBoolean()){
				$bridgePiece = $lastPiece;
			}
		}

		if($bridgePiece !== null){
			foreach(self::TOWER_BRIDGES as [$bridgeRotation, $bridgeX, $bridgeY, $bridgeZ]){
				if($random->nextBoolean()){
					$bridgeStart = $this->addHelper($pieces, $this->addPiece($bridgePiece, $bridgeX, $bridgeY, $bridgeZ, "bridge_end", Rotation::add($rotation, $bridgeRotation), true));
					$this->recursiveChildren(self::TOWER_BRIDGE, $genDepth + 1, $bridgeStart, null, $pieces, $random);
				}
			}
			$this->addHelper($pieces, $this->addPiece($lastPiece, -1, 4, -1, "tower_top", $rotation, true));
		}else{
			if($genDepth !== 7){
				return $this->recursiveChildren(self::FAT_TOWER, $genDepth + 1, $lastPiece, null, $pieces, $random);
			}
			$this->addHelper($pieces, $this->addPiece($lastPiece, -1, 4, -1, "tower_top", $rotation, true));
		}
		return true;
	}

	/**
	 * @param list<EndCityPiece> $pieces
	 */
	private function towerBridge(int $genDepth, EndCityPiece $parent, array &$pieces, RandomSource $random) : bool{
		$rotation = $parent->getRotation();
		$bridgeLength = $random->nextInt(4) + 1;
		$lastPiece = $this->addHelper($pieces, $this->addPiece($parent, 0, 0, -4, "bridge_piece", $rotation, true));
		$lastPiece->setGenDepth(-1);
		$nextY = 0;

		for($i = 0; $i < $bridgeLength; ++$i){
			if($random->nextBoolean()){
				$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, 0, $nextY, -4, "bridge_piece", $rotation, true));
				$nextY = 0;
			}else{
				if($random->nextBoolean()){
					$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, 0, $nextY, -4, "bridge_steep_stairs", $rotation, true));
				}else{
					$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, 0, $nextY, -8, "bridge_gentle_stairs", $rotation, true));
				}
				$nextY = 4;
			}
		}

		if(!$this->shipCreated && $random->nextInt(10 - $genDepth) === 0){
			$shipX = -8 + $random->nextInt(8);
			$shipZ = -70 + $random->nextInt(10);
			$this->addHelper($pieces, $this->addPiece($lastPiece, $shipX, $nextY, $shipZ, "ship", $rotation, true));
			$this->shipCreated = true;
		}elseif(!$this->recursiveChildren(self::HOUSE_TOWER, $genDepth + 1, $lastPiece, [-3, $nextY + 1, -11], $pieces, $random)){
			return false;
		}

		$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, 4, $nextY, 0, "bridge_end", Rotation::add($rotation, Rotation::ROTATE_180), true));
		$lastPiece->setGenDepth(-1);
		return true;
	}

	/**
	 * @param list<EndCityPiece> $pieces
	 */
	private function fatTower(int $genDepth, EndCityPiece $parent, array &$pieces, RandomSource $random) : bool{
		$rotation = $parent->getRotation();
		$lastPiece = $this->addHelper($pieces, $this->addPiece($parent, -3, 4, -3, "fat_tower_base", $rotation, true));
		$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, 0, 4, 0, "fat_tower_middle", $rotation, true));

		for($i = 0; $i < 2 && $random->nextInt(3) !== 0; ++$i){
			$lastPiece = $this->addHelper($pieces, $this->addPiece($lastPiece, 0, 8, 0, "fat_tower_middle", $rotation, true));
			foreach(self::FAT_TOWER_BRIDGES as [$bridgeRotation, $bridgeX, $bridgeY, $bridgeZ]){
				if($random->nextBoolean()){
					$bridgeStart = $this->addHelper($pieces, $this->addPiece($lastPiece, $bridgeX, $bridgeY, $bridgeZ, "bridge_end", Rotation::add($rotation, $bridgeRotation), true));
					$this->recursiveChildren(self::TOWER_BRIDGE, $genDepth + 1, $bridgeStart, null, $pieces, $random);
				}
			}
		}

		$this->addHelper($pieces, $this->addPiece($lastPiece, -2, 8, -2, "fat_tower_top", $rotation, true));
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\structure;

use dimension\generator\object\BlockManager;
use dimension\generator\structure\template\StateBlocks;
use pocketmine\data\bedrock\block\BlockStateData;

/**
 * The blocks of a whole structure at world coordinates, planned without
 * touching the world. Once planned, a structure is written chunk by chunk:
 * each populated chunk takes only the blocks inside it, so a structure
 * larger than the population area comes out whole and seamless.
 */
final class StructureBuffer{

	/** @var array<int, array{int, int, int, BlockStateData}> */
	private array $entries = [];
	/** @var array<int, list<array{int, int, int, BlockStateData}>>|null */
	private ?array $byChunk = null;
	private BoundingBox $bounds;

	public function __construct(){
		$this->bounds = BoundingBox::unknown();
	}

	private static function hash(int $x, int $y, int $z) : int{
		return ((($x + 30000000) & 0x3FFFFFF) << 37)
			| ((($z + 30000000) & 0x3FFFFFF) << 11)
			| ((($y + 400) & 0x3FF) << 1);
	}

	private static function chunkKey(int $chunkX, int $chunkZ) : int{
		return (($chunkX & 0xFFFFFFFF) << 32) | ($chunkZ & 0xFFFFFFFF);
	}

	public function set(int $x, int $y, int $z, BlockStateData $state) : void{
		$this->entries[self::hash($x, $y, $z)] = [$x, $y, $z, $state];
		$this->byChunk = null;
		$bounds = $this->bounds;
		if($x < $bounds->x0){
			$bounds->x0 = $x;
		}
		if($y < $bounds->y0){
			$bounds->y0 = $y;
		}
		if($z < $bounds->z0){
			$bounds->z0 = $z;
		}
		if($x > $bounds->x1){
			$bounds->x1 = $x;
		}
		if($y > $bounds->y1){
			$bounds->y1 = $y;
		}
		if($z > $bounds->z1){
			$bounds->z1 = $z;
		}
	}

	public function remove(int $x, int $y, int $z) : void{
		unset($this->entries[self::hash($x, $y, $z)]);
		$this->byChunk = null;
	}

	public function has(int $x, int $y, int $z) : bool{
		return isset($this->entries[self::hash($x, $y, $z)]);
	}

	public function get(int $x, int $y, int $z) : ?BlockStateData{
		return ($this->entries[self::hash($x, $y, $z)] ?? null)[3] ?? null;
	}

	/**
	 * @return array<int, array{int, int, int, BlockStateData}>
	 */
	public function all() : array{
		return $this->entries;
	}

	/**
	 * Bounds of every block ever set, including removed ones.
	 */
	public function getBounds() : BoundingBox{
		return $this->bounds->copy();
	}

	public function isEmpty() : bool{
		return $this->entries === [];
	}

	/**
	 * Queues into $root the planned blocks lying in the given chunk. States
	 * the server does not know are skipped.
	 */
	public function emit(BlockManager $root, int $chunkX, int $chunkZ) : void{
		if($this->byChunk === null){
			$byChunk = [];
			foreach($this->entries as $entry){
				$byChunk[self::chunkKey($entry[0] >> 4, $entry[2] >> 4)][] = $entry;
			}
			$this->byChunk = $byChunk;
		}
		foreach($this->byChunk[self::chunkKey($chunkX, $chunkZ)] ?? [] as $entry){
			$block = StateBlocks::toBlock($entry[3]);
			if($block !== null){
				$root->setBlockStateAt($entry[0], $entry[1], $entry[2], $block);
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator;

use pocketmine\block\Block;
use pocketmine\block\RuntimeBlockStateRegistry;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\nbt\tag\ByteTag;
use pocketmine\nbt\tag\IntTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\nbt\tag\Tag;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use function array_merge;
use function is_bool;
use function is_int;
use function ksort;
use function serialize;
use function str_contains;

/**
 * Builds blocks from their Bedrock identifier and state map, and gives the
 * Bedrock identifier of a block. Results are cached per thread.
 *
 * States may be given as nbt tags or as plain values: bool becomes a byte
 * tag, int an int tag, string a string tag. States left out take the value
 * of the block's default state.
 */
final class Blocks{

	/** @var array<string, int> */
	private static array $stateIds = [];
	/** @var array<int, string> */
	private static array $identifiers = [];
	/** @var array<string, list<BlockStateData>>|null */
	private static ?array $knownStates = null;

	private function __construct(){
	}

	/**
	 * @param array<string, Tag|bool|int|string> $states
	 *
	 * @throws \InvalidArgumentException when no registered block matches
	 */
	public static function get(string $identifier, array $states = []) : Block{
		if(!str_contains($identifier, ":")){
			$identifier = "minecraft:" . $identifier;
		}
		ksort($states);
		$tags = [];
		foreach($states as $name => $value){
			$tags[$name] = self::toTag($value);
		}
		$key = $identifier . serialize($states);
		$stateId = self::$stateIds[$key] ??= self::resolve($identifier, $tags);
		return RuntimeBlockStateRegistry::getInstance()->fromStateId($stateId);
	}

	/**
	 * Bedrock identifier of the block, like "minecraft:glowstone".
	 */
	public static function id(Block $block) : string{
		$stateId = $block->getStateId();
		return self::$identifiers[$stateId] ??= GlobalBlockStateHandlers::getSerializer()->serialize($stateId)->getName();
	}

	/**
	 * @param array<string, Tag> $tags
	 */
	private static function resolve(string $identifier, array $tags) : int{
		$deserializer = GlobalBlockStateHandlers::getDeserializer();
		try{
			return $deserializer->deserialize(BlockStateData::current($identifier, $tags));
		}catch(\Exception){
		}

		$upgrader = GlobalBlockStateHandlers::getUpgrader();
		try{
			$default = $upgrader->getBlockStateUpgrader()->upgrade($upgrader->upgradeStringIdMeta($identifier, 0));
			if($default->getName() === $identifier){
				return $deserializer->deserialize(BlockStateData::current($identifier, array_merge($default->getStates(), $tags)));
			}
		}catch(\Exception){
		}

		foreach(self::knownStates()[$identifier] ?? [] as $candidate){
			if(self::matches($candidate, $tags)){
				return $deserializer->deserialize($candidate);
			}
		}
		throw new \InvalidArgumentException("No block state matches $identifier");
	}

	/**
	 * @param array<string, Tag> $tags
	 */
	private static function matches(BlockStateData $candidate, array $tags) : bool{
		foreach($tags as $name => $tag){
			$state = $candidate->getState($name);
			if($state === null || !$state->equals($tag)){
				return false;
			}
		}
		return true;
	}

	/**
	 * @return array<string, list<BlockStateData>>
	 */
	private static function knownStates() : array{
		if(self::$knownStates === null){
			$serializer = GlobalBlockStateHandlers::getSerializer();
			$known = [];
			foreach(RuntimeBlockStateRegistry::getInstance()->getAllKnownStates() as $stateId => $block){
				try{
					$data = $serializer->serialize($stateId);
				}catch(\Exception){
					continue;
				}
				$known[$data->getName()][] = $data;
			}
			self::$knownStates = $known;
		}
		return self::$knownStates;
	}

	private static function toTag(Tag|bool|int|string $value) : Tag{
		if($value instanceof Tag){
			return $value;
		}
		if(is_bool($value)){
			return new ByteTag($value ? 1 : 0);
		}
		if(is_int($value)){
			return new IntTag($value);
		}
		return new StringTag($value);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator;

use dimension\generator\end\EndGenerator;
use dimension\generator\nether\NetherGenerator;
use pocketmine\world\generator\GeneratorManager;

/**
 * Registers the nether and end generators. Must run in onLoad, before the
 * worlds are loaded.
 */
final class VanillaGenerators{

	public const NETHER = "vanilla_nether";
	public const END = "vanilla_end";

	public static function register() : void{
		$manager = GeneratorManager::getInstance();
		$manager->addGenerator(NetherGenerator::class, self::NETHER, fn() => null, true);
		$manager->addGenerator(EndGenerator::class, self::END, fn() => null, true);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\random;

use dimension\generator\math\Float32;
use dimension\generator\math\Int64;
use function cos;
use function log;
use function microtime;
use function sqrt;
use const M_PI;

/**
 * Xoroshiro128 generator whose 128-bit state is expanded from a 64-bit seed
 * with SplitMix64.
 *
 * Method mapping: nextInt() returns a non negative 31-bit int, nextInt($max)
 * returns nextInt() % $max (0 when $max is 0), nextRangeInt($min, $max)
 * returns $min + nextInt() % ($max - $min) (upper bound exclusive), and
 * nextBoundedInt($max) is nextInt($max + 1) (upper bound inclusive).
 */
final class Xoroshiro128 implements RandomSource{

	private const GOLDEN_GAMMA = -7046029254386353131;
	private const MIX_1 = -4658895280553007687;
	private const MIX_2 = -7723592293110705685;

	private int $seed = 0;
	private int $s0 = 0;
	private int $s1 = 0;

	public function __construct(?int $seed = null){
		$this->setSeed($seed ?? (int) (microtime(true) * 1000000000));
	}

	public function fork() : Xoroshiro128{
		return new Xoroshiro128($this->nextLong());
	}

	public function identical() : Xoroshiro128{
		return new Xoroshiro128($this->seed);
	}

	public function nextInt(?int $max = null) : int{
		$value = $this->nextLong() & 0x7FFFFFFF;
		if($max === null){
			return $value;
		}
		if($max === 0){
			return 0;
		}
		return $value % $max;
	}

	public function nextRangeInt(int $min, int $max) : int{
		return Int64::toInt32($min + ($this->nextInt() % Int64::toInt32($max - $min)));
	}

	public function nextBoundedInt(int $max) : int{
		return $this->nextInt(Int64::toInt32($max + 1));
	}

	public function nextLong() : int{
		$i = $this->s0;
		$j = $this->s1;
		$k = Int64::add(Int64::rotateLeft(Int64::add($i, $j), 17), $i);
		$j ^= $i;
		$this->s0 = Int64::rotateLeft($i, 49) ^ $j ^ ($j << 21);
		$this->s1 = Int64::rotateLeft($j, 28);
		return $k;
	}

	public function nextBoolean() : bool{
		return ($this->nextLong() & 1) !== 0;
	}

	public function nextFloat() : float{
		return Float32::of(Int64::urshift($this->nextLong(), 40) * (1.0 / 16777216));
	}

	public function nextDouble() : float{
		return Int64::urshift($this->nextLong(), 11) * (1.0 / 9007199254740992);
	}

	public function nextGaussian() : float{
		$u1 = $this->nextDouble();
		$u2 = $this->nextDouble();
		return sqrt(-2.0 * log($u1)) * cos(2 * M_PI * $u2);
	}

	public function setSeed(int $seed) : Xoroshiro128{
		$this->seed = $seed;
		$z = $seed;
		$out = [0, 0];
		for($i = 0; $i < 2; $i++){
			$z = Int64::add($z, self::GOLDEN_GAMMA);
			$r = $z;
			$r = Int64::mul($r ^ Int64::urshift($r, 30), self::MIX_1);
			$r = Int64::mul($r ^ Int64::urshift($r, 27), self::MIX_2);
			$r ^= Int64::urshift($r, 31);
			$out[$i] = $r;
		}
		if($out[0] === 0 && $out[1] === 0){
			$out[0] = self::GOLDEN_GAMMA;
			$out[1] = ~self::GOLDEN_GAMMA;
		}
		$this->s0 = $out[0];
		$this->s1 = $out[1];
		return $this;
	}

	public function getSeed() : int{
		return $this->seed;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\random;

use dimension\generator\math\Float32;
use dimension\generator\math\Int64;
use function cos;
use function count;
use function log;
use function max;
use function microtime;
use function min;
use function mt_getrandmax;
use function mt_rand;
use function sqrt;
use const M_PI;

/**
 * MT19937 Mersenne Twister whose 624-word state is filled from a 64-bit seed
 * (the seed is expanded to 624 ints with a SplitMix64 style mixer, then
 * mixed in with the standard init_by_array procedure).
 *
 * Method mapping: nextInt() is a full signed 32-bit int, nextInt($max) is a
 * uniform int in [0, $max] (inclusive), nextRangeInt($min, $max) and
 * nextRange($min, $max) are uniform ints in [$min, $max] (inclusive), and
 * nextBoundedInt($max) is nextInt($max). A bound below the minimum throws
 * \InvalidArgumentException.
 *
 * nextGaussian() is not reproducible from the seed: it draws from an
 * independent, unseeded source, is gaussian with a standard deviation of
 * 1/3 and is clamped to [-1, 1].
 */
final class LegacyRandom implements RandomSource{

	private const N = 624;
	private const M = 397;
	private const MASK_32 = 0xFFFFFFFF;
	private const UPPER_MASK = 0x80000000;
	private const LOWER_MASK = 0x7FFFFFFF;
	private const MATRIX_A = 0x9908B0DF;
	private const GOLDEN_RATIO = -7046029254386353131;
	private const MIX_1 = -4658895280553007687;
	private const MIX_2 = -7723592293110705685;
	private const EMPTY_BOOLEAN_SOURCE = 1;

	private int $seed = 0;
	/** @var int[] unsigned 32-bit words */
	private array $mt = [];
	private int $mti = self::N;
	private int $booleanSource = self::EMPTY_BOOLEAN_SOURCE;

	public function __construct(?int $seed = null){
		$this->setSeed($seed ?? (int) (microtime(true) * 1000));
	}

	public function fork() : LegacyRandom{
		return new LegacyRandom($this->nextLong());
	}

	public function identical() : LegacyRandom{
		return new LegacyRandom($this->seed);
	}

	public function setSeed(int $seed) : LegacyRandom{
		$this->seed = $seed;
		$this->fillState(self::expandSeed($seed));
		$this->mti = self::N;
		$this->booleanSource = self::EMPTY_BOOLEAN_SOURCE;
		return $this;
	}

	public function getSeed() : int{
		return $this->seed;
	}

	public function nextInt(?int $max = null) : int{
		if($max === null){
			return $this->next();
		}
		return $this->nextIntBounded(Int64::toInt32($max + 1));
	}

	public function nextRangeInt(int $min, int $max) : int{
		return $this->nextIntRange($min, Int64::toInt32($max + 1));
	}

	public function nextRange(int $min, int $max) : int{
		return $this->nextIntRange($min, Int64::toInt32($max + 1));
	}

	public function nextBoundedInt(int $max) : int{
		return $this->nextInt($max);
	}

	public function nextLong() : int{
		$high = $this->next();
		$low = $this->next();
		return ($high << 32) | ($low & self::MASK_32);
	}

	public function nextBoolean() : bool{
		$bits = $this->booleanSource;
		if($bits === 1){
			$bits = $this->next();
			$this->booleanSource = Int64::toInt32(self::UPPER_MASK | (($bits & self::MASK_32) >> 1));
			return ($bits & 1) === 1;
		}
		$this->booleanSource = ($bits & self::MASK_32) >> 1;
		return ($bits & 1) === 1;
	}

	public function nextFloat() : float{
		return Float32::of((($this->next() & self::MASK_32) >> 8) * (1.0 / 16777216));
	}

	public function nextDouble() : float{
		$v = $this->next() & self::MASK_32;
		$w = $this->next() & self::MASK_32;
		return ((($v >> 6) << 27) | ($w >> 5)) * (1.0 / 9007199254740992);
	}

	public function nextGaussian() : float{
		$scale = mt_getrandmax() + 1.0;
		$u1 = (mt_rand() + 1.0) / $scale;
		$u2 = mt_rand() / $scale;
		$sample = sqrt(-2.0 * log($u1)) * cos(2 * M_PI * $u2) * 0.33333;
		return min(1.0, max($sample, -1.0));
	}

	/**
	 * Uniform int in [0, $bound), $bound being a positive 32-bit int.
	 */
	private function nextIntBounded(int $bound) : int{
		if($bound <= 0){
			throw new \InvalidArgumentException("Upper bound $bound must be above zero");
		}
		$m = ($this->next() & self::MASK_32) * $bound;
		$l = $m & self::MASK_32;
		if($l < $bound){
			$t = 4294967296 % $bound;
			while($l < $t){
				$m = ($this->next() & self::MASK_32) * $bound;
				$l = $m & self::MASK_32;
			}
		}
		return $m >> 32;
	}

	/**
	 * Uniform int in [$origin, $bound).
	 */
	private function nextIntRange(int $origin, int $bound) : int{
		if($origin >= $bound){
			throw new \InvalidArgumentException("Range [$origin, $bound) is empty");
		}
		$n = Int64::toInt32($bound - $origin);
		if($n > 0){
			return $this->nextIntBounded($n) + $origin;
		}
		$v = $this->next();
		while($v < $origin || $v >= $bound){
			$v = $this->next();
		}
		return $v;
	}

	/**
	 * Next tempered 32-bit output as a signed int.
	 */
	private function next() : int{
		$mt = &$this->mt;
		if($this->mti >= self::N){
			for($k = 0; $k < self::N - self::M; ++$k){
				$y = ($mt[$k] & self::UPPER_MASK) | ($mt[$k + 1] & self::LOWER_MASK);
				$mt[$k] = $mt[$k + self::M] ^ ($y >> 1) ^ (($y & 1) === 0 ? 0 : self::MATRIX_A);
			}
			for($k = self::N - self::M; $k < self::N - 1; ++$k){
				$y = ($mt[$k] & self::UPPER_MASK) | ($mt[$k + 1] & self::LOWER_MASK);
				$mt[$k] = $mt[$k + (self::M - self::N)] ^ ($y >> 1) ^ (($y & 1) === 0 ? 0 : self::MATRIX_A);
			}
			$y = ($mt[self::N - 1] & self::UPPER_MASK) | ($mt[0] & self::LOWER_MASK);
			$mt[self::N - 1] = $mt[self::M - 1] ^ ($y >> 1) ^ (($y & 1) === 0 ? 0 : self::MATRIX_A);
			$this->mti = 0;
		}

		$y = $mt[$this->mti++];
		$y ^= $y >> 11;
		$y ^= ($y << 7) & 0x9D2C5680;
		$y ^= ($y << 15) & 0xEFC60000;
		$y ^= $y >> 18;
		return Int64::toInt32($y);
	}

	/**
	 * Expands a 64-bit seed into the 624 ints fed to the state initialisation.
	 *
	 * @return int[] signed 32-bit ints
	 */
	private static function expandSeed(int $input) : array{
		$v = $input === Int64::sub(0, self::GOLDEN_RATIO) ? ~$input : $input;
		$output = [];
		for($i = 0; $i < self::N; $i += 2){
			$v = Int64::add($v, self::GOLDEN_RATIO);
			$x = self::mix($v);
			$output[$i] = Int64::toInt32($x);
			$output[$i + 1] = Int64::toInt32(Int64::urshift($x, 32));
		}
		return $output;
	}

	private static function mix(int $x) : int{
		$x = Int64::mul($x ^ Int64::urshift($x, 30), self::MIX_1);
		$x = Int64::mul($x ^ Int64::urshift($x, 27), self::MIX_2);
		return $x ^ Int64::urshift($x, 31);
	}

	/**
	 * @param int[] $seed signed 32-bit ints
	 */
	private function fillState(array $seed) : void{
		$state = [];
		$mt = 19650218;
		$state[0] = $mt;
		for($i = 1; $i < self::N; $i++){
			$mt = (1812433253 * ($mt ^ ($mt >> 30)) + $i) & self::MASK_32;
			$state[$i] = $mt;
		}

		$seedLength = count($seed);
		$i = 1;
		$j = 0;
		for($k = max(self::N, $seedLength); $k > 0; $k--){
			$a = $state[$i];
			$b = $state[$i - 1];
			$c = ($a ^ (($b ^ ($b >> 30)) * 1664525)) + $seed[$j] + $j;
			$state[$i] = $c & self::MASK_32;
			$i++;
			$j++;
			if($i >= self::N){
				$state[0] = $state[self::N - 1];
				$i = 1;
			}
			if($j >= $seedLength){
				$j = 0;
			}
		}

		for($k = self::N - 1; $k > 0; $k--){
			$a = $state[$i];
			$b = $state[$i - 1];
			$c = ($a ^ (($b ^ ($b >> 30)) * 1566083941)) - $i;
			$state[$i] = $c & self::MASK_32;
			$i++;
			if($i >= self::N){
				$state[0] = $state[self::N - 1];
				$i = 1;
			}
		}

		$state[0] = self::UPPER_MASK;
		$this->mt = $state;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\random;

/**
 * A seedable source of randomness used by the terrain generators.
 *
 * Overloads are mapped as follows: nextInt() and nextInt($max) share one
 * method with an optional bound, and the two argument form nextInt(min, max)
 * is nextRangeInt($min, $max).
 */
interface RandomSource{

	/**
	 * Creates a new source seeded with the next random long of this one.
	 */
	public function fork() : RandomSource;

	/**
	 * Creates a new source seeded with the seed this one was last seeded with.
	 */
	public function identical() : RandomSource;

	/**
	 * Without bound: a random int. With a bound: see the implementation for
	 * the exact range.
	 */
	public function nextInt(?int $max = null) : int;

	/**
	 * A random int between $min and $max, see the implementation for the
	 * exact range.
	 */
	public function nextRangeInt(int $min, int $max) : int;

	public function nextBoundedInt(int $max) : int;

	public function nextLong() : int;

	public function nextBoolean() : bool;

	/**
	 * A single precision value in [0, 1).
	 */
	public function nextFloat() : float;

	/**
	 * A value in [0, 1).
	 */
	public function nextDouble() : float;

	/**
	 * A gaussian distributed value.
	 */
	public function nextGaussian() : float;

	public function setSeed(int $seed) : RandomSource;

	public function getSeed() : int;
}
<?php

declare(strict_types=1);

namespace dimension\generator\biome;

/**
 * The biome chosen for a column, as a BiomeIds id.
 */
class BiomeResult{

	public function __construct(
		protected int $biomeId
	){}

	public function getBiomeId() : int{
		return $this->biomeId;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\biome;

/**
 * A nether biome with the climate values it was picked from.
 */
final class NetherBiomeResult extends BiomeResult{

	public function __construct(
		int $biomeId,
		private float $temperature,
		private float $humidity
	){
		parent::__construct($biomeId);
	}

	public function getTemperature() : float{
		return $this->temperature;
	}

	public function getHumidity() : float{
		return $this->humidity;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\biome;

use dimension\generator\math\Float32;
use dimension\generator\noise\NormalNoise;
use dimension\generator\random\LegacyRandom;
use pocketmine\data\bedrock\BiomeIds;

/**
 * Picks the nether biome whose climate point (temperature, humidity,
 * offset) is nearest to the temperature and humidity noises.
 */
final class NetherBiomePicker implements BiomePicker{

	private NormalNoise $temperatureNoise;
	private NormalNoise $humidityNoise;
	/** @var array<int, float[]> biome id => climate point */
	private array $points;

	public function __construct(LegacyRandom $random){
		$this->temperatureNoise = new NormalNoise($random->fork(), -10, [1.5, 0, 1, 0, 0, 0]);
		$this->humidityNoise = new NormalNoise($random->fork(), -8, [1, 1, 0, 0, 0, 0]);
		$this->points = [
			BiomeIds::BASALT_DELTAS => [-0.5, 0.0, Float32::of(0.175)],
			BiomeIds::CRIMSON_FOREST => [Float32::of(0.4), 0.0, 0.0],
			BiomeIds::HELL => [0.0, 0.0, 0.0],
			BiomeIds::SOULSAND_VALLEY => [0.0, -0.5, 0.0],
			BiomeIds::WARPED_FOREST => [0.0, 0.5, Float32::of(0.375)]
		];
	}

	public function pick(int $x, int $y, int $z) : NetherBiomeResult{
		$temperature = $this->temperatureNoise->getValue($x, $y, $z);
		$humidity = $this->humidityNoise->getValue($x, $y, $z);
		$distance = 3.4028234663852886E38;
		$biomeId = BiomeIds::HELL;
		foreach($this->points as $id => [$pointX, $pointY, $pointZ]){
			$dx = $temperature - $pointX;
			$dy = $humidity - $pointY;
			$dz = 0.0 - $pointZ;
			$delta = $dx * $dx + $dy * $dy + $dz * $dz;
			if($delta < $distance){
				$distance = $delta;
				$biomeId = $id;
			}
		}
		return new NetherBiomeResult($biomeId, $temperature, $humidity);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\biome;

interface BiomePicker{

	public function pick(int $x, int $y, int $z) : BiomeResult;
}
<?php

declare(strict_types=1);

namespace dimension\generator\biome;

use pocketmine\data\bedrock\BiomeIds;

/**
 * The end is a single biome.
 */
final class EndBiomePicker implements BiomePicker{

	private BiomeResult $result;

	public function __construct(){
		$this->result = new BiomeResult(BiomeIds::THE_END);
	}

	public function pick(int $x, int $y, int $z) : BiomeResult{
		return $this->result;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end\populator;

use dimension\generator\end\object\ExitPortal;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use pocketmine\world\ChunkManager;

/**
 * Builds the exit portal frame on top of the main island at 0, 0. It runs
 * for chunk 0, 0 so that every block of the frame lies in a chunk the
 * population pass can write.
 */
final class ExitPortalPopulator implements Populator{

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		if($chunkX === 0 && $chunkZ === 0){
			$object = new BlockManager($world);
			(new ExitPortal())->generate($object, 0, WorldQuery::heightMap($world, 0, 0), 0);
			$root->merge($object);
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end\populator;

use dimension\generator\ChunkHash;
use dimension\generator\end\EndIslands;
use dimension\generator\end\object\EndIsland;
use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use dimension\generator\random\LegacyRandom;
use pocketmine\world\ChunkManager;

final class EndIslandPopulator implements Populator{

	private LegacyRandom $random;
	private EndIsland $endIsland;

	public function __construct(){
		$this->random = new LegacyRandom(0);
		$this->endIsland = new EndIsland();
	}

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		if($chunkX * $chunkX + $chunkZ * $chunkZ <= 4096){
			return;
		}
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		if($random->nextBoundedInt(14) === 0){
			$height = EndIslands::height($seed, $chunkX, $chunkZ, 1, 1);
			if($height < -20.0){
				$baseX = $chunkX << 4;
				$baseZ = $chunkZ << 4;
				$object = new BlockManager($world);
				$x = $baseX + 8 + $random->nextBoundedInt(16);
				$y = 55 + $random->nextBoundedInt(16);
				$z = $baseZ + 8 + $random->nextBoundedInt(16);
				$this->endIsland->generate($object, $random, $x, $y, $z);
				if($random->nextBoundedInt(4) === 0){
					$x = $baseX + 8 + $random->nextBoundedInt(16);
					$y = 55 + $random->nextBoundedInt(16);
					$z = $baseZ + 8 + $random->nextBoundedInt(16);
					$this->endIsland->generate($object, $random, $x, $y, $z);
				}
				$root->merge($object);
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end\populator;

use dimension\generator\end\object\ObsidianPillar;
use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use pocketmine\math\Vector3;
use pocketmine\world\ChunkManager;

/**
 * Builds the obsidian spikes whose centre lies in the populated chunk.
 *
 * Each spike carries an end crystal, which cannot be spawned from the
 * generation thread: crystalSpawns() gives where they go so the main thread
 * can spawn them once the chunk is populated.
 */
final class ObsidianPillarPopulator implements Populator{

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		for($i = 0; $i < ObsidianPillar::COUNT; $i++){
			[$x, $z] = ObsidianPillar::position($i);
			if($x >> 4 === $chunkX && $z >> 4 === $chunkZ){
				$object = new BlockManager($world);
				(new ObsidianPillar($i, $seed))->generate($object, $x, $z);
				$root->merge($object);
			}
		}
	}

	/**
	 * End crystal positions of the spikes, with the chunk whose population
	 * builds each spike.
	 *
	 * @return list<array{chunkX: int, chunkZ: int, position: Vector3}>
	 */
	public static function crystalSpawns(int $seed) : array{
		$spawns = [];
		for($i = 0; $i < ObsidianPillar::COUNT; $i++){
			[$x, $z] = ObsidianPillar::position($i);
			$height = (new ObsidianPillar($i, $seed))->getHeight();
			$spawns[] = [
				"chunkX" => $x >> 4,
				"chunkZ" => $z >> 4,
				"position" => new Vector3($x + 0.5, $height + 1, $z + 0.5)
			];
		}
		return $spawns;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end\populator;

use dimension\generator\ChunkHash;
use dimension\generator\end\EndIslands;
use dimension\generator\end\object\EndGateway;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use dimension\generator\random\LegacyRandom;
use pocketmine\world\ChunkManager;

final class EndGatewayPopulator implements Populator{

	private LegacyRandom $random;
	private EndGateway $endGateway;

	public function __construct(){
		$this->random = new LegacyRandom(0);
		$this->endGateway = new EndGateway();
	}

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		if($chunkX * $chunkX + $chunkZ * $chunkZ <= 4096){
			return;
		}
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		if(EndIslands::height($seed, $chunkX, $chunkZ, 1, 1) > 40.0){
			if($random->nextBoundedInt(700) === 0){
				$x = ($chunkX << 4) + $random->nextBoundedInt(16);
				$z = ($chunkZ << 4) + $random->nextBoundedInt(16);
				$y = WorldQuery::heightMap($world, $x, $z) + $random->nextBoundedInt(7) + 3;
				if($y > 1 && $y < 254){
					$object = new BlockManager($world);
					$this->endGateway->generate($object, $x, $y, $z);
					$root->merge($object);
				}
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end\populator;

use dimension\generator\ChunkHash;
use dimension\generator\end\EndIslands;
use dimension\generator\end\object\ChorusPlant;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use dimension\generator\random\Xoroshiro128;
use pocketmine\world\ChunkManager;

final class ChorusFlowerPopulator implements Populator{

	private Xoroshiro128 $random;
	private ChorusPlant $chorusPlant;

	public function __construct(){
		$this->random = new Xoroshiro128(0);
		$this->chorusPlant = new ChorusPlant();
	}

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		if($chunkX * $chunkX + $chunkZ * $chunkZ <= 4096){
			return;
		}
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		if(EndIslands::height($seed, $chunkX, $chunkZ, 1, 1) > 40.0){
			for($i = 0; $i < $random->nextBoundedInt(5); $i++){
				$x = ($chunkX << 4) + $random->nextBoundedInt(16);
				$z = ($chunkZ << 4) + $random->nextBoundedInt(16);
				$y = WorldQuery::heightMap($world, $x, $z);
				if($y > 0){
					if(WorldQuery::blockId($world, $x, $y + 1, $z) === WorldQuery::AIR && WorldQuery::blockId($world, $x, $y, $z) === "minecraft:end_stone"){
						$object = new BlockManager($world);
						$this->chorusPlant->generate($object, $random, $x, $y + 1, $z, 8);
						$root->merge($object);
					}
				}
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end;

use dimension\generator\end\populator\ChorusFlowerPopulator;
use dimension\generator\end\populator\EndGatewayPopulator;
use dimension\generator\end\populator\EndIslandPopulator;
use dimension\generator\end\populator\ExitPortalPopulator;
use dimension\generator\end\populator\ObsidianPillarPopulator;
use dimension\generator\Populator;
use dimension\generator\structure\EndCityPopulator;

/**
 * The end population pass, in execution order.
 */
final class EndPopulators{

	private function __construct(){
	}

	/**
	 * @return list<Populator>
	 */
	public static function create(int $seed, string $resourceFolder) : array{
		return [
			new ObsidianPillarPopulator(),
			new ExitPortalPopulator(),
			new ChorusFlowerPopulator(),
			new EndCityPopulator($seed, $resourceFolder),
			new EndGatewayPopulator(),
			new EndIslandPopulator(),
		];
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end\object;

use dimension\generator\object\BlockManager;
use function abs;

/**
 * Bedrock frame and pillar of the end exit portal, centred on the given
 * position, with its inside cleared.
 */
final class ExitPortal{

	private const BEDROCK = "minecraft:bedrock";

	public function generate(BlockManager $level, int $x, int $y, int $z) : void{
		for($dx = -2; $dx <= 2; $dx++){
			for($dz = -2; $dz <= 2; $dz++){
				if(!(abs($dx) === 2 && abs($dz) === 2)){
					$level->setBlockStateAt($x + $dx, $y - 1, $z + $dz, self::BEDROCK);
					$level->setBlockStateAt($x + $dx, $y, $z + $dz, "minecraft:air");
					if($dx === -2){
						$level->setBlockStateAt($x - 3, $y, $z + $dz, self::BEDROCK);
					}
					if($dx === 2){
						$level->setBlockStateAt($x + 3, $y, $z + $dz, self::BEDROCK);
					}
					if($dz === -2){
						$level->setBlockStateAt($x + $dx, $y, $z - 3, self::BEDROCK);
					}
					if($dz === 2){
						$level->setBlockStateAt($x + $dx, $y, $z + 3, self::BEDROCK);
					}
				}else{
					$level->setBlockStateAt($x + $dx, $y, $z + $dz, self::BEDROCK);
				}
			}
		}
		for($i = 0; $i < 4; $i++){
			$level->setBlockStateAt($x, $y + $i, $z, self::BEDROCK);
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end\object;

use dimension\generator\math\Float32;
use dimension\generator\math\GenerationMath;
use dimension\generator\object\BlockManager;
use dimension\generator\random\LegacyRandom;

/**
 * Small floating end stone island: stacked discs shrinking downward.
 */
final class EndIsland{

	public function generate(BlockManager $level, LegacyRandom $rand, int $posX, int $posY, int $posZ) : void{
		$n = (float) ($rand->nextInt(2) + 4);
		for($y = 0; $n > 0.5; $y--){
			$limit = Float32::of(($n + 1.0) * ($n + 1.0));
			for($x = GenerationMath::floorFloat(-$n); $x <= GenerationMath::ceilFloat($n); $x++){
				for($z = GenerationMath::floorFloat(-$n); $z <= GenerationMath::ceilFloat($n); $z++){
					if((float) ($x * $x + $z * $z) <= $limit){
						$level->setBlockStateAt($posX + $x, $posY + $y, $posZ + $z, "minecraft:end_stone");
					}
				}
			}
			$n = Float32::of($n - ($rand->nextInt(1) + 0.5));
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end\object;

use dimension\generator\Blocks;
use dimension\generator\math\Int64;
use dimension\generator\object\BlockManager;
use function abs;
use function cos;
use function intdiv;
use function sin;
use const M_PI;

/**
 * One of the ten obsidian spikes around the end main island. Its size comes
 * from its index and the low 32 bits of the world seed; spikes 1 and 2 of the
 * size order are caged in iron bars. The top holds infiniburn bedrock and fire.
 */
final class ObsidianPillar{

	public const COUNT = 10;

	public function __construct(
		private int $index,
		private int $seed
	){}

	/**
	 * Centre of pillar $index, in block coordinates.
	 *
	 * @return array{int, int}
	 */
	public static function position(int $index) : array{
		$angle = 2.0 * (-M_PI + (M_PI / 10.0) * $index);
		return [(int) (42.0 * cos($angle)), (int) (42.0 * sin($angle))];
	}

	public function getPillar() : int{
		return abs(Int64::toInt32($this->index * 73 + Int64::toInt32($this->seed)) % 10);
	}

	public function getRadius() : int{
		return 2 + intdiv($this->getPillar(), 3);
	}

	public function getHeight() : int{
		return 76 + $this->getPillar() * 3;
	}

	public function isGuarded() : bool{
		$pillar = $this->getPillar();
		return $pillar === 1 || $pillar === 2;
	}

	public function generate(BlockManager $level, int $x, int $z) : void{
		$height = $this->getHeight();
		$radius = $this->getRadius();
		for($i = 0; $i < $height; $i++){
			for($j = -$radius; $j <= $radius; $j++){
				for($k = -$radius; $k <= $radius; $k++){
					if($j * $j + $k * $k <= $radius * $radius + 1){
						$level->setBlockStateAt($x + $j, $i, $z + $k, "minecraft:obsidian");
					}
				}
			}
		}
		if($this->isGuarded()){
			for($i = -2; $i <= 2; ++$i){
				for($j = -2; $j <= 2; ++$j){
					if(abs($i) === 2 || abs($j) === 2){
						for($k = 0; $k < 3; ++$k){
							$level->setBlockStateAt($x + $i, $height + $k, $z + $j, "minecraft:iron_bars");
						}
					}
					$level->setBlockStateAt($x + $i, $height + 3, $z + $j, "minecraft:iron_bars");
				}
			}
		}
		$level->setBlockStateAt($x, $height, $z, Blocks::get("minecraft:bedrock", ["infiniburn_bit" => true]));
		$level->setBlockStateAt($x, $height + 1, $z, "minecraft:fire");
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end\object;

use dimension\generator\Blocks;
use dimension\generator\object\BlockManager;
use dimension\generator\random\Xoroshiro128;
use pocketmine\block\Block;
use function abs;

/**
 * Fully grown chorus plant: branching chorus stems capped by dead chorus
 * flowers after four levels of branching.
 */
final class ChorusPlant{

	private const AIR = "minecraft:air";
	private const PLANT = "minecraft:chorus_plant";

	/** Horizontal faces in north, east, south, west order, with the index of their opposite. */
	private const HORIZONTALS = [[0, -1], [1, 0], [0, 1], [-1, 0]];
	private const OPPOSITE = [2, 3, 0, 1];

	private ?Block $deadFlower = null;

	private function deadFlower() : Block{
		return $this->deadFlower ??= Blocks::get("minecraft:chorus_flower", ["age" => 5]);
	}

	public function generate(BlockManager $level, Xoroshiro128 $random, int $x, int $y, int $z, int $maxSize) : void{
		$level->setBlockStateAt($x, $y, $z, self::PLANT);
		$this->growImmediately($level, $random, $x, $y, $z, $maxSize, 0);
	}

	private function growImmediately(BlockManager $level, Xoroshiro128 $random, int $x, int $y, int $z, int $maxSize, int $age) : void{
		$height = 1 + $random->nextInt(4);
		if($age === 0){
			$height++;
		}

		for($dy = 1; $dy <= $height; $dy++){
			if(!$this->isHorizontalAir($level, $x, $y + $dy, $z, -1)){
				return;
			}
			$level->setBlockStateAt($x, $y + $dy, $z, self::PLANT);
		}

		if($age < 4){
			$attempt = $random->nextInt(4);
			if($age === 0){
				$attempt++;
			}
			for($i = 0; $i < $attempt; $i++){
				$face = $random->nextInt(4);
				$checkX = $x + self::HORIZONTALS[$face][0];
				$checkY = $y + $height;
				$checkZ = $z + self::HORIZONTALS[$face][1];
				if($level->getBlockIdAt($checkX, $checkY, $checkZ) === self::AIR && $level->getBlockIdAt($checkX, $checkY - 1, $checkZ) === self::AIR){
					if(abs($checkX - $x) < $maxSize && abs($checkZ - $z) < $maxSize && $this->isHorizontalAir($level, $checkX, $checkY, $checkZ, self::OPPOSITE[$face])){
						$level->setBlockStateAt($checkX, $checkY, $checkZ, self::PLANT);
						$this->growImmediately($level, $random, $checkX, $checkY, $checkZ, $maxSize, $age + 1);
					}
				}
			}
		}else{
			$level->setBlockStateAt($x, $y + $height, $z, $this->deadFlower());
		}
	}

	private function isHorizontalAir(BlockManager $level, int $x, int $y, int $z, int $except) : bool{
		foreach(self::HORIZONTALS as $index => [$dx, $dz]){
			if($index !== $except && $level->getBlockIdAt($x + $dx, $y, $z + $dz) !== self::AIR){
				return false;
			}
		}
		return true;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end\object;

use dimension\generator\Blocks;
use dimension\generator\object\BlockManager;
use pocketmine\block\Block;

/**
 * End gateway block wrapped in bedrock above and below. When the server has
 * no end gateway block the centre is left as air.
 */
final class EndGateway{

	private const FACES = [[0, -1, 0], [0, 1, 0], [0, 0, -1], [0, 0, 1], [-1, 0, 0], [1, 0, 0]];

	private ?Block $gateway = null;
	private bool $resolved = false;

	private function gatewayBlock() : ?Block{
		if(!$this->resolved){
			$this->resolved = true;
			try{
				$this->gateway = Blocks::get("minecraft:end_gateway");
			}catch(\InvalidArgumentException){
				$this->gateway = null;
			}
		}
		return $this->gateway;
	}

	public function generate(BlockManager $level, int $x, int $y, int $z) : void{
		foreach(self::FACES as [$dx, $dy, $dz]){
			$level->setBlockStateAt($x + $dx, $y + 1 + $dy, $z + $dz, "minecraft:bedrock");
		}
		foreach(self::FACES as [$dx, $dy, $dz]){
			$level->setBlockStateAt($x + $dx, $y - 1 + $dy, $z + $dz, "minecraft:bedrock");
		}
		$level->setBlockStateAt($x, $y, $z, $this->gatewayBlock() ?? "minecraft:air");
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end;

use dimension\generator\holder\EndObjectHolder;
use dimension\generator\noise\SimplexNoise;
use dimension\generator\random\Xoroshiro128;

/**
 * Island height of the End as the terrain generator computes it, for the
 * populators that decide from it. The island noise is built from the seed
 * exactly like the generator does, once per seed and per thread.
 */
final class EndIslands{

	private static ?int $seed = null;
	private static ?SimplexNoise $noise = null;

	private function __construct(){
	}

	public static function height(int $seed, int $chunkX, int $chunkZ, int $x, int $z) : float{
		return EndGenerator::getIslandHeight($chunkX, $chunkZ, $x, $z, self::noise($seed));
	}

	private static function noise(int $seed) : SimplexNoise{
		if(self::$noise === null || self::$seed !== $seed){
			self::$noise = (new EndObjectHolder(new Xoroshiro128($seed)))->getTerrainHolder()->getIslandNoise();
			self::$seed = $seed;
		}
		return self::$noise;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\end;

use dimension\generator\biome\EndBiomePicker;
use dimension\generator\BiomeFiller;
use dimension\generator\GeneratorOptions;
use dimension\generator\holder\EndObjectHolder;
use dimension\generator\math\Float32;
use dimension\generator\math\GenerationMath;
use dimension\generator\noise\SimplexNoise;
use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use dimension\generator\random\Xoroshiro128;
use pocketmine\block\VanillaBlocks;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;
use pocketmine\world\generator\Generator;
use function abs;
use function fmod;
use function max;
use function sqrt;

/**
 * End terrain: the main island around the origin and the outer islands
 * beyond 1024 blocks, made of end stone between Y 0 and 127. The whole
 * chunk is the end biome.
 */
final class EndGenerator extends Generator{

	private const COORDINATE_SCALE = 684.412;
	private const DETAIL_NOISE_SCALE_X = 80.0;
	private const DETAIL_NOISE_SCALE_Z = 80.0;

	private EndBiomePicker $biomePicker;
	private EndObjectHolder $objectHolder;
	/** @var list<Populator>|null */
	private ?array $populators = null;
	private int $endStone;

	public function __construct(int $seed, string $preset){
		parent::__construct($seed, $preset);
		$this->biomePicker = new EndBiomePicker();
		$this->objectHolder = new EndObjectHolder(new Xoroshiro128($seed));
		$this->endStone = VanillaBlocks::END_STONE()->getStateId();
	}

	public function getObjectHolder() : EndObjectHolder{
		return $this->objectHolder;
	}

	public function generateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void{
		$chunk = $world->getChunk($chunkX, $chunkZ) ?? throw new \InvalidArgumentException("Chunk $chunkX $chunkZ does not yet exist");
		BiomeFiller::fill($chunk, $this->biomePicker->pick($chunkX << 4, 63, $chunkZ << 4)->getBiomeId());
		$this->generateTerrain($chunk, $chunkX, $chunkZ);
	}

	public function populateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void{
		$this->populators ??= EndPopulators::create($this->seed, GeneratorOptions::resourceFolder($this->preset));
		$root = new BlockManager($world);
		foreach($this->populators as $populator){
			try{
				$populator->populate($root, $world, $chunkX, $chunkZ, $this->seed);
			}catch(\Throwable){
			}
		}
		$root->apply();
	}

	private function generateTerrain(Chunk $chunk, int $chunkX, int $chunkZ) : void{
		$noises = $this->objectHolder->getTerrainHolder();
		$densityX = $chunkX << 1;
		$densityZ = $chunkZ << 1;
		$density = [];
		$detailNoise = $noises->getDetailNoiseOctaves()->generateNoiseOctaves(
			$densityX, 0, $densityZ, 3, 33, 3,
			(self::COORDINATE_SCALE * 2) / self::DETAIL_NOISE_SCALE_X,
			4.277575000000001,
			(self::COORDINATE_SCALE * 2) / self::DETAIL_NOISE_SCALE_Z
		);
		$roughnessNoise = $noises->getRoughnessNoiseOctaves()->generateNoiseOctaves(
			$densityX, 0, $densityZ, 3, 33, 3,
			self::COORDINATE_SCALE * 2, self::COORDINATE_SCALE, self::COORDINATE_SCALE * 2
		);
		$roughnessNoise2 = $noises->getRoughnessNoiseOctaves2()->generateNoiseOctaves(
			$densityX, 0, $densityZ, 3, 33, 3,
			self::COORDINATE_SCALE * 2, self::COORDINATE_SCALE, self::COORDINATE_SCALE * 2
		);

		$index = 0;
		for($i = 0; $i < 3; $i++){
			for($j = 0; $j < 3; $j++){
				$noiseHeight = self::getIslandHeight($chunkX, $chunkZ, $i, $j, $noises->getIslandNoise());
				for($k = 0; $k < 33; $k++){
					$noiseR = $roughnessNoise[$index] / 512.0;
					$noiseR2 = $roughnessNoise2[$index] / 512.0;
					$noiseD = ($detailNoise[$index] / 10.0 + 1.0) / 2.0;
					$dens = $noiseD < 0 ? $noiseR : ($noiseD > 1 ? $noiseR2 : $noiseR + ($noiseR2 - $noiseR) * $noiseD);
					$dens = ($dens - 8.0) + $noiseHeight;
					$index++;
					if($k < 8){
						$lowering = Float32::of((8 - $k) / 7.0);
						$dens = $dens * (1.0 - $lowering) + $lowering * -30.0;
					}elseif($k > 14){
						$lowering = GenerationMath::clamp(($k - 14) / 64.0, 0.0, 1.0);
						$dens = $dens * (1.0 - $lowering) + $lowering * -3000.0;
					}
					$density[$i][$j][$k] = $dens;
				}
			}
		}

		for($i = 0; $i < 2; $i++){
			for($j = 0; $j < 2; $j++){
				for($k = 0; $k < 32; $k++){
					$d1 = $density[$i][$j][$k];
					$d2 = $density[$i + 1][$j][$k];
					$d3 = $density[$i][$j + 1][$k];
					$d4 = $density[$i + 1][$j + 1][$k];
					$d5 = ($density[$i][$j][$k + 1] - $d1) / 4;
					$d6 = ($density[$i + 1][$j][$k + 1] - $d2) / 4;
					$d7 = ($density[$i][$j + 1][$k + 1] - $d3) / 4;
					$d8 = ($density[$i + 1][$j + 1][$k + 1] - $d4) / 4;
					for($l = 0; $l < 4; $l++){
						$d9 = $d1;
						$d10 = $d3;
						for($m = 0; $m < 8; $m++){
							$dens = $d9;
							for($n = 0; $n < 8; $n++){
								if($dens > 0){
									$chunk->setBlockStateId($m + ($i << 3), $l + ($k << 2), $n + ($j << 3), $this->endStone);
								}
								$dens += ($d10 - $d9) / 8;
							}
							$d9 += ($d2 - $d1) / 8;
							$d10 += ($d4 - $d3) / 8;
						}
						$d1 += $d5;
						$d3 += $d7;
						$d2 += $d6;
						$d4 += $d8;
					}
				}
			}
		}
	}

	/**
	 * Single precision height offset of the density column ($x, $z) (0-2,
	 * 8 blocks apart) of a chunk: a cone around the origin, raised by any
	 * outer island centred within 12 chunks.
	 */
	public static function getIslandHeight(int $chunkX, int $chunkZ, int $x, int $z, SimplexNoise $islandNoise) : float{
		$threshold = Float32::of(-0.9);
		$x1 = Float32::of($chunkX * 2 + $x);
		$z1 = Float32::of($chunkZ * 2 + $z);
		$islandHeight1 = GenerationMath::clamp(
			Float32::of(100.0 - Float32::of(self::distance($x1, $z1) * 8.0)),
			-100.0,
			80.0
		);

		for($i = -12; $i <= 12; $i++){
			for($j = -12; $j <= 12; $j++){
				$x2 = $chunkX + $i;
				$z2 = $chunkZ + $j;
				if(($x2 * $x2) + ($z2 * $z2) > 4096 && $islandNoise->getValue($x2, $z2) < $threshold){
					$x1 = (float) ($x - $i * 2);
					$z1 = (float) ($z - $j * 2);
					$weight = Float32::of(Float32::of(abs(Float32::of($x2)) * 3439.0) + Float32::of(abs(Float32::of($z2)) * 147.0));
					$weight = Float32::of(fmod($weight, 13.0) + 9.0);
					$islandHeight2 = Float32::of(100.0 - Float32::of(self::distance($x1, $z1) * $weight));
					$islandHeight2 = GenerationMath::clamp($islandHeight2, -100.0, 80.0);
					$islandHeight1 = max($islandHeight1, $islandHeight2);
				}
			}
		}
		return $islandHeight1;
	}

	/**
	 * Single precision sqrt(x^2 + z^2).
	 */
	private static function distance(float $x, float $z) : float{
		return Float32::of(sqrt(Float32::of(Float32::of($x * $x) + Float32::of($z * $z))));
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\holder;

use dimension\generator\random\RandomSource;

/**
 * Holds generation objects built from one seeded random source.
 */
abstract class RandomizedObjectHolder{

	public function __construct(
		protected RandomSource $randomSourceProvider
	){}

	public function getRandomSourceProvider() : RandomSource{
		return $this->randomSourceProvider;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\holder;

use dimension\generator\density\BlendedNoise;
use dimension\generator\density\DensityFunction;
use dimension\generator\density\NetherDensity;
use dimension\generator\noise\NormalNoise;
use dimension\generator\random\RandomSource;

/**
 * Noises of the nether terrain and surface. Each noise is drawn from a
 * fresh source seeded with the world seed.
 */
final class NetherTerrainHolder extends RandomizedObjectHolder{

	private NormalNoise $surfaceNoise;
	private NormalNoise $patchNoise;
	private NormalNoise $soulsandNoise;
	private NormalNoise $netherStateNoise;
	private NormalNoise $netherwartNoise;
	private DensityFunction $base3dNoise;
	private DensityFunction $densityFunction;

	public function __construct(RandomSource $randomSourceProvider){
		parent::__construct($randomSourceProvider);
		$this->surfaceNoise = new NormalNoise($randomSourceProvider->identical(), -6, [1.0, 1.0, 1.0]);
		$this->patchNoise = new NormalNoise($randomSourceProvider->identical(), -5, [1.0, 0.0, 0.0, 0.0, 0.0, 0.013333333333333334]);
		$this->soulsandNoise = new NormalNoise($randomSourceProvider->identical(), -8, [1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.013333333333333334]);
		$this->netherStateNoise = new NormalNoise($randomSourceProvider->identical(), -4, [1.0]);
		$this->netherwartNoise = new NormalNoise($randomSourceProvider->identical(), -3, [1.0, 0.0, 0.0, 0.9]);
		$this->base3dNoise = BlendedNoise::nether($randomSourceProvider->identical());
		$this->densityFunction = NetherDensity::finalDensity($this->base3dNoise);
	}

	public function getSurfaceNoise() : NormalNoise{
		return $this->surfaceNoise;
	}

	public function getPatchNoise() : NormalNoise{
		return $this->patchNoise;
	}

	public function getSoulsandNoise() : NormalNoise{
		return $this->soulsandNoise;
	}

	public function getNetherStateNoise() : NormalNoise{
		return $this->netherStateNoise;
	}

	public function getNetherwartNoise() : NormalNoise{
		return $this->netherwartNoise;
	}

	public function getBase3dNoise() : DensityFunction{
		return $this->base3dNoise;
	}

	public function getDensityFunction() : DensityFunction{
		return $this->densityFunction;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\holder;

use dimension\generator\noise\OctaveNoise;
use dimension\generator\noise\SimplexNoise;
use dimension\generator\random\RandomSource;

/**
 * Noises of the end terrain, drawn one after the other from a single copy
 * of the world seeded source: two 16 octave roughness noises, the 8 octave
 * detail noise that blends them, then the island placement noise.
 */
final class EndTerrainHolder extends RandomizedObjectHolder{

	private OctaveNoise $roughnessNoiseOctaves;
	private OctaveNoise $roughnessNoiseOctaves2;
	private OctaveNoise $detailNoiseOctaves;
	private SimplexNoise $islandNoise;

	public function __construct(RandomSource $randomSourceProvider){
		parent::__construct($randomSourceProvider);
		$random = $randomSourceProvider->identical();
		$this->roughnessNoiseOctaves = new OctaveNoise($random, 16);
		$this->roughnessNoiseOctaves2 = new OctaveNoise($random, 16);
		$this->detailNoiseOctaves = new OctaveNoise($random, 8);
		$this->islandNoise = new SimplexNoise($random);
	}

	public function getRoughnessNoiseOctaves() : OctaveNoise{
		return $this->roughnessNoiseOctaves;
	}

	public function getRoughnessNoiseOctaves2() : OctaveNoise{
		return $this->roughnessNoiseOctaves2;
	}

	public function getDetailNoiseOctaves() : OctaveNoise{
		return $this->detailNoiseOctaves;
	}

	public function getIslandNoise() : SimplexNoise{
		return $this->islandNoise;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\holder;

use dimension\generator\random\RandomSource;

/**
 * Generation objects of the nether, built from the world seed.
 */
final class NetherObjectHolder extends RandomizedObjectHolder{

	private NetherTerrainHolder $terrainHolder;
	private BasaltDeltaHolder $basaltDeltasHolder;

	public function __construct(RandomSource $randomSourceProvider){
		parent::__construct($randomSourceProvider);
		$this->terrainHolder = new NetherTerrainHolder($randomSourceProvider);
		$this->basaltDeltasHolder = new BasaltDeltaHolder($randomSourceProvider);
	}

	public function getTerrainHolder() : NetherTerrainHolder{
		return $this->terrainHolder;
	}

	public function getBasaltDeltasHolder() : BasaltDeltaHolder{
		return $this->basaltDeltasHolder;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\holder;

use dimension\generator\noise\NormalNoise;
use dimension\generator\random\RandomSource;

/**
 * Surface noises of the basalt deltas. Each noise is drawn from a fresh
 * source seeded with the world seed.
 */
final class BasaltDeltaHolder extends RandomizedObjectHolder{

	private NormalNoise $surfaceNoise;
	private NormalNoise $surfaceSecNoise;

	public function __construct(RandomSource $randomSourceProvider){
		parent::__construct($randomSourceProvider);
		$this->surfaceNoise = new NormalNoise($randomSourceProvider->identical(), -6, [1.0, 1.0, 1.0]);
		$this->surfaceSecNoise = new NormalNoise($randomSourceProvider->identical(), -6, [1.0, 0.0, 1.0, 1.0]);
	}

	public function getSurfaceNoise() : NormalNoise{
		return $this->surfaceNoise;
	}

	public function getSurfaceSecNoise() : NormalNoise{
		return $this->surfaceSecNoise;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\holder;

use dimension\generator\random\RandomSource;

/**
 * Generation objects of the end, built from the world seed.
 */
final class EndObjectHolder extends RandomizedObjectHolder{

	private EndTerrainHolder $terrainHolder;

	public function __construct(RandomSource $randomSourceProvider){
		parent::__construct($randomSourceProvider);
		$this->terrainHolder = new EndTerrainHolder($randomSourceProvider);
	}

	public function getTerrainHolder() : EndTerrainHolder{
		return $this->terrainHolder;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator;

use dimension\generator\object\BlockManager;
use pocketmine\world\ChunkManager;

/**
 * A decoration pass run on a chunk once its neighbours exist. A populator
 * seeds its own random from the world seed and the chunk, reads the world as
 * it was before this population pass, and queues its changes into the root
 * BlockManager, which the generator applies once every populator of the chunk
 * has run.
 *
 * Populators run on the generation worker threads: they must not touch the
 * server, a World, or any shared mutable state.
 */
interface Populator{

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void;
}
<?php

declare(strict_types=1);

namespace dimension\generator;

/**
 * 64-bit key of a chunk: the X coordinate in the high 32 bits, the Z
 * coordinate (as unsigned) in the low 32 bits.
 */
final class ChunkHash{

	private function __construct(){
	}

	public static function hash(int $chunkX, int $chunkZ) : int{
		return ($chunkX << 32) | ($chunkZ & 0xFFFFFFFF);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator;

use function is_array;
use function is_string;
use function json_decode;
use function json_encode;
use const JSON_THROW_ON_ERROR;

/**
 * The generator preset of the nether and end worlds. Generators run on worker
 * threads and cannot reach the plugin, so the plugin resource folder, where
 * the structure templates live, travels inside the preset string saved with
 * the world.
 */
final class GeneratorOptions{

	public static function encode(string $resourceFolder) : string{
		return json_encode(["resources" => $resourceFolder], JSON_THROW_ON_ERROR);
	}

	public static function resourceFolder(string $preset) : string{
		$decoded = json_decode($preset, true);
		if(is_array($decoded) && is_string($decoded["resources"] ?? null)){
			return $decoded["resources"];
		}
		return "";
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\math;

use dimension\generator\random\RandomSource;
use function abs;
use function intdiv;
use function min;
use function sin;
use function sqrt;

/**
 * Math helpers of the terrain generators.
 *
 * Integer results follow signed 32-bit (int) or 64-bit (long) wrapping as
 * documented per method. Methods documented as single precision round their
 * result to binary32.
 */
final class GenerationMath{

	/** @var float[]|null */
	private static ?array $sinTable = null;

	private function __construct(){
	}

	public static function floorDouble(float $n) : int{
		$i = Int64::doubleToInt32($n);
		return $n >= $i ? $i : $i - 1;
	}

	public static function ceilDouble(float $n) : int{
		$i = Int64::doubleToInt32($n);
		return $n > $i ? $i + 1 : $i;
	}

	public static function floorFloat(float $n) : int{
		return self::floorDouble($n);
	}

	public static function ceilFloat(float $n) : int{
		return self::ceilDouble($n);
	}

	/**
	 * start + nextInt() % (end + 1 - start), 32-bit wrapping. With no bounds
	 * the range is [0, 0x7fffffff]; with only a start it is [start, 0x7fffffff].
	 */
	public static function randomRange(RandomSource $random, int $start = 0, int $end = 0x7fffffff) : int{
		return Int64::toInt32($start + ($random->nextInt() % Int64::toInt32($end + 1 - $start)));
	}

	public static function randomRangeTriangle(RandomSource $random, int $start, int $end) : int{
		$heightDiff = abs($end - $start);
		$heightDiffHalf = intdiv($heightDiff, 2);
		$heightDiffHalf2 = $heightDiff - $heightDiffHalf;
		return min($start, $end) + self::randomRange($random, 0, $heightDiffHalf2) + self::randomRange($random, 0, $heightDiffHalf);
	}

	/**
	 * Clamps $value into [$min, $max]. Returns an int when all arguments are ints.
	 */
	public static function clamp(int|float $value, int|float $min, int|float $max) : int|float{
		return $value < $min ? $min : ($value > $max ? $max : $value);
	}

	/**
	 * Single precision linear remap of $input from [$inMin, $inMax] to [$outMin, $outMax].
	 */
	public static function remap(float $input, float $inMin, float $inMax, float $outMin, float $outMax) : float{
		$a = Float32::of($input - $inMin);
		$b = Float32::of($inMax - $inMin);
		$c = Float32::of($outMax - $outMin);
		return Float32::of($outMin + Float32::of(Float32::of($a / $b) * $c));
	}

	public static function remapNormalized(float $input, float $inMin, float $inMax) : float{
		return self::remap($input, $inMin, $inMax, -1, 1);
	}

	public static function remapFromNormalized(float $input, float $outMin, float $outMax) : float{
		return self::remap($input, -1, 1, $outMin, $outMax);
	}

	/**
	 * Piecewise linear interpolation of $t between the points ($at, $a) and ($bt, $b), clamped at both ends.
	 */
	public static function lerp(float $t, float $at, float $a, float $bt, float $b) : float{
		if($at < $bt){
			if($t <= $at){
				return $a;
			}
			if($t >= $bt){
				return $b;
			}
		}else{
			if($t >= $at){
				return $a;
			}
			if($t <= $bt){
				return $b;
			}
		}
		return $a * ($t - $bt) / ($at - $bt) + $b * ($t - $at) / ($bt - $at);
	}

	public static function isPowerOf2(int $value) : bool{
		return ($value & Int64::sub(0, $value)) === $value;
	}

	public static function getPow2(int $bits) : int{
		return 1 << $bits;
	}

	public static function getMask(int $bits) : int{
		return $bits >= 64 ? ~0 : (1 << $bits) - 1;
	}

	public static function mask(int $value, int $bits) : int{
		return $value & self::getMask($bits);
	}

	/**
	 * Keeps the low $bits bits of $value and sign extends them.
	 */
	public static function maskSigned(int $value, int $bits) : int{
		return ($value << (64 - $bits)) >> (64 - $bits);
	}

	/**
	 * Multiplicative inverse of an odd $value modulo 2^$bits.
	 */
	public static function modInverse(int $value, int $bits = 64) : int{
		$x = (((($value << 1) ^ $value) & 4) << 1) ^ $value;
		for($i = 0; $i < 4; $i++){
			$x = Int64::mul($x, Int64::sub(2, Int64::mul($value, $x)));
		}
		return self::mask($x, $bits);
	}

	/**
	 * Single precision square root.
	 */
	public static function sqrt(float $value) : float{
		return Float32::of(sqrt($value));
	}

	/**
	 * Single precision table sine of an angle in radians (65536 steps per turn).
	 */
	public static function sin(float $value) : float{
		return self::table()[Int64::doubleToInt32($value * 10430.3779296875) & 0xFFFF];
	}

	/**
	 * Single precision table cosine of an angle in radians (65536 steps per turn).
	 */
	public static function cos(float $value) : float{
		return self::table()[Int64::doubleToInt32($value * 10430.3779296875 + 16384.0) & 0xFFFF];
	}

	/**
	 * Single precision table sine; $value is treated as a binary32 number.
	 */
	public static function sinFloat(float $value) : float{
		return self::table()[Int64::doubleToInt32(Float32::of($value * 10430.3779296875)) & 0xFFFF];
	}

	/**
	 * Single precision table cosine; $value is treated as a binary32 number.
	 */
	public static function cosFloat(float $value) : float{
		return self::table()[Int64::doubleToInt32(Float32::of(Float32::of($value * 10430.3779296875) + 16384.0)) & 0xFFFF];
	}

	/**
	 * Largest 32-bit int not greater than $value.
	 */
	public static function floor(float $value) : int{
		$i = Int64::doubleToInt32($value);
		return $value < $i ? $i - 1 : $i;
	}

	/**
	 * Largest 64-bit int not greater than $value.
	 */
	public static function lfloor(float $value) : int{
		$l = Int64::doubleToInt64($value);
		return $value < $l ? $l - 1 : $l;
	}

	public static function floor_double_long(float $value) : int{
		$l = Int64::doubleToInt64($value);
		return $value >= $l ? $l : $l - 1;
	}

	public static function floor_float_int(float $value) : int{
		$i = Int64::doubleToInt32($value);
		return $value >= $i ? $i : $i - 1;
	}

	public static function abs(int $number) : int{
		return $number > 0 ? $number : Int64::toInt32(-$number);
	}

	/**
	 * Removes the multiples of 2^25 that would lose precision in the noise samplers.
	 */
	public static function maintainPrecision(float $value) : float{
		return $value - self::lfloor($value / 3.3554432E7 + 0.5) * 3.3554432E7;
	}

	/**
	 * Gradient dot product of the 16 perlin gradients selected by the low 4 bits of $hash.
	 */
	public static function grad(int $hash, float $x, float $y, float $z) : float{
		return match($hash & 0xF){
			0x0 => $x + $y,
			0x1 => -$x + $y,
			0x2 => $x - $y,
			0x3 => -$x - $y,
			0x4 => $x + $z,
			0x5 => -$x + $z,
			0x6 => $x - $z,
			0x7 => -$x - $z,
			0x8 => $y + $z,
			0x9, 0xD => -$y + $z,
			0xA => $y - $z,
			0xB, 0xF => -$y - $z,
			0xC => $y + $x,
			default => $y - $x
		};
	}

	/**
	 * @return float[]
	 */
	private static function table() : array{
		if(self::$sinTable === null){
			$table = [];
			for($i = 0; $i < 65536; $i++){
				$table[$i] = Float32::of(sin($i * 3.141592653589793 * 2.0 / 65536.0));
			}
			self::$sinTable = $table;
		}
		return self::$sinTable;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\math;

use const PHP_INT_MAX;
use const PHP_INT_MIN;

/**
 * Two's complement 64-bit and 32-bit integer arithmetic that wraps on
 * overflow instead of turning into a float.
 */
final class Int64{

	private const MASK_32 = 0xFFFFFFFF;

	private function __construct(){
	}

	/**
	 * Wrapping 64-bit addition.
	 */
	public static function add(int $a, int $b) : int{
		$low = ($a & self::MASK_32) + ($b & self::MASK_32);
		$high = (($a >> 32) + ($b >> 32) + ($low >> 32)) & self::MASK_32;
		return ($high << 32) | ($low & self::MASK_32);
	}

	/**
	 * Wrapping 64-bit subtraction.
	 */
	public static function sub(int $a, int $b) : int{
		return self::add($a, self::add(~$b, 1));
	}

	/**
	 * Wrapping 64-bit multiplication.
	 */
	public static function mul(int $a, int $b) : int{
		$aLow = $a & self::MASK_32;
		$aHigh = ($a >> 32) & self::MASK_32;
		$bLow = $b & self::MASK_32;
		$bHigh = ($b >> 32) & self::MASK_32;
		$low = self::add($aLow * ($bLow & 0xFFFF), ($aLow * ($bLow >> 16)) << 16);
		$cross = (self::mulLow32($aHigh, $bLow) + self::mulLow32($aLow, $bHigh)) & self::MASK_32;
		return self::add($low, $cross << 32);
	}

	/**
	 * Logical (unsigned) right shift of a 64-bit value.
	 */
	public static function urshift(int $value, int $bits) : int{
		$bits &= 63;
		if($bits === 0){
			return $value;
		}
		return ($value >> $bits) & (PHP_INT_MAX >> ($bits - 1));
	}

	/**
	 * 64-bit left rotation.
	 */
	public static function rotateLeft(int $value, int $bits) : int{
		return ($value << $bits) | self::urshift($value, 64 - $bits);
	}

	/**
	 * Truncates to a signed 32-bit integer.
	 */
	public static function toInt32(int $value) : int{
		return (($value & self::MASK_32) ^ 0x80000000) - 0x80000000;
	}

	/**
	 * Logical (unsigned) right shift of a signed 32-bit value, returning a signed 32-bit value.
	 */
	public static function urshift32(int $value, int $bits) : int{
		$bits &= 31;
		return self::toInt32(($value & self::MASK_32) >> $bits);
	}

	/**
	 * Wrapping 32-bit multiplication.
	 */
	public static function mul32(int $a, int $b) : int{
		return self::toInt32(self::mulLow32($a & self::MASK_32, $b & self::MASK_32));
	}

	/**
	 * Truncates a float toward zero into a signed 32-bit integer, saturating
	 * out of range values and mapping NaN to zero.
	 */
	public static function doubleToInt32(float $value) : int{
		if($value !== $value){
			return 0;
		}
		if($value >= 2147483647.0){
			return 2147483647;
		}
		if($value <= -2147483648.0){
			return -2147483648;
		}
		return (int) $value;
	}

	/**
	 * Truncates a float toward zero into a signed 64-bit integer, saturating
	 * out of range values and mapping NaN to zero.
	 */
	public static function doubleToInt64(float $value) : int{
		if($value !== $value){
			return 0;
		}
		if($value >= 9.2233720368547758E18){
			return PHP_INT_MAX;
		}
		if($value <= -9.2233720368547758E18){
			return PHP_INT_MIN;
		}
		return (int) $value;
	}

	/**
	 * Low 32 bits of the product of two unsigned 32-bit values.
	 */
	private static function mulLow32(int $a, int $b) : int{
		return (($a * ($b & 0xFFFF)) + ((($a * ($b >> 16)) & 0xFFFF) << 16)) & self::MASK_32;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\math;

use function pack;
use function unpack;

/**
 * Single precision (IEEE 754 binary32) rounding. Every single precision
 * operation is computed in double precision then rounded with of(), which
 * gives the correctly rounded binary32 result for +, -, *, / and sqrt.
 */
final class Float32{

	private function __construct(){
	}

	public static function of(float $value) : float{
		$unpacked = unpack("g", pack("g", $value));
		return $unpacked === false ? $value : (float) $unpacked[1];
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\object;

use dimension\generator\Blocks;
use pocketmine\block\Block;
use pocketmine\block\VanillaBlocks;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Vector3;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;
use function array_values;
use function count;
use function max;
use function min;
use const PHP_INT_MAX;
use const PHP_INT_MIN;

/**
 * Queues block changes over a ChunkManager during population. Reads return
 * the block queued at that position if any, else the world block (world
 * reads are cached). Nothing reaches the world until apply().
 *
 * Blocks can be given as Block objects or as Bedrock identifiers such as
 * "minecraft:glowstone".
 */
class BlockManager{

	/** @var array<int, Block> */
	private array $caches = [];
	/** @var array<int, PlacedBlock> */
	private array $places = [];
	/** @var list<\Closure> */
	protected array $hooks = [];

	public function __construct(
		private ChunkManager $world
	){}

	private static function hashXYZ(int $x, int $y, int $z) : int{
		return ((($x + 30000000) & 0x3FFFFFF) << 37)
			| ((($z + 30000000) & 0x3FFFFFF) << 11)
			| ((($y + 400) & 0x3FF) << 1);
	}

	/**
	 * Registers a callback run by apply() once the blocks are written. The
	 * callback receives the ChunkManager.
	 */
	public function addHook(\Closure $hook) : void{
		$this->hooks[] = $hook;
	}

	/**
	 * @return list<\Closure>
	 */
	public function getHooks() : array{
		return $this->hooks;
	}

	public function getBlockIdIfCachedOrLoaded(int $x, int $y, int $z) : string{
		return Blocks::id($this->getBlockIfCachedOrLoaded($x, $y, $z));
	}

	/**
	 * Bedrock identifier of the block at this position, like "minecraft:air".
	 */
	public function getBlockIdAt(int $x, int $y, int $z) : string{
		$cached = $this->caches[self::hashXYZ($x, $y, $z)] ?? null;
		if($cached !== null){
			return Blocks::id($cached);
		}
		return Blocks::id($this->world->getBlockAt($x, $y, $z));
	}

	/**
	 * The queued or cached block, else the world block when its chunk is
	 * available, else $fallback (air by default).
	 */
	public function getBlockIfCachedOrLoaded(int $x, int $y, int $z, ?Block $fallback = null) : Block{
		$cached = $this->caches[self::hashXYZ($x, $y, $z)] ?? null;
		if($cached !== null){
			return $cached;
		}
		if($this->world->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE) !== null){
			return $this->getBlockAt($x, $y, $z);
		}
		return $fallback ?? VanillaBlocks::AIR();
	}

	public function getBlockAt(int $x, int $y, int $z) : Block{
		return $this->caches[self::hashXYZ($x, $y, $z)] ??= $this->world->getBlockAt($x, $y, $z);
	}

	public function getBlockAtVector(Vector3 $pos) : Block{
		return $this->getBlockAt($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ());
	}

	/**
	 * Same as getBlockAt(): the block holds its full state.
	 */
	public function getBlockStateAt(int $x, int $y, int $z) : Block{
		return $this->getBlockAt($x, $y, $z);
	}

	/**
	 * The queued or cached block, else $fallback (air by default). Never reads the world.
	 */
	public function getCachedBlock(int $x, int $y, int $z, ?Block $fallback = null) : Block{
		return $this->caches[self::hashXYZ($x, $y, $z)] ?? $fallback ?? VanillaBlocks::AIR();
	}

	public function setBlockStateAt(int $x, int $y, int $z, Block|string $state) : void{
		$block = $state instanceof Block ? $state : Blocks::get($state);
		$hash = self::hashXYZ($x, $y, $z);
		$this->places[$hash] = new PlacedBlock($x, $y, $z, $block);
		$this->caches[$hash] = $block;
	}

	public function setBlockStateAtVector(Vector3 $pos, Block|string $state) : void{
		$this->setBlockStateAt($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ(), $state);
	}

	/**
	 * Queues the block only when nothing was queued or read at this position yet.
	 */
	public function setBlockStateAtIfCacheAbsent(int $x, int $y, int $z, Block|string $state) : bool{
		if(!isset($this->caches[self::hashXYZ($x, $y, $z)])){
			$this->setBlockStateAt($x, $y, $z, $state);
			return true;
		}
		return false;
	}

	public function unsetBlockStateAt(int $x, int $y, int $z) : void{
		$hash = self::hashXYZ($x, $y, $z);
		unset($this->places[$hash], $this->caches[$hash]);
	}

	/**
	 * Whether a block was queued or read at this position.
	 */
	public function isCached(int $x, int $y, int $z) : bool{
		return isset($this->caches[self::hashXYZ($x, $y, $z)]);
	}

	/**
	 * Queues every block and hook of $manager into this manager, overriding
	 * blocks queued here at the same positions.
	 */
	public function merge(BlockManager $manager) : void{
		foreach($manager->places as $hash => $placed){
			$this->places[$hash] = $placed;
			$this->caches[$hash] = $placed->block;
		}
		foreach($manager->hooks as $hook){
			$this->hooks[] = $hook;
		}
	}

	public function getWorld() : ChunkManager{
		return $this->world;
	}

	public function getChunk(int $chunkX, int $chunkZ) : ?Chunk{
		return $this->world->getChunk($chunkX, $chunkZ);
	}

	public function getMinHeight() : int{
		return $this->world->getMinY();
	}

	public function getMaxHeight() : int{
		return $this->world->getMaxY();
	}

	/**
	 * @return list<PlacedBlock>
	 */
	public function getBlocks() : array{
		return array_values($this->places);
	}

	/**
	 * Bounding box of the queued blocks (inclusive block coordinates).
	 */
	public function getBounds() : AxisAlignedBB{
		$minX = PHP_INT_MAX;
		$minY = PHP_INT_MAX;
		$minZ = PHP_INT_MAX;
		$maxX = PHP_INT_MIN;
		$maxY = PHP_INT_MIN;
		$maxZ = PHP_INT_MIN;
		foreach($this->places as $placed){
			$minX = min($minX, $placed->x);
			$minY = min($minY, $placed->y);
			$minZ = min($minZ, $placed->z);
			$maxX = max($maxX, $placed->x);
			$maxY = max($maxY, $placed->y);
			$maxZ = max($maxZ, $placed->z);
		}
		if(count($this->places) === 0){
			return new AxisAlignedBB(0, 0, 0, 0, 0, 0);
		}
		return new AxisAlignedBB($minX, $minY, $minZ, $maxX, $maxY, $maxZ);
	}

	/**
	 * Writes the queued blocks into their chunks, runs the hooks, then
	 * clears this manager. Blocks outside the world height or in chunks the
	 * ChunkManager does not hold are dropped.
	 */
	public function apply() : void{
		$minY = $this->world->getMinY();
		$maxY = $this->world->getMaxY();
		foreach($this->places as $placed){
			if($placed->y < $minY || $placed->y >= $maxY){
				continue;
			}
			$chunk = $this->world->getChunk($placed->x >> Chunk::COORD_BIT_SIZE, $placed->z >> Chunk::COORD_BIT_SIZE);
			if($chunk === null){
				continue;
			}
			$chunk->setBlockStateId($placed->x & Chunk::COORD_MASK, $placed->y, $placed->z & Chunk::COORD_MASK, $placed->block->getStateId());
		}
		$hooks = $this->hooks;
		$this->hooks = [];
		foreach($hooks as $hook){
			$hook($this->world);
		}
		$this->places = [];
		$this->caches = [];
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\object;

use pocketmine\block\Block;

/**
 * A block queued in a BlockManager at world coordinates.
 */
final class PlacedBlock{

	public function __construct(
		public readonly int $x,
		public readonly int $y,
		public readonly int $z,
		public readonly Block $block
	){}
}
<?php

declare(strict_types=1);

namespace dimension\generator\noise;

use dimension\generator\math\GenerationMath;
use dimension\generator\random\RandomSource;
use function count;

/**
 * Multi octave perlin noise. Octave i has the frequency 2^(firstOctave + i)
 * and the weight amplitudes[i]; octaves with a zero amplitude are skipped
 * but still consume their share of the random sequence.
 */
final class PerlinNoise{

	/** @var array<int, PerlinNoiseSampler|null> */
	private array $noiseLevels = [];
	private float $lowestFreqValueFactor;
	private float $lowestFreqInputFactor;
	private float $maxValue;

	/**
	 * @param float[] $amplitudes
	 */
	public function __construct(RandomSource $random, int $firstOctave, private array $amplitudes){
		$octaves = count($amplitudes);
		$zeroOctaveIndex = -$firstOctave;
		for($i = 0; $i < $octaves; $i++){
			$this->noiseLevels[$i] = null;
		}

		$zeroOctave = new PerlinNoiseSampler($random);
		if($zeroOctaveIndex >= 0 && $zeroOctaveIndex < $octaves && $amplitudes[$zeroOctaveIndex] != 0.0){
			$this->noiseLevels[$zeroOctaveIndex] = $zeroOctave;
		}

		for($ix = $zeroOctaveIndex - 1; $ix >= 0; --$ix){
			if($ix < $octaves && $amplitudes[$ix] != 0.0){
				$this->noiseLevels[$ix] = new PerlinNoiseSampler($random);
			}else{
				$random->setSeed(Lcg::skip262()->nextSeed($random->getSeed()));
			}
		}

		$this->lowestFreqInputFactor = 2.0 ** (-$zeroOctaveIndex);
		$this->lowestFreqValueFactor = 2.0 ** ($octaves - 1) / (2.0 ** $octaves - 1.0);
		$this->maxValue = $this->edgeValue(2.0);
	}

	public function getValue(float $x, float $y, float $z) : float{
		$value = 0.0;
		$factor = $this->lowestFreqInputFactor;
		$valueFactor = $this->lowestFreqValueFactor;
		foreach($this->noiseLevels as $i => $noise){
			if($noise !== null){
				$noiseValue = $noise->sample(
					GenerationMath::maintainPrecision($x * $factor),
					GenerationMath::maintainPrecision($y * $factor),
					GenerationMath::maintainPrecision($z * $factor),
					0.0,
					0.0
				);
				$value += $this->amplitudes[$i] * $noiseValue * $valueFactor;
			}
			$factor *= 2.0;
			$valueFactor /= 2.0;
		}
		return $value;
	}

	public function maxValue() : float{
		return $this->maxValue;
	}

	private function edgeValue(float $noiseValue) : float{
		$value = 0.0;
		$valueFactor = $this->lowestFreqValueFactor;
		foreach($this->noiseLevels as $i => $noise){
			if($noise !== null){
				$value += $this->amplitudes[$i] * $noiseValue * $valueFactor;
			}
			$valueFactor /= 2.0;
		}
		return $value;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\noise;

use dimension\generator\math\Float32;
use dimension\generator\random\RandomSource;
use function max;
use function min;
use const PHP_INT_MAX;
use const PHP_INT_MIN;

/**
 * Sum of two multi octave perlin noises (the second sampled at a slightly
 * larger scale), normalised so its values roughly spread over [-1, 1].
 * Values are single precision.
 */
final class NormalNoise{

	private const INPUT_FACTOR = 1.0181268882175227;

	private PerlinNoise $first;
	private PerlinNoise $second;
	private float $valueFactor;
	private float $maxValue;

	/**
	 * @param float[] $amplitudes single precision amplitudes, one per octave
	 */
	public function __construct(RandomSource $random, int $firstOctave, array $amplitudes){
		$octaveAmplitudes = [];
		$minIndex = PHP_INT_MAX;
		$maxIndex = PHP_INT_MIN;
		foreach($amplitudes as $i => $amplitude){
			$amplitude = Float32::of((float) $amplitude);
			$octaveAmplitudes[$i] = $amplitude;
			if($amplitude != 0.0){
				$minIndex = min($minIndex, $i);
				$maxIndex = max($maxIndex, $i);
			}
		}
		$this->first = new PerlinNoise($random, $firstOctave, $octaveAmplitudes);
		$this->second = new PerlinNoise($random, $firstOctave, $octaveAmplitudes);
		$this->valueFactor = 1.0 / 6.0 / self::expectedDeviation($maxIndex - $minIndex);
		$this->maxValue = ($this->first->maxValue() + $this->second->maxValue()) * $this->valueFactor;
	}

	public function getValue(float $x, float $y, float $z) : float{
		$x2 = $x * self::INPUT_FACTOR;
		$y2 = $y * self::INPUT_FACTOR;
		$z2 = $z * self::INPUT_FACTOR;
		return Float32::of(($this->first->getValue($x, $y, $z) + $this->second->getValue($x2, $y2, $z2)) * $this->valueFactor);
	}

	public function getMax() : float{
		return $this->maxValue;
	}

	private static function expectedDeviation(int $octaveSpan) : float{
		return 0.1 * (1.0 + 1.0 / ($octaveSpan + 1.0));
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\noise;

use dimension\generator\math\Int64;

/**
 * Linear congruential step seed = (seed * multiplier + addend) mod 2^48,
 * with the constants of the classic 48-bit generator, and its combination
 * over several steps.
 */
final class Lcg{

	private const MULTIPLIER = 25214903917;
	private const ADDEND = 11;
	private const MASK = (1 << 48) - 1;

	private static ?Lcg $skip262 = null;

	private function __construct(
		private int $multiplier,
		private int $addend
	){}

	/**
	 * The generator advanced 262 steps at once: the seed skip of one unused noise octave.
	 */
	public static function skip262() : Lcg{
		return self::$skip262 ??= self::combine(262);
	}

	public function nextSeed(int $seed) : int{
		return Int64::add(Int64::mul($seed, $this->multiplier), $this->addend) & self::MASK;
	}

	private static function combine(int $steps) : Lcg{
		$multiplier = 1;
		$addend = 0;
		$intermediateMultiplier = self::MULTIPLIER;
		$intermediateAddend = self::ADDEND;
		for($k = $steps; $k !== 0; $k = Int64::urshift($k, 1)){
			if(($k & 1) !== 0){
				$multiplier = Int64::mul($multiplier, $intermediateMultiplier);
				$addend = Int64::add(Int64::mul($intermediateMultiplier, $addend), $intermediateAddend);
			}
			$intermediateAddend = Int64::mul(Int64::add($intermediateMultiplier, 1), $intermediateAddend);
			$intermediateMultiplier = Int64::mul($intermediateMultiplier, $intermediateMultiplier);
		}
		return new Lcg($multiplier & self::MASK, $addend & self::MASK);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\noise;

use dimension\generator\math\Int64;
use dimension\generator\random\RandomSource;
use function floor;

/**
 * Single octave 3D improved perlin noise with a random origin and a
 * shuffled 256 entry permutation table.
 */
final class PerlinNoiseSampler{

	private const GRADIENTS = [
		1, 1, 0, 0,
		-1, 1, 0, 0,
		1, -1, 0, 0,
		-1, -1, 0, 0,
		1, 0, 1, 0,
		-1, 0, 1, 0,
		1, 0, -1, 0,
		-1, 0, -1, 0,
		0, 1, 1, 0,
		0, -1, 1, 0,
		0, 1, -1, 0,
		0, -1, -1, 0,
		1, 1, 0, 0,
		0, -1, 1, 0,
		-1, 1, 0, 0,
		0, -1, -1, 0
	];

	public readonly float $originX;
	public readonly float $originY;
	public readonly float $originZ;
	/** @var int[] 0-255 */
	private array $permutations = [];

	public function __construct(RandomSource $random){
		$this->originX = $random->nextDouble() * 256.0;
		$this->originY = $random->nextDouble() * 256.0;
		$this->originZ = $random->nextDouble() * 256.0;
		$permutations = [];
		for($j = 0; $j < 256; ++$j){
			$permutations[$j] = $j;
		}
		for($index = 0; $index < 256; ++$index){
			$randomIndex = $random->nextBoundedInt(255 - $index) + $index;
			$temp = $permutations[$index];
			$permutations[$index] = $permutations[$randomIndex];
			$permutations[$randomIndex] = $temp;
		}
		$this->permutations = $permutations;
	}

	/**
	 * @param float $yAmplification when not zero, the local Y is snapped down to a multiple of it
	 * @param float $minY           upper limit of the local Y used for that snapping, ignored when negative
	 */
	public function sample(float $x, float $y, float $z, float $yAmplification, float $minY) : float{
		$offsetX = $x + $this->originX;
		$offsetY = $y + $this->originY;
		$offsetZ = $z + $this->originZ;
		$floorX = floor($offsetX);
		$floorY = floor($offsetY);
		$floorZ = floor($offsetZ);
		$localX = $offsetX - $floorX;
		$localY = $offsetY - $floorY;
		$localZ = $offsetZ - $floorZ;
		$yOffset = 0.0;
		if($yAmplification != 0.0){
			$yClamp = $minY >= 0.0 && $minY < $localY ? $minY : $localY;
			$yOffset = floor($yClamp / $yAmplification + 1.0E-7) * $yAmplification;
		}
		return $this->sampleSection(
			Int64::doubleToInt32($floorX),
			Int64::doubleToInt32($floorY),
			Int64::doubleToInt32($floorZ),
			$localX,
			$localY - $yOffset,
			$localZ,
			$localY
		);
	}

	private function sampleSection(int $sectionX, int $sectionY, int $sectionZ, float $localX, float $localY, float $localZ, float $fadeLocalY) : float{
		$p = $this->permutations;
		$g = self::GRADIENTS;
		$a = $p[$sectionX & 0xFF];
		$b = $p[($sectionX + 1) & 0xFF];
		$aa = $p[($a + $sectionY) & 0xFF];
		$ba = $p[($b + $sectionY) & 0xFF];
		$ab = $p[($a + $sectionY + 1) & 0xFF];
		$bb = $p[($b + $sectionY + 1) & 0xFF];

		$h000 = ($p[($aa + $sectionZ) & 0xFF] & 15) << 2;
		$h100 = ($p[($ba + $sectionZ) & 0xFF] & 15) << 2;
		$h010 = ($p[($ab + $sectionZ) & 0xFF] & 15) << 2;
		$h110 = ($p[($bb + $sectionZ) & 0xFF] & 15) << 2;
		$h001 = ($p[($aa + $sectionZ + 1) & 0xFF] & 15) << 2;
		$h101 = ($p[($ba + $sectionZ + 1) & 0xFF] & 15) << 2;
		$h011 = ($p[($ab + $sectionZ + 1) & 0xFF] & 15) << 2;
		$h111 = ($p[($bb + $sectionZ + 1) & 0xFF] & 15) << 2;

		$xMinusOne = $localX - 1.0;
		$yMinusOne = $localY - 1.0;
		$zMinusOne = $localZ - 1.0;
		$grad000 = $g[$h000] * $localX + $g[$h000 | 1] * $localY + $g[$h000 | 2] * $localZ;
		$grad100 = $g[$h100] * $xMinusOne + $g[$h100 | 1] * $localY + $g[$h100 | 2] * $localZ;
		$grad010 = $g[$h010] * $localX + $g[$h010 | 1] * $yMinusOne + $g[$h010 | 2] * $localZ;
		$grad110 = $g[$h110] * $xMinusOne + $g[$h110 | 1] * $yMinusOne + $g[$h110 | 2] * $localZ;
		$grad001 = $g[$h001] * $localX + $g[$h001 | 1] * $localY + $g[$h001 | 2] * $zMinusOne;
		$grad101 = $g[$h101] * $xMinusOne + $g[$h101 | 1] * $localY + $g[$h101 | 2] * $zMinusOne;
		$grad011 = $g[$h011] * $localX + $g[$h011 | 1] * $yMinusOne + $g[$h011 | 2] * $zMinusOne;
		$grad111 = $g[$h111] * $xMinusOne + $g[$h111 | 1] * $yMinusOne + $g[$h111 | 2] * $zMinusOne;

		$fadeX = $localX * $localX * $localX * ($localX * ($localX * 6.0 - 15.0) + 10.0);
		$fadeY = $fadeLocalY * $fadeLocalY * $fadeLocalY * ($fadeLocalY * ($fadeLocalY * 6.0 - 15.0) + 10.0);
		$fadeZ = $localZ * $localZ * $localZ * ($localZ * ($localZ * 6.0 - 15.0) + 10.0);

		$x00 = $grad000 + $fadeX * ($grad100 - $grad000);
		$x10 = $grad010 + $fadeX * ($grad110 - $grad010);
		$x01 = $grad001 + $fadeX * ($grad101 - $grad001);
		$x11 = $grad011 + $fadeX * ($grad111 - $grad011);
		$y0 = $x00 + $fadeY * ($x10 - $x00);
		$y1 = $x01 + $fadeY * ($x11 - $x01);
		return $y0 + $fadeZ * ($y1 - $y0);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\noise;

use dimension\generator\random\RandomSource;
use function sqrt;

/**
 * 2D simplex noise with a random origin and a shuffled permutation table.
 */
final class SimplexNoise{

	private const GRAD3 = [[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0], [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1], [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1]];

	private static ?float $sqrt3 = null;

	/** @var int[] 512 entries, the second half repeating the first */
	private array $p = [];
	public float $xo;
	public float $yo;
	public float $zo;

	public function __construct(RandomSource $random){
		$this->xo = $random->nextDouble() * 256.0;
		$this->yo = $random->nextDouble() * 256.0;
		$this->zo = $random->nextDouble() * 256.0;
		$p = [];
		for($i = 0; $i < 512; $i++){
			$p[$i] = $i < 256 ? $i : 0;
		}
		for($l = 0; $l < 256; ++$l){
			$j = $random->nextBoundedInt(255 - $l) + $l;
			$k = $p[$l];
			$p[$l] = $p[$j];
			$p[$j] = $k;
			$p[$l + 256] = $p[$l];
		}
		$this->p = $p;
	}

	private static function fastFloor(float $value) : int{
		return $value > 0.0 ? (int) $value : (int) $value - 1;
	}

	/**
	 * @param int[] $gradient
	 */
	private static function dot(array $gradient, float $x, float $y) : float{
		return $gradient[0] * $x + $gradient[1] * $y;
	}

	public function getValue(float $x, float $y) : float{
		$sqrt3 = self::$sqrt3 ??= sqrt(3.0);
		$f2 = 0.5 * ($sqrt3 - 1.0);
		$s = ($x + $y) * $f2;
		$i = self::fastFloor($x + $s);
		$j = self::fastFloor($y + $s);
		$g2 = (3.0 - $sqrt3) / 6.0;
		$t = ($i + $j) * $g2;
		$x0 = $x - ($i - $t);
		$y0 = $y - ($j - $t);
		if($x0 > $y0){
			$i1 = 1;
			$j1 = 0;
		}else{
			$i1 = 0;
			$j1 = 1;
		}
		$x1 = $x0 - $i1 + $g2;
		$y1 = $y0 - $j1 + $g2;
		$x2 = $x0 - 1.0 + 2.0 * $g2;
		$y2 = $y0 - 1.0 + 2.0 * $g2;
		$ii = $i & 255;
		$jj = $j & 255;
		$p = $this->p;
		$gi0 = $p[$ii + $p[$jj]] % 12;
		$gi1 = $p[$ii + $i1 + $p[$jj + $j1]] % 12;
		$gi2 = $p[$ii + 1 + $p[$jj + 1]] % 12;

		$t0 = 0.5 - $x0 * $x0 - $y0 * $y0;
		if($t0 < 0.0){
			$n0 = 0.0;
		}else{
			$t0 *= $t0;
			$n0 = $t0 * $t0 * self::dot(self::GRAD3[$gi0], $x0, $y0);
		}
		$t1 = 0.5 - $x1 * $x1 - $y1 * $y1;
		if($t1 < 0.0){
			$n1 = 0.0;
		}else{
			$t1 *= $t1;
			$n1 = $t1 * $t1 * self::dot(self::GRAD3[$gi1], $x1, $y1);
		}
		$t2 = 0.5 - $x2 * $x2 - $y2 * $y2;
		if($t2 < 0.0){
			$n2 = 0.0;
		}else{
			$t2 *= $t2;
			$n2 = $t2 * $t2 * self::dot(self::GRAD3[$gi2], $x2, $y2);
		}
		return 70.0 * ($n0 + $n1 + $n2);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\noise;

use dimension\generator\math\GenerationMath;
use dimension\generator\random\RandomSource;
use function array_fill;

/**
 * Several ImprovedNoise octaves summed over a grid, each octave at half the
 * frequency and twice the amplitude of the previous one.
 */
final class OctaveNoise{

	/** @var ImprovedNoise[] */
	private array $generatorCollection = [];

	public function __construct(RandomSource $random, private int $octaves){
		for($i = 0; $i < $octaves; ++$i){
			$this->generatorCollection[$i] = new ImprovedNoise($random);
		}
	}

	/**
	 * Grid of xSize * zSize * ySize samples indexed ((x * zSize) + z) * ySize + y.
	 *
	 * @return float[]
	 */
	public function generateNoiseOctaves(int $xOffset, int $yOffset, int $zOffset, int $xSize, int $ySize, int $zSize, float $xScale, float $yScale, float $zScale) : array{
		$noiseArray = array_fill(0, $xSize * $ySize * $zSize, 0.0);
		$d3 = 1.0;
		for($j = 0; $j < $this->octaves; ++$j){
			$d0 = $xOffset * $d3 * $xScale;
			$d1 = $yOffset * $d3 * $yScale;
			$d2 = $zOffset * $d3 * $zScale;
			$k = GenerationMath::floor_double_long($d0);
			$l = GenerationMath::floor_double_long($d2);
			$d0 -= $k;
			$d2 -= $l;
			$k %= 16777216;
			$l %= 16777216;
			$d0 += $k;
			$d2 += $l;
			$this->generatorCollection[$j]->populateNoiseArray($noiseArray, $d0, $d1, $d2, $xSize, $ySize, $zSize, $xScale * $d3, $yScale * $d3, $zScale * $d3, $d3);
			$d3 /= 2.0;
		}
		return $noiseArray;
	}

	/**
	 * Single layer grid at Y = 10.
	 *
	 * @return float[]
	 */
	public function generateNoiseOctaves2D(int $xOffset, int $zOffset, int $xSize, int $zSize, float $xScale, float $zScale) : array{
		return $this->generateNoiseOctaves($xOffset, 10, $zOffset, $xSize, 1, $zSize, $xScale, 1.0, $zScale);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\noise;

use dimension\generator\random\RandomSource;

/**
 * Single octave improved perlin noise that fills whole grids of samples at
 * once.
 */
final class ImprovedNoise{

	private const GRAD_X = [1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0];
	private const GRAD_Y = [1.0, 1.0, -1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0];
	private const GRAD_Z = [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 0.0, 1.0, 0.0, -1.0];
	private const GRAD_2X = [1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0];
	private const GRAD_2Z = [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 0.0, 1.0, 0.0, -1.0];

	/** @var int[] 512 entries, the second half repeating the first */
	private array $permutations = [];
	public float $xCoord;
	public float $yCoord;
	public float $zCoord;

	public function __construct(RandomSource $random){
		$this->xCoord = $random->nextDouble() * 256.0;
		$this->yCoord = $random->nextDouble() * 256.0;
		$this->zCoord = $random->nextDouble() * 256.0;
		$p = [];
		for($i = 0; $i < 512; $i++){
			$p[$i] = $i < 256 ? $i : 0;
		}
		for($l = 0; $l < 256; ++$l){
			$j = $random->nextBoundedInt(255 - $l) + $l;
			$k = $p[$l];
			$p[$l] = $p[$j];
			$p[$j] = $k;
			$p[$l + 256] = $p[$l];
		}
		$this->permutations = $p;
	}

	public function lerp(float $delta, float $start, float $end) : float{
		return $start + $delta * ($end - $start);
	}

	public function grad2(int $hash, float $x, float $z) : float{
		$i = $hash & 15;
		return self::GRAD_2X[$i] * $x + self::GRAD_2Z[$i] * $z;
	}

	public function grad(int $hash, float $x, float $y, float $z) : float{
		$i = $hash & 15;
		return self::GRAD_X[$i] * $x + self::GRAD_Y[$i] * $y + self::GRAD_Z[$i] * $z;
	}

	/**
	 * Adds this octave into $noiseArray, a grid of xSize * zSize * ySize
	 * samples indexed ((x * zSize) + z) * ySize + y, each sample divided by
	 * $noiseScale.
	 *
	 * @param float[] $noiseArray
	 */
	public function populateNoiseArray(array &$noiseArray, float $xOffset, float $yOffset, float $zOffset, int $xSize, int $ySize, int $zSize, float $xScale, float $yScale, float $zScale, float $noiseScale) : void{
		$p = $this->permutations;
		if($ySize === 1){
			$index = 0;
			$d16 = 1.0 / $noiseScale;
			for($j2 = 0; $j2 < $xSize; ++$j2){
				$d17 = $xOffset + $j2 * $xScale + $this->xCoord;
				$i6 = (int) $d17;
				if($d17 < $i6){
					--$i6;
				}
				$k2 = $i6 & 255;
				$d17 -= $i6;
				$d18 = $d17 * $d17 * $d17 * ($d17 * ($d17 * 6.0 - 15.0) + 10.0);
				for($j6 = 0; $j6 < $zSize; ++$j6){
					$d19 = $zOffset + $j6 * $zScale + $this->zCoord;
					$k6 = (int) $d19;
					if($d19 < $k6){
						--$k6;
					}
					$l6 = $k6 & 255;
					$d19 -= $k6;
					$d20 = $d19 * $d19 * $d19 * ($d19 * ($d19 * 6.0 - 15.0) + 10.0);
					$i5 = $p[$k2];
					$j5 = $p[$i5] + $l6;
					$j = $p[$k2 + 1];
					$k5 = $p[$j] + $l6;
					$d14 = $this->lerp($d18, $this->grad2($p[$j5], $d17, $d19), $this->grad($p[$k5], $d17 - 1.0, 0.0, $d19));
					$d15 = $this->lerp($d18, $this->grad($p[$j5 + 1], $d17, 0.0, $d19 - 1.0), $this->grad($p[$k5 + 1], $d17 - 1.0, 0.0, $d19 - 1.0));
					$d21 = $this->lerp($d20, $d14, $d15);
					$noiseArray[$index++] += $d21 * $d16;
				}
			}
			return;
		}

		$index = 0;
		$d0 = 1.0 / $noiseScale;
		$k = -1;
		$d1 = 0.0;
		$d2 = 0.0;
		$d3 = 0.0;
		$d4 = 0.0;
		for($l2 = 0; $l2 < $xSize; ++$l2){
			$d5 = $xOffset + $l2 * $xScale + $this->xCoord;
			$i3 = (int) $d5;
			if($d5 < $i3){
				--$i3;
			}
			$j3 = $i3 & 255;
			$d5 -= $i3;
			$d6 = $d5 * $d5 * $d5 * ($d5 * ($d5 * 6.0 - 15.0) + 10.0);
			for($k3 = 0; $k3 < $zSize; ++$k3){
				$d7 = $zOffset + $k3 * $zScale + $this->zCoord;
				$l3 = (int) $d7;
				if($d7 < $l3){
					--$l3;
				}
				$i4 = $l3 & 255;
				$d7 -= $l3;
				$d8 = $d7 * $d7 * $d7 * ($d7 * ($d7 * 6.0 - 15.0) + 10.0);
				for($j4 = 0; $j4 < $ySize; ++$j4){
					$d9 = $yOffset + $j4 * $yScale + $this->yCoord;
					$k4 = (int) $d9;
					if($d9 < $k4){
						--$k4;
					}
					$l4 = $k4 & 255;
					$d9 -= $k4;
					$d10 = $d9 * $d9 * $d9 * ($d9 * ($d9 * 6.0 - 15.0) + 10.0);
					if($j4 === 0 || $l4 !== $k){
						$k = $l4;
						$l = $p[$j3] + $l4;
						$i1 = $p[$l] + $i4;
						$j1 = $p[$l + 1] + $i4;
						$k1 = $p[$j3 + 1] + $l4;
						$l1 = $p[$k1] + $i4;
						$i2 = $p[$k1 + 1] + $i4;
						$d1 = $this->lerp($d6, $this->grad($p[$i1], $d5, $d9, $d7), $this->grad($p[$l1], $d5 - 1.0, $d9, $d7));
						$d2 = $this->lerp($d6, $this->grad($p[$j1], $d5, $d9 - 1.0, $d7), $this->grad($p[$i2], $d5 - 1.0, $d9 - 1.0, $d7));
						$d3 = $this->lerp($d6, $this->grad($p[$i1 + 1], $d5, $d9, $d7 - 1.0), $this->grad($p[$l1 + 1], $d5 - 1.0, $d9, $d7 - 1.0));
						$d4 = $this->lerp($d6, $this->grad($p[$j1 + 1], $d5, $d9 - 1.0, $d7 - 1.0), $this->grad($p[$i2 + 1], $d5 - 1.0, $d9 - 1.0, $d7 - 1.0));
					}
					$d11 = $this->lerp($d10, $d1, $d2);
					$d12 = $this->lerp($d10, $d3, $d4);
					$d13 = $this->lerp($d8, $d11, $d12);
					$noiseArray[$index++] += $d13 * $d0;
				}
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\noise;

use dimension\generator\math\Int64;
use dimension\generator\random\RandomSource;
use function count;
use function floor;
use function in_array;
use function is_array;
use function sort;

/**
 * A stack of perlin octaves.
 *
 * Built from an octave count, it holds that many samplers drawn in order.
 * Built from a list of octave numbers (0 is the base frequency, -n is 2^n
 * times lower), octave index i is the frequency 2^-i relative to the
 * highest listed octave; unlisted octaves are null but still consume their
 * share of the random sequence.
 */
final class OctavePerlinNoiseSampler{

	public readonly float $lacunarity;
	public readonly float $persistence;
	/** @var array<int, PerlinNoiseSampler|null> */
	private array $octaveSamplers = [];

	/**
	 * @param int|int[] $octaves octave count, or the list of octave numbers
	 */
	public function __construct(RandomSource $random, int|array $octaves){
		if(!is_array($octaves)){
			for($i = 0; $i < $octaves; $i++){
				$this->octaveSamplers[$i] = new PerlinNoiseSampler($random);
			}
			$this->lacunarity = 1.0;
			$this->persistence = 1.0;
			return;
		}

		sort($octaves);
		if(count($octaves) === 0){
			throw new \InvalidArgumentException("Need some octaves!");
		}
		$start = -$octaves[0];
		$end = $octaves[count($octaves) - 1];
		$length = $start + $end + 1;
		if($length < 1){
			throw new \InvalidArgumentException("Total number of octaves needs to be >= 1");
		}

		$perlin = new PerlinNoiseSampler($random);
		for($i = 0; $i < $length; $i++){
			$this->octaveSamplers[$i] = null;
		}
		if($end >= 0 && $end < $length && in_array(0, $octaves, true)){
			$this->octaveSamplers[$end] = $perlin;
		}

		for($idx = $end + 1; $idx < $length; ++$idx){
			if($idx >= 0 && in_array($end - $idx, $octaves, true)){
				$this->octaveSamplers[$idx] = new PerlinNoiseSampler($random);
			}else{
				$random->setSeed(Lcg::skip262()->nextSeed($random->getSeed()));
			}
		}

		if($end > 0){
			$noiseSeed = Int64::doubleToInt64($perlin->sample(0.0, 0.0, 0.0, 0.0, 0.0) * 9.223372036854776E18);
			$random->setSeed($noiseSeed);
			for($index = $end - 1; $index >= 0; --$index){
				if($index < $length && in_array($end - $index, $octaves, true)){
					$this->octaveSamplers[$index] = new PerlinNoiseSampler($random);
				}else{
					$random->setSeed(Lcg::skip262()->nextSeed($random->getSeed()));
				}
			}
		}

		$this->persistence = 2.0 ** $end;
		$this->lacunarity = 1.0 / (2.0 ** $length - 1.0);
	}

	public function getCount() : int{
		return count($this->octaveSamplers);
	}

	public function getOctave(int $octave) : ?PerlinNoiseSampler{
		return $this->octaveSamplers[$octave];
	}

	public function sample(float $x, float $y, float $z, float $yAmplification = 0.0, float $minY = 0.0, bool $useDefaultY = false) : float{
		$noise = 0.0;
		$persistence = $this->persistence;
		$lacunarity = $this->lacunarity;
		foreach($this->octaveSamplers as $sampler){
			if($sampler !== null){
				$noise += $sampler->sample(
					self::maintainPrecision($x * $persistence),
					$useDefaultY ? -$sampler->originY : self::maintainPrecision($y * $persistence),
					self::maintainPrecision($z * $persistence),
					$yAmplification * $persistence,
					$minY * $persistence
				) * $lacunarity;
			}
			$persistence /= 2.0;
			$lacunarity *= 2.0;
		}
		return $noise;
	}

	public static function maintainPrecision(float $value) : float{
		return $value - floor($value / 3.3554432E7 + 0.5) * 3.3554432E7;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\ChunkHash;
use dimension\generator\math\Float32;
use dimension\generator\math\GenerationMath;
use dimension\generator\holder\BasaltDeltaHolder;
use dimension\generator\nether\object\SeededPopulator;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use dimension\generator\random\LegacyRandom;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;

/**
 * Basalt delta surface: lava pockets in the basalt and the gravel placeholder
 * turned into basalt, blackstone, magma or lava from two surface noises.
 */
final class BasaltDeltaLavaPopulator extends SeededPopulator{

	private const GRAVEL = "minecraft:gravel";
	private const HORIZONTALS = [[0, 0, 1], [-1, 0, 0], [0, 0, -1], [1, 0, 0]];

	private ?int $noiseSeed = null;
	private BasaltDeltaHolder $noises;

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		if($this->noiseSeed !== $seed){
			$this->noises = new BasaltDeltaHolder(new LegacyRandom($seed));
			$this->noiseSeed = $seed;
		}
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$amount = $random->nextInt(64) + 64;
		$object = new BlockManager($world);
		for($i = 0; $i < $amount; ++$i){
			$x = GenerationMath::randomRange($random, $chunkX << 4, ($chunkX << 4) + 15);
			$z = GenerationMath::randomRange($random, $chunkZ << 4, ($chunkZ << 4) + 15);
			if(WorldQuery::biomeId($world, $x, 0, $z) !== BiomeIds::BASALT_DELTAS){
				continue;
			}
			foreach($this->getHighestWorkableBlocks($object, $x, $z) as $y){
				if($y <= 1){
					continue;
				}
				$object->setBlockStateAt($x, $y, $z, "minecraft:flowing_lava");
			}
		}
		$blackstoneLimit = Float32::of(-0.9);
		$basaltLimit = Float32::of(0.8);
		$baseX = $chunkX << 4;
		$baseZ = $chunkZ << 4;
		for($x = 0; $x < 16; ++$x){
			for($z = 0; $z < 16; ++$z){
				if(WorldQuery::biomeId($world, $x + $baseX, 0, $z + $baseZ) !== BiomeIds::BASALT_DELTAS){
					continue;
				}
				for($y = 1; $y < 127; ++$y){
					$worldX = $x + $baseX;
					$worldZ = $z + $baseZ;
					if($object->getBlockIdAt($worldX, $y, $worldZ) !== self::GRAVEL){
						continue;
					}
					$sec = $this->noises->getSurfaceSecNoise()->getValue($worldX, $y, $worldZ);
					$state = $sec < $blackstoneLimit ? "minecraft:blackstone" : ($sec < $basaltLimit ? "minecraft:basalt" : "minecraft:magma");
					if($this->noises->getSurfaceNoise()->getValue($worldX, $y, $worldZ) > 0.0){
						$object->setBlockStateAt($worldX, $y, $worldZ, $state);
					}else{
						$air = false;
						foreach(self::HORIZONTALS as [$dx, $dy, $dz]){
							if($object->getBlockIdAt($worldX + $dx, $y + $dy, $worldZ + $dz) === WorldQuery::AIR){
								$air = true;
							}
						}
						if($air){
							$object->setBlockStateAt($worldX, $y, $worldZ, $state);
						}else{
							$object->setBlockStateAt($worldX, $y, $worldZ, "minecraft:lava");
						}
					}
				}
			}
		}
		$root->merge($object);
	}

	/**
	 * @return int[]
	 */
	private function getHighestWorkableBlocks(BlockManager $level, int $x, int $z) : array{
		$blockYs = [];
		for($y = 128; $y > 0; --$y){
			$b = $level->getBlockIdAt($x, $y, $z);
			if(($b === "minecraft:basalt" || $b === "minecraft:blackstone") &&
				$level->getBlockIdAt($x, $y + 1, $z) === WorldQuery::AIR &&
				$level->getBlockIdAt($x + 1, $y, $z) !== WorldQuery::AIR &&
				$level->getBlockIdAt($x - 1, $y, $z) !== WorldQuery::AIR &&
				$level->getBlockIdAt($x, $y, $z + 1) !== WorldQuery::AIR &&
				$level->getBlockIdAt($x, $y, $z - 1) !== WorldQuery::AIR
			){
				$blockYs[] = $y;
			}
		}
		return $blockYs;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\ChunkHash;
use dimension\generator\math\GenerationMath;
use dimension\generator\nether\object\SeededPopulator;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;

final class WarpedGrassesPopulator extends SeededPopulator{

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$object = new BlockManager($world);
		$amount = $random->nextInt(128) + 192;
		for($i = 0; $i < $amount; ++$i){
			$x = GenerationMath::randomRange($random, $chunkX << 4, ($chunkX << 4) + 15);
			$z = GenerationMath::randomRange($random, $chunkZ << 4, ($chunkZ << 4) + 15);
			if(WorldQuery::biomeId($world, $x, 0, $z) !== BiomeIds::WARPED_FOREST){
				continue;
			}
			foreach($this->getHighestWorkableBlocks($object, $x, $z) as $y){
				if($y <= 1){
					continue;
				}
				$object->setBlockStateAt($x, $y, $z, $random->nextInt(6) === 0 ? "minecraft:warped_fungus" : "minecraft:warped_roots");
			}
		}
		$root->merge($object);
	}

	/**
	 * @return int[]
	 */
	private function getHighestWorkableBlocks(BlockManager $level, int $x, int $z) : array{
		$blockYs = [];
		for($y = 128; $y > 0; --$y){
			if($level->getBlockIdAt($x, $y, $z) === "minecraft:warped_nylium" && $level->getBlockIdAt($x, $y + 1, $z) === WorldQuery::AIR){
				$blockYs[] = $y + 1;
			}
		}
		return $blockYs;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\nether\object\OreGeneratorFeature;

final class LavaOrePopulator extends OreGeneratorFeature{

	public function name() : string{
		return "nether_lava_ore";
	}

	public function getState() : string{
		return "minecraft:lava";
	}

	public function getClusterCount() : int{
		return 32;
	}

	public function getClusterSize() : int{
		return 1;
	}

	public function getMinHeight() : int{
		return 0;
	}

	public function getMaxHeight() : int{
		return 32;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\nether\object\OreGeneratorFeature;

final class SoulsandPopulator extends OreGeneratorFeature{

	public function name() : string{
		return "nether_soulsand";
	}

	public function getState() : string{
		return "minecraft:soul_sand";
	}

	public function getClusterCount() : int{
		return 12;
	}

	public function getClusterSize() : int{
		return 12;
	}

	public function getMinHeight() : int{
		return 0;
	}

	public function getMaxHeight() : int{
		return 32;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\ChunkHash;
use dimension\generator\math\GenerationMath;
use dimension\generator\nether\object\SeededPopulator;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use pocketmine\world\ChunkManager;

final class FirePopulator extends SeededPopulator{

	private const NETHERRACK = "minecraft:netherrack";

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$object = new BlockManager($world);
		$amount = $random->nextInt(2) + 1;

		for($i = 0; $i < $amount; ++$i){
			$x = GenerationMath::randomRange($random, $chunkX << 4, ($chunkX << 4) + 15);
			$z = GenerationMath::randomRange($random, $chunkZ << 4, ($chunkZ << 4) + 15);
			foreach($this->getHighestWorkableBlocks($object, $x, $z) as $y){
				if($y <= 1){
					continue;
				}
				if($random->nextInt(4) === 1){
					continue;
				}
				$object->setBlockStateAt($x, $y, $z, $object->getBlockIdAt($x, $y - 1, $z) === self::NETHERRACK ? "minecraft:fire" : "minecraft:soul_fire");
			}
		}
		$root->merge($object);
	}

	/**
	 * @return int[]
	 */
	private function getHighestWorkableBlocks(BlockManager $level, int $x, int $z) : array{
		$blockYs = [];
		for($y = 128; $y > 0; --$y){
			$b = $level->getBlockIdAt($x, $y, $z);
			if(($b === self::NETHERRACK || $b === "minecraft:soul_sand" || $b === "minecraft:soul_soil") && $level->getBlockIdAt($x, $y + 1, $z) === WorldQuery::AIR){
				$blockYs[] = $y + 1;
			}
		}
		return $blockYs;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\ChunkHash;
use dimension\generator\math\GenerationMath;
use dimension\generator\nether\object\HugeFungus;
use dimension\generator\nether\object\SeededPopulator;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;

final class CrimsonFungiTreePopulator extends SeededPopulator{

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$object = new BlockManager($world);
		$amount = $random->nextInt(6) + 4;
		for($i = 0; $i < $amount; ++$i){
			$x = GenerationMath::randomRange($random, $chunkX << 4, ($chunkX << 4) + 15);
			$z = GenerationMath::randomRange($random, $chunkZ << 4, ($chunkZ << 4) + 15);
			if(WorldQuery::biomeId($world, $x, 0, $z) !== BiomeIds::CRIMSON_FOREST){
				continue;
			}
			foreach($this->getHighestWorkableBlocks($object, $x, $z) as $y){
				if($y <= 1){
					continue;
				}
				if($random->nextInt(4) === 1){
					continue;
				}
				HugeFungus::crimson($random->nextInt(9) + 4)->placeObject($object, $x, $y, $z, $random);
			}
		}
		$root->merge($object);
	}

	/**
	 * @return int[]
	 */
	private function getHighestWorkableBlocks(BlockManager $level, int $x, int $z) : array{
		$blockYs = [];
		for($y = 128; $y > 0; --$y){
			if($level->getBlockIdAt($x, $y, $z) === "minecraft:crimson_nylium" && $level->getBlockAt($x, $y + 1, $z)->canBeReplaced()){
				$blockYs[] = $y + 1;
			}
		}
		return $blockYs;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\ChunkHash;
use dimension\generator\math\GenerationMath;
use dimension\generator\nether\object\SeededPopulator;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use pocketmine\world\ChunkManager;

final class LavaPopulator extends SeededPopulator{

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$object = new BlockManager($world);
		$amount = $random->nextBoundedInt(30) - 29;

		for($i = 0; $i < $amount; ++$i){
			$x = GenerationMath::randomRange($random, $chunkX << 4, ($chunkX << 4) + 15);
			$z = GenerationMath::randomRange($random, $chunkZ << 4, ($chunkZ << 4) + 15);
			$y = $this->getHighestWorkableBlock($object, $x, $z);
			if($y <= 1){
				continue;
			}
			if($random->nextInt(4) === 1){
				continue;
			}
			$object->setBlockStateAt($x, $y + 1, $z, "minecraft:lava");
		}
		$root->merge($object);
	}

	private function getHighestWorkableBlock(BlockManager $level, int $x, int $z) : int{
		for($y = 127; $y >= 0; $y--){
			if($level->getBlockIdAt($x, $y, $z) === WorldQuery::AIR){
				break;
			}
		}
		return $y === 0 ? -1 : $y;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\ChunkHash;
use dimension\generator\math\GenerationMath;
use dimension\generator\nether\object\SeededPopulator;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;
use function intdiv;

final class WarpedTwistingVinesPopulator extends SeededPopulator{

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$object = new BlockManager($world);
		$amount = $random->nextInt(6) + 2;
		for($i = 0; $i < $amount; ++$i){
			$x = GenerationMath::randomRange($random, $chunkX << 4, ($chunkX << 4) + 15);
			$z = GenerationMath::randomRange($random, $chunkZ << 4, ($chunkZ << 4) + 15);
			if(WorldQuery::biomeId($world, $x, 0, $z) !== BiomeIds::WARPED_FOREST){
				continue;
			}
			foreach($this->getHighestWorkableBlocks($object, $x, $z) as $y){
				if($y <= 1){
					continue;
				}
				if($random->nextInt(5) === 0){
					continue;
				}
				$endY = $this->getHighestEndingBlock($object, $x, $y, $z);
				$amountToDecrease = $random->nextInt($endY - $y + 1);
				for($yPos = $y; $yPos < $y + intdiv($amountToDecrease, 2); $yPos++){
					$object->setBlockStateAt($x, $yPos, $z, "minecraft:twisting_vines");
				}
			}
		}
		$root->merge($object);
	}

	private function getHighestEndingBlock(BlockManager $level, int $x, int $y, int $z) : int{
		for(; $y < 128; ++$y){
			if($level->getBlockIdAt($x, $y - 1, $z) === WorldQuery::AIR && $level->getBlockAt($x, $y, $z)->isSolid()){
				break;
			}
		}
		return --$y;
	}

	/**
	 * @return int[]
	 */
	private function getHighestWorkableBlocks(BlockManager $level, int $x, int $z) : array{
		$blockYs = [];
		for($y = 128; $y > 0; --$y){
			$b = $level->getBlockIdAt($x, $y, $z);
			if(($b === "minecraft:warped_nylium" || $b === "minecraft:warped_wart_block") && $level->getBlockAt($x, $y + 1, $z)->canBeReplaced()){
				$blockYs[] = $y + 1;
			}
		}
		return $blockYs;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\nether\object\OreGeneratorFeature;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use dimension\generator\random\Xoroshiro128;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;

class NetherGravelPopulator extends OreGeneratorFeature{

	public function name() : string{
		return "nether_gravel";
	}

	public function getState() : string{
		return "minecraft:gravel";
	}

	public function getClusterCount() : int{
		return 2;
	}

	public function getClusterSize() : int{
		return 33;
	}

	public function getMinHeight() : int{
		return 5;
	}

	public function getMaxHeight() : int{
		return 36;
	}

	protected function spawn(BlockManager $level, ChunkManager $world, Xoroshiro128 $rand, int $x, int $y, int $z) : void{
		if(WorldQuery::biomeId($world, $x, $y, $z) !== BiomeIds::BASALT_DELTAS){
			parent::spawn($level, $world, $rand, $x, $y, $z);
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\nether\object\OreGeneratorFeature;

final class NetherGoldOrePopulator extends OreGeneratorFeature{

	public function name() : string{
		return "nether_nether_gold_ore";
	}

	public function getState() : string{
		return "minecraft:nether_gold_ore";
	}

	public function getClusterCount() : int{
		return 10;
	}

	public function getClusterSize() : int{
		return 10;
	}

	public function getMinHeight() : int{
		return 10;
	}

	public function getMaxHeight() : int{
		return 117;
	}

	public function getConcentration() : int{
		return self::TRIANGLE;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

final class NetherBlackstonePopulator extends NetherGravelPopulator{

	public function name() : string{
		return "nether_blackstone";
	}

	public function getState() : string{
		return "minecraft:blackstone";
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\nether\object\OreGeneratorFeature;

final class AncientDebrisLargePopulator extends OreGeneratorFeature{

	public function name() : string{
		return "nether_ancientdebris_large";
	}

	public function getState() : string{
		return "minecraft:ancient_debris";
	}

	public function getClusterCount() : int{
		return 2;
	}

	public function getClusterSize() : int{
		return 3;
	}

	public function getMinHeight() : int{
		return 8;
	}

	public function getMaxHeight() : int{
		return 23;
	}

	public function getSkipAir() : float{
		return 1.0;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\nether\object\OreGeneratorFeature;

final class AncientDebrisSmallPopulator extends OreGeneratorFeature{

	public function name() : string{
		return "nether_ancientdebris_small";
	}

	public function getState() : string{
		return "minecraft:ancient_debris";
	}

	public function getClusterCount() : int{
		return 3;
	}

	public function getClusterSize() : int{
		return 2;
	}

	public function getMinHeight() : int{
		return 8;
	}

	public function getMaxHeight() : int{
		return 119;
	}

	public function getSkipAir() : float{
		return 1.0;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\ChunkHash;
use dimension\generator\math\GenerationMath;
use dimension\generator\nether\object\SeededPopulator;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;

final class CrimsonGrassesPopulator extends SeededPopulator{

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$object = new BlockManager($world);
		$amount = $random->nextInt(128) + 192;
		for($i = 0; $i < $amount; ++$i){
			$x = GenerationMath::randomRange($random, $chunkX << 4, ($chunkX << 4) + 15);
			$z = GenerationMath::randomRange($random, $chunkZ << 4, ($chunkZ << 4) + 15);
			if(WorldQuery::biomeId($world, $x, 0, $z) !== BiomeIds::CRIMSON_FOREST){
				continue;
			}
			foreach($this->getHighestWorkableBlocks($object, $x, $z) as $y){
				if($y <= 1){
					continue;
				}
				$object->setBlockStateAt($x, $y, $z, $random->nextInt(6) === 0 ? "minecraft:crimson_fungus" : "minecraft:crimson_roots");
			}
		}
		$root->merge($object);
	}

	/**
	 * @return int[]
	 */
	private function getHighestWorkableBlocks(BlockManager $level, int $x, int $z) : array{
		$blockYs = [];
		for($y = 128; $y > 0; --$y){
			if($level->getBlockIdAt($x, $y, $z) === "minecraft:crimson_nylium" && $level->getBlockIdAt($x, $y + 1, $z) === WorldQuery::AIR){
				$blockYs[] = $y + 1;
			}
		}
		return $blockYs;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\ChunkHash;
use dimension\generator\math\GenerationMath;
use dimension\generator\nether\object\HugeFungus;
use dimension\generator\nether\object\SeededPopulator;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;

final class WarpedFungiTreePopulator extends SeededPopulator{

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$object = new BlockManager($world);
		$amount = $random->nextInt(6) + 4;
		for($i = 0; $i < $amount; ++$i){
			$x = GenerationMath::randomRange($random, $chunkX << 4, ($chunkX << 4) + 15);
			$z = GenerationMath::randomRange($random, $chunkZ << 4, ($chunkZ << 4) + 15);
			if(WorldQuery::biomeId($world, $x, 0, $z) !== BiomeIds::WARPED_FOREST){
				continue;
			}
			foreach($this->getHighestWorkableBlocks($object, $x, $z) as $y){
				if($y <= 1){
					continue;
				}
				if($random->nextInt(4) === 1){
					continue;
				}
				HugeFungus::warped($random->nextInt(9) + 4)->placeObject($object, $x, $y, $z, $random);
			}
		}
		$root->merge($object);
	}

	/**
	 * @return int[]
	 */
	private function getHighestWorkableBlocks(BlockManager $level, int $x, int $z) : array{
		$blockYs = [];
		for($y = 128; $y > 0; --$y){
			if($level->getBlockIdAt($x, $y, $z) === "minecraft:warped_nylium" && $level->getBlockAt($x, $y + 1, $z)->canBeReplaced()){
				$blockYs[] = $y + 1;
			}
		}
		return $blockYs;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\ChunkHash;
use dimension\generator\math\GenerationMath;
use dimension\generator\nether\object\SeededPopulator;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use pocketmine\world\ChunkManager;

final class GlowstonePopulator extends SeededPopulator{

	private const GLOWSTONE = "minecraft:glowstone";
	private const NETHERRACK = "minecraft:netherrack";

	private const FACES = [[0, -1, 0], [0, 1, 0], [0, 0, -1], [0, 0, 1], [-1, 0, 0], [1, 0, 0]];

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$object = new BlockManager($world);
		if($random->nextInt(11) === 0){
			$x = GenerationMath::randomRange($random, $chunkX << 4, ($chunkX << 4) + 15);
			$z = GenerationMath::randomRange($random, $chunkZ << 4, ($chunkZ << 4) + 15);
			$y = $this->getHighestWorkableBlock($world, $x, $z);
			if($y !== -1 && WorldQuery::blockId($world, $x, $y, $z) !== self::NETHERRACK){
				$count = GenerationMath::randomRange($random, 40, 60);
				$object->setBlockStateAt($x, $y, $z, self::GLOWSTONE);
				$cyclesNum = 0;
				while($count !== 0){
					if($cyclesNum === 1500){
						break;
					}
					$spawnX = $x + $random->nextInt(9) - $random->nextInt(9);
					$spawnY = $y - $random->nextInt(9);
					$spawnZ = $z + $random->nextInt(9) - $random->nextInt(9);
					if($cyclesNum % 128 === 0 && $cyclesNum !== 0){
						$object->setBlockStateAt($x + $random->nextRangeInt(-3, 3), $y - $random->nextInt(5), $z + $random->nextRangeInt(-3, 3), self::GLOWSTONE);
						$count--;
					}
					if($this->checkAroundBlock($spawnX, $spawnY, $spawnZ, $object)){
						$object->setBlockStateAt($spawnX, $spawnY, $spawnZ, self::GLOWSTONE);
						$count--;
					}
					$cyclesNum++;
				}
			}
		}
		$root->merge($object);
	}

	private function getHighestWorkableBlock(ChunkManager $world, int $x, int $z) : int{
		for($y = 125; $y >= 0; $y--){
			if(WorldQuery::blockId($world, $x, $y, $z) === WorldQuery::AIR){
				break;
			}
		}
		return $y === 0 ? -1 : $y;
	}

	private function checkAroundBlock(int $x, int $y, int $z, BlockManager $level) : bool{
		foreach(self::FACES as [$dx, $dy, $dz]){
			if($level->getBlockIdAt($x + $dx, $y + $dy, $z + $dz) === self::GLOWSTONE){
				return true;
			}
		}
		return false;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\nether\object\OreGeneratorFeature;

final class MagmaPopulator extends OreGeneratorFeature{

	public function name() : string{
		return "nether_magma";
	}

	public function getState() : string{
		return "minecraft:magma";
	}

	public function getClusterCount() : int{
		return 9;
	}

	public function getClusterSize() : int{
		return 28;
	}

	public function getMinHeight() : int{
		return 23;
	}

	public function getMaxHeight() : int{
		return 36;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\nether\object\OreGeneratorFeature;

final class NetherQuartzPopulator extends OreGeneratorFeature{

	public function name() : string{
		return "nether_quartz";
	}

	public function getState() : string{
		return "minecraft:quartz_ore";
	}

	public function getClusterCount() : int{
		return 20;
	}

	public function getClusterSize() : int{
		return 14;
	}

	public function getMinHeight() : int{
		return 10;
	}

	public function getMaxHeight() : int{
		return 117;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\ChunkHash;
use dimension\generator\math\GenerationMath;
use dimension\generator\nether\object\SeededPopulator;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;

final class BasaltDeltaPillarPopulator extends SeededPopulator{

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$object = new BlockManager($world);
		$amount = $random->nextBoundedInt(128) + 128;
		$maxHeight = $world->getMaxY();
		for($i = 0; $i < $amount; ++$i){
			$x = GenerationMath::randomRange($random, $chunkX << 4, ($chunkX << 4) + 15);
			$z = GenerationMath::randomRange($random, $chunkZ << 4, ($chunkZ << 4) + 15);
			if(WorldQuery::biomeId($world, $x, 0, $z) !== BiomeIds::BASALT_DELTAS){
				continue;
			}
			foreach($this->getHighestWorkableBlocks($object, $x, $z) as $y){
				if($y <= 1){
					continue;
				}
				if($random->nextBoundedInt(5) === 0){
					continue;
				}
				for($randomHeight = 0; $randomHeight < $random->nextBoundedInt(5) + 1; $randomHeight++){
					$placeLocation = $y + $randomHeight;
					if($placeLocation >= $maxHeight){
						break;
					}
					$object->setBlockStateAt($x, $placeLocation, $z, "minecraft:basalt");
				}
			}
		}
		$root->merge($object);
	}

	/**
	 * @return int[]
	 */
	private function getHighestWorkableBlocks(BlockManager $level, int $x, int $z) : array{
		$blockYs = [];
		for($y = 126; $y > 1; --$y){
			if($level->getBlockIdAt($x, $y, $z) === "minecraft:blackstone" && $level->getBlockIdAt($x, $y + 1, $z) === WorldQuery::AIR){
				$blockYs[] = $y + 1;
			}
		}
		return $blockYs;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\ChunkHash;
use dimension\generator\math\GenerationMath;
use dimension\generator\nether\object\SeededPopulator;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;
use function in_array;

final class BasaltDeltaMagmaPopulator extends SeededPopulator{

	private const LAVA = ["minecraft:lava", "minecraft:flowing_lava"];

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$object = new BlockManager($world);
		$amount = $random->nextBoundedInt(4) + 20;
		for($i = 0; $i < $amount; ++$i){
			$x = GenerationMath::randomRange($random, $chunkX << 4, ($chunkX << 4) + 15);
			$z = GenerationMath::randomRange($random, $chunkZ << 4, ($chunkZ << 4) + 15);
			if(WorldQuery::biomeId($world, $x, 0, $z) !== BiomeIds::BASALT_DELTAS){
				continue;
			}
			foreach($this->getHighestWorkableBlocks($object, $x, $z) as $y){
				if($y <= 1){
					continue;
				}
				$object->setBlockStateAt($x, $y, $z, "minecraft:magma");
			}
		}
		$root->merge($object);
	}

	/**
	 * @return int[]
	 */
	private function getHighestWorkableBlocks(BlockManager $level, int $x, int $z) : array{
		$blockYs = [];
		for($y = 128; $y > 0; --$y){
			$b = $level->getBlockIdAt($x, $y, $z);
			$b1 = $level->getBlockIdAt($x + 1, $y, $z);
			$b2 = $level->getBlockIdAt($x - 1, $y, $z);
			$b3 = $level->getBlockIdAt($x, $y, $z + 1);
			$b4 = $level->getBlockIdAt($x, $y, $z - 1);
			if(($b === "minecraft:basalt" || $b === "minecraft:blackstone") &&
				$level->getBlockIdAt($x, $y + 1, $z) === WorldQuery::AIR && (
					in_array($b1, self::LAVA, true) ||
					in_array($b2, self::LAVA, true) ||
					in_array($b3, self::LAVA, true) ||
					in_array($b4, self::LAVA, true)
				)
			){
				$blockYs[] = $y;
			}
		}
		return $blockYs;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\populator;

use dimension\generator\ChunkHash;
use dimension\generator\math\GenerationMath;
use dimension\generator\nether\object\SeededPopulator;
use dimension\generator\nether\object\WorldQuery;
use dimension\generator\object\BlockManager;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;
use function in_array;

final class CrimsonWeepingVinesPopulator extends SeededPopulator{

	private const ENDING_BLOCKS = [
		"minecraft:netherrack",
		"minecraft:crimson_nylium",
		"minecraft:nether_wart_block",
		"minecraft:lava",
		"minecraft:flowing_lava",
		"minecraft:crimson_fungus",
		"minecraft:crimson_roots",
		"minecraft:quartz_ore",
		"minecraft:nether_gold_ore",
		"minecraft:ancient_debris",
	];

	public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$random = $this->random;
		$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$object = new BlockManager($world);
		$amount = $random->nextInt(5) + 1;
		for($i = 0; $i < $amount; ++$i){
			$x = GenerationMath::randomRange($random, $chunkX << 4, ($chunkX << 4) + 15);
			$z = GenerationMath::randomRange($random, $chunkZ << 4, ($chunkZ << 4) + 15);
			if(WorldQuery::biomeId($world, $x, 0, $z) !== BiomeIds::CRIMSON_FOREST){
				continue;
			}
			foreach($this->getHighestWorkableBlocks($object, $x, $z) as $y){
				if($y <= 1){
					continue;
				}
				$endY = $this->getHighestEndingBlock($object, $x, $y, $z);
				$amountToDecrease = $random->nextInt($y - $endY);
				for($yPos = $y; $yPos > $y - $amountToDecrease; $yPos--){
					$object->setBlockStateAt($x, $yPos, $z, "minecraft:weeping_vines");
				}
			}
		}
		$root->merge($object);
	}

	private function getHighestEndingBlock(BlockManager $level, int $x, int $y, int $z) : int{
		for(; $y > 0; --$y){
			$b = $level->getBlockIdAt($x, $y, $z);
			if($level->getBlockIdAt($x, $y + 1, $z) === WorldQuery::AIR && in_array($b, self::ENDING_BLOCKS, true)){
				break;
			}
		}
		return ++$y;
	}

	/**
	 * @return int[]
	 */
	private function getHighestWorkableBlocks(BlockManager $level, int $x, int $z) : array{
		$blockYs = [];
		for($y = 128; $y > 0; --$y){
			$b = $level->getBlockIdAt($x, $y, $z);
			if(($b === "minecraft:crimson_nylium" || $b === "minecraft:netherrack") && $level->getBlockIdAt($x, $y - 1, $z) === WorldQuery::AIR){
				$blockYs[] = $y - 1;
			}
		}
		return $blockYs;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether;

use dimension\generator\nether\populator\AncientDebrisLargePopulator;
use dimension\generator\nether\populator\AncientDebrisSmallPopulator;
use dimension\generator\nether\populator\BasaltDeltaLavaPopulator;
use dimension\generator\nether\populator\BasaltDeltaMagmaPopulator;
use dimension\generator\nether\populator\BasaltDeltaPillarPopulator;
use dimension\generator\nether\populator\CrimsonFungiTreePopulator;
use dimension\generator\nether\populator\CrimsonGrassesPopulator;
use dimension\generator\nether\populator\CrimsonWeepingVinesPopulator;
use dimension\generator\nether\populator\FirePopulator;
use dimension\generator\nether\populator\GlowstonePopulator;
use dimension\generator\nether\populator\LavaOrePopulator;
use dimension\generator\nether\populator\LavaPopulator;
use dimension\generator\nether\populator\MagmaPopulator;
use dimension\generator\nether\populator\NetherBlackstonePopulator;
use dimension\generator\nether\populator\NetherGoldOrePopulator;
use dimension\generator\nether\populator\NetherGravelPopulator;
use dimension\generator\nether\populator\NetherQuartzPopulator;
use dimension\generator\nether\populator\SoulsandPopulator;
use dimension\generator\nether\populator\WarpedFungiTreePopulator;
use dimension\generator\nether\populator\WarpedGrassesPopulator;
use dimension\generator\nether\populator\WarpedTwistingVinesPopulator;
use dimension\generator\Populator;
use dimension\generator\structure\BastionRemnantPopulator;
use dimension\generator\structure\NetherFortressPopulator;
use dimension\generator\structure\NetherFossilPopulator;
use dimension\generator\structure\RuinedPortalPopulator;

/**
 * The nether population pass, in execution order.
 */
final class NetherPopulators{

	private function __construct(){
	}

	/**
	 * @return list<Populator>
	 */
	public static function create(int $seed, string $resourceFolder) : array{
		return [
			new GlowstonePopulator(),
			new SoulsandPopulator(),
			new MagmaPopulator(),
			new LavaOrePopulator(),
			new FirePopulator(),
			new LavaPopulator(),
			new NetherGoldOrePopulator(),
			new AncientDebrisSmallPopulator(),
			new AncientDebrisLargePopulator(),
			new NetherQuartzPopulator(),
			new BasaltDeltaLavaPopulator(),
			new BasaltDeltaPillarPopulator(),
			new BasaltDeltaMagmaPopulator(),
			new CrimsonFungiTreePopulator(),
			new CrimsonGrassesPopulator(),
			new CrimsonWeepingVinesPopulator(),
			new WarpedFungiTreePopulator(),
			new WarpedGrassesPopulator(),
			new WarpedTwistingVinesPopulator(),
			new NetherBlackstonePopulator(),
			new NetherGravelPopulator(),
			new BastionRemnantPopulator($seed, $resourceFolder),
			new NetherFortressPopulator($seed, $resourceFolder),
			new RuinedPortalPopulator($seed, $resourceFolder),
			new NetherFossilPopulator($seed, $resourceFolder),
		];
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\object;

use dimension\generator\ChunkHash;
use dimension\generator\math\Float32;
use dimension\generator\math\GenerationMath;
use dimension\generator\math\Int64;
use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use dimension\generator\random\Xoroshiro128;
use pocketmine\world\ChunkManager;
use function in_array;
use function max;
use function min;
use function ord;
use function strlen;
use const M_PI;

/**
 * Ore feature: clusters of a block replacing stone, deepslate or netherrack.
 * The chunk random is salted with the 32-bit string hash of name().
 */
abstract class OreGeneratorFeature implements Populator{

	public const UNIFORM = 0;
	public const TRIANGLE = 1;

	private const REPLACEABLE = ["minecraft:stone", "minecraft:deepslate", "minecraft:netherrack"];

	protected Xoroshiro128 $random;

	public function __construct(){
		$this->random = new Xoroshiro128(0);
	}

	abstract public function name() : string;

	abstract public function getState() : string;

	abstract public function getClusterCount() : int;

	abstract public function getClusterSize() : int;

	abstract public function getMinHeight() : int;

	abstract public function getMaxHeight() : int;

	public function getSkipAir() : float{
		return 0.0;
	}

	public function getConcentration() : int{
		return self::UNIFORM;
	}

	public function isRare() : bool{
		return false;
	}

	public function canBeReplaced(string $id) : bool{
		return in_array($id, self::REPLACEABLE, true);
	}

	private static function stringHash(string $value) : int{
		$hash = 0;
		$length = strlen($value);
		for($i = 0; $i < $length; ++$i){
			$hash = Int64::toInt32($hash * 31 + ord($value[$i]));
		}
		return $hash;
	}

	final public function populate(BlockManager $root, ChunkManager $world, int $chunkX, int $chunkZ, int $seed) : void{
		$random = $this->random;
		$random->setSeed(ChunkHash::hash($chunkX, $chunkZ) ^ Int64::add($seed, self::stringHash($this->name())));
		$sx = $chunkX << 4;
		$sz = $chunkZ << 4;
		$manager = new BlockManager($world);
		for($i = 0; $i < ($this->isRare() ? ($random->nextInt($this->getClusterCount()) === 0 ? 1 : 0) : $this->getClusterCount()); $i++){
			$object = new BlockManager($world);
			$maxY = min($this->getMaxHeight(), $world->getMaxY());
			$minY = max($this->getMinHeight(), $world->getMinY());
			$x = $sx + $random->nextInt(15);
			$z = $sz + $random->nextInt(15);
			if($this->getConcentration() === self::TRIANGLE){
				$y = GenerationMath::randomRangeTriangle($random, $minY, $maxY);
			}else{
				$y = $minY + $random->nextBoundedInt(($maxY - $minY) + 1);
			}

			if(!$this->canBeReplaced(WorldQuery::blockId($world, $x, $y, $z))){
				continue;
			}
			if($this->getClusterSize() === 1){
				$object->setBlockStateAt($x, $y, $z, $this->getState());
			}else{
				$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ) ^ ($x + $y + $z));
				$this->spawn($object, $world, $random, $x, $y, $z);
				$random->setSeed($seed ^ ChunkHash::hash($chunkX, $chunkZ));
			}
			$skip = false;
			if($this->getSkipAir() != 0){
				$air = false;
				foreach($object->getBlocks() as $placed){
					if(WorldQuery::blockId($world, $placed->x, $placed->y, $placed->z) === WorldQuery::AIR){
						$air = true;
						break;
					}
				}
				if($air){
					$skip = $random->nextFloat() < $this->getSkipAir();
				}
			}
			if(!$skip){
				$manager->merge($object);
			}
		}

		$root->merge($manager);
	}

	protected function spawn(BlockManager $level, ChunkManager $world, Xoroshiro128 $rand, int $x, int $y, int $z) : void{
		$size = $this->getClusterSize();
		$pi = Float32::of(M_PI);
		$piScaled = Float32::of($rand->nextFloat() * $pi);
		$sinOffset = Float32::of(Float32::of(GenerationMath::sinFloat($piScaled) * $size) / 8.0);
		$cosOffset = Float32::of(Float32::of(GenerationMath::cosFloat($piScaled) * $size) / 8.0);
		$scaleMaxX = Float32::of(($x + 8) + $sinOffset);
		$scaleMinX = Float32::of(($x + 8) - $sinOffset);
		$scaleMaxZ = Float32::of(($z + 8) + $cosOffset);
		$scaleMinZ = Float32::of(($z + 8) - $cosOffset);
		$scaleMaxY = (float) ($y + $rand->nextBoundedInt(3) - 2);
		$scaleMinY = (float) ($y + $rand->nextBoundedInt(3) - 2);

		for($i = 0; $i < $size; ++$i){
			$sizeIncr = Float32::of($i / $size);
			$scaleX = $scaleMaxX + ($scaleMinX - $scaleMaxX) * $sizeIncr;
			$scaleY = $scaleMaxY + ($scaleMinY - $scaleMaxY) * $sizeIncr;
			$scaleZ = $scaleMaxZ + ($scaleMinZ - $scaleMaxZ) * $sizeIncr;
			$randSizeOffset = $rand->nextDouble() * $size / 16.0;
			$sinFactor = Float32::of(GenerationMath::sinFloat(Float32::of($pi * $sizeIncr)) + 1.0);
			$randVec1 = $sinFactor * $randSizeOffset + 1.0;
			$randVec2 = $sinFactor * $randSizeOffset + 1.0;
			$minX = GenerationMath::floor($scaleX - $randVec1 / 2.0);
			$minY = GenerationMath::floor($scaleY - $randVec2 / 2.0);
			$minZ = GenerationMath::floor($scaleZ - $randVec1 / 2.0);
			$maxX = GenerationMath::floor($scaleX + $randVec1 / 2.0);
			$maxY = GenerationMath::floor($scaleY + $randVec2 / 2.0);
			$maxZ = GenerationMath::floor($scaleZ + $randVec1 / 2.0);

			for($xSeg = $minX; $xSeg <= $maxX; ++$xSeg){
				$xVal = ($xSeg + 0.5 - $scaleX) / ($randVec1 / 2.0);

				if($xVal * $xVal < 1.0){
					for($ySeg = $minY; $ySeg <= $maxY; ++$ySeg){
						if($ySeg < -64){
							continue;
						}
						$yVal = ($ySeg + 0.5 - $scaleY) / ($randVec2 / 2.0);

						if($xVal * $xVal + $yVal * $yVal < 1.0){
							for($zSeg = $minZ; $zSeg <= $maxZ; ++$zSeg){
								$zVal = ($zSeg + 0.5 - $scaleZ) / ($randVec1 / 2.0);

								if($xVal * $xVal + $yVal * $yVal + $zVal * $zVal < 1.0){
									if($this->canBeReplaced($level->getBlockIdAt($xSeg, $ySeg, $zSeg))){
										$level->setBlockStateAt($xSeg, $ySeg, $zSeg, $this->getState());
									}
								}
							}
						}
					}
				}
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\object;

use dimension\generator\object\BlockManager;
use dimension\generator\random\Xoroshiro128;
use function abs;
use function in_array;

/**
 * Crimson or warped fungus tree: a stem, a wart block cap with shroomlights
 * and wart blocks hanging from its rim.
 */
final class HugeFungus{

	private const SHROOMLIGHT = "minecraft:shroomlight";

	private const OVERRIDABLE = [
		"minecraft:air",
		"minecraft:acacia_leaves",
		"minecraft:azalea_leaves",
		"minecraft:birch_leaves",
		"minecraft:azalea_leaves_flowered",
		"minecraft:cherry_leaves",
		"minecraft:dark_oak_leaves",
		"minecraft:jungle_leaves",
		"minecraft:mangrove_leaves",
		"minecraft:oak_leaves",
		"minecraft:spruce_leaves",
		"minecraft:snow_layer",
		"minecraft:acacia_sapling",
		"minecraft:cherry_sapling",
		"minecraft:spruce_sapling",
		"minecraft:bamboo_sapling",
		"minecraft:oak_sapling",
		"minecraft:jungle_sapling",
		"minecraft:dark_oak_sapling",
		"minecraft:leaf_litter",
		"minecraft:wildflowers",
		"minecraft:pink_petals",
		"minecraft:tall_grass",
		"minecraft:birch_sapling",
		"minecraft:short_grass",
		"minecraft:dandelion",
		"minecraft:lily_of_the_valley",
		"minecraft:lilac",
		"minecraft:peony",
		"minecraft:rose_bush",
		"minecraft:large_fern",
		"minecraft:fern",
	];

	public function __construct(
		private string $trunk,
		private string $leaf,
		private int $treeHeight
	){}

	public static function crimson(int $treeHeight) : self{
		return new self("minecraft:crimson_stem", "minecraft:nether_wart_block", $treeHeight);
	}

	public static function warped(int $treeHeight) : self{
		return new self("minecraft:warped_stem", "minecraft:warped_wart_block", $treeHeight);
	}

	private function checkY(int $y) : bool{
		return $y > 126;
	}

	private function isSolid(BlockManager $level, int $x, int $y, int $z) : bool{
		return $level->getBlockAt($x, $y, $z)->isSolid();
	}

	public function placeObject(BlockManager $level, int $x, int $y, int $z, Xoroshiro128 $random) : void{
		if($this->checkY($y)){
			return;
		}
		$this->placeTrunk($level, $x, $y, $z, $this->treeHeight);
		$mid = 2;
		$treeHeight = $this->treeHeight;
		for($yy = $y - 3 + $treeHeight; $yy <= $y + $treeHeight - 1; ++$yy){
			if($this->checkY($yy)){
				continue;
			}
			for($xx = $x - $mid; $xx <= $x + $mid; $xx++){
				$xOff = abs($xx - $x);
				for($zz = $z - $mid; $zz <= $z + $mid; $zz += $mid * 2){
					$zOff = abs($zz - $z);
					if($xOff === $mid && $zOff === $mid && $random->nextInt(2) === 0){
						continue;
					}
					if(!$this->isSolid($level, $xx, $yy, $zz)){
						if($random->nextInt(20) === 0){
							$level->setBlockStateAt($xx, $yy, $zz, self::SHROOMLIGHT);
						}else{
							$level->setBlockStateAt($xx, $yy, $zz, $this->leaf);
						}
					}
				}
			}
			for($zz = $z - $mid; $zz <= $z + $mid; $zz++){
				$zOff = abs($zz - $z);
				for($xx = $x - $mid; $xx <= $x + $mid; $xx += $mid * 2){
					$xOff = abs($xx - $x);
					if($xOff === $mid && $zOff === $mid && $random->nextInt(2) === 0){
						continue;
					}
					if(!$this->isSolid($level, $xx, $yy, $zz)){
						if($random->nextInt(20) === 0){
							$level->setBlockStateAt($xx, $yy, $zz, self::SHROOMLIGHT);
						}else{
							$level->setBlockStateAt($xx, $yy, $zz, $this->leaf);
						}
					}
				}
			}
		}

		for($yy = $y - 4 + $treeHeight; $yy <= $y + $treeHeight - 3; ++$yy){
			if($this->checkY($yy)){
				continue;
			}
			for($xx = $x - $mid; $xx <= $x + $mid; $xx++){
				for($zz = $z - $mid; $zz <= $z + $mid; $zz += $mid * 2){
					if(!$this->isSolid($level, $xx, $yy, $zz)){
						if($random->nextInt(3) === 0){
							for($i = 0; $i < $random->nextInt(5); $i++){
								if(!$this->isSolid($level, $xx, $yy - $i, $zz)){
									$level->setBlockStateAt($xx, $yy - $i, $zz, $this->leaf);
								}
							}
						}
					}
				}
			}
			for($zz = $z - $mid; $zz <= $z + $mid; $zz++){
				for($xx = $x - $mid; $xx <= $x + $mid; $xx += $mid * 2){
					if(!$this->isSolid($level, $xx, $yy, $zz)){
						if($random->nextInt(3) === 0){
							for($i = 0; $i < $random->nextInt(4); $i++){
								if(!$this->isSolid($level, $xx, $yy - $i, $zz)){
									$level->setBlockStateAt($xx, $yy - $i, $zz, $this->leaf);
								}
							}
						}
					}
				}
			}
		}

		for($xCanopy = $x - $mid + 1; $xCanopy <= $x + $mid - 1; $xCanopy++){
			for($zCanopy = $z - $mid + 1; $zCanopy <= $z + $mid - 1; $zCanopy++){
				if(!$this->isSolid($level, $xCanopy, $y + $treeHeight, $zCanopy)){
					$level->setBlockStateAt($xCanopy, $y + $treeHeight, $zCanopy, $this->leaf);
				}
			}
		}
	}

	private function placeTrunk(BlockManager $level, int $x, int $y, int $z, int $trunkHeight) : void{
		$level->setBlockStateAt($x, $y, $z, $this->trunk);
		for($yy = 0; $yy < $trunkHeight; ++$yy){
			if($this->checkY($y + $yy)){
				continue;
			}
			if(in_array($level->getBlockIdAt($x, $y + $yy, $z), self::OVERRIDABLE, true)){
				$level->setBlockStateAt($x, $y + $yy, $z, $this->trunk);
			}
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\object;

use dimension\generator\Populator;
use dimension\generator\random\Xoroshiro128;

/**
 * A populator owning the Xoroshiro128 random it reseeds for every chunk.
 */
abstract class SeededPopulator implements Populator{

	protected Xoroshiro128 $random;

	public function __construct(){
		$this->random = new Xoroshiro128(0);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether\object;

use dimension\generator\Blocks;
use pocketmine\world\ChunkManager;

/**
 * World read helpers used by the populators.
 */
final class WorldQuery{

	public const AIR = "minecraft:air";

	private function __construct(){
	}

	public static function blockId(ChunkManager $world, int $x, int $y, int $z) : string{
		if(!$world->isInWorld($x, $y, $z)){
			return self::AIR;
		}
		return Blocks::id($world->getBlockAt($x, $y, $z));
	}

	public static function biomeId(ChunkManager $world, int $x, int $y, int $z) : int{
		$chunk = $world->getChunk($x >> 4, $z >> 4);
		if($chunk === null){
			return -1;
		}
		return $chunk->getBiomeId($x & 0x0f, $y, $z & 0x0f);
	}

	/**
	 * Highest non-air block of a column;
	 * minY - 1 when the column is empty.
	 */
	public static function heightMap(ChunkManager $world, int $x, int $z) : int{
		$minY = $world->getMinY();
		for($y = $world->getMaxY() - 1; $y >= $minY; --$y){
			if(self::blockId($world, $x, $y, $z) !== self::AIR){
				return $y;
			}
		}
		return $minY - 1;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\nether;

use dimension\generator\biome\NetherBiomePicker;
use dimension\generator\BiomeFiller;
use dimension\generator\ChunkHash;
use dimension\generator\GeneratorOptions;
use dimension\generator\holder\NetherObjectHolder;
use dimension\generator\holder\NetherTerrainHolder;
use dimension\generator\math\Float32;
use dimension\generator\object\BlockManager;
use dimension\generator\Populator;
use dimension\generator\random\LegacyRandom;
use pocketmine\block\VanillaBlocks;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;
use pocketmine\world\generator\Generator;
use function intdiv;

/**
 * Nether terrain between Y 0 and 127: bedrock floor and roof, netherrack
 * shaped by a 3D density, a lava sea up to Y 31 and biome dependent
 * surfaces. Nothing is generated outside that range.
 */
final class NetherGenerator extends Generator{

	public const LAVA_LEVEL = 31;
	private const SEA_LEVEL = 63;
	private const MIN_DENSITY_Y = 1;
	private const MAX_DENSITY_Y = 126;

	private NetherBiomePicker $biomePicker;
	private NetherObjectHolder $objectHolder;
	private LegacyRandom $chunkRandom;
	/** @var list<Populator>|null */
	private ?array $populators = null;

	private int $air;
	private int $bedrock;
	private int $netherrack;
	private int $basalt;
	private int $blackstone;
	private int $gravel;
	private int $soulSand;
	private int $soulSoil;
	private int $warpedWart;
	private int $warpedNylium;
	private int $netherWartBlock;
	private int $crimsonNylium;
	private int $lava;

	private float $patchThreshold;
	private float $crimsonStateThreshold;
	private float $wartThreshold;

	public function __construct(int $seed, string $preset){
		parent::__construct($seed, $preset);
		$this->biomePicker = new NetherBiomePicker(new LegacyRandom($seed));
		$this->objectHolder = new NetherObjectHolder(new LegacyRandom($seed));
		$this->chunkRandom = new LegacyRandom(0);

		$this->air = VanillaBlocks::AIR()->getStateId();
		$this->bedrock = VanillaBlocks::BEDROCK()->getStateId();
		$this->netherrack = VanillaBlocks::NETHERRACK()->getStateId();
		$this->basalt = VanillaBlocks::BASALT()->getStateId();
		$this->blackstone = VanillaBlocks::BLACKSTONE()->getStateId();
		$this->gravel = VanillaBlocks::GRAVEL()->getStateId();
		$this->soulSand = VanillaBlocks::SOUL_SAND()->getStateId();
		$this->soulSoil = VanillaBlocks::SOUL_SOIL()->getStateId();
		$this->warpedWart = VanillaBlocks::WARPED_WART_BLOCK()->getStateId();
		$this->warpedNylium = VanillaBlocks::WARPED_NYLIUM()->getStateId();
		$this->netherWartBlock = VanillaBlocks::NETHER_WART_BLOCK()->getStateId();
		$this->crimsonNylium = VanillaBlocks::CRIMSON_NYLIUM()->getStateId();
		$this->lava = VanillaBlocks::LAVA()->getStillForm()->getStateId();

		$this->patchThreshold = Float32::of(-0.012);
		$this->crimsonStateThreshold = Float32::of(0.54);
		$this->wartThreshold = Float32::of(1.17);
	}

	public function getObjectHolder() : NetherObjectHolder{
		return $this->objectHolder;
	}

	public function generateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void{
		$chunk = $world->getChunk($chunkX, $chunkZ) ?? throw new \InvalidArgumentException("Chunk $chunkX $chunkZ does not yet exist");
		$biomes = $this->generateBiomes($chunk, $chunkX, $chunkZ);
		$this->generateTerrain($chunk, $chunkX, $chunkZ, $biomes);
	}

	public function populateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void{
		$this->populators ??= NetherPopulators::create($this->seed, GeneratorOptions::resourceFolder($this->preset));
		$root = new BlockManager($world);
		foreach($this->populators as $populator){
			try{
				$populator->populate($root, $world, $chunkX, $chunkZ, $this->seed);
			}catch(\Throwable){
			}
		}
		$root->apply();
	}

	/**
	 * @return int[] biome id per column, indexed x * 16 + z
	 */
	private function generateBiomes(Chunk $chunk, int $chunkX, int $chunkZ) : array{
		$biomes = [];
		for($x = 0; $x < 16; $x++){
			$worldX = $chunkX * 16 + $x;
			for($z = 0; $z < 16; $z++){
				$biomes[$x * 16 + $z] = $this->biomePicker->pick($worldX, self::SEA_LEVEL, $chunkZ * 16 + $z)->getBiomeId();
			}
		}
		BiomeFiller::fillColumns($chunk, $biomes);
		return $biomes;
	}

	/**
	 * @param int[] $biomes biome id per column, indexed x * 16 + z
	 */
	private function generateTerrain(Chunk $chunk, int $chunkX, int $chunkZ, array $biomes) : void{
		$baseX = $chunkX << 4;
		$baseZ = $chunkZ << 4;
		$random = $this->chunkRandom;
		$random->setSeed($this->seed ^ ChunkHash::hash($chunkX, $chunkZ));
		$noises = $this->objectHolder->getTerrainHolder();
		$densityFunction = $noises->getDensityFunction();
		$cellMinY = intdiv(self::MIN_DENSITY_Y, 8) * 8;
		$cellMaxY = intdiv(self::MAX_DENSITY_Y, 8) * 8;

		$columns = [];
		for($i = 0; $i < 256; $i++){
			$columns[$i] = [];
		}
		for($cellX = 0; $cellX < 16; $cellX += 4){
			for($cellZ = 0; $cellZ < 16; $cellZ += 4){
				for($cellY = $cellMaxY; $cellY >= $cellMinY; $cellY -= 8){
					for($localX = 0; $localX < 4; $localX++){
						$x = $cellX + $localX;
						for($localZ = 0; $localZ < 4; $localZ++){
							$z = $cellZ + $localZ;
							$column = &$columns[$x * 16 + $z];
							for($localY = 7; $localY >= 0; $localY--){
								$y = $cellY + $localY;
								if($y < self::MIN_DENSITY_Y || $y > self::MAX_DENSITY_Y){
									continue;
								}
								if($densityFunction->compute($baseX + $x, $y, $baseZ + $z) > 0){
									$column[$y] = $this->netherrack;
								}elseif($y <= self::LAVA_LEVEL){
									$column[$y] = $this->lava;
								}else{
									$column[$y] = $this->air;
								}
							}
							unset($column);
						}
					}
				}
			}
		}

		for($x = 0; $x < 16; ++$x){
			for($z = 0; $z < 16; ++$z){
				$column = $columns[$x * 16 + $z];
				$column[0] = $this->bedrock;
				$column[126] = $this->netherrack;
				$column[127] = $this->bedrock;
				$this->decorateSurface($column, $biomes[$x * 16 + $z], $baseX + $x, $baseZ + $z, $noises);

				for($i = 0; $i < $random->nextBoundedInt(6); $i++){
					if($column[126 - $i] === $this->netherrack && $column[125 - $i] !== $this->air){
						$column[126 - $i] = $this->bedrock;
					}
				}

				for($y = 0; $y < 128; $y++){
					if($column[$y] !== $this->air){
						$chunk->setBlockStateId($x, $y, $z, $column[$y]);
					}
				}
			}
		}
	}

	/**
	 * @param int[] $column block state ids indexed by Y (0-127)
	 */
	private function decorateSurface(array &$column, int $biomeId, int $nx, int $nz, NetherTerrainHolder $noises) : void{
		for($y = 1; $y < 127; ++$y){
			switch($biomeId){
				case BiomeIds::BASALT_DELTAS:
					if($column[$y] === $this->netherrack){
						if($this->isCeil($column, $y)){
							$column[$y] = $this->basalt;
							break;
						}elseif($column[$y + 1] === $this->air
							&& ($noises->getNetherStateNoise()->getValue($nx, $y, $nz) >= 0
								|| ($y <= 35 && $y >= 30 && $noises->getPatchNoise()->getValue($nx, $y, $nz) >= $this->patchThreshold))){
							$column[$y] = $this->gravel;
							break;
						}
						if($this->isTop($column, $y) || $this->isCeil($column, $y)){
							$column[$y] = $this->blackstone;
						}
					}
					break;
				case BiomeIds::SOULSAND_VALLEY:
					if($column[$y] === $this->netherrack){
						if($this->isCeil($column, $y)){
							$column[$y] = $noises->getNetherStateNoise()->getValue($nx, $y, $nz) >= 0 ? $this->soulSand : $this->soulSoil;
						}elseif($this->isTop($column, $y)){
							if($noises->getNetherStateNoise()->getValue($nx, $y, $nz) >= 0 || ($y <= 35 && $y >= 30 && $noises->getPatchNoise()->getValue($nx, $y, $nz) >= $this->patchThreshold)){
								$column[$y] = $this->soulSand;
							}else{
								$column[$y] = $this->soulSoil;
							}
						}
					}
					break;
				case BiomeIds::WARPED_FOREST:
					if($column[$y] === $this->netherrack
						&& $column[$y + 1] === $this->air
						&& $y > 31
						&& $noises->getNetherStateNoise()->getValue($nx, $y, $nz) <= 0){
						$column[$y] = $noises->getNetherwartNoise()->getValue($nx, $y, $nz) >= $this->wartThreshold ? $this->warpedWart : $this->warpedNylium;
					}
					break;
				case BiomeIds::CRIMSON_FOREST:
					if($column[$y] === $this->netherrack
						&& $column[$y + 1] === $this->air
						&& $y > 31
						&& $noises->getNetherStateNoise()->getValue($nx, $y, $nz) <= $this->crimsonStateThreshold){
						$column[$y] = $noises->getNetherwartNoise()->getValue($nx, $y, $nz) >= $this->wartThreshold ? $this->netherWartBlock : $this->crimsonNylium;
					}
					break;
				case BiomeIds::HELL:
					if($column[$y] === $this->netherrack && $this->isTop($column, $y)){
						if($y > 31 && $y < 35 && $noises->getSoulsandNoise()->getValue($nx, $y, $nz) >= $this->patchThreshold){
							$column[$y] = $this->gravel;
							break;
						}
						if($y <= 35 && $y >= 30 && $noises->getSoulsandNoise()->getValue($nx, $y, $nz) >= $this->patchThreshold){
							$column[$y] = $this->soulSand;
						}
					}
					break;
			}
		}
	}

	/**
	 * Whether air lies within the 4 blocks above $y (or at $y).
	 *
	 * @param int[] $column
	 */
	private function isTop(array $column, int $y) : bool{
		for($i = 0; $i < 5; $i++){
			$yy = $y + $i;
			if($yy < 1 || $yy > 127){
				continue;
			}
			if($column[$yy] === $this->air){
				return true;
			}
		}
		return false;
	}

	/**
	 * Whether air lies within the 4 blocks below $y (or at $y).
	 *
	 * @param int[] $column
	 */
	private function isCeil(array $column, int $y) : bool{
		for($i = 0; $i < 5; $i++){
			$yy = $y - $i;
			if($yy < 1 || $yy > 127){
				continue;
			}
			if($column[$yy] === $this->air){
				return true;
			}
		}
		return false;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\density;

/**
 * Final terrain density of the nether: the base noise faded toward solid
 * near the floor (Y -8 to 24) and the roof (Y 104 to 128), interpolated per
 * cell, scaled and squeezed.
 */
final class NetherDensity{

	private function __construct(){
	}

	public static function finalDensity(DensityFunction $base3dNoise) : DensityFunction{
		$density = Densities::add(Densities::constant(-0.9375), $base3dNoise);
		$density = Densities::mul(Densities::yClampedGradient(104, 128, 1.0, 0.0), $density);
		$density = Densities::add(Densities::constant(0.9375), $density);
		$density = Densities::add(Densities::constant(-2.5), $density);
		$density = Densities::mul(Densities::yClampedGradient(-8, 24, 0.0, 1.0), $density);
		$density = Densities::add(Densities::constant(2.5), $density);
		$density = Densities::blendDensity($density);
		$density = Densities::interpolated($density);
		$density = Densities::mul(Densities::constant(0.64), $density);
		return Densities::map($density, MappedDensity::SQUEEZE);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\density;

/**
 * A scalar field over block coordinates. Positive terrain density means
 * solid.
 */
interface DensityFunction{

	public function compute(int $x, int $y, int $z) : float;

	/**
	 * Lower bound of compute().
	 */
	public function minValue() : float;

	/**
	 * Upper bound of compute().
	 */
	public function maxValue() : float;
}
<?php

declare(strict_types=1);

namespace dimension\generator\density;

use function count;

/**
 * Samples another function at the corners of 4x8x4 block cells and
 * trilinearly interpolates inside each cell. The last filled cell and the
 * recently sampled corners are kept, so each corner is evaluated once.
 */
final class InterpolatedDensity implements DensityFunction{

	private const CELL_SIZE_XZ = 4;
	private const CELL_SIZE_Y = 8;
	private const MAX_CACHED_CORNERS = 4096;

	/** @var float[] */
	private array $values = [];
	/** @var array<string, float> */
	private array $corners = [];
	private ?int $lastX = null;
	private int $lastY = 0;
	private int $lastZ = 0;

	public function __construct(
		private DensityFunction $wrapped
	){}

	public function compute(int $x, int $y, int $z) : float{
		$cellX = ($x >> 2) << 2;
		$cellY = ($y >> 3) << 3;
		$cellZ = ($z >> 2) << 2;
		if($this->lastX !== $cellX || $this->lastY !== $cellY || $this->lastZ !== $cellZ){
			$this->fillCell($cellX, $cellY, $cellZ);
			$this->lastX = $cellX;
			$this->lastY = $cellY;
			$this->lastZ = $cellZ;
		}
		return $this->values[(($y & 7) << 4) | (($z & 3) << 2) | ($x & 3)];
	}

	public function minValue() : float{
		return $this->wrapped->minValue();
	}

	public function maxValue() : float{
		return $this->wrapped->maxValue();
	}

	private function fillCell(int $cellX, int $cellY, int $cellZ) : void{
		if(count($this->corners) > self::MAX_CACHED_CORNERS){
			$this->corners = [];
		}
		$nextX = $cellX + self::CELL_SIZE_XZ;
		$nextY = $cellY + self::CELL_SIZE_Y;
		$nextZ = $cellZ + self::CELL_SIZE_XZ;
		$d000 = $this->corner($cellX, $cellY, $cellZ);
		$d100 = $this->corner($nextX, $cellY, $cellZ);
		$d010 = $this->corner($cellX, $nextY, $cellZ);
		$d110 = $this->corner($nextX, $nextY, $cellZ);
		$d001 = $this->corner($cellX, $cellY, $nextZ);
		$d101 = $this->corner($nextX, $cellY, $nextZ);
		$d011 = $this->corner($cellX, $nextY, $nextZ);
		$d111 = $this->corner($nextX, $nextY, $nextZ);

		$c000 = $d000;
		$c100 = $d100 - $d000;
		$c010 = $d010 - $d000;
		$c001 = $d001 - $d000;
		$c110 = $d110 - $d100 - $d010 + $d000;
		$c101 = $d101 - $d100 - $d001 + $d000;
		$c011 = $d011 - $d010 - $d001 + $d000;
		$c111 = $d111 - $d110 - $d101 - $d011 + $d100 + $d010 + $d001 - $d000;

		$index = 0;
		$values = [];
		for($localY = 0; $localY < self::CELL_SIZE_Y; $localY++){
			$yAlpha = $localY * (1.0 / self::CELL_SIZE_Y);
			for($localZ = 0; $localZ < self::CELL_SIZE_XZ; $localZ++){
				$zAlpha = $localZ * (1.0 / self::CELL_SIZE_XZ);
				$yz = $yAlpha * $zAlpha;
				$zTerm = $zAlpha * ($c001 + $yAlpha * $c011);
				for($localX = 0; $localX < self::CELL_SIZE_XZ; $localX++){
					$xAlpha = $localX * (1.0 / self::CELL_SIZE_XZ);
					$values[$index++] = $c000
						+ $xAlpha * ($c100 + $yAlpha * $c110 + $zAlpha * $c101 + $yz * $c111)
						+ $yAlpha * $c010
						+ $zTerm;
				}
			}
		}
		$this->values = $values;
	}

	private function corner(int $x, int $y, int $z) : float{
		return $this->corners[$x . ":" . $y . ":" . $z] ??= $this->wrapped->compute($x, $y, $z);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\density;

/**
 * Factories of density function combinations.
 */
final class Densities{

	private function __construct(){
	}

	public static function constant(float $value) : DensityFunction{
		return new ConstantDensity($value);
	}

	public static function add(DensityFunction $f1, DensityFunction $f2) : DensityFunction{
		return TwoArgumentDensity::create(TwoArgumentDensity::ADD, $f1, $f2);
	}

	public static function mul(DensityFunction $f1, DensityFunction $f2) : DensityFunction{
		return TwoArgumentDensity::create(TwoArgumentDensity::MUL, $f1, $f2);
	}

	public static function min(DensityFunction $f1, DensityFunction $f2) : DensityFunction{
		return TwoArgumentDensity::create(TwoArgumentDensity::MIN, $f1, $f2);
	}

	public static function max(DensityFunction $f1, DensityFunction $f2) : DensityFunction{
		return TwoArgumentDensity::create(TwoArgumentDensity::MAX, $f1, $f2);
	}

	public static function yClampedGradient(int $fromY, int $toY, float $fromValue, float $toValue) : DensityFunction{
		return new YClampedGradient($fromY, $toY, $fromValue, $toValue);
	}

	/**
	 * No chunk blending happens here: the input is returned as is.
	 */
	public static function blendDensity(DensityFunction $input) : DensityFunction{
		return $input;
	}

	public static function interpolated(DensityFunction $input) : DensityFunction{
		return new InterpolatedDensity($input);
	}

	/**
	 * @param int $type one of the MappedDensity constants
	 */
	public static function map(DensityFunction $input, int $type) : DensityFunction{
		return MappedDensity::create($type, $input);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\density;

use dimension\generator\math\GenerationMath;
use function abs;
use function max;
use function min;
use const INF;

/**
 * A unary transform of another function.
 */
final class MappedDensity implements DensityFunction{

	public const ABS = 0;
	public const SQUARE = 1;
	public const CUBE = 2;
	public const HALF_NEGATIVE = 3;
	public const QUARTER_NEGATIVE = 4;
	public const INVERT = 5;
	/** clamp to [-1, 1] then x / 2 - x^3 / 24 */
	public const SQUEEZE = 6;

	private function __construct(
		private int $type,
		private DensityFunction $input,
		private float $minValue,
		private float $maxValue
	){}

	public static function create(int $type, DensityFunction $input) : MappedDensity{
		$min = $input->minValue();
		$max = $input->maxValue();
		$transformedMin = self::apply($type, $min);
		$transformedMax = self::apply($type, $max);
		if($type === self::INVERT){
			if($min < 0.0 && $max > 0.0){
				return new self($type, $input, -INF, INF);
			}
			return new self($type, $input, min($transformedMin, $transformedMax), max($transformedMin, $transformedMax));
		}
		if($type === self::ABS || $type === self::SQUARE){
			return new self($type, $input, max(0.0, min($transformedMin, $transformedMax)), max($transformedMin, $transformedMax));
		}
		return new self($type, $input, min($transformedMin, $transformedMax), max($transformedMin, $transformedMax));
	}

	public static function apply(int $type, float $input) : float{
		switch($type){
			case self::ABS:
				return abs($input);
			case self::SQUARE:
				return $input * $input;
			case self::CUBE:
				return $input * $input * $input;
			case self::HALF_NEGATIVE:
				return $input > 0.0 ? $input : $input * 0.5;
			case self::QUARTER_NEGATIVE:
				return $input > 0.0 ? $input : $input * 0.25;
			case self::INVERT:
				return $input == 0.0 ? INF : 1.0 / $input;
			default:
				$clamped = GenerationMath::clamp($input, -1.0, 1.0);
				return $clamped / 2.0 - $clamped * $clamped * $clamped / 24.0;
		}
	}

	public function compute(int $x, int $y, int $z) : float{
		return self::apply($this->type, $this->input->compute($x, $y, $z));
	}

	public function minValue() : float{
		return $this->minValue;
	}

	public function maxValue() : float{
		return $this->maxValue;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\density;

use function max;
use function min;

/**
 * Sum, product, minimum or maximum of two functions. The second argument is
 * skipped when the first one already decides the result.
 */
final class TwoArgumentDensity implements DensityFunction{

	public const ADD = 0;
	public const MUL = 1;
	public const MIN = 2;
	public const MAX = 3;

	private function __construct(
		private int $type,
		private DensityFunction $argument1,
		private DensityFunction $argument2,
		private float $minValue,
		private float $maxValue
	){}

	/**
	 * Builds the combination, folding a constant argument of a sum or a
	 * product into a MulOrAdd.
	 */
	public static function create(int $type, DensityFunction $argument1, DensityFunction $argument2) : DensityFunction{
		$min1 = $argument1->minValue();
		$min2 = $argument2->minValue();
		$max1 = $argument1->maxValue();
		$max2 = $argument2->maxValue();

		$minValue = match($type){
			self::ADD => $min1 + $min2,
			self::MUL => $min1 > 0.0 && $min2 > 0.0
				? $min1 * $min2
				: ($max1 < 0.0 && $max2 < 0.0 ? $max1 * $max2 : min($min1 * $max2, $max1 * $min2)),
			self::MIN => min($min1, $min2),
			default => max($min1, $min2)
		};
		$maxValue = match($type){
			self::ADD => $max1 + $max2,
			self::MUL => $min1 > 0.0 && $min2 > 0.0
				? $max1 * $max2
				: ($max1 < 0.0 && $max2 < 0.0 ? $min1 * $min2 : max($min1 * $min2, $max1 * $max2)),
			self::MIN => min($max1, $max2),
			default => max($max1, $max2)
		};

		if(($type === self::ADD || $type === self::MUL) && $argument1 instanceof ConstantDensity){
			return new MulOrAdd($type === self::ADD ? MulOrAdd::ADD : MulOrAdd::MUL, $argument2, $minValue, $maxValue, $argument1->value);
		}
		if(($type === self::ADD || $type === self::MUL) && $argument2 instanceof ConstantDensity){
			return new MulOrAdd($type === self::ADD ? MulOrAdd::ADD : MulOrAdd::MUL, $argument1, $minValue, $maxValue, $argument2->value);
		}
		return new self($type, $argument1, $argument2, $minValue, $maxValue);
	}

	public function compute(int $x, int $y, int $z) : float{
		$v1 = $this->argument1->compute($x, $y, $z);
		return match($this->type){
			self::ADD => $v1 + $this->argument2->compute($x, $y, $z),
			self::MUL => $v1 == 0.0 ? 0.0 : $v1 * $this->argument2->compute($x, $y, $z),
			self::MIN => $v1 < $this->argument2->minValue() ? $v1 : min($v1, $this->argument2->compute($x, $y, $z)),
			default => $v1 > $this->argument2->maxValue() ? $v1 : max($v1, $this->argument2->compute($x, $y, $z))
		};
	}

	public function minValue() : float{
		return $this->minValue;
	}

	public function maxValue() : float{
		return $this->maxValue;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\density;

use dimension\generator\math\GenerationMath;
use function max;
use function min;

/**
 * Linear gradient from $fromValue at $fromY to $toValue at $toY, constant
 * outside that range.
 */
final class YClampedGradient implements DensityFunction{

	public function __construct(
		private int $fromY,
		private int $toY,
		private float $fromValue,
		private float $toValue
	){}

	public function compute(int $x, int $y, int $z) : float{
		if($this->fromY === $this->toY){
			return $y < $this->fromY ? $this->fromValue : $this->toValue;
		}
		$t = GenerationMath::clamp(($y - $this->fromY) / ($this->toY - $this->fromY), 0.0, 1.0);
		return $this->fromValue + $t * ($this->toValue - $this->fromValue);
	}

	public function minValue() : float{
		return min($this->fromValue, $this->toValue);
	}

	public function maxValue() : float{
		return max($this->fromValue, $this->toValue);
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\density;

use dimension\generator\math\GenerationMath;
use dimension\generator\noise\OctavePerlinNoiseSampler;
use dimension\generator\random\RandomSource;

/**
 * Base 3D terrain noise: a main noise selects, per block, a blend between a
 * lower and an upper limit noise, all of them smeared vertically.
 */
final class BlendedNoise implements DensityFunction{

	private const MAIN_NOISE_DIVISOR = 10.0;
	private const LIMIT_NOISE_DIVISOR = 512.0;
	private const RESULT_DIVISOR = 128.0;
	private const BASE_SCALE = 684.412;

	private function __construct(
		private OctavePerlinNoiseSampler $minLimitNoise,
		private OctavePerlinNoiseSampler $maxLimitNoise,
		private OctavePerlinNoiseSampler $mainNoise,
		private float $xzScale,
		private float $yScale,
		private float $xzFactor,
		private float $yFactor,
		private float $smearScaleMultiplier
	){}

	/**
	 * Draws the lower limit, upper limit and main noises from three forks of $random, in that order.
	 */
	public static function create(RandomSource $random, float $xzScale, float $yScale, float $xzFactor, float $yFactor, float $smearScaleMultiplier) : BlendedNoise{
		$limitOctaves = self::descendingOctaves(-15, 0);
		$mainOctaves = self::descendingOctaves(-7, 0);
		return new BlendedNoise(
			new OctavePerlinNoiseSampler($random->fork(), $limitOctaves),
			new OctavePerlinNoiseSampler($random->fork(), $limitOctaves),
			new OctavePerlinNoiseSampler($random->fork(), $mainOctaves),
			$xzScale,
			$yScale,
			$xzFactor,
			$yFactor,
			$smearScaleMultiplier
		);
	}

	public static function overworld(RandomSource $random) : BlendedNoise{
		return self::create($random, 0.25, 0.125, 80.0, 160.0, 8.0);
	}

	public static function nether(RandomSource $random) : BlendedNoise{
		return self::create($random, 0.25, 0.375, 80.0, 60.0, 8.0);
	}

	/**
	 * @return int[]
	 */
	private static function descendingOctaves(int $minInclusive, int $maxInclusive) : array{
		$octaves = [];
		for($i = $maxInclusive; $i >= $minInclusive; $i--){
			$octaves[] = $i;
		}
		return $octaves;
	}

	public function compute(int $x, int $y, int $z) : float{
		$scaledXZ = self::BASE_SCALE * $this->xzScale;
		$scaledY = self::BASE_SCALE * $this->yScale;
		$mainScaledXZ = $scaledXZ / $this->xzFactor;
		$mainScaledY = $scaledY / $this->yFactor;
		$smearScale = $scaledY * $this->smearScaleMultiplier;

		$mainValue = 0.0;
		$frequency = 1.0;
		$count = $this->mainNoise->getCount();
		for($octave = 0; $octave < $count; $octave++){
			$sampler = $this->mainNoise->getOctave($octave);
			if($sampler !== null){
				$mainValue += $sampler->sample(
					$x * $mainScaledXZ * $frequency,
					$y * $mainScaledY * $frequency,
					$z * $mainScaledXZ * $frequency,
					$smearScale * $frequency,
					$y * $mainScaledY * $frequency
				) / $frequency;
			}
			$frequency /= 2.0;
		}

		$blend = GenerationMath::clamp($mainValue / self::MAIN_NOISE_DIVISOR + 1.0, 0.0, 2.0) * 0.5;
		$useOnlyMax = $blend >= 1.0;
		$useOnlyMin = $blend <= 0.0;

		$minValue = 0.0;
		$maxValue = 0.0;
		$frequency = 1.0;
		$octaveCount = $this->minLimitNoise->getCount() < $this->maxLimitNoise->getCount() ? $this->minLimitNoise->getCount() : $this->maxLimitNoise->getCount();
		for($octave = 0; $octave < $octaveCount; $octave++){
			$sampleX = $x * $scaledXZ * $frequency;
			$sampleY = $y * $scaledY * $frequency;
			$sampleZ = $z * $scaledXZ * $frequency;
			$sampleSmear = $smearScale * $frequency;
			if(!$useOnlyMax){
				$sampler = $this->minLimitNoise->getOctave($octave);
				if($sampler !== null){
					$minValue += $sampler->sample($sampleX, $sampleY, $sampleZ, $sampleSmear, $sampleY) / $frequency;
				}
			}
			if(!$useOnlyMin){
				$sampler = $this->maxLimitNoise->getOctave($octave);
				if($sampler !== null){
					$maxValue += $sampler->sample($sampleX, $sampleY, $sampleZ, $sampleSmear, $sampleY) / $frequency;
				}
			}
			$frequency /= 2.0;
		}

		$lower = $minValue / self::LIMIT_NOISE_DIVISOR;
		$upper = $maxValue / self::LIMIT_NOISE_DIVISOR;
		return ($lower + $blend * ($upper - $lower)) / self::RESULT_DIVISOR;
	}

	public function minValue() : float{
		return -1.5;
	}

	public function maxValue() : float{
		return 1.5;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\density;

/**
 * Adds a constant to, or multiplies by a constant, another function.
 */
final class MulOrAdd implements DensityFunction{

	public const ADD = 0;
	public const MUL = 1;

	public function __construct(
		private int $type,
		private DensityFunction $input,
		private float $minValue,
		private float $maxValue,
		private float $argument
	){}

	public function compute(int $x, int $y, int $z) : float{
		$value = $this->input->compute($x, $y, $z);
		return $this->type === self::MUL ? $value * $this->argument : $value + $this->argument;
	}

	public function minValue() : float{
		return $this->minValue;
	}

	public function maxValue() : float{
		return $this->maxValue;
	}
}
<?php

declare(strict_types=1);

namespace dimension\generator\density;

final class ConstantDensity implements DensityFunction{

	public function __construct(
		public readonly float $value
	){}

	public function compute(int $x, int $y, int $z) : float{
		return $this->value;
	}

	public function minValue() : float{
		return $this->value;
	}

	public function maxValue() : float{
		return $this->value;
	}
}
<?php

declare(strict_types=1);

namespace dimension;

use pocketmine\network\mcpe\protocol\types\DimensionIds;
use function is_array;
use function is_bool;
use function is_string;

/**
 * The "dimensions" section of config.yml.
 */
final class DimensionSettings{

	public function __construct(
		public readonly bool $enabled,
		public readonly bool $netherEnabled,
		public readonly string $netherWorld,
		public readonly bool $endEnabled,
		public readonly string $endWorld
	){}

	/**
	 * @param array<mixed> $section
	 */
	public static function fromConfig(array $section) : self{
		$nether = is_array($section["nether"] ?? null) ? $section["nether"] : [];
		$end = is_array($section["end"] ?? null) ? $section["end"] : [];
		return new self(
			self::flag($section, "enabled"),
			self::flag($nether, "enabled"),
			self::name($nether, "world_nether"),
			self::flag($end, "enabled"),
			self::name($end, "world_the_end")
		);
	}

	/**
	 * @return array<int, string> dimension id => world folder name
	 */
	public function getWorldNames() : array{
		$names = [];
		if($this->netherEnabled){
			$names[DimensionIds::NETHER] = $this->netherWorld;
		}
		if($this->endEnabled){
			$names[DimensionIds::THE_END] = $this->endWorld;
		}
		return $names;
	}

	/**
	 * @param array<mixed> $section
	 */
	private static function flag(array $section, string $key) : bool{
		$value = $section[$key] ?? null;
		return is_bool($value) ? $value : true;
	}

	/**
	 * @param array<mixed> $section
	 */
	private static function name(array $section, string $default) : string{
		$value = $section["world"] ?? null;
		return is_string($value) && $value !== "" ? $value : $default;
	}
}
<?php

declare(strict_types=1);

namespace dimension;

use dimension\generator\end\populator\ObsidianPillarPopulator;
use pocketmine\entity\Location;
use pocketmine\entity\object\EndCrystal;
use pocketmine\event\Listener;
use pocketmine\event\world\ChunkPopulateEvent;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\utils\Random;

/**
 * Places the end crystal on top of each obsidian spike. Spikes are built on
 * the generation threads, which cannot create entities, so the crystal of a
 * spike is spawned on the main thread once the chunk that builds it has been
 * populated.
 */
final class EndCrystalSpawner implements Listener{

	private ?int $seed = null;

	/** @var list<array{chunkX: int, chunkZ: int, position: \pocketmine\math\Vector3}> */
	private array $spawns = [];

	public function __construct(
		private Dimensions $dimensions
	){}

	/**
	 * @priority MONITOR
	 */
	public function onChunkPopulate(ChunkPopulateEvent $event) : void{
		$world = $event->getWorld();
		if($this->dimensions->getDimension($world) !== DimensionIds::THE_END){
			return;
		}
		if($this->seed !== $world->getSeed()){
			$this->seed = $world->getSeed();
			$this->spawns = ObsidianPillarPopulator::crystalSpawns($this->seed);
		}
		foreach($this->spawns as $spawn){
			if($spawn["chunkX"] !== $event->getChunkX() || $spawn["chunkZ"] !== $event->getChunkZ()){
				continue;
			}
			$yaw = (new Random($this->seed ^ ($spawn["chunkX"] * 31 + $spawn["chunkZ"])))->nextFloat() * 360;
			$crystal = new EndCrystal(Location::fromObject($spawn["position"], $world, $yaw, 0.0));
			$crystal->setShowBase(true);
			$crystal->spawnToAll();
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\network;

use dimension\Dimensions;
use pocketmine\block\RespawnAnchor;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerPostChunkSendEvent;
use pocketmine\event\player\PlayerRespawnEvent;
use pocketmine\event\server\DataPacketReceiveEvent;
use pocketmine\event\server\DataPacketSendEvent;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\AddPlayerPacket;
use pocketmine\network\mcpe\protocol\ChangeDimensionPacket;
use pocketmine\network\mcpe\protocol\PlayerActionPacket;
use pocketmine\network\mcpe\protocol\SetActorDataPacket;
use pocketmine\network\mcpe\protocol\SetSpawnPositionPacket;
use pocketmine\network\mcpe\protocol\SpawnParticleEffectPacket;
use pocketmine\network\mcpe\protocol\StartGamePacket;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties;
use pocketmine\network\mcpe\protocol\types\entity\IntMetadataProperty;
use pocketmine\network\mcpe\protocol\types\entity\MetadataProperty;
use pocketmine\network\mcpe\protocol\types\PlayerAction;
use pocketmine\network\mcpe\protocol\types\SpawnSettings;
use pocketmine\player\Player;
use pocketmine\plugin\PluginBase;
use pocketmine\world\format\Chunk;
use pocketmine\world\Position;
use WeakMap;
use function array_key_first;
use function count;

/**
 * Makes the client believe it is in the dimension of the world it stands in.
 *
 * - StartGamePacket carries the dimension of the join world.
 * - Every world change is detected on the first packet the session sends
 *   afterwards (the core always sends the new world's time right after the
 *   switch, before any chunk of the new world). When the dimension differs,
 *   a ChangeDimensionPacket is queued ahead of it, the chunk cache of the new
 *   world is set to that dimension, and once the chunk under the player has
 *   been sent the server acknowledges the change (PlayerAction
 *   DIMENSION_CHANGE_ACK) so the client leaves its loading screen.
 * - Chunks of nether/end worlds are serialized for their height and carry
 *   their dimension id (see ChunkCacheDimension).
 * - SetSpawnPosition, particle effects and the death dimension metadata get
 *   the right dimension; dying in the nether or the end without a respawn
 *   anchor respawns at the overworld spawn.
 *
 * There is no transfer API: portals and any other code just call
 * Player::teleport() with a Position in the target world, which works for
 * every path that moves a player across worlds (commands, respawn, plugins).
 */
final class DimensionNetworkListener implements Listener{

	/** @var WeakMap<NetworkSession, int> */
	private WeakMap $clientDimensions;
	/** @var WeakMap<NetworkSession, int> */
	private WeakMap $clientWorlds;
	/** @var WeakMap<NetworkSession, true> */
	private WeakMap $pendingAcks;

	public function __construct(
		private PluginBase $plugin,
		private Dimensions $dimensions
	){
		$this->clientDimensions = new WeakMap();
		$this->clientWorlds = new WeakMap();
		$this->pendingAcks = new WeakMap();
	}

	/**
	 * @priority HIGHEST
	 */
	public function onDataPacketSend(DataPacketSendEvent $event) : void{
		$targets = $event->getTargets();
		foreach($targets as $session){
			$this->syncWorld($session);
		}

		$player = count($targets) === 1 ? $targets[array_key_first($targets)]->getPlayer() : null;
		foreach($event->getPackets() as $packet){
			if($packet instanceof StartGamePacket){
				$this->onStartGame($targets, $packet);
			}elseif($packet instanceof SetSpawnPositionPacket && $player !== null){
				$this->rewriteSpawnPosition($player, $packet);
			}elseif($packet instanceof SpawnParticleEffectPacket){
				$this->rewriteParticle($targets, $packet);
			}elseif($packet instanceof SetActorDataPacket || $packet instanceof AddPlayerPacket){
				$packet->metadata = $this->rewriteDeathDimension($packet->actorRuntimeId, $packet->metadata);
			}
		}
	}

	/**
	 * @priority HIGHEST
	 */
	public function onDataPacketReceive(DataPacketReceiveEvent $event) : void{
		$packet = $event->getPacket();
		if($packet instanceof PlayerActionPacket && $packet->action === PlayerAction::DIMENSION_CHANGE_ACK){
			$event->cancel();
		}
	}

	/**
	 * @priority MONITOR
	 */
	public function onPostChunkSend(PlayerPostChunkSendEvent $event) : void{
		$player = $event->getPlayer();
		$session = $player->getNetworkSession();
		if(!isset($this->pendingAcks[$session])){
			return;
		}
		$position = $player->getPosition();
		if($event->getChunkX() !== ($position->getFloorX() >> Chunk::COORD_BIT_SIZE) || $event->getChunkZ() !== ($position->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
			return;
		}
		unset($this->pendingAcks[$session]);
		$blockPosition = BlockPosition::fromVector3($position);
		$session->sendDataPacket(PlayerActionPacket::create($player->getId(), PlayerAction::DIMENSION_CHANGE_ACK, $blockPosition, $blockPosition, 0));
	}

	/**
	 * @priority HIGH
	 */
	public function onRespawn(PlayerRespawnEvent $event) : void{
		$position = $event->getRespawnPosition();
		if(!$position->isValid()){
			return;
		}
		$dimension = $this->dimensions->getDimension($position->getWorld());
		if($dimension === DimensionIds::OVERWORLD){
			return;
		}
		if($dimension === DimensionIds::NETHER && $position->getWorld()->getBlock($position) instanceof RespawnAnchor){
			return;
		}
		$overworld = $this->plugin->getServer()->getWorldManager()->getDefaultWorld();
		if($overworld !== null){
			$event->setRespawnPosition($overworld->getSpawnLocation());
		}
	}

	/**
	 * @param NetworkSession[] $targets
	 */
	private function onStartGame(array $targets, StartGamePacket $packet) : void{
		foreach($targets as $session){
			$player = $session->getPlayer();
			if($player === null || !$player->getLocation()->isValid()){
				continue;
			}
			$world = $player->getWorld();
			$dimension = $this->dimensions->getDimension($world);
			$this->clientDimensions[$session] = $dimension;
			$this->clientWorlds[$session] = $world->getId();
			ChunkCacheDimension::apply($world, $session->getCompressor(), $dimension);

			$settings = $packet->levelSettings->spawnSettings;
			$packet->levelSettings->spawnSettings = new SpawnSettings($settings->getBiomeType(), $settings->getBiomeName(), $dimension);
		}
	}

	/**
	 * Detects that the session's player moved to another world since the
	 * last packet and switches the client's dimension when needed.
	 */
	private function syncWorld(NetworkSession $session) : void{
		if(!isset($this->clientWorlds[$session])){
			return;
		}
		$player = $session->getPlayer();
		if($player === null || !$player->getLocation()->isValid()){
			return;
		}
		$world = $player->getWorld();
		if($this->clientWorlds[$session] === $world->getId()){
			return;
		}
		$this->clientWorlds[$session] = $world->getId();
		$dimension = $this->dimensions->getDimension($world);
		ChunkCacheDimension::apply($world, $session->getCompressor(), $dimension);
		if($this->clientDimensions[$session] === $dimension){
			return;
		}
		$this->clientDimensions[$session] = $dimension;
		$this->pendingAcks[$session] = true;
		$session->sendDataPacket(ChangeDimensionPacket::create($dimension, $player->getPosition(), false, null));
	}

	private function rewriteSpawnPosition(Player $player, SetSpawnPositionPacket $packet) : void{
		if($packet->spawnType === SetSpawnPositionPacket::TYPE_WORLD_SPAWN){
			$packet->dimension = $this->dimensions->getDimension($player->getWorld());
			return;
		}
		$spawn = $player->getSpawn();
		if($spawn instanceof Position && $spawn->isValid()){
			$packet->dimension = $this->dimensions->getDimension($spawn->getWorld());
		}
	}

	/**
	 * @param NetworkSession[] $targets
	 */
	private function rewriteParticle(array $targets, SpawnParticleEffectPacket $packet) : void{
		foreach($targets as $session){
			$player = $session->getPlayer();
			if($player !== null && $player->getLocation()->isValid()){
				$packet->dimensionId = $this->dimensions->getDimension($player->getWorld());
				return;
			}
		}
	}

	/**
	 * @param MetadataProperty[] $metadata
	 * @phpstan-param array<int, MetadataProperty> $metadata
	 *
	 * @return MetadataProperty[]
	 * @phpstan-return array<int, MetadataProperty>
	 */
	private function rewriteDeathDimension(int $actorRuntimeId, array $metadata) : array{
		if(!isset($metadata[EntityMetadataProperties::PLAYER_DEATH_DIMENSION])){
			return $metadata;
		}
		foreach($this->plugin->getServer()->getOnlinePlayers() as $player){
			if($player->getId() !== $actorRuntimeId){
				continue;
			}
			$deathPosition = $player->getDeathPosition();
			if($deathPosition !== null && $deathPosition->isValid()){
				$metadata[EntityMetadataProperties::PLAYER_DEATH_DIMENSION] = new IntMetadataProperty($this->dimensions->getDimension($deathPosition->getWorld()));
			}
			break;
		}
		return $metadata;
	}
}
<?php

declare(strict_types=1);

namespace dimension\network;

use pocketmine\network\mcpe\cache\ChunkCache;
use pocketmine\network\mcpe\compression\Compressor;
use pocketmine\world\World;
use ReflectionProperty;

/**
 * Sets the dimension a world's chunk packet cache serializes for.
 *
 * The core already knows how to serialize nether and end chunks (subchunk
 * range and biome count per dimension, LevelChunkPacket dimension id), but
 * ChunkCache only takes the dimension through its private constructor and
 * always builds itself for the overworld. Chunks are sent as pre-compressed
 * batches that no event sees, so the private dimensionId of the cache is the
 * only place to choose the serialization. Reflection is kept to this class.
 */
final class ChunkCacheDimension{

	private static ?ReflectionProperty $dimensionProperty = null;
	private static ?ReflectionProperty $cachesProperty = null;

	private function __construct(){
	}

	public static function apply(World $world, Compressor $compressor, int $dimension) : void{
		$cache = ChunkCache::getInstance($world, $compressor);
		$dimensionProperty = self::$dimensionProperty ??= new ReflectionProperty(ChunkCache::class, "dimensionId");
		if($dimensionProperty->getValue($cache) === $dimension){
			return;
		}
		$dimensionProperty->setValue($cache, $dimension);
		$cachesProperty = self::$cachesProperty ??= new ReflectionProperty(ChunkCache::class, "caches");
		$cachesProperty->setValue($cache, []);
	}
}
<?php

declare(strict_types=1);

namespace dimension\portal;

use Closure;
use dimension\Dimensions;
use pocketmine\block\BlockTypeIds;
use pocketmine\block\NetherPortal;
use pocketmine\block\VanillaBlocks;
use pocketmine\entity\Entity;
use pocketmine\entity\Location;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\player\Player;
use pocketmine\Server;
use pocketmine\world\format\Chunk;
use pocketmine\world\Position;
use pocketmine\world\World;
use function count;
use function floor;
use function max;
use function min;

/**
 * Moves entities standing in portal blocks between dimensions.
 *
 * Nether portals send an entity after it spent the portal delay inside them
 * (a single tick in creative) and do not send it again until it has left
 * every portal block. End portals send an entity as soon as it enters them.
 */
final class PortalTravel{

	public const NETHER_PORTAL_DELAY = 80;
	public const NETHER_SCALE = 8;
	public const WORLD_LIMIT = 29999872;

	public const END_SPAWN_X = 100;
	public const END_SPAWN_Y = 49;
	public const END_SPAWN_Z = 0;

	/** @var array<int, int> */
	private array $netherPortalTicks = [];
	/** @var array<int, true> */
	private array $inEndPortal = [];
	/** @var array<int, true> */
	private array $pending = [];

	private int $endPortalTypeId;

	public function __construct(
		private Dimensions $dimensions
	){
		$this->endPortalTypeId = PortalContent::endPortal()->getTypeId();
	}

	public function tick(Server $server) : void{
		foreach($server->getWorldManager()->getWorlds() as $world){
			foreach($world->getEntities() as $entity){
				$this->tickEntity($entity);
			}
		}
	}

	public function forget(int $entityId) : void{
		unset($this->netherPortalTicks[$entityId], $this->inEndPortal[$entityId], $this->pending[$entityId]);
	}

	private function tickEntity(Entity $entity) : void{
		$id = $entity->getId();
		if(!$this->canTravel($entity)){
			$this->forget($id);
			return;
		}

		[$inNetherPortal, $inEndPortal] = $this->findPortals($entity);

		if($inEndPortal){
			if(!isset($this->inEndPortal[$id])){
				$this->inEndPortal[$id] = true;
				if(!isset($this->pending[$id]) && $entity->getVehicle() === null && count($entity->getPassengers()) === 0){
					$this->travelThroughEndPortal($entity);
				}
			}
		}else{
			unset($this->inEndPortal[$id]);
		}

		if($inNetherPortal){
			$ticks = $this->netherPortalTicks[$id] ?? 0;
			if($entity instanceof Player && $entity->isCreative() && $ticks < self::NETHER_PORTAL_DELAY){
				$ticks = self::NETHER_PORTAL_DELAY;
			}else{
				$ticks = min($ticks + 1, self::NETHER_PORTAL_DELAY + 1);
			}
			$this->netherPortalTicks[$id] = $ticks;
			if($ticks === self::NETHER_PORTAL_DELAY && !isset($this->pending[$id])){
				$this->travelThroughNetherPortal($entity);
			}
		}else{
			unset($this->netherPortalTicks[$id]);
		}
	}

	private function canTravel(Entity $entity) : bool{
		if($entity->isClosed() || $entity->isFlaggedForDespawn() || !$entity->isAlive()){
			return false;
		}
		if($entity instanceof Player){
			return $entity->isConnected() && $entity->spawned && !$entity->isSpectator();
		}
		return true;
	}

	/**
	 * @return bool[] whether the entity touches a nether portal and an end portal
	 * @phpstan-return array{bool, bool}
	 */
	private function findPortals(Entity $entity) : array{
		$world = $entity->getWorld();
		$box = $entity->getBoundingBox();
		$nether = false;
		$end = false;
		$minY = max((int) floor($box->minY + 0.001), $world->getMinY());
		$maxY = min((int) floor($box->maxY - 0.001), $world->getMaxY() - 1);
		for($x = (int) floor($box->minX + 0.001); $x <= (int) floor($box->maxX - 0.001); $x++){
			for($z = (int) floor($box->minZ + 0.001); $z <= (int) floor($box->maxZ - 0.001); $z++){
				for($y = $minY; $y <= $maxY; $y++){
					$block = $world->getBlockAt($x, $y, $z);
					if($block instanceof NetherPortal){
						$nether = true;
					}elseif($block->getTypeId() === $this->endPortalTypeId){
						$end = true;
					}
				}
			}
		}
		return [$nether, $end];
	}

	private function travelThroughNetherPortal(Entity $entity) : void{
		$from = $this->dimensions->getDimension($entity->getWorld());
		if($from === DimensionIds::OVERWORLD){
			$to = DimensionIds::NETHER;
		}elseif($from === DimensionIds::NETHER){
			$to = DimensionIds::OVERWORLD;
		}else{
			return;
		}
		if(!$this->dimensions->isEnabled($to)){
			return;
		}
		$target = $this->dimensions->getWorld($to);
		if($target === null || $target === $entity->getWorld()){
			return;
		}

		$position = $entity->getPosition();
		if($to === DimensionIds::NETHER){
			$x = (int) floor($position->x / self::NETHER_SCALE);
			$z = (int) floor($position->z / self::NETHER_SCALE);
		}else{
			$x = (int) floor($position->x) * self::NETHER_SCALE;
			$z = (int) floor($position->z) * self::NETHER_SCALE;
		}
		$x = max(-self::WORLD_LIMIT, min(self::WORLD_LIMIT, $x));
		$z = max(-self::WORLD_LIMIT, min(self::WORLD_LIMIT, $z));
		$this->populate($entity, $target, $x, $z, function() use($entity, $target, $to, $x, $z) : void{
			$y = NetherPortalLocator::findBaseY($target, $to, $x, $z);
			$portal = NetherPortalLocator::findNearest($target, $to, $x, $y, $z);
			if($portal !== null){
				$destination = $portal->add(0.5, 0, 0.5);
			}else{
				$destination = NetherPortalLocator::build($target, $x, $y, $z);
			}
			$this->netherPortalTicks[$entity->getId()] = self::NETHER_PORTAL_DELAY + 1;
			$entity->teleport(Position::fromObject($destination, $target));
		});
	}

	private function travelThroughEndPortal(Entity $entity) : void{
		$from = $this->dimensions->getDimension($entity->getWorld());
		if(!$this->dimensions->isEnabled(DimensionIds::THE_END)){
			return;
		}
		if($from === DimensionIds::OVERWORLD){
			$target = $this->dimensions->getWorld(DimensionIds::THE_END);
			if($target === null || $target === $entity->getWorld()){
				return;
			}
			$this->populate($entity, $target, self::END_SPAWN_X, self::END_SPAWN_Z, function() use($entity, $target) : void{
				$this->buildEndPlatform($target);
				$entity->teleport(new Location(self::END_SPAWN_X + 0.5, self::END_SPAWN_Y, self::END_SPAWN_Z + 0.5, $target, 90.0, 0.0));
			});
		}elseif($from === DimensionIds::THE_END){
			$overworld = $this->dimensions->getWorld(DimensionIds::OVERWORLD);
			if($overworld === null){
				return;
			}
			$spawn = $entity instanceof Player ? $entity->getSpawn() : $overworld->getSpawnLocation();
			if(!$spawn instanceof Position || !$spawn->isValid() || $this->dimensions->getDimension($spawn->getWorld()) !== DimensionIds::OVERWORLD){
				$spawn = $overworld->getSpawnLocation();
			}
			$spawnWorld = $spawn->getWorld();
			$this->populate($entity, $spawnWorld, $spawn->getFloorX(), $spawn->getFloorZ(), function() use($entity, $spawnWorld, $spawn) : void{
				$entity->teleport($spawnWorld->getSafeSpawn($spawn));
			});
		}
	}

	/**
	 * The obsidian platform entities arrive on in the end, with room to
	 * stand above it.
	 */
	private function buildEndPlatform(World $world) : void{
		$obsidian = VanillaBlocks::OBSIDIAN();
		$air = VanillaBlocks::AIR();
		for($x = self::END_SPAWN_X - 2; $x <= self::END_SPAWN_X + 2; $x++){
			for($z = self::END_SPAWN_Z - 2; $z <= self::END_SPAWN_Z + 2; $z++){
				if($world->getBlockAt($x, self::END_SPAWN_Y - 1, $z)->getTypeId() !== BlockTypeIds::OBSIDIAN){
					$world->setBlockAt($x, self::END_SPAWN_Y - 1, $z, $obsidian);
				}
				for($y = self::END_SPAWN_Y; $y <= self::END_SPAWN_Y + 2; $y++){
					if($world->getBlockAt($x, $y, $z)->getTypeId() !== BlockTypeIds::AIR){
						$world->setBlockAt($x, $y, $z, $air);
					}
				}
			}
		}
	}

	/**
	 * Runs the callback once the chunk holding the destination column is
	 * generated and populated, if the entity can still travel by then.
	 */
	private function populate(Entity $entity, World $world, int $x, int $z, Closure $callback) : void{
		$id = $entity->getId();
		$this->pending[$id] = true;
		$world->orderChunkPopulation($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
			function() use($id, $entity, $world, $callback) : void{
				unset($this->pending[$id]);
				if(!$world->isLoaded() || !$this->canTravel($entity)){
					return;
				}
				$callback();
			},
			function() use($id) : void{
				unset($this->pending[$id]);
			}
		);
	}
}
<?php

declare(strict_types=1);

namespace dimension\portal;

use dimension\Dimensions;
use pocketmine\block\BlockTypeIds;
use pocketmine\block\Liquid;
use pocketmine\block\VanillaBlocks;
use pocketmine\math\Axis;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\world\format\Chunk;
use pocketmine\world\World;
use function abs;
use function in_array;
use function max;
use function min;

/**
 * Finds the nether portal an entity arrives in, or builds one with its
 * obsidian frame and platform when none is close enough.
 */
final class NetherPortalLocator{

	public const OVERWORLD_SEARCH_RADIUS = 128;
	public const NETHER_SEARCH_RADIUS = 16;
	public const NETHER_MAX_PORTAL_Y = 115;

	private function __construct(){
	}

	/**
	 * Returns the bottom block of the closest portal column: horizontal
	 * distance first, then the vertical distance to the given height.
	 */
	public static function findNearest(World $world, int $dimension, int $x, int $y, int $z) : ?Vector3{
		$radius = $dimension === DimensionIds::NETHER ? self::NETHER_SEARCH_RADIUS : self::OVERWORLD_SEARCH_RADIUS;
		$minY = max($world->getMinY(), Dimensions::getMinY($dimension));
		$maxY = min($world->getMaxY(), Dimensions::getMaxY($dimension));
		$ids = [
			VanillaBlocks::NETHER_PORTAL()->setAxis(Axis::X)->getStateId(),
			VanillaBlocks::NETHER_PORTAL()->setAxis(Axis::Z)->getStateId()
		];

		$best = null;
		$bestHorizontal = 0;
		$bestVertical = 0;
		for($chunkX = ($x - $radius) >> Chunk::COORD_BIT_SIZE; $chunkX <= ($x + $radius) >> Chunk::COORD_BIT_SIZE; $chunkX++){
			for($chunkZ = ($z - $radius) >> Chunk::COORD_BIT_SIZE; $chunkZ <= ($z + $radius) >> Chunk::COORD_BIT_SIZE; $chunkZ++){
				$chunk = $world->loadChunk($chunkX, $chunkZ);
				if($chunk === null){
					continue;
				}
				for($subY = $minY >> Chunk::COORD_BIT_SIZE; $subY <= ($maxY - 1) >> Chunk::COORD_BIT_SIZE; $subY++){
					$subChunk = $chunk->getSubChunk($subY);
					$layers = $subChunk->getBlockLayers();
					if(!isset($layers[0])){
						continue;
					}
					$found = false;
					foreach($layers[0]->getPalette() as $stateId){
						if(in_array($stateId, $ids, true)){
							$found = true;
							break;
						}
					}
					if(!$found){
						continue;
					}
					for($lx = 0; $lx < 16; $lx++){
						$wx = ($chunkX << Chunk::COORD_BIT_SIZE) + $lx;
						if(abs($wx - $x) > $radius){
							continue;
						}
						for($lz = 0; $lz < 16; $lz++){
							$wz = ($chunkZ << Chunk::COORD_BIT_SIZE) + $lz;
							if(abs($wz - $z) > $radius){
								continue;
							}
							for($ly = 0; $ly < 16; $ly++){
								$wy = ($subY << Chunk::COORD_BIT_SIZE) + $ly;
								if($wy < $minY || $wy >= $maxY || !in_array($subChunk->getBlockStateId($lx, $ly, $lz), $ids, true)){
									continue;
								}
								if($wy - 1 >= $world->getMinY() && in_array($chunk->getBlockStateId($lx, $wy - 1, $lz), $ids, true)){
									continue;
								}
								$horizontal = ($wx - $x) ** 2 + ($wz - $z) ** 2;
								$vertical = ($wy - $y) ** 2;
								if($best === null || $horizontal < $bestHorizontal || ($horizontal === $bestHorizontal && $vertical < $bestVertical)){
									$best = new Vector3($wx, $wy, $wz);
									$bestHorizontal = $horizontal;
									$bestVertical = $vertical;
								}
							}
						}
					}
				}
			}
		}
		return $best;
	}

	/**
	 * Picks the height of a new portal in a column: the highest spot with five
	 * free blocks above solid ground, below the roof in the nether.
	 */
	public static function findBaseY(World $world, int $dimension, int $x, int $z) : int{
		$minY = max($world->getMinY(), Dimensions::getMinY($dimension));
		$maxY = min($world->getMaxY(), Dimensions::getMaxY($dimension));
		$start = $world->getHighestBlockAt($x, $z) ?? $maxY - 6;
		if($dimension === DimensionIds::NETHER){
			$start = min($start, self::NETHER_MAX_PORTAL_Y);
		}
		$y = $start;
		for($i = $start; $i > $minY + 2; $i--){
			$ground = $world->getBlockAt($x, $i - 1, $z);
			if(!$ground->isSolid() || $ground instanceof Liquid || $ground->getTypeId() === BlockTypeIds::BEDROCK){
				continue;
			}
			$space = true;
			for($h = 0; $h < 5; $h++){
				if($world->getBlockAt($x, $i + $h, $z)->getTypeId() !== BlockTypeIds::AIR){
					$space = false;
					break;
				}
			}
			if($space){
				$y = $i;
				break;
			}
		}
		return max($minY + 1, min($maxY - 6, $y));
	}

	/**
	 * Builds a 2x3 portal along the X axis whose left portal column is at the
	 * given position, on a small obsidian platform, and returns where an
	 * entity stands inside it.
	 */
	public static function build(World $world, int $x, int $y, int $z) : Vector3{
		$air = VanillaBlocks::AIR();
		$obsidian = VanillaBlocks::OBSIDIAN();
		$portal = VanillaBlocks::NETHER_PORTAL()->setAxis(Axis::X);

		for($bx = $x - 2; $bx <= $x + 3; $bx++){
			for($bz = $z - 1; $bz <= $z + 1; $bz++){
				for($by = $y; $by <= $y + 4; $by++){
					if($world->getBlockAt($bx, $by, $bz)->getTypeId() !== BlockTypeIds::BEDROCK){
						$world->setBlockAt($bx, $by, $bz, $air);
					}
				}
			}
		}

		foreach([[0, -1], [1, -1], [-1, 0], [0, 0], [1, 0], [2, 0], [0, 1], [1, 1]] as [$ox, $oz]){
			$world->setBlockAt($x + $ox, $y, $z + $oz, $obsidian);
		}
		for($h = 1; $h <= 3; $h++){
			$world->setBlockAt($x - 1, $y + $h, $z, $obsidian);
			$world->setBlockAt($x + 2, $y + $h, $z, $obsidian);
		}
		for($ox = -1; $ox <= 2; $ox++){
			$world->setBlockAt($x + $ox, $y + 4, $z, $obsidian);
		}
		for($h = 1; $h <= 3; $h++){
			$world->setBlockAt($x, $y + $h, $z, $portal);
			$world->setBlockAt($x + 1, $y + $h, $z, $portal);
		}

		return new Vector3($x + 1, $y + 1, $z + 0.5);
	}
}
<?php

declare(strict_types=1);

namespace dimension\portal;

use pocketmine\block\EndPortalFrame;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\world\World;

/**
 * Fills a ring of twelve end portal frames with end portal blocks once every
 * frame holds an eye and faces the centre of the ring.
 */
final class EndPortalActivator{

	private function __construct(){
	}

	/**
	 * Tries every ring the given frame can belong to and activates the first
	 * complete one.
	 */
	public static function tryActivate(World $world, int $x, int $y, int $z, int $facing) : bool{
		[$fx, , $fz] = Facing::OFFSET[$facing];
		[$px, , $pz] = Facing::OFFSET[Facing::rotateY($facing, true)];
		for($k = -1; $k <= 1; $k++){
			$cx = $x + $fx * 2 + $px * $k;
			$cz = $z + $fz * 2 + $pz * $k;
			if(self::isComplete($world, $cx, $y, $cz)){
				self::fill($world, $cx, $y, $cz);
				return true;
			}
		}
		return false;
	}

	private static function isComplete(World $world, int $cx, int $y, int $cz) : bool{
		for($i = -1; $i <= 1; $i++){
			foreach([
				[$cx - 2, $cz + $i, Facing::EAST],
				[$cx + 2, $cz + $i, Facing::WEST],
				[$cx + $i, $cz - 2, Facing::SOUTH],
				[$cx + $i, $cz + 2, Facing::NORTH]
			] as [$bx, $bz, $expected]){
				$block = $world->getBlockAt($bx, $y, $bz);
				if(!$block instanceof EndPortalFrame || !$block->hasEye() || $block->getFacing() !== $expected){
					return false;
				}
			}
		}
		return true;
	}

	private static function fill(World $world, int $cx, int $y, int $cz) : void{
		for($dx = -1; $dx <= 1; $dx++){
			for($dz = -1; $dz <= 1; $dz++){
				$current = $world->getBlockAt($cx + $dx, $y, $cz + $dz);
				if($current->getTypeId() !== PortalContent::endPortal()->getTypeId() && !$current->canBeReplaced()){
					$world->useBreakOn(new Vector3($cx + $dx, $y, $cz + $dz));
				}
				$world->setBlockAt($cx + $dx, $y, $cz + $dz, PortalContent::endPortal());
			}
		}
		$world->addSound(new Vector3($cx + 0.5, $y + 0.5, $cz + 0.5), new EndPortalSpawnSound());
	}
}
<?php

declare(strict_types=1);

namespace dimension\portal;

use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
use pocketmine\world\sound\Sound;

final class EndPortalSpawnSound implements Sound{

	public function encode(Vector3 $pos) : array{
		return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BLOCK_END_PORTAL_SPAWN, $pos, false)];
	}
}
<?php

declare(strict_types=1);

namespace dimension\portal\block;

use pocketmine\block\Transparent;
use pocketmine\block\utils\SupportType;
use pocketmine\item\Item;

/**
 * The end portal block: an unbreakable, non-solid light source. Entities
 * entering it are moved between the overworld and the end by the portal
 * listener.
 */
class EndPortal extends Transparent{

	public function getLightLevel() : int{
		return 15;
	}

	public function isSolid() : bool{
		return false;
	}

	public function canBeReplaced() : bool{
		return false;
	}

	protected function recalculateCollisionBoxes() : array{
		return [];
	}

	public function getSupportType(int $facing) : SupportType{
		return SupportType::NONE;
	}

	public function getDrops(Item $item) : array{
		return [];
	}
}
<?php

declare(strict_types=1);

namespace dimension\portal;

use dimension\Dimensions;
use dimension\portal\item\EnderEye;
use pocketmine\block\BaseFire;
use pocketmine\block\EndPortalFrame;
use pocketmine\block\NetherPortal;
use pocketmine\block\VanillaBlocks;
use pocketmine\event\block\BlockUpdateEvent;
use pocketmine\event\entity\EntityDespawnEvent;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerInteractEvent;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\plugin\PluginBase;
use pocketmine\scheduler\ClosureTask;
use pocketmine\world\World;

/**
 * Nether and end portals: lighting nether portal frames with fire, keeping
 * lit portals standing only inside a complete frame, filling end portal
 * frames with eyes of ender and sending entities through portals.
 */
final class PortalListener implements Listener{

	private PortalTravel $travel;

	public function __construct(
		private PluginBase $plugin,
		private Dimensions $dimensions
	){
		$server = $plugin->getServer();
		PortalContent::register($server->getAsyncPool(), $server->getCraftingManager());
		$this->travel = new PortalTravel($dimensions);
		$plugin->getScheduler()->scheduleRepeatingTask(new ClosureTask(function() : void{
			$this->travel->tick($this->plugin->getServer());
		}), 1);
	}

	/**
	 * @param BlockUpdateEvent $event
	 * @priority LOW
	 */
	public function handleBlockUpdate(BlockUpdateEvent $event) : void{
		$block = $event->getBlock();
		$position = $block->getPosition();
		$world = $position->getWorld();
		$x = $position->getFloorX();
		$y = $position->getFloorY();
		$z = $position->getFloorZ();

		if($block instanceof BaseFire){
			if(!$this->canLightNetherPortal($world)){
				return;
			}
			$frame = NetherPortalFrame::find($world, $x, $y, $z);
			if($frame !== null){
				$frame->light();
				$event->cancel();
			}
		}elseif($block instanceof NetherPortal){
			if(!NetherPortalFrame::isPortalBlockSupported($world, $block, $x, $y, $z)){
				$world->setBlockAt($x, $y, $z, VanillaBlocks::AIR());
				$event->cancel();
			}
		}
	}

	/**
	 * @param PlayerInteractEvent $event
	 * @priority NORMAL
	 */
	public function handleInteract(PlayerInteractEvent $event) : void{
		if($event->getAction() !== PlayerInteractEvent::RIGHT_CLICK_BLOCK){
			return;
		}
		$item = $event->getItem();
		$block = $event->getBlock();
		if(!$item instanceof EnderEye || !$block instanceof EndPortalFrame || $block->hasEye()){
			return;
		}
		$event->cancel();

		$player = $event->getPlayer();
		$position = $block->getPosition();
		$world = $position->getWorld();
		$world->setBlock($position, $block->setEye(true));
		$world->addSound($position->add(0.5, 0.5, 0.5), new EndPortalFrameFillSound());
		if(!$player->isCreative()){
			$item->pop();
			$player->getInventory()->setItemInHand($item);
		}

		if($this->dimensions->getWorld(DimensionIds::THE_END) !== null){
			EndPortalActivator::tryActivate($world, $position->getFloorX(), $position->getFloorY(), $position->getFloorZ(), $block->getFacing());
		}
	}

	/**
	 * @param EntityDespawnEvent $event
	 * @priority MONITOR
	 */
	public function handleEntityDespawn(EntityDespawnEvent $event) : void{
		$this->travel->forget($event->getEntity()->getId());
	}

	private function canLightNetherPortal(World $world) : bool{
		$dimension = $this->dimensions->getDimension($world);
		if($dimension !== DimensionIds::OVERWORLD && $dimension !== DimensionIds::NETHER){
			return false;
		}
		return $this->dimensions->getWorld(DimensionIds::NETHER) !== null && $this->dimensions->getWorld(DimensionIds::OVERWORLD) !== null;
	}
}
<?php

declare(strict_types=1);

namespace dimension\portal\item;

use pocketmine\item\Item;

/**
 * Eye of ender. Placing it in an end portal frame is handled by the portal
 * listener.
 */
class EnderEye extends Item{

}
<?php

declare(strict_types=1);

namespace dimension\portal;

use dimension\portal\block\EndPortal;
use dimension\portal\item\EnderEye;
use pocketmine\block\BlockBreakInfo;
use pocketmine\block\BlockIdentifier;
use pocketmine\block\BlockTypeIds;
use pocketmine\block\BlockTypeInfo;
use pocketmine\block\RuntimeBlockStateRegistry;
use pocketmine\crafting\CraftingManager;
use pocketmine\crafting\ExactRecipeIngredient;
use pocketmine\crafting\ShapelessRecipe;
use pocketmine\crafting\ShapelessRecipeType;
use pocketmine\data\bedrock\block\BlockTypeNames;
use pocketmine\data\bedrock\item\ItemTypeNames;
use pocketmine\data\bedrock\item\SavedItemData;
use pocketmine\inventory\CreativeInventory;
use pocketmine\item\ItemIdentifier;
use pocketmine\item\ItemTypeIds;
use pocketmine\item\StringToItemParser;
use pocketmine\item\VanillaItems;
use pocketmine\scheduler\AsyncPool;
use pocketmine\scheduler\AsyncTask;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use pocketmine\world\format\io\GlobalItemDataHandlers;
use InvalidArgumentException;

/**
 * Registers the end portal block and the eye of ender item, which the server
 * does not provide. The block is registered on every thread with the type id
 * chosen on the main thread so runtime state ids match across threads.
 */
final class PortalContent{

	private static ?EndPortal $endPortal = null;
	private static ?EnderEye $enderEye = null;

	private function __construct(){
	}

	public static function endPortal() : EndPortal{
		if(self::$endPortal === null){
			throw new \LogicException("Portal content is not registered");
		}
		return clone self::$endPortal;
	}

	public static function enderEye() : EnderEye{
		if(self::$enderEye === null){
			throw new \LogicException("Portal content is not registered");
		}
		return clone self::$enderEye;
	}

	public static function register(AsyncPool $pool, CraftingManager $craftingManager) : void{
		if(self::$endPortal !== null){
			return;
		}
		$typeId = BlockTypeIds::newId();
		self::registerBlock($typeId);
		self::registerItem();
		self::registerRecipe($craftingManager);

		$pool->addWorkerStartHook(function(int $worker) use($pool, $typeId) : void{
			$pool->submitTaskToWorker(new class($typeId) extends AsyncTask{
				public function __construct(
					private int $typeId
				){
				}

				public function onRun() : void{
					PortalContent::registerBlock($this->typeId);
				}
			}, $worker);
		});
	}

	/**
	 * @internal
	 */
	public static function registerBlock(int $typeId) : void{
		if(self::$endPortal !== null){
			return;
		}
		$block = new EndPortal(new BlockIdentifier($typeId), "End Portal", new BlockTypeInfo(BlockBreakInfo::indestructible()));
		RuntimeBlockStateRegistry::getInstance()->register($block);
		GlobalBlockStateHandlers::getDeserializer()->mapSimple(BlockTypeNames::END_PORTAL, fn() => clone $block);
		GlobalBlockStateHandlers::getSerializer()->mapSimple($block, BlockTypeNames::END_PORTAL);
		self::$endPortal = $block;
	}

	private static function registerItem() : void{
		$item = new EnderEye(new ItemIdentifier(ItemTypeIds::newId()), "Eye of Ender");
		GlobalItemDataHandlers::getDeserializer()->map(ItemTypeNames::ENDER_EYE, fn() => clone $item);
		GlobalItemDataHandlers::getSerializer()->map($item, fn() => new SavedItemData(ItemTypeNames::ENDER_EYE));
		foreach(["ender_eye", "eye_of_ender"] as $name){
			try{
				StringToItemParser::getInstance()->register($name, fn() => clone $item);
			}catch(InvalidArgumentException){
				StringToItemParser::getInstance()->override($name, fn() => clone $item);
			}
		}
		CreativeInventory::getInstance()->add($item);
		self::$enderEye = $item;
	}

	private static function registerRecipe(CraftingManager $craftingManager) : void{
		$craftingManager->registerShapelessRecipe(new ShapelessRecipe(
			[
				new ExactRecipeIngredient(VanillaItems::ENDER_PEARL()),
				new ExactRecipeIngredient(VanillaItems::BLAZE_POWDER())
			],
			[self::enderEye()],
			ShapelessRecipeType::CRAFTING
		));
	}
}
<?php

declare(strict_types=1);

namespace dimension\portal;

use pocketmine\block\Air;
use pocketmine\block\BaseFire;
use pocketmine\block\Block;
use pocketmine\block\BlockTypeIds;
use pocketmine\block\NetherPortal;
use pocketmine\block\VanillaBlocks;
use pocketmine\math\Axis;
use pocketmine\world\World;
use function max;

/**
 * Detection of an obsidian nether portal frame around an empty position and
 * the checks that keep a lit portal standing.
 */
final class NetherPortalFrame{

	public const MIN_WIDTH = 2;
	public const MAX_WIDTH = 21;
	public const MIN_HEIGHT = 3;
	public const MAX_HEIGHT = 21;

	private function __construct(
		private World $world,
		private int $axis,
		private int $x,
		private int $y,
		private int $z,
		private int $width,
		private int $height
	){
	}

	/**
	 * Finds a complete frame enclosing the given position, trying the X axis
	 * first and then the Z axis.
	 */
	public static function find(World $world, int $x, int $y, int $z) : ?self{
		foreach([Axis::X, Axis::Z] as $axis){
			$frame = self::findOnAxis($world, $x, $y, $z, $axis);
			if($frame !== null){
				return $frame;
			}
		}
		return null;
	}

	private static function findOnAxis(World $world, int $x, int $y, int $z, int $axis) : ?self{
		[$dx, $dz] = $axis === Axis::X ? [1, 0] : [0, 1];
		if(!self::isEmpty($world->getBlockAt($x, $y, $z))){
			return null;
		}

		$lowest = max($world->getMinY(), $y - self::MAX_HEIGHT);
		while($y > $lowest && self::isEmpty($world->getBlockAt($x, $y - 1, $z))){
			$y--;
		}

		$left = null;
		for($i = 0; $i <= self::MAX_WIDTH; $i++){
			$bx = $x - $dx * $i;
			$bz = $z - $dz * $i;
			$block = $world->getBlockAt($bx, $y, $bz);
			if(self::isFrame($block)){
				$left = $i - 1;
				break;
			}
			if(!self::isEmpty($block) || !self::isFrame($world->getBlockAt($bx, $y - 1, $bz))){
				return null;
			}
		}
		if($left === null){
			return null;
		}
		$startX = $x - $dx * $left;
		$startZ = $z - $dz * $left;

		$width = null;
		for($i = 0; $i <= self::MAX_WIDTH; $i++){
			$bx = $startX + $dx * $i;
			$bz = $startZ + $dz * $i;
			$block = $world->getBlockAt($bx, $y, $bz);
			if(self::isFrame($block)){
				$width = $i;
				break;
			}
			if(!self::isEmpty($block) || !self::isFrame($world->getBlockAt($bx, $y - 1, $bz))){
				return null;
			}
		}
		if($width === null || $width < self::MIN_WIDTH || $width > self::MAX_WIDTH){
			return null;
		}

		$height = self::MAX_HEIGHT;
		for($h = 0; $h < self::MAX_HEIGHT; $h++){
			$by = $y + $h;
			if(
				!self::isFrame($world->getBlockAt($startX - $dx, $by, $startZ - $dz)) ||
				!self::isFrame($world->getBlockAt($startX + $dx * $width, $by, $startZ + $dz * $width))
			){
				$height = $h;
				break;
			}
			$complete = true;
			for($i = 0; $i < $width; $i++){
				if(!self::isEmpty($world->getBlockAt($startX + $dx * $i, $by, $startZ + $dz * $i))){
					$complete = false;
					break;
				}
			}
			if(!$complete){
				$height = $h;
				break;
			}
		}
		if($height < self::MIN_HEIGHT || $height > self::MAX_HEIGHT){
			return null;
		}
		for($i = 0; $i < $width; $i++){
			if(!self::isFrame($world->getBlockAt($startX + $dx * $i, $y + $height, $startZ + $dz * $i))){
				return null;
			}
		}

		return new self($world, $axis, $startX, $y, $startZ, $width, $height);
	}

	/**
	 * Fills the inside of the frame with nether portal blocks facing the
	 * frame axis.
	 */
	public function light() : void{
		[$dx, $dz] = $this->axis === Axis::X ? [1, 0] : [0, 1];
		$portal = VanillaBlocks::NETHER_PORTAL()->setAxis($this->axis);
		for($h = 0; $h < $this->height; $h++){
			for($i = 0; $i < $this->width; $i++){
				$this->world->setBlockAt($this->x + $dx * $i, $this->y + $h, $this->z + $dz * $i, $portal);
			}
		}
	}

	/**
	 * A portal block stays while the four neighbours in its plane are portal
	 * blocks of the same axis or frame blocks.
	 */
	public static function isPortalBlockSupported(World $world, NetherPortal $portal, int $x, int $y, int $z) : bool{
		[$dx, $dz] = $portal->getAxis() === Axis::X ? [1, 0] : [0, 1];
		foreach([[$dx, 0, $dz], [-$dx, 0, -$dz], [0, 1, 0], [0, -1, 0]] as [$ox, $oy, $oz]){
			$side = $world->getBlockAt($x + $ox, $y + $oy, $z + $oz);
			if(self::isFrame($side)){
				continue;
			}
			if($side instanceof NetherPortal && $side->getAxis() === $portal->getAxis()){
				continue;
			}
			return false;
		}
		return true;
	}

	public static function isFrame(Block $block) : bool{
		return $block->getTypeId() === BlockTypeIds::OBSIDIAN;
	}

	private static function isEmpty(Block $block) : bool{
		return $block instanceof Air || $block instanceof BaseFire || $block instanceof NetherPortal;
	}
}
<?php

declare(strict_types=1);

namespace dimension\portal;

use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
use pocketmine\world\sound\Sound;

final class EndPortalFrameFillSound implements Sound{

	public function encode(Vector3 $pos) : array{
		return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BLOCK_END_PORTAL_FRAME_FILL, $pos, false)];
	}
}
<?php

declare(strict_types=1);

namespace dimension\rule;

use dimension\Dimensions;
use pocketmine\block\BedBase;
use pocketmine\block\Sponge;
use pocketmine\block\VanillaBlocks;
use pocketmine\block\Water;
use pocketmine\event\block\BlockPlaceEvent;
use pocketmine\event\block\BlockPreExplodeEvent;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerBucketEmptyEvent;
use pocketmine\event\player\PlayerInteractEvent;
use pocketmine\event\player\PlayerRespawnAnchorUseEvent;
use pocketmine\item\LiquidBucket;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\player\Player;
use pocketmine\plugin\PluginBase;
use pocketmine\world\Explosion;
use pocketmine\world\Position;
use pocketmine\world\sound\FizzSound;
use pocketmine\world\World;

/**
 * Gameplay rules that depend on the dimension a world stands for:
 *
 * - beds explode when used in the nether or the end;
 * - respawn anchors set the spawn in the nether and explode elsewhere;
 * - water poured from a bucket in the nether evaporates;
 * - wet sponges placed in the nether dry at once;
 * - ice leaves no water in the nether, and lava flows faster and farther there.
 */
final class DimensionRulesListener implements Listener{

	private const BED_EXPLOSION_RADIUS = 5.0;

	public function __construct(
		private PluginBase $plugin,
		private Dimensions $dimensions
	){
		DimensionLookup::setDimensions($dimensions);
		BlockOverrides::register();
	}

	/**
	 * @priority HIGH
	 * @handleCancelled false
	 */
	public function onInteract(PlayerInteractEvent $event) : void{
		if($event->getAction() !== PlayerInteractEvent::RIGHT_CLICK_BLOCK || !$event->useBlock()){
			return;
		}
		$block = $event->getBlock();
		if(!$block instanceof BedBase){
			return;
		}
		$dimension = $this->dimensions->getDimension($block->getPosition()->getWorld());
		if($dimension !== DimensionIds::NETHER && $dimension !== DimensionIds::THE_END){
			return;
		}
		$event->cancel();
		$this->explodeBed($block, $event->getPlayer());
	}

	/**
	 * @priority HIGH
	 * @handleCancelled false
	 */
	public function onRespawnAnchorUse(PlayerRespawnAnchorUseEvent $event) : void{
		$world = $event->getBlock()->getPosition()->getWorld();
		if($this->dimensions->getDimension($world) === DimensionIds::NETHER){
			$event->setAction(PlayerRespawnAnchorUseEvent::ACTION_SET_SPAWN);
		}else{
			$event->setAction(PlayerRespawnAnchorUseEvent::ACTION_EXPLODE);
		}
	}

	/**
	 * @priority HIGH
	 * @handleCancelled false
	 */
	public function onBucketEmpty(PlayerBucketEmptyEvent $event) : void{
		$bucket = $event->getBucket();
		if(!$bucket instanceof LiquidBucket || !$bucket->getLiquid() instanceof Water){
			return;
		}
		$target = $event->getBlockClicked()->getPosition();
		$world = $target->getWorld();
		if(!$this->isNether($world)){
			return;
		}
		$event->cancel();

		$player = $event->getPlayer();
		if($player->hasFiniteResources()){
			$player->getInventory()->setItemInHand($event->getItem());
		}
		$this->playEvaporation($world, $target);
	}

	/**
	 * @priority HIGHEST
	 * @handleCancelled false
	 */
	public function onBlockPlace(BlockPlaceEvent $event) : void{
		$world = $event->getPlayer()->getWorld();
		if(!$this->isNether($world)){
			return;
		}
		$transaction = $event->getTransaction();
		foreach($transaction->getBlocks() as [$x, $y, $z, $block]){
			if($block instanceof Sponge && $block->isWet()){
				$dry = clone $block;
				$transaction->addBlockAt($x, $y, $z, $dry->setWet(false));
				$this->playEvaporation($world, new Vector3($x, $y, $z));
			}
		}
	}

	private function isNether(World $world) : bool{
		return $this->dimensions->getDimension($world) === DimensionIds::NETHER;
	}

	private function playEvaporation(World $world, Vector3 $block) : void{
		$center = $block->floor()->add(0.5, 0.5, 0.5);
		$world->addSound($center, new FizzSound());
		$world->addParticle($center, new EvaporateParticle());
	}

	private function explodeBed(BedBase $bed, Player $player) : void{
		$event = new BlockPreExplodeEvent($bed, self::BED_EXPLOSION_RADIUS, $player);
		$event->setIncendiary(true);
		$event->call();
		if($event->isCancelled()){
			return;
		}

		$world = $bed->getPosition()->getWorld();
		$other = $bed->getOtherHalf();
		$head = ($other === null || $bed->isHeadPart()) ? $bed : $other;
		$center = Position::fromObject($head->getPosition()->add(0.5, 0.5, 0.5), $world);

		foreach([$bed, $other] as $half){
			if($half === null){
				continue;
			}
			$world->getTile($half->getPosition())?->onBlockDestroyed();
			$world->setBlock($half->getPosition(), VanillaBlocks::AIR());
		}

		$explosion = new Explosion($center, $event->getRadius(), $bed);
		$explosion->setFireChance($event->getFireChance());
		if($event->isBlockBreaking()){
			$explosion->explodeA();
		}
		$explosion->explodeB();
	}
}
<?php

declare(strict_types=1);

namespace dimension\rule;

use dimension\Dimensions;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\world\Position;
use pocketmine\world\World;

/**
 * Gives block classes, which are created without any plugin context, access
 * to the dimension of the world they stand in.
 */
final class DimensionLookup{

	private static ?Dimensions $dimensions = null;

	private function __construct(){
	}

	public static function setDimensions(Dimensions $dimensions) : void{
		self::$dimensions = $dimensions;
	}

	public static function getDimension(World $world) : int{
		if(self::$dimensions === null){
			return DimensionIds::OVERWORLD;
		}
		return self::$dimensions->getDimension($world);
	}

	public static function isNether(World $world) : bool{
		return self::getDimension($world) === DimensionIds::NETHER;
	}

	/**
	 * Whether the position is bound to a loaded world of the nether.
	 */
	public static function isInNether(Position $position) : bool{
		return $position->isValid() && self::isNether($position->getWorld());
	}
}
<?php

declare(strict_types=1);

namespace dimension\rule\block;

use dimension\rule\DimensionLookup;
use pocketmine\block\Ice;
use pocketmine\block\utils\BlockEventHelper;
use pocketmine\block\VanillaBlocks;
use pocketmine\item\Item;
use pocketmine\player\Player;

/**
 * Ice that leaves no water behind in the nether, whether it is broken or
 * melts.
 */
class DimensionIce extends Ice{

	public function onBreak(Item $item, ?Player $player = null, array &$returnedItems = []) : bool{
		if(DimensionLookup::isInNether($this->position)){
			$this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR());
			return true;
		}
		return parent::onBreak($item, $player, $returnedItems);
	}

	public function onRandomTick() : void{
		if(!DimensionLookup::isInNether($this->position)){
			parent::onRandomTick();
			return;
		}
		$world = $this->position->getWorld();
		if($world->getHighestAdjacentBlockLight($this->position->x, $this->position->y, $this->position->z) >= 12){
			BlockEventHelper::melt($this, VanillaBlocks::AIR());
		}
	}
}
<?php

declare(strict_types=1);

namespace dimension\rule\block;

use dimension\rule\DimensionLookup;
use pocketmine\block\Lava;

/**
 * Lava that flows eight blocks every ten ticks in the nether, and four blocks
 * every thirty ticks everywhere else.
 */
class DimensionLava extends Lava{

	private const NETHER_TICK_RATE = 10;
	private const NETHER_FLOW_DECAY_PER_BLOCK = 1;

	public function tickRate() : int{
		if(DimensionLookup::isInNether($this->position)){
			return self::NETHER_TICK_RATE;
		}
		return parent::tickRate();
	}

	public function getFlowDecayPerBlock() : int{
		if(DimensionLookup::isInNether($this->position)){
			return self::NETHER_FLOW_DECAY_PER_BLOCK;
		}
		return parent::getFlowDecayPerBlock();
	}
}
<?php

declare(strict_types=1);

namespace dimension\rule;

use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
use pocketmine\network\mcpe\protocol\types\LevelEvent;
use pocketmine\world\particle\Particle;

/**
 * The puff of steam shown when water evaporates in the nether.
 */
final class EvaporateParticle implements Particle{

	public function encode(Vector3 $pos) : array{
		return [LevelEventPacket::create(LevelEvent::PARTICLE_EVAPORATE, 0, $pos)];
	}
}
<?php

declare(strict_types=1);

namespace dimension\rule;

use dimension\rule\block\DimensionIce;
use dimension\rule\block\DimensionLava;
use pocketmine\block\Block;
use pocketmine\block\BlockTypeInfo;
use pocketmine\block\RuntimeBlockStateRegistry;
use pocketmine\block\VanillaBlocks;
use ReflectionProperty;

/**
 * Swaps the runtime class of lava and ice for dimension aware subclasses.
 * The type id and every state id stay the same, so saved worlds, the network
 * palette and the block serializers keep working unchanged: only the object
 * the world hands out when a state id is read changes.
 */
final class BlockOverrides{

	private function __construct(){
	}

	public static function register() : void{
		$lava = VanillaBlocks::LAVA();
		self::replace(new DimensionLava($lava->getIdInfo(), $lava->getName(), self::typeInfo($lava)));

		$ice = VanillaBlocks::ICE();
		self::replace(new DimensionIce($ice->getIdInfo(), $ice->getName(), self::typeInfo($ice)));
	}

	private static function typeInfo(Block $block) : BlockTypeInfo{
		return new BlockTypeInfo($block->getBreakInfo(), $block->getTypeTags(), $block->getEnchantmentTags());
	}

	private static function replace(Block $replacement) : void{
		$registry = RuntimeBlockStateRegistry::getInstance();

		$fullListProperty = new ReflectionProperty(RuntimeBlockStateRegistry::class, "fullList");
		$fullList = $fullListProperty->getValue($registry);
		foreach($replacement->generateStatePermutations() as $state){
			$fullList[$state->getStateId()] = $state;
		}
		$fullListProperty->setValue($registry, $fullList);

		$typeIndexProperty = new ReflectionProperty(RuntimeBlockStateRegistry::class, "typeIndex");
		$typeIndex = $typeIndexProperty->getValue($registry);
		$typeIndex[$replacement->getTypeId()] = clone $replacement;
		$typeIndexProperty->setValue($registry, $typeIndex);
	}
}
<?php

declare(strict_types=1);

namespace dimension;

use dimension\generator\GeneratorOptions;
use dimension\generator\VanillaGenerators;
use dimension\network\DimensionNetworkListener;
use dimension\portal\PortalListener;
use dimension\rule\DimensionRulesListener;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\plugin\PluginBase;
use pocketmine\scheduler\ClosureTask;
use pocketmine\world\generator\GeneratorManager;
use pocketmine\world\WorldCreationOptions;

/**
 * The nether and the end: their generators, their worlds, the client side
 * dimension switch and the portals that link them to the overworld.
 */
final class DimensionModule{

	private Dimensions $dimensions;

	public function __construct(
		private PluginBase $plugin,
		private DimensionSettings $settings
	){
		$this->dimensions = new Dimensions($plugin->getServer(), $settings->getWorldNames());
	}

	public function load() : void{
		VanillaGenerators::register();
	}

	public function enable() : void{
		$this->plugin->getScheduler()->scheduleDelayedTask(new ClosureTask(function() : void{
			$this->prepareWorld(DimensionIds::NETHER, VanillaGenerators::NETHER);
			$this->prepareWorld(DimensionIds::THE_END, VanillaGenerators::END);
		}), 0);

		$pluginManager = $this->plugin->getServer()->getPluginManager();
		$pluginManager->registerEvents(new DimensionNetworkListener($this->plugin, $this->dimensions), $this->plugin);
		$pluginManager->registerEvents(new PortalListener($this->plugin, $this->dimensions), $this->plugin);
		$pluginManager->registerEvents(new DimensionRulesListener($this->plugin, $this->dimensions), $this->plugin);
		$pluginManager->registerEvents(new EndCrystalSpawner($this->dimensions), $this->plugin);
	}

	public function getDimensions() : Dimensions{
		return $this->dimensions;
	}

	private function prepareWorld(int $dimension, string $generatorName) : void{
		$name = $this->dimensions->getWorldName($dimension);
		if($name === null){
			return;
		}
		$worldManager = $this->plugin->getServer()->getWorldManager();
		if($worldManager->getWorldByName($name) !== null){
			return;
		}
		if($worldManager->isWorldGenerated($name)){
			$worldManager->loadWorld($name, true);
			return;
		}

		$generator = GeneratorManager::getInstance()->getGenerator($generatorName);
		if($generator === null){
			$this->plugin->getLogger()->error("Dimensions: generator $generatorName is not registered, $name is not created");
			return;
		}
		$seed = $worldManager->getDefaultWorld()?->getSeed();
		$options = WorldCreationOptions::create()
			->setGeneratorClass($generator->getGeneratorClass())
			->setGeneratorOptions(GeneratorOptions::encode($this->plugin->getResourceFolder()));
		if($seed !== null){
			$options->setSeed($seed);
		}
		$worldManager->generateWorld($name, $options);
		$this->plugin->getLogger()->info("Dimensions: created $name");
	}
}
name: Vanilla-Altay
main: vanillaaltay\VanillaAltay
version: 0.2.0
api: 5.44.3
load: STARTUP
Àeêï‰œ°fü1¤t�·¥?§	÷‹<½r¼OqI   GBMB