From 5084ccd43f1606f162171f729532a7fa2c188c4e Mon Sep 17 00:00:00 2001 From: spr-equinox <66425054+spr-equinox@users.noreply.github.com> Date: Fri, 1 Aug 2025 00:38:06 +0800 Subject: [PATCH] Add Entity spawning on Chunk generation (#1054) * Support Spawn * Update natural_spawner.rs * fix distance * fix error * fix * use heigh map * Update living.rs * impl get_rough_biome and merge * fix dropper and entity disappear * merge --------- Co-authored-by: Alexander Medvedev --- assets/entities.json | 602 ++++++++++++++++++ pumpkin-data/build/biome.rs | 408 +++++++----- pumpkin-data/build/entity_type.rs | 171 ++++- pumpkin-data/build/spawn_egg.rs | 4 +- pumpkin-util/src/math/vector2.rs | 7 + pumpkin-world/src/block/entities/dropper.rs | 4 +- pumpkin-world/src/chunk/mod.rs | 36 +- pumpkin-world/src/level.rs | 46 +- pumpkin/src/block/blocks/composter.rs | 2 +- pumpkin/src/block/blocks/fire/fire.rs | 2 +- pumpkin/src/block/blocks/nether_portal.rs | 6 +- pumpkin/src/block/blocks/pumpkin.rs | 2 +- .../src/block/blocks/redstone/comparator.rs | 2 +- pumpkin/src/block/blocks/redstone/dropper.rs | 2 +- pumpkin/src/block/blocks/tnt.rs | 4 +- .../src/command/args/summonable_entities.rs | 2 +- .../src/entity/ai/goal/active_target_goal.rs | 8 +- pumpkin/src/entity/ai/goal/look_at_entity.rs | 16 +- pumpkin/src/entity/experience_orb.rs | 2 +- pumpkin/src/entity/living.rs | 4 +- pumpkin/src/entity/mob/zombie.rs | 4 +- pumpkin/src/entity/mod.rs | 11 +- pumpkin/src/entity/player.rs | 4 +- pumpkin/src/entity/type.rs | 8 +- pumpkin/src/item/items/egg.rs | 2 +- pumpkin/src/item/items/hoe.rs | 2 +- pumpkin/src/item/items/minecart.rs | 14 +- pumpkin/src/item/items/snowball.rs | 2 +- pumpkin/src/net/java/play.rs | 2 +- pumpkin/src/world/mod.rs | 143 ++++- pumpkin/src/world/natural_spawner.rs | 563 ++++++++++++++++ 31 files changed, 1830 insertions(+), 255 deletions(-) create mode 100644 pumpkin/src/world/natural_spawner.rs diff --git a/assets/entities.json b/assets/entities.json index 8127a0b15..ee0d47063 100644 --- a/assets/entities.json +++ b/assets/entities.json @@ -2,8 +2,12 @@ "acacia_boat": { "id": 0, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -17,8 +21,12 @@ "acacia_chest_boat": { "id": 1, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -33,8 +41,12 @@ "id": 2, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.35, 0.6 @@ -52,8 +64,12 @@ "area_effect_cloud": { "id": 3, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": true, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 6.0, 0.5 @@ -68,8 +84,12 @@ "id": 4, "max_health": 12.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.7, 0.65 @@ -88,8 +108,12 @@ "id": 5, "max_health": 20.0, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.5, 1.975 @@ -107,8 +131,12 @@ "arrow": { "id": 6, "attackable": false, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.5, 0.5 @@ -123,8 +151,12 @@ "id": 7, "max_health": 14.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "AXOLOTLS", + "can_spawn_far_from_player": false, "dimension": [ 0.75, 0.42 @@ -142,8 +174,12 @@ "bamboo_chest_raft": { "id": 8, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -157,8 +193,12 @@ "bamboo_raft": { "id": 9, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -173,8 +213,12 @@ "id": 10, "max_health": 6.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "AMBIENT", + "can_spawn_far_from_player": false, "dimension": [ 0.5, 0.9 @@ -193,8 +237,12 @@ "id": 11, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.7, 0.6 @@ -212,8 +260,12 @@ "birch_boat": { "id": 12, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -227,8 +279,12 @@ "birch_chest_boat": { "id": 13, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -243,8 +299,12 @@ "id": 14, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": true, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.8 @@ -299,8 +359,12 @@ "block_display": { "id": 15, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.0, 0.0 @@ -315,8 +379,12 @@ "id": 16, "max_health": 16.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.99 @@ -437,8 +505,12 @@ "id": 17, "max_health": 30.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.77 @@ -493,8 +565,12 @@ "breeze_wind_charge": { "id": 18, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.3125, 0.3125 @@ -509,8 +585,12 @@ "id": 19, "max_health": 32.0, "attackable": true, + "mob": true, + "limit_per_chunk": 6, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 1.7, 2.375 @@ -529,8 +609,12 @@ "id": 20, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.6, 0.7 @@ -572,8 +656,12 @@ "id": 21, "max_health": 12.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.7, 0.5 @@ -658,8 +746,12 @@ "cherry_boat": { "id": 22, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -673,8 +765,12 @@ "cherry_chest_boat": { "id": 23, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -688,8 +784,12 @@ "chest_minecart": { "id": 24, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.98, 0.7 @@ -704,8 +804,12 @@ "id": 25, "max_health": 4.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.4, 0.7 @@ -813,8 +917,12 @@ "id": 26, "max_health": 3.0, "attackable": true, + "mob": true, + "limit_per_chunk": 8, "summonable": true, "fire_immune": false, + "category": "WATER_AMBIENT", + "can_spawn_far_from_player": false, "dimension": [ 0.5, 0.3 @@ -898,8 +1006,12 @@ "command_block_minecart": { "id": 27, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.98, 0.7 @@ -914,8 +1026,12 @@ "id": 28, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.9, 1.4 @@ -1032,8 +1148,12 @@ "id": 29, "max_health": 1.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.9, 2.7 @@ -1052,8 +1172,12 @@ "id": 30, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.7 @@ -1123,8 +1247,12 @@ "dark_oak_boat": { "id": 31, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -1138,8 +1266,12 @@ "dark_oak_chest_boat": { "id": 32, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -1154,8 +1286,12 @@ "id": 33, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "WATER_CREATURE", + "can_spawn_far_from_player": false, "dimension": [ 0.9, 0.6 @@ -1242,8 +1378,12 @@ "id": 34, "max_health": 53.0, "attackable": true, + "mob": true, + "limit_per_chunk": 6, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 1.3964844, 1.5 @@ -1293,8 +1433,12 @@ "dragon_fireball": { "id": 35, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.0, 1.0 @@ -1309,8 +1453,12 @@ "id": 36, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.95 @@ -1385,8 +1533,12 @@ "egg": { "id": 37, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.25, 0.25 @@ -1401,8 +1553,12 @@ "id": 38, "max_health": 80.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 1.9975, 1.9975 @@ -1622,8 +1778,12 @@ "id": 39, "max_health": 40.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 2.9 @@ -1674,8 +1834,12 @@ "id": 40, "max_health": 8.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.4, 0.3 @@ -1694,8 +1858,12 @@ "id": 41, "max_health": 200.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": true, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 16.0, 8.0 @@ -1713,8 +1881,12 @@ "ender_pearl": { "id": 42, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.25, 0.25 @@ -1728,8 +1900,12 @@ "end_crystal": { "id": 43, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": true, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 2.0, 2.0 @@ -1744,8 +1920,12 @@ "id": 44, "max_health": 24.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.95 @@ -1810,8 +1990,12 @@ "evoker_fangs": { "id": 45, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.5, 0.8 @@ -1825,8 +2009,12 @@ "experience_bottle": { "id": 46, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.25, 0.25 @@ -1840,8 +2028,12 @@ "experience_orb": { "id": 47, "attackable": false, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.5, 0.5 @@ -1855,8 +2047,12 @@ "eye_of_ender": { "id": 48, "attackable": false, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.25, 0.25 @@ -1870,8 +2066,12 @@ "falling_block": { "id": 49, "attackable": false, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.98, 0.98 @@ -1885,8 +2085,12 @@ "fireball": { "id": 50, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.0, 1.0 @@ -1900,8 +2104,12 @@ "firework_rocket": { "id": 51, "attackable": false, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.25, 0.25 @@ -1916,8 +2124,12 @@ "id": 52, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.6, 0.7 @@ -1936,8 +2148,12 @@ "id": 53, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.5, 0.5 @@ -1955,8 +2171,12 @@ "furnace_minecart": { "id": 54, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.98, 0.7 @@ -1971,8 +2191,12 @@ "id": 55, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 1, "summonable": true, "fire_immune": true, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 4.0, 4.0 @@ -2089,8 +2313,12 @@ "id": 56, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 1, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 4.0, 4.0 @@ -2109,8 +2337,12 @@ "id": 57, "max_health": 100.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 3.6, 12.0 @@ -2128,8 +2360,12 @@ "glow_item_frame": { "id": 58, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.5, 0.5 @@ -2144,8 +2380,12 @@ "id": 59, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "UNDERGROUND_WATER_CREATURE", + "can_spawn_far_from_player": false, "dimension": [ 0.8, 0.8 @@ -2196,8 +2436,12 @@ "id": 60, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.9, 1.3 @@ -2216,8 +2460,12 @@ "id": 61, "max_health": 30.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.85, 0.85 @@ -2408,8 +2656,12 @@ "id": 62, "max_health": 40.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 1.3964844, 1.4 @@ -2525,8 +2777,12 @@ "hopper_minecart": { "id": 63, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.98, 0.7 @@ -2541,8 +2797,12 @@ "id": 64, "max_health": 53.0, "attackable": true, + "mob": true, + "limit_per_chunk": 6, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 1.3964844, 1.6 @@ -2593,8 +2853,12 @@ "id": 65, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.95 @@ -2716,8 +2980,12 @@ "id": 66, "max_health": 32.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.95 @@ -2735,8 +3003,12 @@ "interaction": { "id": 67, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.0, 0.0 @@ -2751,8 +3023,12 @@ "id": 68, "max_health": 100.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.4, 2.7 @@ -2814,8 +3090,12 @@ "item": { "id": 69, "attackable": false, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.25, 0.25 @@ -2829,8 +3109,12 @@ "item_display": { "id": 70, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.0, 0.0 @@ -2844,8 +3128,12 @@ "item_frame": { "id": 71, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.5, 0.5 @@ -2859,8 +3147,12 @@ "jungle_boat": { "id": 72, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -2874,8 +3166,12 @@ "jungle_chest_boat": { "id": 73, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -2889,8 +3185,12 @@ "leash_knot": { "id": 74, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.375, 0.5 @@ -2904,8 +3204,12 @@ "lightning_bolt": { "id": 75, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.0, 0.0 @@ -2920,8 +3224,12 @@ "id": 76, "max_health": 53.0, "attackable": true, + "mob": true, + "limit_per_chunk": 6, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.9, 1.87 @@ -2971,8 +3279,12 @@ "llama_spit": { "id": 77, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.25, 0.25 @@ -2987,8 +3299,12 @@ "id": 78, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": true, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.52, 0.52 @@ -3135,8 +3451,12 @@ "mangrove_boat": { "id": 79, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -3150,8 +3470,12 @@ "mangrove_chest_boat": { "id": 80, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -3165,8 +3489,12 @@ "marker": { "id": 81, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.0, 0.0 @@ -3180,8 +3508,12 @@ "minecart": { "id": 82, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.98, 0.7 @@ -3196,8 +3528,12 @@ "id": 83, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.9, 1.4 @@ -3314,8 +3650,12 @@ "id": 84, "max_health": 53.0, "attackable": true, + "mob": true, + "limit_per_chunk": 6, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 1.3964844, 1.6 @@ -3365,8 +3705,12 @@ "oak_boat": { "id": 85, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -3380,8 +3724,12 @@ "oak_chest_boat": { "id": 86, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -3396,8 +3744,12 @@ "id": 87, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.6, 0.7 @@ -3415,8 +3767,12 @@ "ominous_item_spawner": { "id": 88, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.25, 0.25 @@ -3430,8 +3786,12 @@ "painting": { "id": 89, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.5, 0.5 @@ -3445,8 +3805,12 @@ "pale_oak_boat": { "id": 90, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -3460,8 +3824,12 @@ "pale_oak_chest_boat": { "id": 91, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -3476,8 +3844,12 @@ "id": 92, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 1.3, 1.25 @@ -3515,8 +3887,12 @@ "id": 93, "max_health": 6.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.5, 0.9 @@ -3567,8 +3943,12 @@ "id": 94, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.9, 0.5 @@ -3624,8 +4004,12 @@ "id": 95, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.9, 0.9 @@ -3712,8 +4096,12 @@ "id": 96, "max_health": 16.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.95 @@ -3732,8 +4120,12 @@ "id": 97, "max_health": 50.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.95 @@ -3752,8 +4144,12 @@ "id": 98, "max_health": 24.0, "attackable": true, + "mob": true, + "limit_per_chunk": 1, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": true, "dimension": [ 0.6, 1.95 @@ -3811,8 +4207,12 @@ "id": 99, "max_health": 30.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 1.4, 1.4 @@ -3959,8 +4359,12 @@ "splash_potion": { "id": 100, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.25, 0.25 @@ -3974,8 +4378,12 @@ "lingering_potion": { "id": 101, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.25, 0.25 @@ -3990,8 +4398,12 @@ "id": 102, "max_health": 3.0, "attackable": true, + "mob": true, + "limit_per_chunk": 8, "summonable": true, "fire_immune": false, + "category": "WATER_AMBIENT", + "can_spawn_far_from_player": false, "dimension": [ 0.7, 0.7 @@ -4045,8 +4457,12 @@ "id": 103, "max_health": 3.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.4, 0.5 @@ -4184,8 +4600,12 @@ "id": 104, "max_health": 100.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 1.95, 2.2 @@ -4223,8 +4643,12 @@ "id": 105, "max_health": 3.0, "attackable": true, + "mob": true, + "limit_per_chunk": 5, "summonable": true, "fire_immune": false, + "category": "WATER_AMBIENT", + "can_spawn_far_from_player": false, "dimension": [ 0.7, 0.4 @@ -4309,8 +4733,12 @@ "id": 106, "max_health": 8.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.9, 1.3 @@ -4712,8 +5140,12 @@ "id": 107, "max_health": 30.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": true, + "category": "MONSTER", + "can_spawn_far_from_player": true, "dimension": [ 1.0, 1.0 @@ -4755,8 +5187,12 @@ "shulker_bullet": { "id": 108, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.3125, 0.3125 @@ -4771,8 +5207,12 @@ "id": 109, "max_health": 8.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.4, 0.3 @@ -4791,8 +5231,12 @@ "id": 110, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.99 @@ -4873,8 +5317,12 @@ "id": 111, "max_health": 15.0, "attackable": true, + "mob": true, + "limit_per_chunk": 6, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 1.3964844, 1.6 @@ -4925,8 +5373,12 @@ "id": 112, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.52, 0.52 @@ -5022,8 +5474,12 @@ "small_fireball": { "id": 113, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.3125, 0.3125 @@ -5038,8 +5494,12 @@ "id": 114, "max_health": 14.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 1.9, 1.75 @@ -5057,8 +5517,12 @@ "snowball": { "id": 115, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.25, 0.25 @@ -5073,8 +5537,12 @@ "id": 116, "max_health": 4.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.7, 1.9 @@ -5115,8 +5583,12 @@ "spawner_minecart": { "id": 117, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.98, 0.7 @@ -5130,8 +5602,12 @@ "spectral_arrow": { "id": 118, "attackable": false, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.5, 0.5 @@ -5146,8 +5622,12 @@ "id": 119, "max_health": 16.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 1.4, 0.9 @@ -5232,8 +5712,12 @@ "spruce_boat": { "id": 120, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -5247,8 +5731,12 @@ "spruce_chest_boat": { "id": 121, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 1.375, 0.5625 @@ -5263,8 +5751,12 @@ "id": 122, "max_health": 10.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "WATER_CREATURE", + "can_spawn_far_from_player": false, "dimension": [ 0.8, 0.8 @@ -5315,8 +5807,12 @@ "id": 123, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.99 @@ -5437,8 +5933,12 @@ "id": 124, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": true, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.9, 1.7 @@ -5489,8 +5989,12 @@ "id": 125, "max_health": 6.0, "attackable": true, + "mob": true, + "limit_per_chunk": 8, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.4, 0.3 @@ -5508,8 +6012,12 @@ "text_display": { "id": 126, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.0, 0.0 @@ -5523,8 +6031,12 @@ "tnt": { "id": 127, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": true, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.98, 0.98 @@ -5538,8 +6050,12 @@ "tnt_minecart": { "id": 128, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.98, 0.7 @@ -5554,8 +6070,12 @@ "id": 129, "max_health": 53.0, "attackable": true, + "mob": true, + "limit_per_chunk": 6, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.9, 1.87 @@ -5605,8 +6125,12 @@ "trident": { "id": 130, "attackable": false, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.5, 0.5 @@ -5621,8 +6145,12 @@ "id": 131, "max_health": 3.0, "attackable": true, + "mob": true, + "limit_per_chunk": 8, "summonable": true, "fire_immune": false, + "category": "WATER_AMBIENT", + "can_spawn_far_from_player": false, "dimension": [ 0.5, 0.4 @@ -5676,8 +6204,12 @@ "id": 132, "max_health": 30.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 1.2, 0.4 @@ -5752,8 +6284,12 @@ "id": 133, "max_health": 14.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": true, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.4, 0.8 @@ -5772,8 +6308,12 @@ "id": 134, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.6, 1.95 @@ -5792,8 +6332,12 @@ "id": 135, "max_health": 24.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.95 @@ -5849,8 +6393,12 @@ "id": 136, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.6, 1.95 @@ -5869,8 +6417,12 @@ "id": 137, "max_health": 500.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": true, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.9, 2.9 @@ -5900,8 +6452,12 @@ "wind_charge": { "id": 138, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.3125, 0.3125 @@ -5916,8 +6472,12 @@ "id": 139, "max_health": 26.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.95 @@ -6123,8 +6683,12 @@ "id": 140, "max_health": 300.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": true, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.9, 3.5 @@ -6143,8 +6707,12 @@ "id": 141, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": true, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.7, 2.4 @@ -6249,8 +6817,12 @@ "wither_skull": { "id": 142, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": true, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.3125, 0.3125 @@ -6265,8 +6837,12 @@ "id": 143, "max_health": 8.0, "attackable": true, + "mob": true, + "limit_per_chunk": 8, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 0.6, 0.85 @@ -6285,8 +6861,12 @@ "id": 144, "max_health": 40.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": true, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 1.3964844, 1.4 @@ -6337,8 +6917,12 @@ "id": 145, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.95 @@ -6487,8 +7071,12 @@ "id": 146, "max_health": 15.0, "attackable": true, + "mob": true, + "limit_per_chunk": 6, "summonable": true, "fire_immune": false, + "category": "CREATURE", + "can_spawn_far_from_player": true, "dimension": [ 1.3964844, 1.6 @@ -6539,8 +7127,12 @@ "id": 147, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": false, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.95 @@ -6662,8 +7254,12 @@ "id": 148, "max_health": 20.0, "attackable": true, + "mob": true, + "limit_per_chunk": 4, "summonable": true, "fire_immune": true, + "category": "MONSTER", + "can_spawn_far_from_player": false, "dimension": [ 0.6, 1.95 @@ -6769,6 +7365,8 @@ "id": 149, "summonable": false, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.6, 1.8 @@ -6786,8 +7384,12 @@ "fishing_bobber": { "id": 150, "attackable": true, + "mob": false, + "limit_per_chunk": 0, "summonable": false, "fire_immune": false, + "category": "MISC", + "can_spawn_far_from_player": true, "dimension": [ 0.25, 0.25 diff --git a/pumpkin-data/build/biome.rs b/pumpkin-data/build/biome.rs index 06e3cf66d..76aa538e5 100644 --- a/pumpkin-data/build/biome.rs +++ b/pumpkin-data/build/biome.rs @@ -16,6 +16,7 @@ pub struct Biome { features: Vec>, creature_spawn_probability: Option, spawners: SpawnGroups, + spawn_costs: HashMap, pub id: u8, } @@ -31,17 +32,43 @@ struct SpawnGroups { water_creature: Vec, } +#[allow(non_snake_case)] #[derive(Deserialize, Hash, PartialEq, Eq)] struct Spawner { r#type: String, + minCount: i32, + maxCount: i32, } impl Spawner { pub fn to_tokens(&self) -> TokenStream { let r#type = &self.r#type; + let min_count = &self.minCount; + let max_count = &self.maxCount; quote! { Spawner { r#type: #r#type, + min_count: #min_count, + max_count: #max_count, + } + } + } +} + +#[derive(Deserialize, PartialEq)] +struct SpawnCosts { + energy_budget: f64, + charge: f64, +} + +impl SpawnCosts { + pub fn to_tokens(&self) -> TokenStream { + let energy_budget = &self.energy_budget; + let charge = &self.charge; + quote! { + SpawnCosts { + energy_budget: #energy_budget, + charge: #charge, } } } @@ -217,6 +244,18 @@ pub(crate) fn build() -> TokenStream { } }; + let spawn_costs: Vec<_> = biome + .spawn_costs + .iter() + .map(|(name, cost)| { + let cost_token = cost.to_tokens(); + let entity_type = name.strip_prefix("minecraft:").unwrap(); + quote! { + #entity_type => #cost_token + } + }) + .collect(); + let temperature_modifier = match temperature_modifier { TemperatureModifier::Frozen => quote! { TemperatureModifier::Frozen }, TemperatureModifier::None => quote! { TemperatureModifier::None }, @@ -225,17 +264,20 @@ pub(crate) fn build() -> TokenStream { variants.extend([quote! { pub const #format_name: Biome = Biome { - id: #index, - registry_id: #name, - weather: Weather::new( - #has_precipitation, - #temperature, - #temperature_modifier, - #downfall - ), - features: &[#(&[#(#features),*]),*], - creature_spawn_probability: #creature_spawn_probability, - spawners: #spawners, + id: #index, + registry_id: #name, + weather: Weather::new( + #has_precipitation, + #temperature, + #temperature_modifier, + #downfall + ), + features: &[#(&[#(#features),*]),*], + creature_spawn_probability: #creature_spawn_probability, + spawners: #spawners, + spawn_costs: phf::phf_map! { + #(#spawn_costs),* + }, }; }]); @@ -246,197 +288,223 @@ pub(crate) fn build() -> TokenStream { let overworld_tree = biome_trees.overworld.into_token_stream(); let nether_tree = biome_trees.nether.into_token_stream(); quote! { + use crate::biome::de::Deserialize; + use crate::entity_type::EntityType; + use crate::tag::Taggable; + use crate::tag::RegistryKey; + use pumpkin_util::biome::{TemperatureModifier, Weather}; + use serde::{Deserializer, de}; + use std::{fmt, hash::{Hasher, Hash}}; - use pumpkin_util::biome::{TemperatureModifier, Weather}; - use serde::{Deserializer, de}; - use crate::biome::de::Deserialize; - use std::{fmt, hash::{Hasher, Hash}}; + #[derive(Debug)] + pub struct Biome { + pub id: u8, + pub registry_id: &'static str, + pub weather: Weather, + // carvers: &'static [&str], + pub features: &'static [&'static [&'static str]], + pub creature_spawn_probability: f32, + pub spawners: SpawnGroups, + pub spawn_costs: phf::Map<&'static str, SpawnCosts>, + } - #[derive(Debug)] - pub struct Biome { - pub id: u8, - pub registry_id: &'static str, - pub weather: Weather, - // carvers: &'static [&str], - pub features: &'static [&'static [&'static str]], - pub creature_spawn_probability: f32, - pub spawners: SpawnGroups, + #[derive(Debug)] + pub struct SpawnGroups { + pub monster: &'static [Spawner], + pub ambient: &'static [Spawner], + pub axolotls: &'static [Spawner], + pub creature: &'static [Spawner], + pub misc: &'static [Spawner], + pub underground_water_creature: &'static [Spawner], + pub water_ambient: &'static [Spawner], + pub water_creature: &'static [Spawner], + } + + #[derive(Debug)] + pub struct Spawner { + pub r#type: &'static str, + pub min_count: i32, + pub max_count: i32, + } + + impl PartialEq for Biome { + fn eq(&self, other: &Biome) -> bool { + self.id == other.id } + } - #[derive(Debug)] - pub struct SpawnGroups { - pub monster: &'static [Spawner], - pub ambient: &'static [Spawner], - pub axolotls: &'static [Spawner], - pub creature: &'static [Spawner], - pub misc: &'static [Spawner], - pub underground_water_creature: &'static [Spawner], - pub water_ambient: &'static [Spawner], - pub water_creature: &'static [Spawner], + impl Eq for Biome {} + + impl Hash for Biome { + fn hash(&self, state: &mut H) { + self.id.hash(state); } + } - #[derive(Debug)] - pub struct Spawner { - pub r#type: &'static str, - } + #[derive(Debug)] + pub struct SpawnCosts { + pub energy_budget: f64, + pub charge: f64, + } - impl PartialEq for Biome { - fn eq(&self, other: &Biome) -> bool { - self.id == other.id - } - } + impl<'de> Deserialize<'de> for &'static Biome { + fn deserialize>(deserializer: D) -> Result { + struct BiomeVisitor; - impl Eq for Biome {} + impl de::Visitor<'_> for BiomeVisitor { + type Value = &'static Biome; - impl Hash for Biome { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } - } - - impl<'de> Deserialize<'de> for &'static Biome { - fn deserialize>(deserializer: D) -> Result { - struct BiomeVisitor; - - impl de::Visitor<'_> for BiomeVisitor { - type Value = &'static Biome; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a biome name as a string") - } - - fn visit_string(self, v: String) -> Result { - self.visit_str(&v) - } - - fn visit_str(self, value: &str) -> Result { - let biome = Biome::from_name(value.strip_prefix("minecraft:").unwrap_or(value)); - biome.ok_or_else(|| E::unknown_variant(value, &["unknown biome"])) - } + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a biome name as a string") } - deserializer.deserialize_str(BiomeVisitor) - } - } + fn visit_string(self, v: String) -> Result { + self.visit_str(&v) + } - impl Biome { - #variants - - pub fn from_name(name: &str) -> Option<&'static Self> { - match name { - #name_to_type - _ => None + fn visit_str(self, value: &str) -> Result { + let biome = Biome::from_name(value.strip_prefix("minecraft:").unwrap_or(value)); + biome.ok_or_else(|| E::unknown_variant(value, &["unknown biome"])) } } - pub const fn from_id(id: u8) -> Option<&'static Self> { - match id { - #id_to_type - _ => None - } + deserializer.deserialize_str(BiomeVisitor) + } + } + + impl Biome { + #variants + + pub fn from_name(name: &str) -> Option<&'static Self> { + match name { + #name_to_type + _ => None } } - #[derive(PartialEq)] - pub struct ParameterRange { - min: i64, - max: i64, + pub const fn from_id(id: u8) -> Option<&'static Self> { + match id { + #id_to_type + _ => None + } } + } - impl ParameterRange { - fn calc_distance(&self, noise: i64) -> i64 { - if noise > self.max { - noise - self.max - } else if noise < self.min { - self.min - noise - } else { - 0 + impl Taggable for Biome { + #[inline] + fn registry_id(&self) -> u16 { + self.id as u16 + } + #[inline] + fn tag_key() -> RegistryKey { + RegistryKey::WorldgenBiome + } + #[inline] + fn registry_key(&self) -> &str { + self.registry_id + } + } + + #[derive(PartialEq)] + pub struct ParameterRange { + min: i64, + max: i64, + } + + impl ParameterRange { + fn calc_distance(&self, noise: i64) -> i64 { + if noise > self.max { + noise - self.max + } else if noise < self.min { + self.min - noise + } else { + 0 + } + } + } + + #[derive(PartialEq)] + pub enum BiomeTree { + Leaf { + parameters: [ParameterRange; 7], + biome: &'static Biome, + }, + Branch { + parameters: [ParameterRange; 7], + nodes: &'static [BiomeTree], + }, + } + + + impl BiomeTree { + pub fn get( + &'static self, + point_list: &[i64; 7], + previous_result_node: &mut Option<&'static BiomeTree>, + ) -> &'static Biome { + let result_node = self.get_resulting_node(point_list, *previous_result_node); + match result_node { + BiomeTree::Leaf { biome, .. } => { + *previous_result_node = Some(result_node); + biome } + _ => unreachable!(), } } - #[derive(PartialEq)] - pub enum BiomeTree { - Leaf { - parameters: [ParameterRange; 7], - biome: &'static Biome, - }, - Branch { - parameters: [ParameterRange; 7], - nodes: &'static [BiomeTree], - }, - } + fn get_resulting_node( + &'static self, + point_list: &[i64; 7], + previous_result_node: Option<&'static BiomeTree>, + ) -> &'static BiomeTree { + match self { + Self::Leaf { .. } => self, + Self::Branch { nodes, .. } => { + let mut distance = previous_result_node + .map(|node| node.get_squared_distance(point_list)) + .unwrap_or(i64::MAX); + let mut best_node = previous_result_node; + for node in *nodes { + let node_distance = node.get_squared_distance(point_list); + if distance > node_distance { + let node2 = node.get_resulting_node(point_list, best_node); + let node2_distance = if node == node2 { + node_distance + } else { + node2.get_squared_distance(point_list) + }; - impl BiomeTree { - pub fn get( - &'static self, - point_list: &[i64; 7], - previous_result_node: &mut Option<&'static BiomeTree>, - ) -> &'static Biome { - let result_node = self.get_resulting_node(point_list, *previous_result_node); - match result_node { - BiomeTree::Leaf { biome, .. } => { - *previous_result_node = Some(result_node); - biome - } - _ => unreachable!(), - } - } - - fn get_resulting_node( - &'static self, - point_list: &[i64; 7], - previous_result_node: Option<&'static BiomeTree>, - ) -> &'static BiomeTree { - match self { - Self::Leaf { .. } => self, - Self::Branch { nodes, .. } => { - let mut distance = previous_result_node - .map(|node| node.get_squared_distance(point_list)) - .unwrap_or(i64::MAX); - let mut best_node = previous_result_node; - - for node in *nodes { - let node_distance = node.get_squared_distance(point_list); - if distance > node_distance { - let node2 = node.get_resulting_node(point_list, best_node); - let node2_distance = if node == node2 { - node_distance - } else { - node2.get_squared_distance(point_list) - }; - - if distance > node2_distance { - distance = node2_distance; - best_node = Some(node2); - } + if distance > node2_distance { + distance = node2_distance; + best_node = Some(node2); } } - - best_node.expect("This should be populated after traversing the tree") } + + best_node.expect("This should be populated after traversing the tree") } } - - fn get_squared_distance(&self, point_list: &[i64; 7]) -> i64 { - let parameters = match self { - Self::Leaf { parameters, .. } => parameters, - Self::Branch { parameters, .. } => parameters, - }; - - parameters - .iter() - .zip(point_list) - .map(|(bound, value)| { - let distance = bound.calc_distance(*value); - distance * distance - }) - .sum() - } } - pub const OVERWORLD_BIOME_SOURCE: BiomeTree = #overworld_tree; - pub const NETHER_BIOME_SOURCE: BiomeTree = #nether_tree; + fn get_squared_distance(&self, point_list: &[i64; 7]) -> i64 { + let parameters = match self { + Self::Leaf { parameters, .. } => parameters, + Self::Branch { parameters, .. } => parameters, + }; + + parameters + .iter() + .zip(point_list) + .map(|(bound, value)| { + let distance = bound.calc_distance(*value); + distance * distance + }) + .sum() + } } + + pub const OVERWORLD_BIOME_SOURCE: BiomeTree = #overworld_tree; + pub const NETHER_BIOME_SOURCE: BiomeTree = #nether_tree; + } } diff --git a/pumpkin-data/build/entity_type.rs b/pumpkin-data/build/entity_type.rs index c3b97985b..ba180272b 100644 --- a/pumpkin-data/build/entity_type.rs +++ b/pumpkin-data/build/entity_type.rs @@ -13,9 +13,13 @@ pub struct EntityType { pub id: u16, pub max_health: Option, pub attackable: Option, + pub mob: Option, + pub limit_per_chunk: Option, pub loot_table: Option, pub summonable: bool, pub fire_immune: bool, + pub category: MobCategory, + pub can_spawn_far_from_player: bool, pub dimension: [f32; 2], pub eye_height: f32, pub spawn_restriction: SpawnRestriction, @@ -36,6 +40,20 @@ pub enum SpawnLocation { Unrestricted, } +#[derive(Deserialize)] +#[allow(non_camel_case_types)] +#[allow(clippy::upper_case_acronyms)] +pub enum MobCategory { + MONSTER, + CREATURE, + AMBIENT, + AXOLOTLS, + UNDERGROUND_WATER_CREATURE, + WATER_CREATURE, + WATER_AMBIENT, + MISC, +} + pub struct NamedEntityType<'a>(&'a str, &'a EntityType); impl ToTokens for NamedEntityType<'_> { @@ -75,9 +93,31 @@ impl ToTokens for NamedEntityType<'_> { heightmap: #spawn_restriction_heightmap, }}; + let spawn_category = match entity.category { + MobCategory::MONSTER => quote! { MobCategory::MONSTER }, + MobCategory::CREATURE => quote! { MobCategory::CREATURE }, + MobCategory::AMBIENT => quote! { MobCategory::AMBIENT }, + MobCategory::AXOLOTLS => quote! { MobCategory::AXOLOTLS }, + MobCategory::UNDERGROUND_WATER_CREATURE => { + quote! { MobCategory::UNDERGROUND_WATER_CREATURE } + } + MobCategory::WATER_CREATURE => quote! { MobCategory::WATER_CREATURE }, + MobCategory::WATER_AMBIENT => quote! { MobCategory::WATER_AMBIENT }, + MobCategory::MISC => quote! { MobCategory::MISC }, + }; + let summonable = entity.summonable; let fire_immune = entity.fire_immune; let eye_height = entity.eye_height; + if entity.mob.is_none() && name != "player" { + panic!("missing field 'mob', entity name {name}"); + } + if entity.limit_per_chunk.is_none() && name != "player" { + panic!("missing field 'mob', entity name {name}"); + } + let mob = entity.mob.unwrap_or(false); + let limit_per_chunk = entity.limit_per_chunk.unwrap_or(0); + let can_spawn_far_from_player = entity.can_spawn_far_from_player; let dimension0 = entity.dimension[0]; let dimension1 = entity.dimension[1]; @@ -95,8 +135,12 @@ impl ToTokens for NamedEntityType<'_> { id: #id, max_health: #max_health, attackable: #attackable, + mob: #mob, + limit_per_chunk: #limit_per_chunk, summonable: #summonable, fire_immune: #fire_immune, + category: &#spawn_category, + can_spawn_far_from_player: #can_spawn_far_from_player, loot_table: #loot_table, dimension: [#dimension0, #dimension1], // Correctly construct the array eye_height: #eye_height, @@ -130,24 +174,28 @@ pub(crate) fn build() -> TokenStream { }); type_from_raw_id_arms.extend(quote! { - #id_lit => Some(Self::#upper_name), + #id_lit => Some(&Self::#upper_name), }); type_from_name.extend(quote! { - #name => Some(Self::#upper_name), + #name => Some(&Self::#upper_name), }); } quote! { use pumpkin_util::loot_table::*; use pumpkin_util::HeightMap; - #[derive(Clone, Copy, Debug, PartialEq)] + #[derive(Debug)] pub struct EntityType { pub id: u16, pub max_health: Option, pub attackable: Option, + pub mob: bool, + pub limit_per_chunk: i32, pub summonable: bool, pub fire_immune: bool, + pub category: &'static MobCategory, + pub can_spawn_far_from_player: bool, pub loot_table: Option, pub dimension: [f32; 2], pub eye_height: f32, @@ -155,31 +203,122 @@ pub(crate) fn build() -> TokenStream { pub resource_name: &'static str, } - #[derive(Clone, Copy, Debug, PartialEq)] - pub struct SpawnRestriction { - location: SpawnLocation, - heightmap: HeightMap, + impl PartialEq for EntityType { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } } - #[derive(Clone, Copy, Debug, PartialEq)] - pub enum SpawnLocation { - InLava, - InWater, - OnGround, - Unrestricted - } + #[derive(Debug)] + pub struct SpawnRestriction { + pub location: SpawnLocation, + pub heightmap: HeightMap, + } + + #[derive(Debug)] + pub enum SpawnLocation { + InLava, + InWater, + OnGround, + Unrestricted, + } + + #[derive(Debug)] + pub struct MobCategory { + pub id: usize, // mojang don't have this field + pub max: i32, + pub is_friendly: bool, + pub is_persistent: bool, + pub despawn_distance: i32, + } + + impl PartialEq for MobCategory { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } + } + + impl MobCategory { + pub const NO_DESPAWN_DISTANCE: i32 = 32; + pub const MONSTER: MobCategory = MobCategory { + id: 0, + max: 70, + is_friendly: false, + is_persistent: false, + despawn_distance: 128, + }; + pub const CREATURE: MobCategory = MobCategory { + id: 1, + max: 10, + is_friendly: true, + is_persistent: true, + despawn_distance: 128, + }; + pub const AMBIENT: MobCategory = MobCategory { + id: 2, + max: 15, + is_friendly: true, + is_persistent: false, + despawn_distance: 128, + }; + pub const AXOLOTLS: MobCategory = MobCategory { + id: 3, + max: 5, + is_friendly: true, + is_persistent: false, + despawn_distance: 128, + }; + pub const UNDERGROUND_WATER_CREATURE: MobCategory = MobCategory { + id: 4, + max: 5, + is_friendly: true, + is_persistent: false, + despawn_distance: 128, + }; + pub const WATER_CREATURE: MobCategory = MobCategory { + id: 5, + max: 5, + is_friendly: true, + is_persistent: true, + despawn_distance: 128, + }; + pub const WATER_AMBIENT: MobCategory = MobCategory { + id: 6, + max: 20, + is_friendly: true, + is_persistent: false, + despawn_distance: 64, + }; + pub const MISC: MobCategory = MobCategory { + id: 7, + max: -1, + is_friendly: true, + is_persistent: true, + despawn_distance: 128, + }; + pub const SPAWNING_CATEGORIES: [&'static Self; 8] = [ + &Self::MONSTER, + &Self::CREATURE, + &Self::AMBIENT, + &Self::AXOLOTLS, + &Self::UNDERGROUND_WATER_CREATURE, + &Self::WATER_CREATURE, + &Self::WATER_AMBIENT, + &Self::MISC, + ]; + } impl EntityType { #consts - pub const fn from_raw(id: u16) -> Option { + pub const fn from_raw(id: u16) -> Option<&'static Self> { match id { #type_from_raw_id_arms _ => None } } - pub fn from_name(name: &str) -> Option { + pub fn from_name(name: &str) -> Option<&'static Self> { match name { #type_from_name _ => None diff --git a/pumpkin-data/build/spawn_egg.rs b/pumpkin-data/build/spawn_egg.rs index e1cfdc49e..ddbdb6e90 100644 --- a/pumpkin-data/build/spawn_egg.rs +++ b/pumpkin-data/build/spawn_egg.rs @@ -15,12 +15,12 @@ pub(crate) fn build() -> TokenStream { for (egg, entity) in &eggs { let entity = entity.to_shouty_snake_case(); let entity = format_ident!("{}", entity); - names.extend(quote! { #egg => Some(EntityType::#entity), }); + names.extend(quote! { #egg => Some(&EntityType::#entity), }); } quote! { use crate::entity_type::EntityType; - pub fn entity_from_egg(id: u16) -> Option { + pub fn entity_from_egg(id: u16) -> Option<&'static EntityType> { match id { #names _ => None diff --git a/pumpkin-util/src/math/vector2.rs b/pumpkin-util/src/math/vector2.rs index 7b7df6704..e3aa8cf53 100644 --- a/pumpkin-util/src/math/vector2.rs +++ b/pumpkin-util/src/math/vector2.rs @@ -27,6 +27,13 @@ impl Vector2 { } } + pub fn add_raw(&self, x: T, y: T) -> Self { + Vector2 { + x: self.x + x, + y: self.y + y, + } + } + pub fn sub(&self, other: &Vector2) -> Self { Vector2 { x: self.x - other.x, diff --git a/pumpkin-world/src/block/entities/dropper.rs b/pumpkin-world/src/block/entities/dropper.rs index 32c8d28d4..a9fa2fcc3 100644 --- a/pumpkin-world/src/block/entities/dropper.rs +++ b/pumpkin-world/src/block/entities/dropper.rs @@ -73,11 +73,11 @@ impl DropperBlockEntity { pub async fn get_random_slot(&self) -> Option> { // this.unpackLootTable(null); let mut ret = None; - let mut j = 0; + let mut j = 1; for i in &self.items { let item = i.lock().await; if !item.is_empty() { - if rng().random_range(0..=j) == 0 { + if rng().random_range(0..j) == 0 { ret = Some(item); } j += 1; diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index c8c37306d..25c77b4af 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -303,6 +303,26 @@ impl ChunkSections { } } + pub fn get_rough_biome_absolute_y( + &self, + relative_x: usize, + y: i32, + relative_z: usize, + ) -> Option { + let y = y - self.min_y; + if y < 0 { + None + } else { + let relative_y = y as usize; + self.get_noise_biome( + relative_y / BlockPalette::SIZE, + relative_x >> 2 & 3, + relative_y >> 2 & 3, + relative_z >> 2 & 3, + ) + } + } + /// Returns the replaced block state ID pub fn set_block_absolute_y( &mut self, @@ -391,6 +411,20 @@ impl ChunkSections { .set(relative_x, relative_y, relative_z, biome_id); } } + + pub fn get_noise_biome( + &self, + index: usize, + scale_x: usize, + scale_y: usize, + scale_z: usize, + ) -> Option { + debug_assert!(scale_x < BiomePalette::SIZE); + debug_assert!(scale_z < BiomePalette::SIZE); + self.sections + .get(index) + .map(|section| section.biomes.get(scale_x, scale_y, scale_z)) + } } impl ChunkData { @@ -438,7 +472,7 @@ impl ChunkData { } //TODO: Tracking heightmaps update. - pub async fn calculate_heightmap(&mut self) -> ChunkHeightmaps { + pub fn calculate_heightmap(&mut self) -> ChunkHeightmaps { let highest_non_empty_subchunk = self.get_highest_non_empty_subchunk(); let mut heightmaps = ChunkHeightmaps::default(); diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 5b144bd8f..fd9f76a19 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -1,7 +1,21 @@ +use crate::{ + BlockStateId, + block::{RawBlockState, entities::BlockEntity}, + chunk::{ + ChunkData, ChunkEntityData, ChunkParsingError, ChunkReadingError, + format::{anvil::AnvilChunkFile, linear::LinearFile}, + io::{Dirtiable, FileIO, LoadedData, file_manager::ChunkFileManager}, + }, + dimension::Dimension, + generation::{Seed, get_world_gen, implementation::WorldGenerator}, + tick::{OrderedTick, ScheduledTick, TickPriority}, + world::BlockRegistryExt, +}; use dashmap::{DashMap, Entry}; use log::trace; use num_traits::Zero; use pumpkin_config::{advanced_config, chunk::ChunkFormat}; +use pumpkin_data::biome::Biome; use pumpkin_data::{Block, block_properties::has_random_ticks, fluid::Fluid}; use pumpkin_util::math::{position::BlockPos, vector2::Vector2}; use rand::{Rng, SeedableRng, rngs::SmallRng}; @@ -24,20 +38,6 @@ use tokio::{ }; use tokio_util::task::TaskTracker; -use crate::{ - BlockStateId, - block::{RawBlockState, entities::BlockEntity}, - chunk::{ - ChunkData, ChunkEntityData, ChunkParsingError, ChunkReadingError, - format::{anvil::AnvilChunkFile, linear::LinearFile}, - io::{Dirtiable, FileIO, LoadedData, file_manager::ChunkFileManager}, - }, - dimension::Dimension, - generation::{Seed, get_world_gen, implementation::WorldGenerator}, - tick::{OrderedTick, ScheduledTick, TickPriority}, - world::BlockRegistryExt, -}; - pub type SyncChunk = Arc>; pub type SyncEntityChunk = Arc>; @@ -607,6 +607,21 @@ impl Level { RawBlockState(id) } + pub async fn get_rough_biome(self: &Arc, position: &BlockPos) -> &'static Biome { + let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position(); + let chunk = self.get_chunk(chunk_coordinate).await; + + let chunk = chunk.read().await; + let Some(id) = chunk.section.get_rough_biome_absolute_y( + relative.x as usize, + relative.y, + relative.z as usize, + ) else { + return &Biome::THE_VOID; + }; + + Biome::from_id(id).unwrap() + } pub async fn set_block_state( self: &Arc, @@ -815,11 +830,12 @@ impl Level { // Deduplicate chunk generation using chunk_generation_locks // We are responsible for generating the chunk - let generated_chunk = world_gen.generate_chunk( + let mut generated_chunk = world_gen.generate_chunk( &self_clone, block_registry.as_ref(), &pos, ); + generated_chunk.heightmap = generated_chunk.calculate_heightmap(); let arc_chunk = Arc::new(RwLock::new(generated_chunk)); loaded_chunks.insert(pos, arc_chunk.clone()); diff --git a/pumpkin/src/block/blocks/composter.rs b/pumpkin/src/block/blocks/composter.rs index a0c23e495..b34511fec 100644 --- a/pumpkin/src/block/blocks/composter.rs +++ b/pumpkin/src/block/blocks/composter.rs @@ -133,7 +133,7 @@ impl ComposterBlock { Uuid::new_v4(), world.clone(), item_position, - EntityType::ITEM, + &EntityType::ITEM, false, ), ItemStack::new(1, &Item::BONE_MEAL), diff --git a/pumpkin/src/block/blocks/fire/fire.rs b/pumpkin/src/block/blocks/fire/fire.rs index 91f529267..bf4d2d5b1 100644 --- a/pumpkin/src/block/blocks/fire/fire.rs +++ b/pumpkin/src/block/blocks/fire/fire.rs @@ -202,7 +202,7 @@ impl BlockBehaviour for FireBlock { let ticks = base_entity.fire_ticks.load(Ordering::Relaxed); if ticks < 0 { base_entity.fire_ticks.store(ticks + 1, Ordering::Relaxed); - } else if base_entity.entity_type == EntityType::PLAYER { + } else if base_entity.entity_type == &EntityType::PLAYER { let rnd_ticks = rand::rng().random_range(1..3); base_entity .fire_ticks diff --git a/pumpkin/src/block/blocks/nether_portal.rs b/pumpkin/src/block/blocks/nether_portal.rs index 49253c4b5..65325c0c6 100644 --- a/pumpkin/src/block/blocks/nether_portal.rs +++ b/pumpkin/src/block/blocks/nether_portal.rs @@ -21,8 +21,10 @@ impl NetherPortalBlock { async fn get_portal_time(world: &Arc, entity: &dyn EntityBase) -> u32 { let entity_type = entity.get_entity().entity_type; - match entity_type { - EntityType::PLAYER => (world.get_player_by_id(entity.get_entity().entity_id).await) + match entity_type.id { + id if id == EntityType::PLAYER.id => (world + .get_player_by_id(entity.get_entity().entity_id) + .await) .map_or(80, |player| match player.gamemode.load() { GameMode::Creative => 0, _ => 80, diff --git a/pumpkin/src/block/blocks/pumpkin.rs b/pumpkin/src/block/blocks/pumpkin.rs index 7f54ef258..402c23b87 100644 --- a/pumpkin/src/block/blocks/pumpkin.rs +++ b/pumpkin/src/block/blocks/pumpkin.rs @@ -33,7 +33,7 @@ impl crate::block::BlockBehaviour for PumpkinBlock { Uuid::new_v4(), args.world.clone(), args.position.to_f64(), - EntityType::ITEM, + &EntityType::ITEM, false, ); let item_entity = diff --git a/pumpkin/src/block/blocks/redstone/comparator.rs b/pumpkin/src/block/blocks/redstone/comparator.rs index 6116452d5..69ec2fe93 100644 --- a/pumpkin/src/block/blocks/redstone/comparator.rs +++ b/pumpkin/src/block/blocks/redstone/comparator.rs @@ -319,7 +319,7 @@ impl ComparatorBlock { .await .into_iter() .filter(|entity| { - entity.get_entity().entity_type == EntityType::ITEM_FRAME + entity.get_entity().entity_type == &EntityType::ITEM_FRAME && entity.get_entity().get_horizontal_facing() == facing }); if let Some(_itemframe) = itemframes.next() { diff --git a/pumpkin/src/block/blocks/redstone/dropper.rs b/pumpkin/src/block/blocks/redstone/dropper.rs index edc88177a..0506ab252 100644 --- a/pumpkin/src/block/blocks/redstone/dropper.rs +++ b/pumpkin/src/block/blocks/redstone/dropper.rs @@ -195,7 +195,7 @@ impl BlockBehaviour for DropperBlock { Uuid::new_v4(), args.world.clone(), position, - EntityType::ITEM, + &EntityType::ITEM, false, ); let rd = rng().random::() * 0.1 + 0.2; diff --git a/pumpkin/src/block/blocks/tnt.rs b/pumpkin/src/block/blocks/tnt.rs index 3cb03c3f0..2a65222ed 100644 --- a/pumpkin/src/block/blocks/tnt.rs +++ b/pumpkin/src/block/blocks/tnt.rs @@ -29,7 +29,7 @@ impl TNTBlock { Uuid::new_v4(), world.clone(), location.to_f64(), - EntityType::TNT, + &EntityType::TNT, false, ); let pos = entity.pos.load(); @@ -81,7 +81,7 @@ impl BlockBehaviour for TNTBlock { Uuid::new_v4(), args.world.clone(), args.position.to_f64(), - EntityType::TNT, + &EntityType::TNT, false, ); let angle = rand::random::() * std::f64::consts::TAU; diff --git a/pumpkin/src/command/args/summonable_entities.rs b/pumpkin/src/command/args/summonable_entities.rs index 14de47f60..cba83be77 100644 --- a/pumpkin/src/command/args/summonable_entities.rs +++ b/pumpkin/src/command/args/summonable_entities.rs @@ -54,7 +54,7 @@ impl DefaultNameArgConsumer for SummonableEntitiesArgumentConsumer { } impl<'a> FindArg<'a> for SummonableEntitiesArgumentConsumer { - type Data = EntityType; + type Data = &'static EntityType; fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { match args.get(name) { diff --git a/pumpkin/src/entity/ai/goal/active_target_goal.rs b/pumpkin/src/entity/ai/goal/active_target_goal.rs index d8d4c345b..42a77567e 100644 --- a/pumpkin/src/entity/ai/goal/active_target_goal.rs +++ b/pumpkin/src/entity/ai/goal/active_target_goal.rs @@ -19,14 +19,14 @@ pub struct ActiveTargetGoal { track_target_goal: TrackTargetGoal, target: Mutex>>, reciprocal_chance: i32, - target_type: EntityType, + target_type: &'static EntityType, target_predicate: TargetPredicate, } impl ActiveTargetGoal { pub async fn new( mob: &MobEntity, - target_type: EntityType, + target_type: &'static EntityType, reciprocal_chance: i32, check_visibility: bool, check_can_navigate: bool, @@ -55,7 +55,7 @@ impl ActiveTargetGoal { #[must_use] pub async fn with_default( mob: &MobEntity, - target_type: EntityType, + target_type: &'static EntityType, check_visibility: bool, ) -> Self { let track_target_goal = TrackTargetGoal::with_default(check_visibility); @@ -74,7 +74,7 @@ impl ActiveTargetGoal { async fn find_closest_target(&self, mob: &MobEntity) { let mut target = self.target.lock().await; let world = mob.living_entity.entity.world.read().await; - if self.target_type == EntityType::PLAYER { + if self.target_type == &EntityType::PLAYER { *target = world .get_closest_player( mob.living_entity.entity.pos.load(), diff --git a/pumpkin/src/entity/ai/goal/look_at_entity.rs b/pumpkin/src/entity/ai/goal/look_at_entity.rs index f0ff57347..2658fbf6f 100644 --- a/pumpkin/src/entity/ai/goal/look_at_entity.rs +++ b/pumpkin/src/entity/ai/goal/look_at_entity.rs @@ -19,7 +19,7 @@ pub struct LookAtEntityGoal { look_time: AtomicI32, chance: f64, look_forward: bool, - target_type: EntityType, + target_type: &'static EntityType, target_predicate: TargetPredicate, } @@ -27,7 +27,7 @@ impl LookAtEntityGoal { #[must_use] pub fn new( mob_weak: Weak, - target_type: EntityType, + target_type: &'static EntityType, range: f64, chance: f64, look_forward: bool, @@ -46,18 +46,22 @@ impl LookAtEntityGoal { } #[must_use] - pub fn with_default(mob_weak: Weak, target_type: EntityType, range: f64) -> Self { + pub fn with_default( + mob_weak: Weak, + target_type: &'static EntityType, + range: f64, + ) -> Self { Self::new(mob_weak, target_type, range, 0.02, false) } fn create_target_predicate( mob_weak: Weak, - target_type: EntityType, + target_type: &'static EntityType, range: f64, ) -> TargetPredicate { let mut target_predicate = TargetPredicate::non_attackable(); target_predicate.base_max_distance = range; - if target_type == EntityType::PLAYER { + if target_type == &EntityType::PLAYER { target_predicate.set_predicate(move |living_entity, _world| { let mob_weak = mob_weak.clone(); async move { @@ -92,7 +96,7 @@ impl Goal for LookAtEntityGoal { drop(mob_target); let world = mob_entity.living_entity.entity.world.read().await; - if self.target_type == EntityType::PLAYER { + if self.target_type == &EntityType::PLAYER { *target = world .get_closest_player(mob_entity.living_entity.entity.pos.load(), self.range) .await diff --git a/pumpkin/src/entity/experience_orb.rs b/pumpkin/src/entity/experience_orb.rs index 23cb90230..45991b7f6 100644 --- a/pumpkin/src/entity/experience_orb.rs +++ b/pumpkin/src/entity/experience_orb.rs @@ -38,7 +38,7 @@ impl ExperienceOrbEntity { Uuid::new_v4(), world.clone(), position, - EntityType::EXPERIENCE_ORB, + &EntityType::EXPERIENCE_ORB, false, ); let orb = Arc::new(Self::new(entity, i)); diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index 2f0fab5cd..768693147 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -258,7 +258,7 @@ impl LivingEntity { ) .await; let params = LootContextParameters { - killed_by_player: cause.map(|c| c.get_entity().entity_type == EntityType::PLAYER), + killed_by_player: cause.map(|c| c.get_entity().entity_type == &EntityType::PLAYER), ..Default::default() }; @@ -267,7 +267,7 @@ impl LivingEntity { let level_info = world.level_info.read().await; let game_rules = &level_info.game_rules; - if self.entity.entity_type == EntityType::PLAYER && game_rules.show_death_messages { + if self.entity.entity_type == &EntityType::PLAYER && game_rules.show_death_messages { //TODO: KillCredit let death_message = if let Some(death_message_type) = damage_type.death_message_type { diff --git a/pumpkin/src/entity/mob/zombie.rs b/pumpkin/src/entity/mob/zombie.rs index d61f16b01..93c3113f4 100644 --- a/pumpkin/src/entity/mob/zombie.rs +++ b/pumpkin/src/entity/mob/zombie.rs @@ -42,7 +42,7 @@ impl Zombie { 8, Arc::new(LookAtEntityGoal::with_default( mob_weak, - EntityType::PLAYER, + &EntityType::PLAYER, 8.0, )), ) @@ -58,7 +58,7 @@ impl Zombie { .add_goal( 2, Arc::new( - ActiveTargetGoal::with_default(&mob_arc.mob_entity, EntityType::PLAYER, true) + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true) .await, ), ) diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 110aabf81..e323abcf0 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -219,7 +219,7 @@ pub struct Entity { /// A persistent, unique identifier for the entity pub entity_uuid: uuid::Uuid, /// The type of entity (e.g., player, zombie, item) - pub entity_type: EntityType, + pub entity_type: &'static EntityType, /// The world in which the entity exists. pub world: Arc>>, /// The entity's current position in the world @@ -286,7 +286,7 @@ impl Entity { entity_uuid: uuid::Uuid, world: Arc, position: Vector3, - entity_type: EntityType, + entity_type: &'static EntityType, invulnerable: bool, ) -> Self { let floor_x = position.x.floor() as i32; @@ -305,7 +305,10 @@ impl Entity { on_ground: AtomicBool::new(false), pos: AtomicCell::new(position), block_pos: AtomicCell::new(BlockPos(Vector3::new(floor_x, floor_y, floor_z))), - chunk_pos: AtomicCell::new(Vector2::new(floor_x, floor_z)), + chunk_pos: AtomicCell::new(Vector2::new( + get_section_cord(floor_x), + get_section_cord(floor_z), + )), sneaking: AtomicBool::new(false), world: Arc::new(RwLock::new(world)), sprinting: AtomicBool::new(false), @@ -436,7 +439,7 @@ impl Entity { } fn default_portal_cooldown(&self) -> u32 { - if self.entity_type == EntityType::PLAYER { + if self.entity_type == &EntityType::PLAYER { 10 } else { 300 diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 064648ffc..d10cfe14a 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -302,7 +302,7 @@ impl Player { player_uuid, world, Vector3::new(0.0, 0.0, 0.0), - EntityType::PLAYER, + &EntityType::PLAYER, matches!(gamemode, GameMode::Creative | GameMode::Spectator), )); @@ -1452,7 +1452,7 @@ impl Player { Uuid::new_v4(), self.world().await, item_pos, - EntityType::ITEM, + &EntityType::ITEM, false, ); diff --git a/pumpkin/src/entity/type.rs b/pumpkin/src/entity/type.rs index 42c3295e0..20aa26750 100644 --- a/pumpkin/src/entity/type.rs +++ b/pumpkin/src/entity/type.rs @@ -10,7 +10,7 @@ use crate::{ }; pub async fn from_type( - entity_type: EntityType, + entity_type: &'static EntityType, position: Vector3, world: &Arc, uuid: Uuid, @@ -18,9 +18,9 @@ pub async fn from_type( let entity = Entity::new(uuid, world.clone(), position, entity_type, false); #[allow(clippy::single_match)] - let mob: Arc = match entity_type { - EntityType::ZOMBIE => Zombie::make(entity).await, - EntityType::PAINTING => Arc::new(PaintingEntity::new(entity)), + let mob: Arc = match entity_type.id { + id if id == EntityType::ZOMBIE.id => Zombie::make(entity).await, + id if id == EntityType::PAINTING.id => Arc::new(PaintingEntity::new(entity)), // TODO _ => Arc::new(entity), // Fallback Entity }; diff --git a/pumpkin/src/item/items/egg.rs b/pumpkin/src/item/items/egg.rs index 3c52e73ff..cf893ce86 100644 --- a/pumpkin/src/item/items/egg.rs +++ b/pumpkin/src/item/items/egg.rs @@ -37,7 +37,7 @@ impl ItemBehaviour for EggItem { Uuid::new_v4(), world.clone(), position, - EntityType::EGG, + &EntityType::EGG, false, ); let egg = ThrownItemEntity::new(entity, &player.living_entity.entity); diff --git a/pumpkin/src/item/items/hoe.rs b/pumpkin/src/item/items/hoe.rs index 70fcb2f99..b56c54d38 100644 --- a/pumpkin/src/item/items/hoe.rs +++ b/pumpkin/src/item/items/hoe.rs @@ -85,7 +85,7 @@ impl ItemBehaviour for HoeItem { Uuid::new_v4(), world.clone(), location, - EntityType::SNOWBALL, + &EntityType::SNOWBALL, false, ); // TODO: Merge stacks together diff --git a/pumpkin/src/item/items/minecart.rs b/pumpkin/src/item/items/minecart.rs index 31f80e350..719c54ce1 100644 --- a/pumpkin/src/item/items/minecart.rs +++ b/pumpkin/src/item/items/minecart.rs @@ -20,14 +20,14 @@ use uuid::Uuid; pub struct MinecartItem; impl MinecartItem { - fn item_to_entity(item: &Item) -> EntityType { + fn item_to_entity(item: &Item) -> &'static EntityType { match item.id { - val if val == Item::MINECART.id => EntityType::MINECART, - val if val == Item::TNT_MINECART.id => EntityType::TNT_MINECART, - val if val == Item::CHEST_MINECART.id => EntityType::CHEST_MINECART, - val if val == Item::HOPPER_MINECART.id => EntityType::HOPPER_MINECART, - val if val == Item::FURNACE_MINECART.id => EntityType::FURNACE_MINECART, - val if val == Item::COMMAND_BLOCK_MINECART.id => EntityType::COMMAND_BLOCK_MINECART, + val if val == Item::MINECART.id => &EntityType::MINECART, + val if val == Item::TNT_MINECART.id => &EntityType::TNT_MINECART, + val if val == Item::CHEST_MINECART.id => &EntityType::CHEST_MINECART, + val if val == Item::HOPPER_MINECART.id => &EntityType::HOPPER_MINECART, + val if val == Item::FURNACE_MINECART.id => &EntityType::FURNACE_MINECART, + val if val == Item::COMMAND_BLOCK_MINECART.id => &EntityType::COMMAND_BLOCK_MINECART, _ => unreachable!(), } } diff --git a/pumpkin/src/item/items/snowball.rs b/pumpkin/src/item/items/snowball.rs index 9530318b6..6757989bd 100644 --- a/pumpkin/src/item/items/snowball.rs +++ b/pumpkin/src/item/items/snowball.rs @@ -36,7 +36,7 @@ impl ItemBehaviour for SnowBallItem { Uuid::new_v4(), world.clone(), position, - EntityType::SNOWBALL, + &EntityType::SNOWBALL, false, ); let snowball = ThrownItemEntity::new(entity, &player.living_entity.entity); diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index e9c57be90..c946f8c7a 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -1709,7 +1709,7 @@ impl JavaClient { async fn spawn_entity_from_egg( &self, player: &Player, - entity_type: EntityType, + entity_type: &'static EntityType, location: BlockPos, face: BlockDirection, ) { diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index dc5ffc215..55b6c9a36 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -1,5 +1,6 @@ use std::sync::Weak; use std::sync::atomic::Ordering::Relaxed; +use std::time::Duration; use std::{ collections::HashMap, sync::{Arc, atomic::Ordering}, @@ -37,6 +38,7 @@ use bytes::BufMut; use explosion::Explosion; use pumpkin_config::BasicConfiguration; use pumpkin_data::data_component_impl::EquipmentSlot; +use pumpkin_data::entity::MobCategory; use pumpkin_data::fluid::{Falling, FluidProperties}; use pumpkin_data::{ Block, @@ -115,6 +117,7 @@ use pumpkin_world::{ }; use pumpkin_world::{level::Level, tick::TickPriority}; use pumpkin_world::{world::BlockFlags, world_info::LevelData}; +use rand::seq::SliceRandom; use rand::{Rng, rng}; use scoreboard::Scoreboard; use serde::Serialize; @@ -125,10 +128,13 @@ use tokio::sync::RwLock; pub mod border; pub mod bossbar; pub mod custom_bossbar; +pub mod natural_spawner; pub mod scoreboard; pub mod weather; +use crate::world::natural_spawner::{SpawnState, spawn_for_chunk}; use pumpkin_data::effect::StatusEffect; +use pumpkin_world::chunk::ChunkHeightmapType::MotionBlocking; use pumpkin_world::generation::settings::GenerationSettings; use uuid::Uuid; use weather::Weather; @@ -176,6 +182,7 @@ pub struct World { /// The type of dimension the world is in. pub dimension_type: VanillaDimensionType, pub sea_level: i32, + pub min_y: i32, /// The world's weather, including rain and thunder levels. pub weather: Mutex, /// Block Behaviour @@ -221,6 +228,7 @@ impl World { weather: Mutex::new(Weather::new()), block_registry, sea_level: generation_settings.sea_level, + min_y: i32::from(generation_settings.shape.min_y), synced_block_event_queue: Mutex::new(Vec::new()), unsent_block_changes: Mutex::new(HashMap::new()), server, @@ -683,12 +691,141 @@ impl World { } } */ + let spawn_entity_clock_start = tokio::time::Instant::now(); + + let mut spawning_chunks_map = HashMap::new(); + // TODO use FixedPlayerDistanceChunkTracker + + for i in self.players.read().await.values() { + let center = i.living_entity.entity.chunk_pos.load(); + for dx in -8i32..=8 { + for dy in -8i32..=8 { + // if dx.abs() <= 2 || dy.abs() <= 2 || dx.abs() >= 6 || dy.abs() >= 6 { // this is only for debug, spawning runs too slow + // continue; + // } + let chunk_pos = center.add_raw(dx, dy); + if let Some(chunk) = self.level.try_get_chunk(&chunk_pos) { + spawning_chunks_map + .entry(chunk_pos) + .or_insert(chunk.value().clone()); + } + } + } + } + + let mut spawning_chunks = Vec::with_capacity(spawning_chunks_map.len()); + for i in spawning_chunks_map { + spawning_chunks.push(i); + } + + let get_chunks_clock = spawn_entity_clock_start.elapsed(); + // log::debug!("spawning chunks size {}", spawning_chunks.len()); + + let mut spawn_state = + SpawnState::new(spawning_chunks.len() as i32, &self.entities, self).await; // TODO store it + + // TODO gamerule this.spawnEnemies || this.spawnFriendlies + let spawn_passives = self.level_time.lock().await.time_of_day % 400 == 0; + let spawn_list: Vec<&'static MobCategory> = + natural_spawner::get_filtered_spawning_categories( + &spawn_state, + true, + true, + spawn_passives, + ); + + // log::debug!("spawning list size {}", spawn_list.len()); + log::debug!("spawning counter {:?}", spawn_state.mob_category_counts); + + spawning_chunks.shuffle(&mut rng()); + + // TODO i think it can be multithread + for (pos, chunk) in &spawning_chunks { + self.tick_spawning_chunk(pos, chunk, &spawn_list, &mut spawn_state) + .await; + } + log::debug!( + "Spawning entity took {:?}, getting chunks {:?}, spawning chunks: {}, avg {:?} per chunk", + spawn_entity_clock_start.elapsed(), + get_chunks_clock, + spawning_chunks.len(), + spawn_entity_clock_start + .elapsed() + .checked_div(spawning_chunks.len() as u32) + .unwrap_or(Duration::new(0, 0)) + ); + for block_entity in tick_data.block_entities { let world: Arc = self.clone(); block_entity.tick(world).await; } } + pub async fn tick_spawning_chunk( + self: &Arc, + chunk_pos: &Vector2, + chunk: &Arc>, + spawn_list: &Vec<&'static MobCategory>, + spawn_state: &mut SpawnState, + ) { + // this.level.tickThunder(chunk); + //TODO check in simulation distance + if self.weather.lock().await.raining + && self.weather.lock().await.thundering + && rng().random_range(0..100_000) == 0 + { + let rand_value = rng().random::() >> 2; + let delta = Vector3::new(rand_value & 15, rand_value >> 16 & 15, rand_value >> 8 & 15); + let random_pos = Vector3::new( + chunk_pos.x << 4, + chunk.read().await.heightmap.get_height( + MotionBlocking, + chunk_pos.x << 4, + chunk_pos.y << 4, + self.min_y, + ), + chunk_pos.y << 4, + ) + .add(&delta); + // TODO this.getBrightness(LightLayer.SKY, blockPos) >= 15; + // TODO heightmap + + // TODO findLightningRod(blockPos) + // TODO encapsulatingFullBlocks + if true { + // TODO biome.getPrecipitationAt(pos, this.getSeaLevel()) == Biome.Precipitation.RAIN + // TODO this.getCurrentDifficultyAt(blockPos); + if rng().random::() < 0.0675 + && self.get_block(&random_pos.to_block_pos().down()).await + != &Block::LIGHTNING_ROD + { + let entity = Entity::new( + Uuid::new_v4(), + self.clone(), + random_pos.to_f64(), + &EntityType::SKELETON_HORSE, + false, + ); + self.spawn_entity(Arc::new(entity)).await; + } + let entity = Entity::new( + Uuid::new_v4(), + self.clone(), + random_pos.to_f64().add_raw(0.5, 0., 0.5), + &EntityType::LIGHTNING_BOLT, + false, + ); + self.spawn_entity(Arc::new(entity)).await; + } + } + + if spawn_list.is_empty() { + return; + } + // TODO this.level.canSpawnEntitiesInChunk(chunkPos) + spawn_for_chunk(self, chunk_pos, chunk, spawn_state, spawn_list).await; + } + pub fn generation_settings(&self) -> &GenerationSettings { // TODO: this is bad match self.dimension_type { @@ -1768,7 +1905,7 @@ impl World { &self, pos: Vector3, radius: f64, - entity_types: Option<&[EntityType]>, + entity_types: Option<&[&'static EntityType]>, ) -> Option> { // Get regular entities let entities = self.get_nearby_entities(pos, radius).await; @@ -2158,7 +2295,7 @@ impl World { f64::from(pos.0.z) + 0.5 + rand::rng().random_range(-0.25..0.25), ); - let entity = Entity::new(Uuid::new_v4(), self.clone(), pos, EntityType::ITEM, false); + let entity = Entity::new(Uuid::new_v4(), self.clone(), pos, &EntityType::ITEM, false); let item_entity = Arc::new(ItemEntity::new(entity, stack).await); self.spawn_entity(item_entity).await; } @@ -2208,7 +2345,7 @@ impl World { Uuid::new_v4(), self.clone(), Vector3::new(x, y, z), - EntityType::ITEM, + &EntityType::ITEM, false, ); let entity = Arc::new(ItemEntity::new_with_velocity(entity, item, velocity, 10).await); diff --git a/pumpkin/src/world/natural_spawner.rs b/pumpkin/src/world/natural_spawner.rs new file mode 100644 index 000000000..777acf8a8 --- /dev/null +++ b/pumpkin/src/world/natural_spawner.rs @@ -0,0 +1,563 @@ +use crate::entity::EntityBase; +use crate::entity::r#type::from_type; +use crate::world::World; +use pumpkin_data::biome::Spawner; +use pumpkin_data::entity::{EntityType, MobCategory, SpawnLocation}; +use pumpkin_data::tag::Block::MINECRAFT_PREVENT_MOB_SPAWNING_INSIDE; +use pumpkin_data::tag::Fluid::{MINECRAFT_LAVA, MINECRAFT_WATER}; +use pumpkin_data::tag::Taggable; +use pumpkin_data::tag::WorldgenBiome::MINECRAFT_REDUCE_WATER_AMBIENT_SPAWNS; +use pumpkin_data::{Block, BlockDirection, BlockState}; +use pumpkin_util::GameMode; +use pumpkin_util::math::get_section_cord; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector2::Vector2; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_world::chunk::{ChunkData, ChunkHeightmapType}; +use rand::seq::IndexedRandom; +use rand::{Rng, rng}; +use std::collections::HashMap; +use std::collections::hash_map::Entry; +use std::fmt; +use std::sync::Arc; +use tokio::sync::RwLock; +use uuid::Uuid; + +const MAGIC_NUMBER: i32 = 17 * 17; + +#[derive(Default, Debug)] +pub struct MobCounts([i32; 8]); + +impl MobCounts { + #[inline] + const fn add(&mut self, category: &'static MobCategory) { + self.0[category.id] += 1; + } + #[inline] + const fn can_spawn(&self, category: &'static MobCategory) -> bool { + self.0[category.id] < category.max + } +} + +pub struct LocalMobCapCalculator { + world: Arc, + player_mob_counts: HashMap, + players_near_chunk: HashMap, Vec>, +} + +impl fmt::Debug for LocalMobCapCalculator { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("LocalMobCapCalculator") + .field("world", &"") + .field("player_mob_counts", &self.player_mob_counts) + .field("players_near_chunk", &self.players_near_chunk) + .finish() + } +} + +impl LocalMobCapCalculator { + pub fn new(world: &Arc) -> Self { + Self { + world: world.clone(), // can anybody get rid of this clone? + player_mob_counts: HashMap::new(), + players_near_chunk: HashMap::new(), + } + } + + const fn calc_distance(chunk_pos: Vector2, player_pos: &Vector3) -> f64 { + let dx = ((chunk_pos.x << 4) + 8) as f64 - player_pos.x; + let dy = ((chunk_pos.y << 4) + 8) as f64 - player_pos.z; + dx * dx + dy * dy + } + + async fn get_players_near<'b>( + players_near_chunk: &'b mut HashMap, Vec>, + world: &Arc, + chunk_pos: &Vector2, + ) -> &'b Vec { + match players_near_chunk.entry(*chunk_pos) { + Entry::Occupied(value) => { + // debug!("chunk {chunk_pos:?} near player {:?}", value.get()); + value.into_mut() + } + Entry::Vacant(entry) => { + let mut players = Vec::new(); + for (_uuid, player) in world.players.read().await.iter() { + if player.gamemode.load() == GameMode::Spectator { + continue; + } + if Self::calc_distance(*chunk_pos, &player.position()) < 16384. { + players.push(player.entity_id()); + } + } + // debug!("chunk {chunk_pos:?} near player {:?}", players); + entry.insert(players) + } + } + } + pub async fn add_mob(&mut self, chunk_pos: &Vector2, category: &'static MobCategory) { + let players = + Self::get_players_near(&mut self.players_near_chunk, &self.world, chunk_pos).await; + for player in players { + self.player_mob_counts + .entry(*player) + .or_default() + .add(category); + } + // debug!("player_mob_counts {:?}", self.player_mob_counts); + // debug!("players_near_chunk {:?}", self.players_near_chunk); + // debug!("chunk_pos {:?}", chunk_pos); + // debug!("players {:?}", players); + } + pub async fn can_spawn( + &mut self, + category: &'static MobCategory, + chunk_pos: &Vector2, + ) -> bool { + let players = + Self::get_players_near(&mut self.players_near_chunk, &self.world, chunk_pos).await; + for player in players { + if let Some(count) = self.player_mob_counts.get(player) { + if count.can_spawn(category) { + return true; + } + } else { + return true; + } + } + false + } +} + +#[derive(Debug)] +struct PointCharge(Vector3, f64); // pos charge + +impl PointCharge { + fn get_potential_change(&self, pos: &BlockPos) -> f64 { + let dst = self.0.sub(&pos.to_f64()).length(); + self.1 / dst + } +} + +#[derive(Default, Debug)] +struct PotentialCalculator(Vec); + +impl PotentialCalculator { + pub fn add_charge(&mut self, pos: &BlockPos, charge: f64) { + if charge != 0. { + self.0.push(PointCharge(pos.to_f64(), charge)); + } + } + pub fn get_potential_energy_change(&self, pos: &BlockPos, charge: f64) -> f64 { + if charge == 0. { + return 0.; + } + let mut sum: f64 = 0.; + for i in &self.0 { + sum += i.get_potential_change(pos); + } + sum * charge + } +} + +pub struct SpawnState { + spawnable_chunk_count: i32, + pub mob_category_counts: MobCounts, + spawn_potential: PotentialCalculator, + local_mob_cap_calculator: LocalMobCapCalculator, + // unmodifiable_mob_category_counts: MobCounts, seems only for debug + last_checked_pos: BlockPos, + last_checked_type: &'static EntityType, + last_charge: f64, +} + +impl fmt::Debug for SpawnState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("SpawnState") + .field("spawnable_chunk_count", &self.spawnable_chunk_count) + .field("mob_category_counts", &self.mob_category_counts) + .field("spawn_potential", &self.spawn_potential) + .field("local_mob_cap_calculator", &self.local_mob_cap_calculator) + .field("last_checked_pos", &self.last_checked_pos) + .field("last_checked_type", &self.last_checked_type.resource_name) + .field("last_charge", &self.last_charge) + .finish() + } +} + +impl SpawnState { + pub async fn new( + chunk_count: i32, + entities: &Arc>>>, + world: &Arc, + ) -> Self { + let mut potential = PotentialCalculator::default(); + let mut local_mob_cap = LocalMobCapCalculator::new(world); + let mut counter = MobCounts::default(); + for entity in entities.read().await.values() { + let entity_type = &entity.get_entity().entity_type; + #[allow(clippy::overly_complex_bool_expr)] + if entity_type.mob && false || entity_type.category == &MobCategory::MISC { + // TODO (mob.isPersistenceRequired() || mob.requiresCustomPersistence()) + continue; + } + let entity_pos = &entity.get_entity().block_pos.load(); + let biome = world.level.get_rough_biome(entity_pos).await; + if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) { + potential.add_charge(entity_pos, cost.charge); + } + if entity_type.mob { + local_mob_cap + .add_mob(&entity.get_entity().chunk_pos.load(), entity_type.category) + .await; + } + counter.add(entity_type.category); + } + Self { + spawnable_chunk_count: chunk_count, + mob_category_counts: counter, + spawn_potential: potential, + local_mob_cap_calculator: local_mob_cap, + // unmodifiable_mob_category_counts: counter, + last_checked_pos: BlockPos::new(i32::MAX, i32::MAX, i32::MAX), + last_checked_type: &EntityType::PLAYER, + last_charge: 0., + } + } + #[inline] + fn can_spawn_for_category_global(&self, category: &'static MobCategory) -> bool { + self.mob_category_counts.0[category.id] + < category.max * self.spawnable_chunk_count / MAGIC_NUMBER + } + async fn can_spawn_for_category_local( + &mut self, + category: &'static MobCategory, + chunk_pos: &Vector2, + ) -> bool { + self.local_mob_cap_calculator + .can_spawn(category, chunk_pos) + .await + } + async fn can_spawn( + &mut self, + entity_type: &'static EntityType, + pos: &BlockPos, + world: &Arc, + ) -> bool { + self.last_checked_pos = *pos; + self.last_checked_type = entity_type; + // TODO get biome + let biome = world.level.get_rough_biome(pos).await; + if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) { + self.last_charge = cost.charge; + self.spawn_potential + .get_potential_energy_change(pos, cost.charge) + <= cost.energy_budget + } else { + self.last_charge = 0.; + true + } + } + async fn after_spawn( + &mut self, + entity_type: &'static EntityType, + pos: &BlockPos, + world: &Arc, + ) { + let charge; + if self.last_checked_pos.eq(pos) && self.last_checked_type == entity_type { + charge = self.last_charge; + } else { + // TODO get biome + let biome = world.level.get_rough_biome(pos).await; + if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) { + charge = cost.charge; + } else { + charge = 0.; + } + } + + self.spawn_potential.add_charge(pos, charge); + self.mob_category_counts.add(entity_type.category); + self.local_mob_cap_calculator + .add_mob( + &Vector2::::new(get_section_cord(pos.0.x), get_section_cord(pos.0.z)), + entity_type.category, + ) + .await; + } +} + +#[must_use] +pub fn get_filtered_spawning_categories( + state: &SpawnState, + spawn_friendlies: bool, + spawn_enemies: bool, + spawn_passives: bool, +) -> Vec<&'static MobCategory> { + let mut ret = Vec::with_capacity(8); + for category in MobCategory::SPAWNING_CATEGORIES { + if (spawn_friendlies || !category.is_friendly) + && (spawn_enemies || category.is_friendly) + && (spawn_passives || !category.is_persistent) + && state.can_spawn_for_category_global(category) + { + ret.push(category); + } + } + ret +} + +pub async fn spawn_for_chunk( + world: &Arc, + chunk_pos: &Vector2, + chunk: &Arc>, + spawn_state: &mut SpawnState, + spawn_list: &Vec<&'static MobCategory>, +) { + // debug!("spawn for chunk {:?}", chunk_pos); + for category in spawn_list { + if spawn_state + .can_spawn_for_category_local(category, chunk_pos) + .await + { + let random_pos = get_random_pos_within(world.min_y, chunk_pos, chunk).await; + // debug!("try random pos: {:?}", random_pos); + if random_pos.0.y > world.min_y { + spawn_category_for_position(category, world, random_pos, chunk_pos, spawn_state) + .await; + } + } + } +} +pub async fn get_random_pos_within( + min_y: i32, + chunk_pos: &Vector2, + chunk: &Arc>, +) -> BlockPos { + let x = (chunk_pos.x << 4) + rng().random_range(0..16); + let z = (chunk_pos.y << 4) + rng().random_range(0..16); + let temp_y = + chunk + .read() + .await + .heightmap + .get_height(ChunkHeightmapType::WorldSurface, x, z, min_y) + + 1; + let y = rng().random_range(min_y..=temp_y); + BlockPos::new(x, y, z) +} + +pub async fn spawn_category_for_position( + category: &'static MobCategory, + world: &Arc, + pos: BlockPos, + chunk_pos: &Vector2, + spawn_state: &mut SpawnState, +) { + // TODO StructureManager structureManager = level.structureManager(); + // TODO blockState.isRedstoneConductor(chunk, pos) is true then return + let mut spawn_cluster_size = 0; + let mut new_pos = pos; + for _ in 0..3 { + let mut new_x = new_pos.0.x; + let mut new_z = new_pos.0.z; + let mut random_group_size = (rng().random::() * 4.).ceil() as i32; + let mut inc = 0; + #[allow(unused_variables)] + let mut group_size = 0; + 'outer: while inc < random_group_size { + new_x += rng().random_range(0..6) - rng().random_range(0..6); + new_z += rng().random_range(0..6) - rng().random_range(0..6); + new_pos = BlockPos::new(new_x, new_pos.0.y, new_z); + let new_pos_center = new_pos.to_centered_f64(); + let player_distance = get_nearest_player(&new_pos_center, world).await; + if player_distance == f64::MAX { + // debug!("player_distance infinity"); + return; + } + if !is_right_distance_to_player_and_spawn_point( + &new_pos, + player_distance, + world, + chunk_pos, + ) { + // debug!("{new_pos:?} failed, too near to player or spawn point dst: {player_distance}"); + inc += 1; + continue; + } + let Some(spawner) = get_random_spawn_mob_at(world, category, &new_pos).await else { + // debug!("{new_pos:?} failed, no random spawn mob at category: {category:?}"); + break 'outer; + }; + random_group_size = rng().random_range(spawner.min_count..=spawner.max_count); + let entity_type = + &EntityType::from_name(spawner.r#type.strip_prefix("minecraft:").unwrap()).unwrap(); + if !is_valid_spawn_position_for_type( + world, + &new_pos, + category, + entity_type, + player_distance, + ) + .await + { + // debug!("{new_pos:?} failed, not valid spawn position"); + inc += 1; + continue; + } + if !spawn_state.can_spawn(entity_type, &new_pos, world).await { + // debug!("{new_pos:?} failed, can't spawn at"); + inc += 1; + continue; + } + let entity = from_type(entity_type, new_pos_center, world, Uuid::new_v4()).await; + entity + .get_entity() + .set_rotation(rng().random::() * 360., 0.); + // TODO isValidPositionForMob(level, mob, f) + // TODO spawnGroupData = mob.finalizeSpawn(level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.NATURAL, spawnGroupData); + spawn_cluster_size += 1; + group_size += 1; + world.spawn_entity(entity).await; + spawn_state.after_spawn(entity_type, &new_pos, world).await; + if spawn_cluster_size >= entity_type.limit_per_chunk { + return; + } + + //TODO mob.isMaxGroupSizeReached(p) + inc += 1; + } + } +} + +pub async fn get_nearest_player(pos: &Vector3, world: &Arc) -> f64 { + let mut dst = f64::MAX; + for (_uuid, player) in world.players.read().await.iter() { + if player.gamemode.load() == GameMode::Spectator { + continue; + } + let cur_dst = player.position().squared_distance_to_vec(*pos); + if cur_dst < dst { + dst = cur_dst; + } + } + dst +} +pub fn is_right_distance_to_player_and_spawn_point( + pos: &BlockPos, + distance: f64, + _world: &Arc, + chunk_pos: &Vector2, +) -> bool { + if distance <= 24. * 24. { + return false; + } + // TODO getSharedSpawnPos/WorldSpawnPoint + if pos.to_centered_f64().squared_distance_to(0., 0., 0.) <= 24. * 24. { + return false; + } + #[allow(clippy::overly_complex_bool_expr)] + #[allow(clippy::nonminimal_bool)] + { + chunk_pos == &Vector2::new(get_section_cord(pos.0.x), get_section_cord(pos.0.z)) || false // TODO canSpawnEntitiesInChunk(ChunkPos chunkPos) + } +} + +#[must_use] +pub async fn get_random_spawn_mob_at( + world: &Arc, + category: &'static MobCategory, + block_pos: &BlockPos, +) -> Option<&'static Spawner> { + // TODO Holder holder = level.getBiome(pos); + let biome = world.level.get_rough_biome(block_pos).await; + if category == &MobCategory::WATER_AMBIENT + && biome.is_tagged_with_by_tag(&MINECRAFT_REDUCE_WATER_AMBIENT_SPAWNS) + && rng().random::() < 0.98f32 + { + None + } else { + // TODO isInNetherFortressBounds(pos, level, cetagory, structureManager) then NetherFortressStructure.FORTRESS_ENEMIES + // TODO structureManager.getAllStructuresAt(pos); ChunkGenerator::getMobsAt + match category.id { + id if id == MobCategory::MONSTER.id => biome.spawners.monster, + id if id == MobCategory::CREATURE.id => biome.spawners.creature, + id if id == MobCategory::AMBIENT.id => biome.spawners.ambient, + id if id == MobCategory::AXOLOTLS.id => biome.spawners.axolotls, + id if id == MobCategory::UNDERGROUND_WATER_CREATURE.id => { + biome.spawners.underground_water_creature + } + id if id == MobCategory::WATER_CREATURE.id => biome.spawners.water_creature, + id if id == MobCategory::WATER_AMBIENT.id => biome.spawners.water_ambient, + id if id == MobCategory::MISC.id => biome.spawners.misc, + _ => panic!(), + } + .choose(&mut rng()) + } +} + +pub async fn is_valid_spawn_position_for_type( + world: &Arc, + block_pos: &BlockPos, + category: &'static MobCategory, + entity_type: &'static EntityType, + distance: f64, +) -> bool { + // TODO !SpawnPlacements.checkSpawnRules(entityType, level, EntitySpawnReason.NATURAL, pos, level.random) + // TODO level.noCollision(entityType.getSpawnAABB(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5)) + !(category == &MobCategory::MISC + || (!entity_type.can_spawn_far_from_player + && distance + > f64::from(entity_type.category.despawn_distance) + * f64::from(entity_type.category.despawn_distance)) + || !entity_type.summonable + || !is_spawn_position_ok(world, block_pos, entity_type).await) +} + +pub async fn is_spawn_position_ok( + world: &Arc, + block_pos: &BlockPos, + entity_type: &'static EntityType, +) -> bool { + match entity_type.spawn_restriction.location { + SpawnLocation::InLava => world + .get_fluid(block_pos) + .await + .is_tagged_with_by_tag(&MINECRAFT_LAVA), + SpawnLocation::InWater => { + // TODO !level.getBlockState(blockPos).isRedstoneConductor(level, blockPos) + world + .get_fluid(block_pos) + .await + .is_tagged_with_by_tag(&MINECRAFT_WATER) + } + SpawnLocation::OnGround => { + let down = world.get_block_state(&block_pos.down()).await; + let up = world.get_block_state(&block_pos.up()).await; + let cur = world.get_block_state(block_pos).await; + if down.is_side_solid(BlockDirection::Up) { + is_valid_empty_spawn_block(cur) && is_valid_empty_spawn_block(up) + } else { + false + } + } + SpawnLocation::Unrestricted => true, + } +} + +#[must_use] +pub fn is_valid_empty_spawn_block(state: &'static BlockState) -> bool { + if state.is_solid() { + false + } else if false { + // TODO isSignalSource + false + } else if state.is_liquid() { + false + } else { + // TODO !entityType.isBlockDangerous(blockState); + !Block::from_state_id(state.id) + .is_tagged_with_by_tag(&MINECRAFT_PREVENT_MOB_SPAWNING_INSIDE) + } +}