Potential spawns hook: borrow the list and keep the fast path without mods

Fix the local player's UUID, merge the SpawnEntry import, and move the
hook docs into the fragments' summaries.
This commit is contained in:
arismoko committed 2026-11-02 14:10:38 +00:00
1 parent 2f470c7a82
commit 84e2ee50ca
6 files changed
+59 -25

No files matched your search

@@ -452,7 +452,6 @@
//! Bodies have no `cfg` and no macros: horizon-mixins refuses a target with
//! either (`tests::hook_points_stay_hookable`).
use super::native_data_maps::NativeDataMapType;
use horizon_client::biome::SpawnEntry;
use super::native_item_energy::ItemEnergyBuffer;
use super::{
Game, GameLevel, LevelDimension, Screen,
@@ -473,7 +472,7 @@ pub(crate) use horizon_client::mod_api::{
};
use horizon_client::{
atlas_sources::qualified,
biome::BiomeColors,
biome::{BiomeColors, SpawnEntry},
block_entities::Pos,
chat::ChatSource,
containers::{
@@ -234,6 +234,9 @@ impl SpawnLevel for GameLevel<'_, '_> {
}
}
impl NaturalLevel for GameLevel<'_, '_> {
fn spawn_hooks(&self) -> bool {
!horizon_client::mod_api::mods().is_empty()
}
fn potential_spawns(&self, category: Category, pos: [i32; 3], entries: &mut Vec<SpawnEntry>) {
let mut canceled = false;
self.game.native_potential_spawns(
@@ -464,7 +467,10 @@ impl<'a> Game<'a> {
/// `level_players` one at a time, without collecting them.
fn level_players_iter(&self) -> impl Iterator<Item = LevelPlayer> + '_ {
let local = self.player_view().map(|p| LevelPlayer {
uuid: self.level.player_uuid,
uuid: self
.level
.player_uuid
.map_or(0, horizon_client::lan::playerdata::ints_uuid),
position: p.position,
bounds: p.bounds,
alive: p.alive,
@@ -21,7 +21,7 @@ use horizon_core::{
random::RandomSource,
};
use serde_json::Value;
use std::{collections::HashMap, fs, path::Path};
use std::{borrow::Cow, collections::HashMap, fs, path::Path};
pub use crate::biome::{SpawnCost, parse_costs, parse_entries};
@@ -301,8 +301,19 @@ fn spawns_far_from_player(s: Species, category: Category) -> bool {
pub type MobType = (Species, f32, f32, Category);
pub trait NaturalLevel: SpawnLevel {
/// Let server mods change the possible spawns at a position.
fn potential_spawns(&self, _category: Category, _pos: [i32; 3], _entries: &mut Vec<SpawnEntry>) {}
/// Whether server mods hear `potential_spawns`.
fn spawn_hooks(&self) -> bool {
false
}
/// `LevelEvent.PotentialSpawns`: server mods change the possible spawns
/// at a position.
fn potential_spawns(
&self,
_category: Category,
_pos: [i32; 3],
_entries: &mut Vec<SpawnEntry>,
) {
}
fn min_y(&self) -> i32;
/// `getMaxY()`: the highest buildable block.
fn max_y(&self) -> i32;
@@ -667,18 +678,23 @@ pub fn fortress_enemies() -> &'static [SpawnEntry] {
/// `NaturalSpawner.mobsAt`: the fortress bounds on nether bricks, then
/// `ChunkGenerator.getMobsAt` (the first referenced structure whose override
/// for the category holds the position), else the biome's list. A spawn
/// attempt looks it up up to twice, allowing mods to change each result.
/// for the category holds the position), else the biome's list, as mods
/// change it. Borrowed from the level when no mod hears it: a spawn attempt
/// looks it up up to twice.
pub fn mobs_at<L: NaturalLevel>(
l: &L,
category: Category,
x: i32,
y: i32,
z: i32,
) -> Vec<SpawnEntry> {
let mut entries = vanilla_mobs_at(l, category, x, y, z).to_vec();
) -> Cow<'_, [SpawnEntry]> {
let entries = vanilla_mobs_at(l, category, x, y, z);
if !l.spawn_hooks() {
return Cow::Borrowed(entries);
}
let mut entries = entries.to_vec();
l.potential_spawns(category, [x, y, z], &mut entries);
entries
Cow::Owned(entries)
}
fn vanilla_mobs_at<L: NaturalLevel>(
@@ -1047,8 +1063,8 @@ pub fn spawn_category_for_position<L: NaturalLevel, R: RandomSource>(
let mut nearest = Nearest::new(start);
for _ in 0..3 {
let (mut x, mut z) = (x0, z0);
// The group's entry and its type.
let mut current: Option<(SpawnEntry, Option<MobType>)> = None;
// The group's entry, its type and where it was picked.
let mut current: Option<(SpawnEntry, Option<MobType>, [i32; 3])> = None;
let mut group_index = 0u32;
let mut max = (random.next_float() * 4.0).ceil() as i32;
let mut ll = 0;
@@ -1078,15 +1094,18 @@ pub fn spawn_category_for_position<L: NaturalLevel, R: RandomSource>(
};
max = sample_count(&entry, random) as i32;
let kind = l.mob_type(&entry.entity);
current = Some((entry, kind));
current = Some((entry, kind, [x, y, z]));
}
let (entry, kind) = current.as_ref().unwrap_or_else(|| unreachable!());
let (entry, kind, picked_at) = current.as_ref().unwrap_or_else(|| unreachable!());
// The list the entry was picked from holds it where it was picked.
let listed = *picked_at == [x, y, z];
let valid = valid_position_for_type(
l,
random,
category,
entry,
*kind,
listed,
[x, y, z],
distance,
);
@@ -1205,12 +1224,15 @@ fn right_distance<L: NaturalLevel>(l: &L, chunk: (i32, i32), pos: [i32; 3], dist
(cx, cz) == chunk || l.can_spawn_in_chunk(cx, cz)
}
/// `isValidSpawnPostitionForType`, for the entry's type (`mob_type`).
/// `listed`: the position is where the entry was picked from `mobsAt`.
#[allow(clippy::too_many_arguments)]
fn valid_position_for_type<L: NaturalLevel, R: RandomSource>(
l: &L,
random: &mut R,
category: Category,
entry: &SpawnEntry,
kind: Option<MobType>,
listed: bool,
pos: [i32; 3],
distance: f64,
) -> bool {
@@ -1225,15 +1247,26 @@ fn valid_position_for_type<L: NaturalLevel, R: RandomSource>(
return false;
}
let [x, y, z] = pos;
if !mobs_at(l, category, x, y, z)
.iter()
.any(|e| same_spawner(e, entry))
{
let hooked = l.spawn_hooks();
let in_list = || {
mobs_at(l, category, x, y, z)
.iter()
.any(|e| same_spawner(e, entry))
};
// `canSpawnMobAt` comes first in vanilla, and mods hear each of its
// lookups. Without them it and `isSpawnPositionOk` only read the level,
// so their order cannot change the answer: the few block reads of the
// placement turn most positions away before the spawn list (structures,
// biome) is looked up again.
if hooked && !in_list() {
return false;
}
if !spawn_rules::is_spawn_position_ok(species, l, x, y, z) {
return false;
}
if !hooked && !listed && !in_list() {
return false;
}
if !spawn_rules::check_spawn_rules(species, l, Reason::Natural, random, x, y, z) {
return false;
}
-4
View File
@@ -934,10 +934,6 @@ API 142 adds an energy buffer on a mod block's block entity (`Capabilities.Energ
API 143 adds data maps (`RegisterDataMapTypesEvent`). `native_data_map_types(types)` pushes the types a mod registers: an `id` (`mod_id:name`, not in `minecraft`) for a `registry`, `minecraft:item` or `minecraft:block` so far; asked once per load of the server's data (a world's first open and every `/reload`). The values come from `data/<mod_id>/data_maps/<registry path>/<name>.json` in the world's data packs and from files a mod pushes through `native_data_pack_files` under the key `data_maps/<mod_id>:<registry path>/<name>`: `{"replace": false, "values": {"minecraft:coal": ..., "#minecraft:logs": ...}, "remove": [...]}`, an entry's value being the data itself or `{"value": ..., "replace": false}`; `replace` clears what earlier files set, a tag gives every entry in it the value, `remove` applies after the file's own values, and a newer value overrides an older one. `native_data_map(registry, type, entry)` reads an entry's value (the JSON the file held) and `native_data_map_entries(registry, type)` lists them. Server side only and not synced to clients; mergers, conditions, other registries and mod datapack registries are not here yet. Bundles need "hooks": 143.
`native_potential_spawns(dimension, category, pos, entries, canceled)` runs on the server when natural spawning builds its possible spawn list (NeoForge's `LevelEvent.PotentialSpawns`, vanilla's `NaturalSpawner.mobsAt`). Mods may add or remove entries; canceling empties the list. Added entries must be globally unique instances. Bundles need "hooks": @API:native_potential_spawns@.
`native_village_siege(dimension, player, attempted_pos, canceled)` runs on the server when a village siege finds a spawn position (NeoForge's `VillageSiegeEvent`, vanilla's `VillageSiege.tryToSetupSiege`). `player` is the player's UUID, `attempted_pos` is the found position, and the siege is the dimension's singleton. Set `canceled` to stop the siege. Bundles need "hooks": @API:native_village_siege@.
API 144 adds a player's or mob's breathing, on the single player server thread and on a dedicated server: `native_living_breathe(entity, kind, can_breathe, consume, refill)` every tick for a player and for a mob with the generic air logic (NeoForge's `LivingBreatheEvent`, vanilla's `LivingEntity.baseTick`), with Horizon's answers as the defaults: set `can_breathe` and how much air `consume` takes or `refill` gives (up to 300); at -20 the entity is set to 0 air and takes 2 drowning damage. It is a free function in `horizon_client::breathing`, so it answers and cannot call helpers. Axolotls and `WaterAnimal`s keep their own air logic. Bundles that use it need "hooks": 144.
API 145 adds blocks and items a native mod declares in code, as NeoForge's `DeferredRegister.Blocks` and `.Items` drained by `RegisterEvent`, instead of one content file each: `native_blocks(blocks)` and `native_items(items)`, asked once at start on every side, take `horizon_mods::content::Block` and `Item`, the declarations a content mod's `data/<id>/horizon/block/` and `item/` files hold (a vanilla template and what differs; `item: true` gives a block its item). Ids are in the mod's namespace. They join the content mods' blocks and items, inside the content mod whose id is that namespace when it is on (its models, textures, loot and recipes), else on their own with the template's look, so their network ids, saves and the joining client's content check are the files' (docs/mods.md). They pass the files' checks; a namespace whose declarations don't is logged and left out. Bundles that use them need `"hooks": 145`.
@@ -13,5 +13,5 @@
}
],
"rust_summary": "Possible natural spawn entries, mutable and cancellable (`native_potential_spawns`).",
"doc_summary": "`native_potential_spawns(dimension, category, pos, entries, canceled)` lets server mods add or remove possible natural spawn entries, or cancel to empty the list. Added entries must be globally unique instances. Bundles need \"hooks\": @API:native_potential_spawns@."
"doc_summary": "`native_potential_spawns(dimension, category, pos, entries, canceled)`, run on the server when natural spawning builds its possible spawn list (NeoForge's `LevelEvent.PotentialSpawns`, vanilla's `NaturalSpawner.mobsAt`). Mods may add or remove entries; canceling empties the list. Added entries must be globally unique instances. Bundles need \"hooks\": @API:native_potential_spawns@."
}
@@ -13,5 +13,5 @@
}
],
"rust_summary": "A village siege found a spawn position, cancellable (`native_village_siege`).",
"doc_summary": "`native_village_siege(dimension, player, attempted_pos, canceled)` lets server mods stop a village siege. Bundles need \"hooks\": @API:native_village_siege@."
"doc_summary": "`native_village_siege(dimension, player, attempted_pos, canceled)`, run on the server when a village siege finds a spawn position (NeoForge's `VillageSiegeEvent`, vanilla's `VillageSiege.tryToSetupSiege`). `player` is the player's UUID, `attempted_pos` is the found position, and the siege is the dimension's singleton. Set `canceled` to stop the siege. Bundles need \"hooks\": @API:native_village_siege@."
}