mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1086c2831a | ||
|
|
fa50f136fe | ||
|
|
061751a023 | ||
|
|
5851a80ce0 | ||
|
|
bf6c0b8a24 | ||
|
|
05f5c2197b | ||
|
|
05114a3673 | ||
|
|
df583d34af | ||
|
|
737657443d | ||
|
|
101d3b0bec | ||
|
|
0359138d94 | ||
|
|
b5ec91342e | ||
|
|
6883b74274 | ||
|
|
655b331b1e | ||
|
|
51c10dd31d | ||
|
|
2be6cda974 | ||
|
|
4c802c1342 | ||
|
|
a32ab2c5e4 | ||
|
|
5f5d0434cb | ||
|
|
11159a1ed5 | ||
|
|
1c16f98098 | ||
|
|
a536b00c30 | ||
|
|
e4d8570fbc | ||
|
|
30da32b83c | ||
|
|
084394a849 | ||
|
|
23c5335910 | ||
|
|
e93427276f | ||
|
|
4464c5d64a | ||
|
|
c41e95bf53 | ||
|
|
3c8a73f86f | ||
|
|
7703ebcf95 | ||
|
|
0b6c48be77 | ||
|
|
136349ae95 | ||
|
|
21773ca7f5 | ||
|
|
8401cde519 | ||
|
|
9bb3fc077e | ||
|
|
2aa9056259 | ||
|
|
8117e2767f | ||
|
|
bc5ec6fb46 | ||
|
|
e5ebeddb90 | ||
|
|
d0b42fb88e | ||
|
|
bca77d1ac4 | ||
|
|
815c09b1f8 | ||
|
|
0104eeaf33 | ||
|
|
729a224eef | ||
|
|
22054eee90 | ||
|
|
52f0e38d41 | ||
|
|
1a80673d50 |
No files matched your search
@@ -865,13 +865,23 @@ const VIEW_DISTANCE: f64 = 64.0;
|
||||
|
||||
impl BlockEntityRenderer {
|
||||
pub fn load(root: &Path, registry: &BlockRegistry) -> crate::Result<Self> {
|
||||
let visibility = crate::models::load_visibility(root, registry)?;
|
||||
Self::load_with_visibility(root, registry, visibility)
|
||||
}
|
||||
/// [`BlockEntityRenderer::load`] over the face visibility the meshes use
|
||||
/// (`MeshTables::visibility`): a clone shares its shapes and their faces.
|
||||
pub fn load_with_visibility(
|
||||
root: &Path,
|
||||
registry: &BlockRegistry,
|
||||
visibility: FaceVisibility,
|
||||
) -> crate::Result<Self> {
|
||||
let all: Value = serde_json::from_slice(&std::fs::read(root.join("entity-models.json"))?)?;
|
||||
let models = MODELS
|
||||
.iter()
|
||||
.filter_map(|(key, layer)| all.get(*layer).map(|v| (*key, ModelPart::parse(v))))
|
||||
.collect();
|
||||
let mut renderer = Self::new(models, registry);
|
||||
renderer.visibility = Some(Mutex::new(crate::models::load_visibility(root, registry)?));
|
||||
renderer.visibility = Some(Mutex::new(visibility));
|
||||
// Sherd items: provides_pottery_pattern -> decorated_pot_pattern asset id.
|
||||
if let Ok(bytes) = std::fs::read(root.join("items.json")) {
|
||||
let items: Value = serde_json::from_slice(&bytes)?;
|
||||
|
||||
@@ -310,6 +310,8 @@ pub struct Streaming {
|
||||
deferred: Vec<Done>,
|
||||
sequence: u64,
|
||||
pub center: (i32, i32),
|
||||
/// The view distance around `center`; below 0, no area there (a dedicated
|
||||
/// server nobody is on).
|
||||
pub radius: i32,
|
||||
/// `/forceload`ed chunks: kept loaded whatever the distance.
|
||||
pub forced: HashSet<(i32, i32)>,
|
||||
@@ -1105,7 +1107,9 @@ impl Streaming {
|
||||
.iter()
|
||||
.filter(|&(&(x, z), p)| {
|
||||
p.is_some()
|
||||
&& i64::from(x - center.0).pow(2) + i64::from(z - center.1).pow(2) > r4 * r4
|
||||
&& (self.radius < 0
|
||||
|| i64::from(x - center.0).pow(2) + i64::from(z - center.1).pow(2)
|
||||
> r4 * r4)
|
||||
&& !ranges.in_extra(x, z, 4)
|
||||
&& !ranges.in_band(x, z, 4)
|
||||
&& !ranges.forced.iter().any(|f| {
|
||||
@@ -1556,6 +1560,16 @@ impl Streaming {
|
||||
fn in_range(&self, x: i32, z: i32, margin: i32) -> bool {
|
||||
self.ranges().in_range(x, z, margin)
|
||||
}
|
||||
/// Rank the 3x3 a chunk loaded to FULL decorates, in one generation
|
||||
/// task's order (`ChunkGenerationTask`: x-outer, z-inner), after every
|
||||
/// chunk ranked before.
|
||||
pub fn rank_full_task(&mut self, x: i32, z: i32) {
|
||||
for dx in -1..=1 {
|
||||
for dz in -1..=1 {
|
||||
self.decoration_order.rank_or_append(x + dx, z + dz);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn band(&self) -> Option<Band> {
|
||||
self.band
|
||||
}
|
||||
@@ -1861,7 +1875,7 @@ impl Ranges<'_> {
|
||||
let (dx, dz) = (gap(x - self.center.0), gap(z - self.center.1));
|
||||
let r = i64::from(self.radius + 1);
|
||||
// The extra areas are squares: their finished square grown by `reach`.
|
||||
dx * dx + dz * dz <= r * r
|
||||
(self.radius >= 0 && dx * dx + dz * dz <= r * r)
|
||||
|| self.in_extra(x, z, 1 + reach)
|
||||
|| self.in_band(x, z, 1 + reach)
|
||||
}
|
||||
@@ -1894,7 +1908,7 @@ impl Ranges<'_> {
|
||||
fn in_main_range(&self, x: i32, z: i32, margin: i32) -> bool {
|
||||
let r = i64::from(self.radius + margin);
|
||||
let (dx, dz) = (i64::from(x - self.center.0), i64::from(z - self.center.1));
|
||||
if dx * dx + dz * dz <= r * r {
|
||||
if self.radius >= 0 && dx * dx + dz * dz <= r * r {
|
||||
return true;
|
||||
}
|
||||
// A forced chunk holds a FORCED ticket at level 31
|
||||
@@ -2529,6 +2543,34 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// An idle dedicated server keeps no area around its centre: only a spawn
|
||||
/// search's chunk (FULL alone: its 3x3 decorated, 5x5 generated) and the
|
||||
/// forced chunks.
|
||||
#[test]
|
||||
fn no_area_around_an_idle_servers_centre() {
|
||||
let forced = HashSet::from([(30, 30)]);
|
||||
let extra = [((5, -3), -1)];
|
||||
let r = Ranges {
|
||||
center: (5, -3),
|
||||
radius: -1,
|
||||
extra: &extra,
|
||||
forced: &forced,
|
||||
cover: None,
|
||||
band: None,
|
||||
};
|
||||
for x in -5i32..=15 {
|
||||
for z in -13i32..=7 {
|
||||
let d = (x - 5).abs().max((z + 3).abs());
|
||||
assert_eq!(r.in_range(x, z, 1), d == 0, "({x}, {z})");
|
||||
assert_eq!(r.chunk_kept(x, z, true), d <= 2, "({x}, {z})");
|
||||
assert_eq!(r.requested(x, z, true), d <= 2, "({x}, {z})");
|
||||
}
|
||||
}
|
||||
assert!(r.in_range(30, 30, 1));
|
||||
let idle = Ranges { extra: &[], ..r };
|
||||
assert!(!idle.requested(5, -3, true) && idle.unloaded(5, -3, true));
|
||||
}
|
||||
|
||||
/// A server player's area (`lan_update_streaming`: the server view
|
||||
/// distance + 1) finishes vanilla's FULL square: PLAYER_LOADING tickets at
|
||||
/// level 31 out to the view distance, FULL (33) two chunks past it. Every
|
||||
|
||||
@@ -143,8 +143,8 @@ const GENES: [&str; 7] = [
|
||||
"aggressive",
|
||||
];
|
||||
const MEMORIES: [&str; 7] = [
|
||||
"long_jump_cooldown",
|
||||
"ram_cooldown",
|
||||
"long_jump_cooling_down",
|
||||
"ram_cooldown_ticks",
|
||||
"attack_target_cooldown",
|
||||
"hunted_recently",
|
||||
"dig_cooldown",
|
||||
@@ -507,6 +507,7 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
difficulty,
|
||||
data: &data,
|
||||
created: Vec::new(),
|
||||
jockey_rider: None,
|
||||
};
|
||||
group = finalize::finalize(&mut cx, &mut mob, reason, group);
|
||||
let created = std::mem::take(&mut cx.created);
|
||||
|
||||
@@ -225,6 +225,8 @@ fn goal_name(m: &Mob, name: &str) -> String {
|
||||
"MoveThroughVillage" => "MoveThroughVillageGoal",
|
||||
"RestrictSun" => "RestrictSunGoal",
|
||||
"FleeSun" => "FleeSunGoal",
|
||||
// `Cat.CatAvoidEntityGoal`, `Fox.FoxAvoidEntityGoal`... name their subclass.
|
||||
"AvoidEntity" if m.species() == Species::Cat => "CatAvoidEntityGoal",
|
||||
"AvoidEntity" => "AvoidEntityGoal",
|
||||
"BowAttack" => "RangedBowAttackGoal",
|
||||
"Swell" => "SwellGoal",
|
||||
|
||||
@@ -796,7 +796,7 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
chunk,
|
||||
&mut gen_random,
|
||||
&mut rules,
|
||||
&mut spawn,
|
||||
&mut |l, r, _| spawn(l, r).is_some(),
|
||||
&mut |e| match e {
|
||||
GenEvent::Column(x, z) => log.push(format!("col {x} {z}")),
|
||||
GenEvent::Spawn(t, p, yaw) => spawns.push(format!(
|
||||
|
||||
@@ -87,6 +87,11 @@ impl Base<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The mobs that save `Age` and `AgeLocked`.
|
||||
fn saves_age(s: crate::mobs::Species) -> bool {
|
||||
use crate::mobs::{Species, drops::can_be_baby, finalize::ageable};
|
||||
s == Species::Tadpole || (ageable(s) && can_be_baby(s))
|
||||
}
|
||||
/// `LivingEntity` + `Mob` (+ `AgeableMob`, creeper and skeleton specifics).
|
||||
/// The mobs whose class extends vanilla's `Animal` (and so save `InLove`):
|
||||
/// not allays, villagers, golems, dolphins or fish.
|
||||
@@ -409,19 +414,14 @@ pub fn mob_to_nbt(m: &Mob, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
|
||||
map.entry(key.into()).or_insert(value);
|
||||
}
|
||||
let id = m.kind.id.as_str();
|
||||
// Only breedable animals have an age; a loaded creature that was saved without
|
||||
// one (a frog) stays without.
|
||||
let aged = raw.is_none_or(|r| r.get("Age").is_some());
|
||||
if (m.kind.category == "creature" || m.species() == crate::mobs::Species::Hoglin) && aged {
|
||||
// AgeableMob.addAdditionalSaveData, only if canBeABaby (not a frog or a
|
||||
// parrot); a tadpole saves its own age.
|
||||
let species = m.species();
|
||||
if saves_age(species) {
|
||||
map.insert("Age".into(), Tag::Int(m.extra.age));
|
||||
map.insert("AgeLocked".into(), Tag::Byte(i8::from(m.extra.age_locked)));
|
||||
map.insert("ForcedAge".into(), Tag::Int(m.extra.forced_age));
|
||||
for (key, value) in [
|
||||
("Age", Tag::Int(0)),
|
||||
("ForcedAge", Tag::Int(0)),
|
||||
("AgeLocked", Tag::Byte(0)),
|
||||
] {
|
||||
map.entry(key.into()).or_insert(value);
|
||||
if species != crate::mobs::Species::Tadpole {
|
||||
map.insert("ForcedAge".into(), Tag::Int(m.extra.forced_age));
|
||||
}
|
||||
}
|
||||
// Animal.addAdditionalSaveData: the love ticks left, whatever the save had.
|
||||
@@ -660,6 +660,8 @@ pub fn mob_to_nbt(m: &Mob, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
|
||||
}
|
||||
"minecraft:villager" => {
|
||||
map.insert("VillagerData".into(), villager_data(m));
|
||||
// A loaded villager with VillagerData is finalized.
|
||||
map.insert("VillagerDataFinalized".into(), Tag::Byte(1));
|
||||
let t = m.extra.trade.as_deref().cloned().unwrap_or_default();
|
||||
map.insert("Xp".into(), Tag::Int(t.xp));
|
||||
map.insert("LastRestock".into(), Tag::Long(t.last_restock));
|
||||
@@ -865,9 +867,64 @@ pub fn mob_to_nbt(m: &Mob, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
|
||||
) {
|
||||
map.insert("IsBaby".into(), Tag::Byte(i8::from(m.is_baby())));
|
||||
}
|
||||
// NeutralMob.addPersistentAngerSaveData; the bee saves its own.
|
||||
if matches!(
|
||||
id,
|
||||
"minecraft:iron_golem"
|
||||
| "minecraft:wolf"
|
||||
| "minecraft:polar_bear"
|
||||
| "minecraft:zombified_piglin"
|
||||
| "minecraft:enderman"
|
||||
) {
|
||||
map.entry("anger_end_time".into()).or_insert(Tag::Long(-1));
|
||||
}
|
||||
if let Some(a) = m.extra.attributes.as_deref() {
|
||||
put_attributes(map, m, a);
|
||||
}
|
||||
// The attribute their load makes, which AttributeMap.pack then saves:
|
||||
// Zombie and Piglin.setBaby (even for an adult) the speed, Rabbit.setVariant
|
||||
// the attack damage.
|
||||
use crate::mobs::finalize::Attr;
|
||||
let made = if crate::mobs::doors::is_zombie(species) || species == crate::mobs::Species::Piglin
|
||||
{
|
||||
Some(Attr::MovementSpeed)
|
||||
} else if species == crate::mobs::Species::Rabbit {
|
||||
Some(Attr::AttackDamage)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(attr) = made {
|
||||
let base = crate::mobs::finalize::base_value(m, attr);
|
||||
let mut list = match map.get(&"attributes".into()) {
|
||||
Some(Tag::List(l)) => l.clone(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let attr_id = format!("minecraft:{}", attr.name());
|
||||
if !list
|
||||
.iter()
|
||||
.any(|t| text(t.get("id")).as_deref() == Some(attr_id.as_str()))
|
||||
{
|
||||
list.push(Tag::compound([
|
||||
("id", string(&attr_id)),
|
||||
("base", Tag::Double(base.to_bits())),
|
||||
]));
|
||||
map.insert("attributes".into(), Tag::List(list));
|
||||
}
|
||||
}
|
||||
// Mob.addAdditionalSaveData: the chances that are not the default.
|
||||
if let Some(chances) = &m.extra.drop_chances {
|
||||
let set: Vec<(&str, Tag)> = crate::mobs::drops::SLOTS
|
||||
.iter()
|
||||
.zip(chances.iter())
|
||||
.filter(|(_, c)| **c != crate::mobs::drops::DEFAULT_DROP_CHANCE)
|
||||
.map(|(slot, c)| (*slot, Tag::Float(c.to_bits())))
|
||||
.collect();
|
||||
if set.is_empty() {
|
||||
map.remove(&"drop_chances".into());
|
||||
} else {
|
||||
map.insert("drop_chances".into(), Tag::compound(set));
|
||||
}
|
||||
}
|
||||
crate::mobs::raider::write_nbt(m, map);
|
||||
crate::mobs::riders::write_nbt(m, map);
|
||||
// LivingEntity equipment: what the mob holds now.
|
||||
@@ -1191,7 +1248,16 @@ pub fn mob_from_nbt(tag: &Tag, types: &HashMap<String, Arc<MobType>>, id: u32) -
|
||||
m.custom_name = custom_name(tag);
|
||||
m.custom_name_text = tag.get("CustomName").map(name_text);
|
||||
m.extra.left_handed = flag(tag, "LeftHanded");
|
||||
if m.kind.category == "creature" || m.species() == crate::mobs::Species::Hoglin {
|
||||
if let Some(Tag::Compound(c)) = tag.get("drop_chances") {
|
||||
let mut chances = [crate::mobs::drops::DEFAULT_DROP_CHANCE; 8];
|
||||
for (chance, slot) in chances.iter_mut().zip(crate::mobs::drops::SLOTS) {
|
||||
if let Some(Tag::Float(v)) = c.get(&slot.into()) {
|
||||
*chance = f32::from_bits(*v);
|
||||
}
|
||||
}
|
||||
m.extra.drop_chances = Some(Box::new(chances));
|
||||
}
|
||||
if saves_age(m.species()) {
|
||||
m.extra.age = number(tag.get("Age")).unwrap_or(0) as i32;
|
||||
m.extra.forced_age = number(tag.get("ForcedAge")).unwrap_or(0) as i32;
|
||||
m.extra.age_locked = flag(tag, "AgeLocked");
|
||||
@@ -3170,6 +3236,33 @@ mod sheep_nbt_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_baby_villager_keeps_its_age() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client")
|
||||
.join(horizon_core::VANILLA_VERSION);
|
||||
if !root.join("items.json").exists() {
|
||||
return;
|
||||
}
|
||||
let data = crate::mobs::MobTypes::load(&root).unwrap();
|
||||
let types: HashMap<String, Arc<MobType>> = data
|
||||
.types
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), Arc::new(v.clone())))
|
||||
.collect();
|
||||
let saved = Tag::compound([
|
||||
("id", string("minecraft:villager")),
|
||||
("Pos", doubles(&[0.5, 64.0, 0.5])),
|
||||
("Age", Tag::Int(-21359)),
|
||||
]);
|
||||
let m = mob_from_nbt(&saved, &types, 1).unwrap();
|
||||
assert_eq!(m.extra.age, -21359);
|
||||
let tag = mob_to_nbt(&m, [1, 2, 3, 4], None);
|
||||
assert_eq!(number(tag.get("Age")), Some(-21359));
|
||||
assert_eq!(number(tag.get("AgeLocked")), Some(0));
|
||||
assert_eq!(number(tag.get("VillagerDataFinalized")), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foxes_mooshrooms_rabbits_and_goats_keep_their_kind() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
|
||||
@@ -13130,6 +13130,7 @@ impl Game<'_> {
|
||||
let (uploads, unloaded) =
|
||||
horizon_client::spans::time("streaming", || self.streaming.update(&mut self.world));
|
||||
self.streaming.extra.truncate(areas);
|
||||
self.spawn_fresh_chunks();
|
||||
if let Some(lod) = &mut self.lod {
|
||||
self.lod_observe
|
||||
.extend(uploads.iter().map(|(key, _)| (key.0, key.2)));
|
||||
@@ -15387,6 +15388,9 @@ fn load_resources(
|
||||
prepared_at.elapsed().as_millis()
|
||||
);
|
||||
}
|
||||
// The block entity renderer's face visibility: the meshes' own, whose
|
||||
// shapes already hold their faces, not a second load of the same tables.
|
||||
let visibility = pack.context.tables.visibility.clone();
|
||||
let rest_loader = if menu {
|
||||
let resources = resources.clone();
|
||||
let root = root.clone();
|
||||
@@ -15401,6 +15405,7 @@ fn load_resources(
|
||||
&assets,
|
||||
&behaviour_json,
|
||||
&gameplay_projection,
|
||||
visibility,
|
||||
&progress,
|
||||
started,
|
||||
)
|
||||
@@ -15428,6 +15433,7 @@ fn load_resources(
|
||||
&assets,
|
||||
&behaviour_json,
|
||||
&gameplay_projection,
|
||||
visibility,
|
||||
progress,
|
||||
started,
|
||||
)?;
|
||||
@@ -15455,6 +15461,7 @@ fn load_world_rest(
|
||||
assets: &Assets,
|
||||
behaviour_json: &serde_json::Value,
|
||||
gameplay_projection: &[horizon_client::blocks::GameplayProjection],
|
||||
visibility: horizon_world::block_render::FaceVisibility,
|
||||
progress: &Progress,
|
||||
started: Instant,
|
||||
) -> Result<WorldRest> {
|
||||
@@ -15499,10 +15506,12 @@ fn load_world_rest(
|
||||
recipes,
|
||||
transformers,
|
||||
providers: Providers::load(root),
|
||||
block_entity_render: horizon_client::block_entity_render::BlockEntityRenderer::load(
|
||||
root,
|
||||
registry_owner.registry,
|
||||
)?,
|
||||
block_entity_render:
|
||||
horizon_client::block_entity_render::BlockEntityRenderer::load_with_visibility(
|
||||
root,
|
||||
registry_owner.registry,
|
||||
visibility,
|
||||
)?,
|
||||
animate,
|
||||
fog,
|
||||
music: horizon_client::music::MusicTables::load(root),
|
||||
|
||||
@@ -113,7 +113,8 @@ impl Game<'_> {
|
||||
"explosion" | "player_explosion" | "bad_respawn_point" => DamageKind::Explosion,
|
||||
"fall" | "ender_pearl" | "fly_into_wall" => DamageKind::Fall,
|
||||
"on_fire" => DamageKind::OnFire,
|
||||
"in_fire" | "campfire" | "hot_floor" => DamageKind::InFire,
|
||||
"in_fire" | "hot_floor" => DamageKind::InFire,
|
||||
"campfire" => DamageKind::Campfire,
|
||||
"lava" => DamageKind::Lava,
|
||||
"drown" => DamageKind::Drown,
|
||||
"dry_out" => DamageKind::DryOut,
|
||||
|
||||
@@ -536,9 +536,9 @@ impl Game<'_> {
|
||||
};
|
||||
spawning::LocalDifficulty::new(base, self.world.day_time, inhabited, moon)
|
||||
}
|
||||
/// Advance every mob one tick, then apply their events.
|
||||
pub(super) fn tick_creatures(&mut self) -> Result<()> {
|
||||
self.attack_ticks = self.attack_ticks.saturating_add(1);
|
||||
/// Generation's animals for the chunks generated since, once lit: chunk
|
||||
/// generation places them whether the level ticks or not.
|
||||
pub(super) fn spawn_fresh_chunks(&mut self) {
|
||||
let fresh: Vec<(i32, i32)> = self
|
||||
.world
|
||||
.chunks
|
||||
@@ -552,6 +552,10 @@ impl Game<'_> {
|
||||
}
|
||||
self.spawn_generation_creatures(key.0, key.1);
|
||||
}
|
||||
}
|
||||
/// Advance every mob one tick, then apply their events.
|
||||
pub(super) fn tick_creatures(&mut self) -> Result<()> {
|
||||
self.attack_ticks = self.attack_ticks.saturating_add(1);
|
||||
self.tick_leads();
|
||||
self.spawn_natural();
|
||||
// ServerChunkCache.tickChunks: the custom spawners tick only with spawn_mobs.
|
||||
@@ -5073,6 +5077,7 @@ pub(super) fn damage_type(kind: horizon_client::mobs::DamageKind, player: bool)
|
||||
K::FallingStalactite => "minecraft:falling_stalactite",
|
||||
K::OnFire => "minecraft:on_fire",
|
||||
K::InFire => "minecraft:in_fire",
|
||||
K::Campfire => "minecraft:campfire",
|
||||
K::Lava => "minecraft:lava",
|
||||
K::Drown => "minecraft:drown",
|
||||
K::DryOut => "minecraft:dry_out",
|
||||
|
||||
@@ -672,7 +672,8 @@ impl Game<'_> {
|
||||
}
|
||||
self.settings.render_distance = props.view_distance.clamp(2, 32);
|
||||
self.settings.simulation_distance = props.simulation_distance.clamp(2, 32);
|
||||
self.streaming.radius = self.settings.render_distance;
|
||||
// Nothing is kept around the spawn while nobody is on.
|
||||
self.streaming.radius = -1;
|
||||
self.settle_spawn()?;
|
||||
let gate_lists = lists.clone();
|
||||
let gate: LoginGate = Arc::new(move |address, uuid, name| {
|
||||
@@ -846,10 +847,39 @@ impl Game<'_> {
|
||||
self.stop_dedicated()
|
||||
}
|
||||
|
||||
/// A dedicated server of its own, not single player's server thread
|
||||
/// (which runs with `options.dedicated` too).
|
||||
pub(super) fn standalone_server(&self) -> bool {
|
||||
self.options.dedicated.is_some() && self.serving.is_none()
|
||||
}
|
||||
/// Load the spawn area and find the world spawn as single player does,
|
||||
/// without keeping a player there.
|
||||
/// without keeping a player there. A dedicated server only finds the world
|
||||
/// spawn (MinecraftServer.setInitialSpawn): a player's spawn is found as
|
||||
/// one joins.
|
||||
pub(super) fn settle_spawn(&mut self) -> Result<()> {
|
||||
let deadline = Instant::now() + Duration::from_secs(600);
|
||||
if self.standalone_server() {
|
||||
if self.level.spawn.is_none() {
|
||||
let [x, y, z] = loop {
|
||||
self.update_world();
|
||||
if let Some(pos) = self.find_world_spawn()? {
|
||||
break pos;
|
||||
}
|
||||
if Instant::now() > deadline {
|
||||
return Err("the spawn area did not load".into());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(2));
|
||||
};
|
||||
self.level.spawn = Some([f64::from(x), f64::from(y), f64::from(z)]);
|
||||
self.level.world_spawn.get_or_insert([x, y, z]);
|
||||
self.streaming.extra.clear();
|
||||
}
|
||||
self.spawn = self
|
||||
.level
|
||||
.spawn
|
||||
.map(|s| horizon_core::geometry::Vec3::new(s[0] + 0.5, s[1], s[2] + 0.5));
|
||||
return Ok(());
|
||||
}
|
||||
while self.player.is_none() {
|
||||
self.update_world();
|
||||
self.spawn_player()?;
|
||||
@@ -862,24 +892,28 @@ impl Game<'_> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The chunks to keep loaded: the players' (lan_update_streaming), or
|
||||
/// the spawn chunk while nobody is online.
|
||||
/// The chunks to keep loaded: the players' (lan_update_streaming). While
|
||||
/// nobody is online a dedicated server keeps none (vanilla has no spawn
|
||||
/// ticket), an integrated one the spawn chunk's.
|
||||
pub(super) fn update_dedicated_streaming(&mut self) {
|
||||
let center = self
|
||||
.lan
|
||||
.as_ref()
|
||||
.and_then(|h| {
|
||||
h.hub
|
||||
.guests
|
||||
.iter()
|
||||
.find(|g| h.hub.here(g))
|
||||
.map(|g| g.chunk())
|
||||
})
|
||||
.or_else(|| {
|
||||
self.spawn
|
||||
.map(|s| ((s.x.floor() as i32) >> 4, (s.z.floor() as i32) >> 4))
|
||||
});
|
||||
if let Some(c) = center {
|
||||
let guest = self.lan.as_ref().and_then(|h| {
|
||||
h.hub
|
||||
.guests
|
||||
.iter()
|
||||
.find(|g| h.hub.here(g))
|
||||
.map(|g| g.chunk())
|
||||
});
|
||||
if self.standalone_server() {
|
||||
self.streaming.radius = if guest.is_some() {
|
||||
self.settings.render_distance
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
}
|
||||
let spawn = self
|
||||
.spawn
|
||||
.map(|s| ((s.x.floor() as i32) >> 4, (s.z.floor() as i32) >> 4));
|
||||
if let Some(c) = guest.or(spawn) {
|
||||
self.streaming.center = c;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,17 @@ impl Game<'_> {
|
||||
while let Some((dx, dz)) = self.spawn_spiral.current() {
|
||||
if self.spawn_spiral.tested() {
|
||||
let (cx, cz) = (ox + dx, oz + dz);
|
||||
if !self.streaming.is_lit(cx, cz) {
|
||||
if self.standalone_server() {
|
||||
// getChunk(FULL) of the tested chunk alone: its 3x3 is decorated.
|
||||
if !self.world.chunks.contains_key(&(cx, cz)) {
|
||||
let area = [((cx, cz), -1)];
|
||||
if self.streaming.extra != area {
|
||||
self.streaming.extra = area.to_vec();
|
||||
self.streaming.rank_full_task(cx, cz);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
} else if !self.streaming.is_lit(cx, cz) {
|
||||
self.streaming.center = (cx, cz);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! it reads, the spawning chunks and caps of `ServerChunkCache.tickChunks`,
|
||||
//! and the mobs it adds (finalized with the game's `finalizeSpawn`).
|
||||
use super::{Dim, Game};
|
||||
use horizon_client::vanillagen::VanillaTerrain;
|
||||
use horizon_client::{
|
||||
biome::{BiomeInfo, SpawnEntry},
|
||||
blocks::BlockFacts,
|
||||
@@ -19,9 +20,12 @@ use horizon_client::{
|
||||
};
|
||||
use horizon_core::{
|
||||
geometry::{Aabb, Vec3},
|
||||
random::{LegacyRandom, RandomSource, WorldgenRandom},
|
||||
random::{LegacyRandom, RandomSource, WorldgenRandom, XoroshiroRandom},
|
||||
};
|
||||
use horizon_world::{
|
||||
nbt::{Compound, Tag},
|
||||
registry::BlockStateId,
|
||||
};
|
||||
use horizon_world::registry::BlockStateId;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Reused buffers for one pass over the spawning chunks: the candidate chunks
|
||||
@@ -162,6 +166,16 @@ impl GameLevel<'_, '_> {
|
||||
let chunk = self.game.world.chunks.get(&(x >> 4, z >> 4))?;
|
||||
Some(self.game.res.biomes().get(chunk.biome(x, y, z)))
|
||||
}
|
||||
/// The block whose noise cell `BiomeManager.getBiome` reads.
|
||||
fn zoomed(&self, x: i32, y: i32, z: i32) -> [i32; 3] {
|
||||
let zoom = self.game.streaming.biome_zoom;
|
||||
let [qx, qy, qz] = zoom.quart_at_block(horizon_core::position::BlockPos::new(x, y, z));
|
||||
[qx << 2, qy << 2, qz << 2]
|
||||
}
|
||||
fn zoomed_biome(&self, x: i32, y: i32, z: i32) -> Option<&BiomeInfo> {
|
||||
let [x, y, z] = self.zoomed(x, y, z);
|
||||
self.biome(x, y, z)
|
||||
}
|
||||
}
|
||||
impl SpawnLevel for GameLevel<'_, '_> {
|
||||
fn state(&self, x: i32, y: i32, z: i32) -> BlockStateId {
|
||||
@@ -176,7 +190,9 @@ impl SpawnLevel for GameLevel<'_, '_> {
|
||||
fn block_tag(&self, tag: &str, block: &str) -> bool {
|
||||
self.w.block_tag(tag, block)
|
||||
}
|
||||
// Level.getBiome: the zoomed biome.
|
||||
fn biome_tag(&self, tag: &str, x: i32, y: i32, z: i32) -> bool {
|
||||
let [x, y, z] = self.zoomed(x, y, z);
|
||||
self.w.biome_tag(tag, x, y, z)
|
||||
}
|
||||
fn sky_light(&self, x: i32, y: i32, z: i32) -> i32 {
|
||||
@@ -242,11 +258,13 @@ impl NaturalLevel for GameLevel<'_, '_> {
|
||||
Category::from_name(&t.category),
|
||||
))
|
||||
}
|
||||
// The `natural_mob_spawns` attribute reads the zoomed biome
|
||||
// (fullResolutionBiomes), the spawn probability the noise cell.
|
||||
fn biome_spawns(&self, x: i32, y: i32, z: i32) -> Option<&HashMap<String, Vec<SpawnEntry>>> {
|
||||
self.biome(x, y, z).map(|b| &b.spawns)
|
||||
self.zoomed_biome(x, y, z).map(|b| &b.spawns)
|
||||
}
|
||||
fn spawn_cost(&self, entity: &str, x: i32, y: i32, z: i32) -> Option<SpawnCost> {
|
||||
self.biome(x, y, z)?.spawn_costs.get(entity).copied()
|
||||
self.zoomed_biome(x, y, z)?.spawn_costs.get(entity).copied()
|
||||
}
|
||||
fn creature_probability(&self, x: i32, y: i32, z: i32) -> f32 {
|
||||
self.biome(x, y, z).map_or(0.1, |b| b.creature_probability)
|
||||
@@ -580,37 +598,68 @@ impl<'a> Game<'a> {
|
||||
}
|
||||
/// `NoiseBasedChunkGenerator.spawnOriginalMobs`: creature packs for a
|
||||
/// freshly generated chunk, from the biome at its corner column's top,
|
||||
/// with the chunk's decoration-seeded random (mobs are not generated in
|
||||
/// with the chunk's decoration-seeded random, finalized with the
|
||||
/// region's random as generation adds them (mobs are not generated in
|
||||
/// the End).
|
||||
pub(super) fn spawn_generation_creatures(&mut self, cx: i32, cz: i32) {
|
||||
if !self.world.chunks.contains_key(&(cx, cz)) || self.dim == Dim::End {
|
||||
return;
|
||||
}
|
||||
let Some(terrain) = self.end.locator() else {
|
||||
return;
|
||||
};
|
||||
let mut region = terrain.region_random(cx, cz);
|
||||
let any = region.as_any();
|
||||
let added = if let Some(r) = any.downcast_mut::<LegacyRandom>() {
|
||||
self.generation_creatures(cx, cz, &terrain, r)
|
||||
} else if let Some(r) = any.downcast_mut::<XoroshiroRandom>() {
|
||||
self.generation_creatures(cx, cz, &terrain, r)
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
for nbt in added {
|
||||
self.load_mob(Tag::Compound(nbt), None);
|
||||
}
|
||||
}
|
||||
fn generation_creatures<Q: RandomSource + 'static>(
|
||||
&self,
|
||||
cx: i32,
|
||||
cz: i32,
|
||||
terrain: &VanillaTerrain,
|
||||
region: &mut Q,
|
||||
) -> Vec<Compound> {
|
||||
let mut random = WorldgenRandom::new(LegacyRandom::new(0));
|
||||
random.set_decoration_seed(self.level.seed, cx * 16, cz * 16);
|
||||
// The region's own random: no creature's spawn predicate draws from it.
|
||||
let mut rules = LegacyRandom::new(self.level.seed ^ (i64::from(cx) << 32 | i64::from(cz)));
|
||||
let mut pending = Vec::new();
|
||||
{
|
||||
let level = self.natural_level();
|
||||
let source = [cx * 16, level.max_y(), cz * 16];
|
||||
let mut spawn = |l: &GameLevel, r: &SpawnRequest| {
|
||||
let spawned = l.game.finalize_spawned(r, &mut pending);
|
||||
if let Some(s) = &spawned {
|
||||
l.pending.borrow_mut().push(s.bounds);
|
||||
let mut added = Vec::new();
|
||||
let mut group = None;
|
||||
let level = self.natural_level();
|
||||
let source = [cx * 16, level.max_y(), cz * 16];
|
||||
let mut spawn = |l: &GameLevel, r: &SpawnRequest, region: &mut Q| {
|
||||
// Each pack starts without group data.
|
||||
if r.group_index == 0 {
|
||||
group = None;
|
||||
}
|
||||
let [x, y, z] = r.position;
|
||||
let (bx, by, bz) = (x.floor() as i32, y.floor() as i32, z.floor() as i32);
|
||||
let biome = l.zoomed_biome(bx, by, bz).map_or("", |b| b.name.as_str());
|
||||
let at = Vec3::new(x, y, z);
|
||||
match terrain.spawn_generated(r.entity, at, r.yaw, biome, region, &mut group) {
|
||||
Some(nbt) => {
|
||||
added.push(nbt);
|
||||
true
|
||||
}
|
||||
spawned
|
||||
};
|
||||
spawn_selection::spawn_mobs_for_chunk_generation(
|
||||
&level,
|
||||
source,
|
||||
(cx, cz),
|
||||
&mut random,
|
||||
&mut rules,
|
||||
&mut spawn,
|
||||
&mut |_| {},
|
||||
);
|
||||
}
|
||||
self.add_spawned(pending, true);
|
||||
None => false,
|
||||
}
|
||||
};
|
||||
spawn_selection::spawn_mobs_for_chunk_generation(
|
||||
&level,
|
||||
source,
|
||||
(cx, cz),
|
||||
&mut random,
|
||||
region,
|
||||
&mut spawn,
|
||||
&mut |_| {},
|
||||
);
|
||||
added
|
||||
}
|
||||
}
|
||||
@@ -1172,8 +1172,23 @@ impl Hub {
|
||||
}
|
||||
}
|
||||
NetEvent::Packet(conn, packet) => {
|
||||
if let Some(g) = self.guest_mut(conn) {
|
||||
g.inbox.push_back(*packet);
|
||||
let Some(g) = self.guest_mut(conn) else {
|
||||
continue;
|
||||
};
|
||||
// A kicked player's packets wait for nothing.
|
||||
if !g.left_reason.is_empty() {
|
||||
continue;
|
||||
}
|
||||
g.inbox.push_back(*packet);
|
||||
// A client sending faster than its budget is handled
|
||||
// would grow the inbox until the server runs out of memory.
|
||||
if g.inbox.len() > INBOX_LIMIT {
|
||||
g.inbox = Default::default();
|
||||
events.push(HostEvent::Warn(format!(
|
||||
"{} sent more packets than the server handles ({INBOX_LIMIT} waiting)",
|
||||
g.name
|
||||
)));
|
||||
self.kick(conn, translatable("disconnect.exceeded_packet_rate", &[]));
|
||||
}
|
||||
}
|
||||
NetEvent::Duplicate(uuid) => {
|
||||
@@ -2126,7 +2141,7 @@ impl Hub {
|
||||
let g = &mut self.guests[i];
|
||||
g.first_good = g.position;
|
||||
g.known_moves = g.received_moves;
|
||||
let mut budget = 400;
|
||||
let mut budget = PACKETS_PER_TICK;
|
||||
while budget > 0 {
|
||||
let Some(packet) = self.guests[i].inbox.pop_front() else {
|
||||
break;
|
||||
@@ -2833,6 +2848,12 @@ fn wrap_degrees(degrees: f32) -> f32 {
|
||||
wrapped
|
||||
}
|
||||
|
||||
/// The packets of one player handled in a tick; the rest wait for the next.
|
||||
const PACKETS_PER_TICK: usize = 400;
|
||||
/// The packets a player may have waiting (a second of its budget) before it is
|
||||
/// disconnected, as vanilla's rate-limit does: no real client gets near it.
|
||||
const INBOX_LIMIT: usize = 20 * PACKETS_PER_TICK;
|
||||
|
||||
fn invalid_movement() -> Tag {
|
||||
translatable("multiplayer.disconnect.invalid_player_movement", &[])
|
||||
}
|
||||
|
||||
@@ -525,6 +525,8 @@ pub enum DamageKind {
|
||||
FallingStalactite,
|
||||
OnFire,
|
||||
InFire,
|
||||
/// `campfire`: standing in a lit campfire (`CampfireBlock.entityInside`).
|
||||
Campfire,
|
||||
Lava,
|
||||
Drown,
|
||||
/// `dry_out`: an axolotl out of water for too long.
|
||||
@@ -657,6 +659,7 @@ impl Damage {
|
||||
| DamageKind::Spit
|
||||
| DamageKind::Stalagmite
|
||||
| DamageKind::Starve
|
||||
| DamageKind::Campfire
|
||||
)
|
||||
}
|
||||
/// DamageTypeTags.BYPASSES_ARMOR
|
||||
@@ -686,6 +689,7 @@ impl Damage {
|
||||
self.kind,
|
||||
DamageKind::OnFire
|
||||
| DamageKind::InFire
|
||||
| DamageKind::Campfire
|
||||
| DamageKind::Lava
|
||||
| DamageKind::Fireball
|
||||
| DamageKind::SulfurCubeHot
|
||||
@@ -699,6 +703,7 @@ impl Damage {
|
||||
| DamageKind::Fall
|
||||
| DamageKind::OnFire
|
||||
| DamageKind::InFire
|
||||
| DamageKind::Campfire
|
||||
| DamageKind::Lava
|
||||
| DamageKind::Drown
|
||||
| DamageKind::DryOut
|
||||
|
||||
@@ -668,6 +668,18 @@ fn effects_from_blocks(m: &mut Mob, w: &MobWorld) {
|
||||
"minecraft:cactus" => {
|
||||
m.damage(1.0, Damage::of(DamageKind::Cactus));
|
||||
}
|
||||
// CampfireBlock.entityInside: a lit campfire hurts a
|
||||
// living entity by its fire damage (1, soul 2).
|
||||
name @ ("minecraft:campfire" | "minecraft:soul_campfire")
|
||||
if w.prop(x, y, z, "lit") == Some("true") =>
|
||||
{
|
||||
let damage = if name == "minecraft:campfire" {
|
||||
1.0
|
||||
} else {
|
||||
2.0
|
||||
};
|
||||
m.damage(damage, Damage::of(DamageKind::Campfire));
|
||||
}
|
||||
// WebBlock.entityInside -> makeStuckInBlock, which a
|
||||
// spider ignores for cobwebs and a wither always.
|
||||
"minecraft:cobweb"
|
||||
@@ -1130,6 +1142,9 @@ fn ai_step(m: &mut Mob, w: &MobWorld, level_random: &mut LegacyRandom, events: &
|
||||
// setAge at the age boundary: refreshDimensions.
|
||||
if m.extra.age >= 0 {
|
||||
m.fudge_position_after_size_change(w, bw, bh);
|
||||
if m.species() == Species::Villager {
|
||||
super::villager_brain::age_boundary_reached(m, w, level_random, events);
|
||||
}
|
||||
// Turtle.ageBoundaryReached: a grown turtle drops a scute.
|
||||
if m.species() == Species::Turtle {
|
||||
events.push(MobEvent::GiftLoot {
|
||||
|
||||
@@ -444,4 +444,15 @@ mod tests {
|
||||
}
|
||||
assert!(hiding_in_shell(&m), "the model shows the shell");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn campfire_damage_is_fire_and_has_no_knockback_but_does_not_cause_panic() {
|
||||
// `campfire` is in #is_fire and #no_knockback, but in neither
|
||||
// #panic_causes nor #panic_environmental_causes (#4037).
|
||||
let source = Damage::of(DamageKind::Campfire);
|
||||
assert!(source.is_fire());
|
||||
assert!(source.no_knockback());
|
||||
assert!(!source.causes_panic());
|
||||
assert!(!environmental(DamageKind::Campfire));
|
||||
}
|
||||
}
|
||||
@@ -389,6 +389,10 @@ pub struct Ctx<'a, L: FinalizeLevel + ?Sized, R: RandomSource> {
|
||||
/// Jockeys and mounts made along the way, finalized, with their riding
|
||||
/// links set (`Extra::mount` / `Extra::rider`).
|
||||
pub created: Vec<Mob>,
|
||||
/// The rider as it was when its new chicken was added to the level, before
|
||||
/// its equipment and attributes: what a generated chunk keeps of it
|
||||
/// (`ProtoChunk.addEntity` saves the chicken with its passenger then).
|
||||
pub jockey_rider: Option<Mob>,
|
||||
}
|
||||
|
||||
// ---- attributes -----------------------------------------------------------
|
||||
@@ -835,12 +839,12 @@ pub fn goat_spawn(m: &mut Mob) {
|
||||
.random
|
||||
.next_int_between_inclusive(600, 1200)
|
||||
.unwrap_or(600);
|
||||
set_memory(m, "long_jump_cooldown", i64::from(jump), i64::MAX);
|
||||
set_memory(m, "long_jump_cooling_down", i64::from(jump), i64::MAX);
|
||||
let ram = m
|
||||
.random
|
||||
.next_int_between_inclusive(600, 6000)
|
||||
.unwrap_or(600);
|
||||
set_memory(m, "ram_cooldown", i64::from(ram), i64::MAX);
|
||||
set_memory(m, "ram_cooldown_ticks", i64::from(ram), i64::MAX);
|
||||
m.extra.screaming = m.random.next_double() < 0.02;
|
||||
age_boundary_reached(m);
|
||||
if !m.is_baby() && m.random.next_float() < 0.1 {
|
||||
@@ -1290,6 +1294,7 @@ impl<L: FinalizeLevel + ?Sized, R: RandomSource> Ctx<'_, L, R> {
|
||||
self.finalize(&mut chicken, Reason::Jockey, None);
|
||||
chicken.extra.jockey = true;
|
||||
self.ride(m, &mut chicken);
|
||||
self.jockey_rider = Some(m.clone());
|
||||
self.created.push(chicken);
|
||||
}
|
||||
}
|
||||
@@ -1641,7 +1646,7 @@ impl<L: FinalizeLevel + ?Sized, R: RandomSource> Ctx<'_, L, R> {
|
||||
S::Frog => {
|
||||
self.variant(m, |r| &r.frog);
|
||||
let jump = self.uniform(100, 140);
|
||||
set_memory(m, "long_jump_cooldown", i64::from(jump), i64::MAX);
|
||||
set_memory(m, "long_jump_cooling_down", i64::from(jump), i64::MAX);
|
||||
self.ageable(m, g)
|
||||
}
|
||||
S::ZombieNautilus => {
|
||||
@@ -2024,9 +2029,9 @@ fn set_memory(m: &mut Mob, name: &'static str, value: i64, ttl: i64) {
|
||||
/// cooldown (600 to 6000), drawn from `random`.
|
||||
pub fn goat_init_memories(m: &mut Mob, random: &mut impl RandomSource) {
|
||||
let jump = random.next_int_between_inclusive(600, 1200).unwrap_or(600);
|
||||
set_memory(m, "long_jump_cooldown", i64::from(jump), i64::MAX);
|
||||
set_memory(m, "long_jump_cooling_down", i64::from(jump), i64::MAX);
|
||||
let ram = random.next_int_between_inclusive(600, 6000).unwrap_or(600);
|
||||
set_memory(m, "ram_cooldown", i64::from(ram), i64::MAX);
|
||||
set_memory(m, "ram_cooldown_ticks", i64::from(ram), i64::MAX);
|
||||
}
|
||||
/// `Panda.Gene.getRandom` (enum order: normal, lazy, worried, playful,
|
||||
/// brown, weak, aggressive).
|
||||
|
||||
@@ -36,7 +36,7 @@ const SCAN_RATE: i32 = 20;
|
||||
/// `FrogAi.TIME_BETWEEN_LONG_JUMPS` and the brain memory
|
||||
/// `LONG_JUMP_COOLDOWN_TICKS` (saved with the others, as `finalize` sets it).
|
||||
const TIME_BETWEEN_LONG_JUMPS: (i32, i32) = (100, 140);
|
||||
const LONG_JUMP_COOLDOWN: &str = "long_jump_cooldown";
|
||||
const LONG_JUMP_COOLDOWN: &str = "long_jump_cooling_down";
|
||||
/// `FrogAi`'s `LongJumpToPreferredBlock`: `MAX_LONG_JUMP_WIDTH`,
|
||||
/// `MAX_LONG_JUMP_HEIGHT`, `MAX_JUMP_VELOCITY_MULTIPLIER` and the chance to
|
||||
/// look for a lily pad or big dripleaf first.
|
||||
|
||||
@@ -181,6 +181,7 @@ pub fn protection_points(g: &Gear, kind: DamageKind) -> f32 {
|
||||
kind,
|
||||
DamageKind::OnFire
|
||||
| DamageKind::InFire
|
||||
| DamageKind::Campfire
|
||||
| DamageKind::Lava
|
||||
| DamageKind::Fireball
|
||||
| DamageKind::SulfurCubeHot
|
||||
|
||||
@@ -39,7 +39,7 @@ struct Candidate {
|
||||
}
|
||||
|
||||
/// The lowered head and the ram behaviours' state. `RAM_COOLDOWN_TICKS` is a
|
||||
/// brain memory (`ram_cooldown`), saved with the others.
|
||||
/// brain memory (`ram_cooldown_ticks`), saved with the others.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct State {
|
||||
lower_head_tick: i32,
|
||||
@@ -96,7 +96,7 @@ fn set_ram_cooldown(m: &mut Mob, ticks: i32) {
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
const RAM_COOLDOWN: &str = "ram_cooldown";
|
||||
const RAM_COOLDOWN: &str = "ram_cooldown_ticks";
|
||||
/// `Goat.getRammingXHeadRot`.
|
||||
pub fn ramming_x_head_rot(m: &Mob) -> f32 {
|
||||
let max = if m.is_baby() { 52.5_f32 } else { 30.0 };
|
||||
|
||||
@@ -535,11 +535,6 @@ pub fn to_nbt(m: &Mob, map: &mut horizon_world::nbt::Compound) {
|
||||
map.remove(&"Owner".into());
|
||||
}
|
||||
map.insert("Sitting".into(), Tag::Byte(0));
|
||||
// AgeableMob and Animal (a water creature is not saved with them above).
|
||||
map.insert("Age".into(), Tag::Int(m.extra.age));
|
||||
map.insert("AgeLocked".into(), Tag::Byte(i8::from(m.extra.age_locked)));
|
||||
map.insert("InLove".into(), Tag::Int(m.extra.in_love.max(0)));
|
||||
map.entry("ForcedAge".into()).or_insert(Tag::Int(0));
|
||||
if zombie(m) {
|
||||
let v = VARIANTS[usize::from(m.extra.variant).min(1)];
|
||||
map.insert(
|
||||
@@ -573,14 +568,6 @@ pub fn to_nbt(m: &Mob, map: &mut horizon_world::nbt::Compound) {
|
||||
}
|
||||
pub fn from_nbt(m: &mut Mob, tag: &horizon_world::nbt::Tag) {
|
||||
use horizon_world::nbt::Tag;
|
||||
let int = |k: &str| match tag.get(k) {
|
||||
Some(Tag::Int(v)) => Some(*v),
|
||||
Some(Tag::Byte(v)) => Some(i32::from(*v)),
|
||||
_ => None,
|
||||
};
|
||||
m.extra.age = int("Age").unwrap_or(0);
|
||||
m.extra.age_locked = int("AgeLocked").unwrap_or(0) != 0;
|
||||
m.extra.in_love = int("InLove").unwrap_or(0);
|
||||
m.extra.owner = crate::save::uuid_from_tag(tag.get("Owner"));
|
||||
m.extra.tame = m.extra.owner.is_some();
|
||||
if let Some(Tag::String(v)) = tag.get("variant") {
|
||||
|
||||
@@ -539,18 +539,21 @@ pub fn spawn_aabb(s: Species, x: f64, y: f64, z: f64, width: f32, height: f32) -
|
||||
Vec3::new(x + half, y + f64::from(scale * height), z + half),
|
||||
)
|
||||
}
|
||||
/// `checkSpawnObstruction` of a freshly created mob with the given box.
|
||||
/// `checkSpawnObstruction` of a freshly created mob with the given box, with
|
||||
/// the mobs already `spawned` in the way; None in a generation region, which
|
||||
/// has no entities (`WorldGenRegion.getEntities`).
|
||||
pub(crate) fn spawn_obstruction<L: NaturalLevel>(
|
||||
s: Species,
|
||||
l: &L,
|
||||
bounds: Aabb,
|
||||
spawned: &[Aabb],
|
||||
spawned: Option<&[Aabb]>,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
) -> bool {
|
||||
let unobstructed =
|
||||
|l: &L| l.unobstructed(bounds) && !spawned.iter().any(|b| b.intersects(bounds));
|
||||
let unobstructed = |l: &L| {
|
||||
spawned.is_none_or(|s| l.unobstructed(bounds) && !s.iter().any(|b| b.intersects(bounds)))
|
||||
};
|
||||
match obstruction(s) {
|
||||
Obstruction::NoLiquidUnobstructed => !contains_any_liquid(l, bounds) && unobstructed(l),
|
||||
Obstruction::Unobstructed => unobstructed(l),
|
||||
@@ -1084,7 +1087,7 @@ pub fn spawn_category_for_position<L: NaturalLevel, R: RandomSource>(
|
||||
let far = distance > despawn && removed_when_far(species);
|
||||
if far
|
||||
|| !mob_spawn_rules(species, l, x, y, z)
|
||||
|| !spawn_obstruction(species, l, bounds, &spawned, x, y, z)
|
||||
|| !spawn_obstruction(species, l, bounds, Some(&spawned), x, y, z)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1237,8 +1240,8 @@ pub enum GenEvent {
|
||||
Spawn(String, [f64; 3], f32),
|
||||
}
|
||||
/// `NaturalSpawner.spawnMobsForChunkGeneration`: creature packs placed on
|
||||
/// the chunk's surface. `rules` is the level's own random, which the spawn
|
||||
/// predicates draw from.
|
||||
/// the chunk's surface. `rules` is the region's random, which the spawn
|
||||
/// predicates and `spawn` (finalizeSpawn) draw from.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spawn_mobs_for_chunk_generation<L: NaturalLevel, R: RandomSource, Q: RandomSource>(
|
||||
l: &L,
|
||||
@@ -1246,7 +1249,7 @@ pub fn spawn_mobs_for_chunk_generation<L: NaturalLevel, R: RandomSource, Q: Rand
|
||||
chunk: (i32, i32),
|
||||
random: &mut R,
|
||||
rules: &mut Q,
|
||||
spawn: &mut dyn FnMut(&L, &SpawnRequest) -> Option<Spawned>,
|
||||
spawn: &mut dyn FnMut(&L, &SpawnRequest, &mut Q) -> bool,
|
||||
trace: &mut dyn FnMut(GenEvent),
|
||||
) {
|
||||
if !l.spawn_mobs() {
|
||||
@@ -1269,7 +1272,6 @@ pub fn spawn_mobs_for_chunk_generation<L: NaturalLevel, R: RandomSource, Q: Rand
|
||||
};
|
||||
let count = sample_count(&entry, random);
|
||||
let mut group_index = 0u32;
|
||||
let mut spawned: Vec<Aabb> = Vec::new();
|
||||
let mut x = xo + random.next_int_bound(16).unwrap_or(0);
|
||||
let mut z = zo + random.next_int_bound(16).unwrap_or(0);
|
||||
let (start_x, start_z) = (x, z);
|
||||
@@ -1311,7 +1313,7 @@ pub fn spawn_mobs_for_chunk_generation<L: NaturalLevel, R: RandomSource, Q: Rand
|
||||
let yaw = random.next_float() * 360.0;
|
||||
let bounds = spawn_box(fx, fy, fz, width, height);
|
||||
if mob_spawn_rules(species, l, bx, pos[1], bz)
|
||||
&& spawn_obstruction(species, l, bounds, &spawned, bx, pos[1], bz)
|
||||
&& spawn_obstruction(species, l, bounds, None, bx, pos[1], bz)
|
||||
{
|
||||
let request = SpawnRequest {
|
||||
entity: &entry.entity,
|
||||
@@ -1321,9 +1323,8 @@ pub fn spawn_mobs_for_chunk_generation<L: NaturalLevel, R: RandomSource, Q: Rand
|
||||
group_index,
|
||||
reason: Reason::ChunkGeneration,
|
||||
};
|
||||
if let Some(result) = spawn(l, &request) {
|
||||
if spawn(l, &request, rules) {
|
||||
group_index += 1;
|
||||
spawned.push(result.bounds);
|
||||
trace(GenEvent::Spawn(entry.entity.clone(), [fx, fy, fz], yaw));
|
||||
success = true;
|
||||
}
|
||||
|
||||
@@ -584,7 +584,15 @@ impl SpawnerExt for Spawner {
|
||||
let rules_ok = data.custom_spawn_rules.is_some()
|
||||
|| spawn_selection::mob_spawn_rules(s, l, own[0], own[1], own[2]);
|
||||
if !rules_ok
|
||||
|| !spawn_selection::spawn_obstruction(s, l, bounds, &added, own[0], own[1], own[2])
|
||||
|| !spawn_selection::spawn_obstruction(
|
||||
s,
|
||||
l,
|
||||
bounds,
|
||||
Some(&added),
|
||||
own[0],
|
||||
own[1],
|
||||
own[2],
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -839,11 +839,6 @@ pub fn register_cat(m: &mut Mob) {
|
||||
g.add(2, SitWhenOrdered);
|
||||
g.add(3, CatRelaxOnOwner::default());
|
||||
g.add(4, CatTempt(super::goals::Tempt::new(0.6, food, true)));
|
||||
g.add(
|
||||
4,
|
||||
super::goals::AvoidEntity::new(super::goals::Avoid::Player, 16.0, 0.8, 1.33)
|
||||
.when(|m| !m.extra.tame),
|
||||
);
|
||||
g.add(
|
||||
5,
|
||||
CatLieOnBlock(super::goals::MoveToBlock {
|
||||
@@ -858,6 +853,13 @@ pub fn register_cat(m: &mut Mob) {
|
||||
g.add(10, Breed::new(0.8));
|
||||
g.add(11, Stroll::avoiding_water(0.8));
|
||||
g.add(12, LookAtPlayer::new(10.0));
|
||||
// `reassessTameGoals` adds the avoidance after `registerGoals`: goals run
|
||||
// in insertion order, so it comes last.
|
||||
g.add(
|
||||
4,
|
||||
super::goals::AvoidEntity::new(super::goals::Avoid::Player, 16.0, 0.8, 1.33)
|
||||
.when(|m| !m.extra.tame),
|
||||
);
|
||||
m.ai.targets.add(
|
||||
1,
|
||||
NearestTarget::species(&[Species::Rabbit], false).when(|m, _| !m.extra.tame),
|
||||
|
||||
@@ -1081,7 +1081,15 @@ impl TrialSpawnerExt for TrialSpawner {
|
||||
width * scale,
|
||||
height * scale,
|
||||
);
|
||||
if !spawn_selection::spawn_obstruction(species, l, bounds, &[], own[0], own[1], own[2]) {
|
||||
if !spawn_selection::spawn_obstruction(
|
||||
species,
|
||||
l,
|
||||
bounds,
|
||||
Some(&[]),
|
||||
own[0],
|
||||
own[1],
|
||||
own[2],
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let request = SpawnerSpawn {
|
||||
|
||||
@@ -55,6 +55,8 @@ pub fn register(m: &mut Mob) {
|
||||
// ZombifiedPiglin.addBehaviourGoals: neutral until provoked.
|
||||
let g = &mut m.ai.goals;
|
||||
g.add(1, BreakDoor::zombie());
|
||||
// `Zombie.registerGoals`, which `ZombifiedPiglin` extends.
|
||||
g.add(4, AttackTurtleEgg::default());
|
||||
g.add(8, LookAtPlayer::new(8.0));
|
||||
g.add(8, RandomLookAround::default());
|
||||
g.add(2, MeleeAttack::new(1.0, false).zombie());
|
||||
|
||||
@@ -503,6 +503,28 @@ pub fn tick(m: &mut Mob, w: &MobWorld, lr: &mut LegacyRandom, events: &mut Vec<M
|
||||
sync(m, w.game_time);
|
||||
}
|
||||
|
||||
/// `Villager.ageBoundaryReached`: a villager that grew up gets a new brain
|
||||
/// (the schedule it follows is the adult's), as `refreshBrain` builds it.
|
||||
pub fn age_boundary_reached(
|
||||
m: &mut Mob,
|
||||
w: &MobWorld,
|
||||
lr: &mut LegacyRandom,
|
||||
events: &mut Vec<MobEvent>,
|
||||
) {
|
||||
if brain(m).is_none() {
|
||||
return;
|
||||
}
|
||||
let mut list = std::mem::take(&mut brain_mut(m).list);
|
||||
let mut cx = Bx {
|
||||
w,
|
||||
lr,
|
||||
events,
|
||||
now: w.game_time,
|
||||
refresh: false,
|
||||
};
|
||||
refresh(m, &mut list, usize::MAX, &mut cx);
|
||||
}
|
||||
|
||||
/// `Villager.refreshBrain`: stop every running behaviour, then rebuild the
|
||||
/// brain from the remembered memories.
|
||||
fn refresh(m: &mut Mob, list: &mut [Entry], current: usize, cx: &mut Bx) {
|
||||
|
||||
@@ -21,6 +21,7 @@ use horizon_world::{
|
||||
GeneratorData, GeneratorWorker, OverworldGenerator, ProtoChunk, SurfaceSample,
|
||||
},
|
||||
overrides::WorldgenOverrides,
|
||||
structure::legacy::PieceRandom,
|
||||
terrain::PreLightChunkStage,
|
||||
},
|
||||
};
|
||||
@@ -252,6 +253,24 @@ impl VanillaTerrain {
|
||||
pub fn set_spawn_conditions(&self, difficulty: u8, day_time: i64) {
|
||||
self.structure_mobs.set_conditions(difficulty, day_time);
|
||||
}
|
||||
/// The random a chunk's generation steps draw from (`WorldGenRegion`).
|
||||
pub fn region_random(&self, cx: i32, cz: i32) -> Box<dyn PieceRandom> {
|
||||
self.generator.region_random(ChunkPos::new(cx, cz))
|
||||
}
|
||||
/// A mob chunk generation spawns, as the chunk saves it
|
||||
/// (`StructureMobFinalizer::spawn_generated`).
|
||||
pub fn spawn_generated(
|
||||
&self,
|
||||
kind: &str,
|
||||
position: horizon_core::geometry::Vec3,
|
||||
yaw: f32,
|
||||
biome: &str,
|
||||
random: &mut dyn PieceRandom,
|
||||
group: &mut Option<crate::mobs::finalize::Group>,
|
||||
) -> Option<horizon_world::nbt::Compound> {
|
||||
self.structure_mobs
|
||||
.spawn_generated(kind, position, yaw, biome, random, group)
|
||||
}
|
||||
/// A proto chunk's blocks, biomes, scheduled ticks and post-processing
|
||||
/// marks as a column.
|
||||
fn column(&self, chunk: &ProtoChunk) -> Chunk {
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
items::Items,
|
||||
mobs::{
|
||||
Mob, MobType, MobTypes,
|
||||
finalize::{self, Ctx, FinalizeLevel, Reason, SpawnData, SpawnRegistries},
|
||||
finalize::{self, Ctx, FinalizeLevel, Group, Reason, SpawnData, SpawnRegistries},
|
||||
spawning::{LocalDifficulty, MOON_BRIGHTNESS},
|
||||
},
|
||||
};
|
||||
@@ -140,6 +140,8 @@ fn run<R: RandomSource>(
|
||||
difficulty: LocalDifficulty,
|
||||
data: &SpawnData<'_>,
|
||||
m: &mut Mob,
|
||||
reason: Reason,
|
||||
group: &mut Option<Group>,
|
||||
) -> Vec<Mob> {
|
||||
let mut cx = Ctx {
|
||||
level,
|
||||
@@ -147,8 +149,13 @@ fn run<R: RandomSource>(
|
||||
difficulty,
|
||||
data,
|
||||
created: Vec::new(),
|
||||
jockey_rider: None,
|
||||
};
|
||||
cx.finalize(m, Reason::Structure, None);
|
||||
*group = cx.finalize(m, reason, group.take());
|
||||
if let Some(rider) = cx.jockey_rider {
|
||||
debug_assert_eq!(rider.id, m.id);
|
||||
*m = rider;
|
||||
}
|
||||
cx.created
|
||||
}
|
||||
|
||||
@@ -159,6 +166,18 @@ fn tree(mobs: &[(Mob, Option<Tag>)], i: usize, dimension: &str) -> Compound {
|
||||
return Compound::new();
|
||||
};
|
||||
c.remove(&"UUID".into());
|
||||
// Keys in vanilla's templates that no entity reads: the chunk keeps the
|
||||
// entity as its save writes it (ProtoChunk.addEntity), without them.
|
||||
for key in [
|
||||
"CatType",
|
||||
"OwnerUUID",
|
||||
"Riches",
|
||||
"Willing",
|
||||
"food_level",
|
||||
"lastRestock",
|
||||
] {
|
||||
c.remove(&key.into());
|
||||
}
|
||||
entity_nbt::put_brain(&mut c, m, dimension);
|
||||
let riders: Vec<Tag> = (0..mobs.len())
|
||||
.filter(|&j| mobs[j].0.extra.mount == Some(m.id))
|
||||
@@ -178,23 +197,78 @@ impl StructureMobs for StructureMobFinalizer {
|
||||
structures: &[String],
|
||||
random: &mut dyn PieceRandom,
|
||||
) -> bool {
|
||||
let Some(data) = self.data.get_or_init(|| {
|
||||
let data = load(&self.root);
|
||||
if data.is_none() {
|
||||
log::error!("Horizon: no mob data, structure mobs are finalized when loaded");
|
||||
}
|
||||
data
|
||||
}) else {
|
||||
let Some(data) = self.data() else {
|
||||
return false;
|
||||
};
|
||||
let mut raw = Tag::Compound(nbt.clone());
|
||||
if let Tag::Compound(c) = &mut raw {
|
||||
c.remove(&"horizon:finalize".into());
|
||||
}
|
||||
let Some(mut m) = entity_nbt::mob_from_nbt(&raw, &data.types, 1) else {
|
||||
let Some(m) = entity_nbt::mob_from_nbt(&raw, &data.types, 1) else {
|
||||
// Not a mob: vanilla finalizes only Mob instances.
|
||||
return true;
|
||||
};
|
||||
let reason = Reason::Structure;
|
||||
match self.finish(
|
||||
data,
|
||||
m,
|
||||
Some(raw),
|
||||
biome,
|
||||
structures,
|
||||
random,
|
||||
reason,
|
||||
&mut None,
|
||||
) {
|
||||
Some(saved) => {
|
||||
*nbt = saved;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StructureMobFinalizer {
|
||||
fn data(&self) -> Option<&Data> {
|
||||
self.data
|
||||
.get_or_init(|| {
|
||||
let data = load(&self.root);
|
||||
if data.is_none() {
|
||||
log::error!("Horizon: no mob data, structure mobs are finalized when loaded");
|
||||
}
|
||||
data
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
/// A mob chunk generation spawns (`spawnOriginalMobs`): a new one of
|
||||
/// `kind` finalized as a structure mob is, as the chunk saves it; `group`
|
||||
/// carries a pack's group data from one member to the next.
|
||||
pub fn spawn_generated(
|
||||
&self,
|
||||
kind: &str,
|
||||
position: Vec3,
|
||||
yaw: f32,
|
||||
biome: &str,
|
||||
random: &mut dyn PieceRandom,
|
||||
group: &mut Option<Group>,
|
||||
) -> Option<Compound> {
|
||||
let data = self.data()?;
|
||||
let m = Mob::new(1, data.types.get(kind)?.clone(), position, yaw);
|
||||
let reason = Reason::ChunkGeneration;
|
||||
self.finish(data, m, None, biome, &[], random, reason, group)
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn finish(
|
||||
&self,
|
||||
data: &Data,
|
||||
mut m: Mob,
|
||||
raw: Option<Tag>,
|
||||
biome: &str,
|
||||
structures: &[String],
|
||||
random: &mut dyn PieceRandom,
|
||||
reason: Reason,
|
||||
group: &mut Option<Group>,
|
||||
) -> Option<Compound> {
|
||||
let base = self.difficulty.load(Ordering::Relaxed);
|
||||
let day_time = self.day_time.load(Ordering::Relaxed);
|
||||
let mut level = Level {
|
||||
@@ -216,21 +290,20 @@ impl StructureMobs for StructureMobFinalizer {
|
||||
};
|
||||
let any = random.as_any();
|
||||
let created = if let Some(r) = any.downcast_mut::<LegacyRandom>() {
|
||||
run(&mut level, r, difficulty, &spawn, &mut m)
|
||||
run(&mut level, r, difficulty, &spawn, &mut m, reason, group)
|
||||
} else if let Some(r) = any.downcast_mut::<XoroshiroRandom>() {
|
||||
run(&mut level, r, difficulty, &spawn, &mut m)
|
||||
run(&mut level, r, difficulty, &spawn, &mut m, reason, group)
|
||||
} else {
|
||||
log::error!("Horizon: unknown region random, structure mobs are finalized when loaded");
|
||||
return false;
|
||||
return None;
|
||||
};
|
||||
let mut mobs = vec![(m, Some(raw))];
|
||||
let mut mobs = vec![(m, raw)];
|
||||
mobs.extend(created.into_iter().map(|c| (c, None)));
|
||||
let root = mobs
|
||||
.iter()
|
||||
.position(|(m, _)| m.extra.mount.is_none())
|
||||
.unwrap_or(0);
|
||||
*nbt = tree(&mobs, root, &self.dimension);
|
||||
true
|
||||
Some(tree(&mobs, root, &self.dimension))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// Block states by block name and by tag name ([`FeatureBlockBindings::bind_tags`]).
|
||||
pub type BoundBlockTags = (
|
||||
Arc<BTreeMap<String, Arc<BTreeSet<BlockStateId>>>>,
|
||||
Arc<BTreeMap<String, Arc<BTreeSet<BlockStateId>>>>,
|
||||
);
|
||||
pub struct FeatureBlockBindings<'r> {
|
||||
registry: &'r BlockRegistry,
|
||||
pub(crate) simple_blocks: Option<(
|
||||
@@ -27,8 +32,8 @@ pub struct FeatureBlockBindings<'r> {
|
||||
biomes: Option<Arc<super::biome_predicate::BiomePredicateBindings>>,
|
||||
pub(crate) fluids: super::fluid_predicate::FluidPredicateBindings,
|
||||
pub(crate) codec: BlockStateJsonCodec<'r>,
|
||||
pub(crate) blocks: BTreeMap<String, Arc<BTreeSet<BlockStateId>>>,
|
||||
pub(crate) tags: BTreeMap<String, Arc<BTreeSet<BlockStateId>>>,
|
||||
pub(crate) blocks: Arc<BTreeMap<String, Arc<BTreeSet<BlockStateId>>>>,
|
||||
pub(crate) tags: Arc<BTreeMap<String, Arc<BTreeSet<BlockStateId>>>>,
|
||||
pub(crate) air: [BlockStateId; 3],
|
||||
tree_blocks: std::sync::OnceLock<Arc<super::tree_blocks::TreeBlocks>>,
|
||||
pub(crate) structures: Option<Arc<super::structure::StructureResources>>,
|
||||
@@ -126,8 +131,15 @@ impl<'r> FeatureBlockBindings<'r> {
|
||||
integers: BTreeSet<(String, String)>,
|
||||
tags: &BTreeMap<String, Vec<String>>,
|
||||
) -> Result<Self, DecodeError> {
|
||||
let codec = BlockStateJsonCodec::new(registry, integers)
|
||||
.map_err(|e| DecodeError::Registry(e.to_string()))?;
|
||||
let bound = Self::bind_tags(registry, tags)?;
|
||||
Self::with_bound_tags(registry, integers, bound)
|
||||
}
|
||||
/// Every block's states and every tag's, by name: what `with_tags` binds,
|
||||
/// to share between the bindings made over the same registry and tags.
|
||||
pub fn bind_tags(
|
||||
registry: &BlockRegistry,
|
||||
tags: &BTreeMap<String, Vec<String>>,
|
||||
) -> Result<BoundBlockTags, DecodeError> {
|
||||
let mut blocks = BTreeMap::<String, BTreeSet<BlockStateId>>::new();
|
||||
for id in 0..registry.len() {
|
||||
let id = BlockStateId(id as u32);
|
||||
@@ -153,6 +165,25 @@ impl<'r> FeatureBlockBindings<'r> {
|
||||
return Err(DecodeError::Field("duplicate block tag"));
|
||||
}
|
||||
}
|
||||
Ok((
|
||||
Arc::new(
|
||||
blocks
|
||||
.into_iter()
|
||||
.map(|(name, states)| (name, Arc::new(states)))
|
||||
.collect(),
|
||||
),
|
||||
Arc::new(bound_tags),
|
||||
))
|
||||
}
|
||||
/// [`FeatureBlockBindings::with_tags`] over tags already bound by
|
||||
/// [`FeatureBlockBindings::bind_tags`].
|
||||
pub fn with_bound_tags(
|
||||
registry: &'r BlockRegistry,
|
||||
integers: BTreeSet<(String, String)>,
|
||||
(blocks, tags): BoundBlockTags,
|
||||
) -> Result<Self, DecodeError> {
|
||||
let codec = BlockStateJsonCodec::new(registry, integers)
|
||||
.map_err(|e| DecodeError::Registry(e.to_string()))?;
|
||||
let get = |name| {
|
||||
registry
|
||||
.default_state(name)
|
||||
@@ -167,11 +198,8 @@ impl<'r> FeatureBlockBindings<'r> {
|
||||
occlusion_faces: None,
|
||||
can_occlude: None,
|
||||
fluids: super::fluid_predicate::FluidPredicateBindings::new(registry)?,
|
||||
blocks: blocks
|
||||
.into_iter()
|
||||
.map(|(name, states)| (name, Arc::new(states)))
|
||||
.collect(),
|
||||
tags: bound_tags,
|
||||
blocks,
|
||||
tags,
|
||||
tree_blocks: std::sync::OnceLock::new(),
|
||||
structures: None,
|
||||
air: [
|
||||
|
||||
@@ -28,7 +28,7 @@ use std::{
|
||||
|
||||
struct RegionBlockQueries {
|
||||
fluids: super::fluid_predicate::FluidPredicateBindings,
|
||||
tags: BTreeMap<String, Arc<BTreeSet<BlockStateId>>>,
|
||||
tags: Arc<BTreeMap<String, Arc<BTreeSet<BlockStateId>>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -184,6 +184,9 @@ pub struct GeneratorData {
|
||||
biome_carvers: Vec<Vec<Arc<WorldCarver>>>,
|
||||
carving_blocks: CarvingBlocks,
|
||||
pub(super) block_tags: BTreeMap<String, Vec<String>>,
|
||||
/// `block_tags` bound to block states, once for the feature bindings of
|
||||
/// decoration, structures and live features (`feature_block_bindings`).
|
||||
bound_block_tags: std::sync::OnceLock<super::block_feature::BoundBlockTags>,
|
||||
pub(super) biome_tags: BTreeMap<String, Vec<String>>,
|
||||
pub(super) feature_resources: Option<FeatureResources>,
|
||||
/// Templates, processor lists and per-state template transforms.
|
||||
@@ -557,6 +560,7 @@ impl GeneratorData {
|
||||
feature_resources,
|
||||
ring_cache: Mutex::new(Vec::new()),
|
||||
decorations: Mutex::new(Vec::new()),
|
||||
bound_block_tags: std::sync::OnceLock::new(),
|
||||
}))
|
||||
}
|
||||
/// The concentric ring positions (strongholds) of the generator for
|
||||
@@ -676,6 +680,21 @@ impl GeneratorData {
|
||||
pub(super) fn integers(&self) -> &BTreeSet<(String, String)> {
|
||||
&self.integers
|
||||
}
|
||||
/// Feature block bindings over this data's blocks and block tags. The tags'
|
||||
/// state sets are bound once and shared: each binding held 5 MB of them.
|
||||
pub(super) fn feature_block_bindings(
|
||||
&self,
|
||||
) -> Result<super::block_feature::FeatureBlockBindings<'static>, DecodeError> {
|
||||
use super::block_feature::FeatureBlockBindings;
|
||||
let bound = match self.bound_block_tags.get() {
|
||||
Some(bound) => bound.clone(),
|
||||
None => {
|
||||
let bound = FeatureBlockBindings::bind_tags(self.registry, &self.block_tags)?;
|
||||
self.bound_block_tags.get_or_init(|| bound).clone()
|
||||
}
|
||||
};
|
||||
FeatureBlockBindings::with_bound_tags(self.registry, self.integers.clone(), bound)
|
||||
}
|
||||
}
|
||||
|
||||
/// A generated chunk with its quart biomes. Blocks and heightmaps live in
|
||||
@@ -1454,6 +1473,17 @@ impl<R: RandomSource> Pipeline<R>
|
||||
where
|
||||
R::Positional: Clone + Send + Sync,
|
||||
{
|
||||
/// WorldGenRegion's level random, positioned at the center chunk.
|
||||
fn region_random_at(&self, center: ChunkPos) -> R {
|
||||
self.root
|
||||
.from_hash_of("minecraft:worldgen_region_random")
|
||||
.fork_positional()
|
||||
.at(BlockPos::new(
|
||||
center.x.wrapping_shl(4),
|
||||
0,
|
||||
center.z.wrapping_shl(4),
|
||||
))
|
||||
}
|
||||
/// ChunkGenerator.getBaseHeight: the first free Y above the noise column
|
||||
/// (with aquifers, without terrain adaptation) for a heightmap type.
|
||||
fn base_height(&self, x: i32, z: i32, kind: HeightmapType) -> i32 {
|
||||
@@ -2032,6 +2062,7 @@ trait SeededPipeline: Send + Sync {
|
||||
fn forget_structure_starts(&self);
|
||||
fn ring_positions(&self, worker: &mut WorkerState, set: &str) -> Option<Vec<ChunkPos>>;
|
||||
fn spawn_position(&self) -> Result<[i32; 2], GeneratorError>;
|
||||
fn region_random(&self, center: ChunkPos) -> Box<dyn super::structure::legacy::PieceRandom>;
|
||||
fn locate(
|
||||
&self,
|
||||
worker: &mut WorkerState,
|
||||
@@ -2070,17 +2101,7 @@ where
|
||||
) -> Result<DecorateStats, GeneratorError> {
|
||||
match &self.decoration {
|
||||
Some(decoration) => {
|
||||
let center = region[4].position();
|
||||
// WorldGenRegion's level random, positioned at the center chunk.
|
||||
let mut random = self
|
||||
.root
|
||||
.from_hash_of("minecraft:worldgen_region_random")
|
||||
.fork_positional()
|
||||
.at(BlockPos::new(
|
||||
center.x.wrapping_shl(4),
|
||||
0,
|
||||
center.z.wrapping_shl(4),
|
||||
));
|
||||
let mut random = self.region_random_at(region[4].position());
|
||||
decoration.decorate(
|
||||
&self.data,
|
||||
self.seed,
|
||||
@@ -2169,6 +2190,9 @@ where
|
||||
set,
|
||||
)
|
||||
}
|
||||
fn region_random(&self, center: ChunkPos) -> Box<dyn super::structure::legacy::PieceRandom> {
|
||||
Box::new(self.region_random_at(center))
|
||||
}
|
||||
fn spawn_position(&self) -> Result<[i32; 2], GeneratorError> {
|
||||
if let Some(position) = self.spawn_position.get() {
|
||||
return Ok(*position);
|
||||
@@ -2918,6 +2942,14 @@ impl OverworldGenerator {
|
||||
pub fn spawn_position(&self) -> Result<[i32; 2], GeneratorError> {
|
||||
self.pipeline.spawn_position()
|
||||
}
|
||||
/// A new `WorldGenRegion` random centred on the chunk: what its generation
|
||||
/// steps draw from (`level.getRandom()`).
|
||||
pub fn region_random(
|
||||
&self,
|
||||
center: ChunkPos,
|
||||
) -> Box<dyn super::structure::legacy::PieceRandom> {
|
||||
self.pipeline.region_random(center)
|
||||
}
|
||||
/// Create per-thread state. Workers are cheap; keep one per thread.
|
||||
pub fn worker(&self) -> GeneratorWorker {
|
||||
let data = self.pipeline.data();
|
||||
|
||||
@@ -134,10 +134,10 @@ impl Decoration {
|
||||
.collect(),
|
||||
&data.biome_tags,
|
||||
)?);
|
||||
let mut bindings =
|
||||
FeatureBlockBindings::with_tags(registry, data.integers().clone(), &data.block_tags)?
|
||||
.with_fluid_tags(&resources.fluid_tags)?
|
||||
.with_biomes(biomes.clone());
|
||||
let mut bindings = data
|
||||
.feature_block_bindings()?
|
||||
.with_fluid_tags(&resources.fluid_tags)?
|
||||
.with_biomes(biomes.clone());
|
||||
if let Some(structures) = &data.structures {
|
||||
bindings = bindings.with_structures(structures.clone());
|
||||
}
|
||||
|
||||
@@ -203,11 +203,7 @@ impl StructureState {
|
||||
for (i, def) in registry.structures.iter().enumerate() {
|
||||
by_step[def.step].push(i);
|
||||
}
|
||||
let mut bindings = FeatureBlockBindings::with_tags(
|
||||
data.registry(),
|
||||
data.integers().clone(),
|
||||
&data.block_tags,
|
||||
)?;
|
||||
let mut bindings = data.feature_block_bindings()?;
|
||||
// Processors that test what a block is made of read canOcclude.
|
||||
if let Some(blocks) = data
|
||||
.feature_resources
|
||||
|
||||
@@ -103,10 +103,10 @@ impl LiveFeatures {
|
||||
.collect(),
|
||||
&data.biome_tags,
|
||||
)?);
|
||||
let mut bindings =
|
||||
FeatureBlockBindings::with_tags(registry, data.integers().clone(), &data.block_tags)?
|
||||
.with_fluid_tags(&resources.fluid_tags)?
|
||||
.with_biomes(biomes.clone());
|
||||
let mut bindings = data
|
||||
.feature_block_bindings()?
|
||||
.with_fluid_tags(&resources.fluid_tags)?
|
||||
.with_biomes(biomes.clone());
|
||||
if let Some(structures) = &data.structures {
|
||||
bindings = bindings.with_structures(structures.clone());
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ public final class VanillaFinalizeSpawnOracle {
|
||||
Map.entry("jump_strength", Attributes.JUMP_STRENGTH));
|
||||
|
||||
static final List<Map.Entry<String, MemoryModuleType<?>>> MEMORIES = List.of(
|
||||
Map.entry("long_jump_cooldown", MemoryModuleType.LONG_JUMP_COOLDOWN_TICKS), Map.entry("ram_cooldown", MemoryModuleType.RAM_COOLDOWN_TICKS),
|
||||
Map.entry("long_jump_cooling_down", MemoryModuleType.LONG_JUMP_COOLDOWN_TICKS), Map.entry("ram_cooldown_ticks", MemoryModuleType.RAM_COOLDOWN_TICKS),
|
||||
Map.entry("attack_target_cooldown", MemoryModuleType.ATTACK_TARGET_COOLDOWN), Map.entry("hunted_recently", MemoryModuleType.HUNTED_RECENTLY),
|
||||
Map.entry("dig_cooldown", MemoryModuleType.DIG_COOLDOWN), Map.entry("is_emerging", MemoryModuleType.IS_EMERGING),
|
||||
Map.entry("home", MemoryModuleType.HOME));
|
||||
|
||||
@@ -171,6 +171,8 @@ public final class VanillaNaturalSpawnOracle {
|
||||
int tick = -1;
|
||||
List<String> log;
|
||||
boolean logColumns;
|
||||
// A WorldGenRegion sees no entities (getEntities is empty).
|
||||
boolean generationRegion;
|
||||
int next = 1;
|
||||
@Override public RegistryAccess registryAccess() { return registries; }
|
||||
@Override public Difficulty getDifficulty() { return difficulty; }
|
||||
@@ -281,11 +283,13 @@ public final class VanillaNaturalSpawnOracle {
|
||||
@Override public Iterable<Entity> getAllEntities() { return entities; }
|
||||
@Override public List<Entity> getEntities(Entity except, AABB bb, Predicate<? super Entity> selector) {
|
||||
List<Entity> out = new ArrayList<>();
|
||||
if (generationRegion) return out;
|
||||
for (Entity e : entities) if (e != except && e.getBoundingBox().intersects(bb) && selector.test(e)) out.add(e);
|
||||
return out;
|
||||
}
|
||||
@Override public <T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> type, AABB bb, Predicate<? super T> selector) {
|
||||
List<T> out = new ArrayList<>();
|
||||
if (generationRegion) return out;
|
||||
for (Entity e : entities) {
|
||||
T t = type.tryCast(e);
|
||||
if (t != null && e.getBoundingBox().intersects(bb) && selector.test(t)) out.add(t);
|
||||
@@ -811,11 +815,13 @@ public final class VanillaNaturalSpawnOracle {
|
||||
vaultCase(level, c, access, blockLookup, mainRandom, log);
|
||||
} else if (mode.equals("gen")) {
|
||||
level.logColumns = true;
|
||||
level.generationRegion = true;
|
||||
int n = level.entities.size();
|
||||
RandomSource random = RandomSource.create(Long.parseLong(c.getOrDefault("gseed", "0")));
|
||||
BlockPos sourcePos = new BlockPos(chunk.at.getMinBlockX(), level.getMaxY(), chunk.at.getMinBlockZ());
|
||||
NaturalSpawner.spawnMobsForChunkGeneration(level, sourcePos, chunk.at, random);
|
||||
level.logColumns = false;
|
||||
level.generationRegion = false;
|
||||
for (Entity e : level.entities.subList(n, level.entities.size()))
|
||||
if (e.getVehicle() == null)
|
||||
log.add("spawn " + BuiltInRegistries.ENTITY_TYPE.getKey(e.getType()) + " " + bits(e.getX()) + " " + bits(e.getY()) + " " + bits(e.getZ()) + " " + yaw(e));
|
||||
|
||||
@@ -327,6 +327,13 @@ def scenarios():
|
||||
# Constructor path maluses: an Animal avoids fire (FIRE -1, FIRE_IN_NEIGHBOR 16).
|
||||
add('pig_tempt_fire', 100, ['player 9.5 -60 0.5 hand=minecraft:carrot', 'fill 3 -61 -3 3 -61 3 minecraft:netherrack',
|
||||
'fill 3 -60 -3 3 -60 3 minecraft:fire', 'mob minecraft:pig 0.5 -60 0.5 0 221 persistent'])
|
||||
# A baby villager grows up: Villager.ageBoundaryReached rebuilds its brain, so the adult schedule
|
||||
# (work, meet) replaces the baby's (play) at once.
|
||||
for seed in (201, 202):
|
||||
add(f'villager_grows_up_{seed}', 120, ['player 8.5 -60 0.5',
|
||||
f'mob minecraft:villager 0.5 -60 0.5 0 {seed} persistent age=-30'], 'time=2000')
|
||||
# A zombified piglin runs Zombie.registerGoals too (the turtle egg goal draws its start delay).
|
||||
add('zombified_piglin_player', 120, ['player 7.5 -60 0.5', 'mob minecraft:zombified_piglin 0.5 -60 0.5 0 241 persistent'])
|
||||
add('wither_skeleton_player', 100, ['player 9.5 -60 0.5', 'mob minecraft:wither_skeleton 0.5 -60 0.5 0 222 persistent'])
|
||||
# Chickens fleeing a hit (PanicGoal) beside a pool.
|
||||
for seed in range(191, 193):
|
||||
@@ -335,6 +342,9 @@ def scenarios():
|
||||
+ [f'hurt {t} 0' for t in range(5, 60, 30)])
|
||||
add('chicken_panic_dry', 60, ['player 12.5 -60 0.5', 'mob minecraft:chicken 0.5 -60 0.5 0 195 persistent', 'hurt 5 0'])
|
||||
add('pig_panic_dry', 60, ['player 12.5 -60 0.5', 'mob minecraft:pig 0.5 -60 0.5 0 196 persistent', 'hurt 5 0'])
|
||||
# A wild cat avoids the player it sees (Cat.reassessTameGoals adds that goal last) and sprints while
|
||||
# it dashes at 1.33 (Cat.customServerAiStep).
|
||||
add('cat_player', 120, ['player 6.5 -60 0.5', 'mob minecraft:cat 0.5 -60 0.5 0 261 persistent'])
|
||||
# Axolotls (#64): idle swimming, hunting a cod, and playing dead after hits, in a pool sunk into
|
||||
# the ground so no water flows out of it (the probe doesn't tick fluids).
|
||||
POOL = 'fill -5 -63 -5 5 -61 5 minecraft:water'
|
||||
|
||||
Reference in new issue
Block a user