mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Merge pull request #11725 from MSedgeMC/test/spawn-obstruction
test: compare mob spawn obstruction
This commit is contained in:
4 files changed
+158
-5
No files matched your search
@@ -11,6 +11,10 @@ use horizon_client::{
|
||||
MobTypes,
|
||||
data::MobData,
|
||||
spawn_rules::{self, Dimension, Reason, SpawnLevel},
|
||||
spawn_selection::{
|
||||
self, Heightmap, MobType as SelectionMobType, NaturalLevel, SpawnCost, StructureAt,
|
||||
StructureSpawns, spawn_aabb,
|
||||
},
|
||||
},
|
||||
};
|
||||
use horizon_core::random::{LegacyRandom, RandomSource};
|
||||
@@ -44,6 +48,9 @@ struct Column<'a> {
|
||||
slime: f32,
|
||||
/// A player position and whether it is survival or adventure.
|
||||
player: Option<([f64; 3], bool)>,
|
||||
/// `LevelReader.isUnobstructed` for the obstruction family.
|
||||
unobstructed: bool,
|
||||
structure_spawns: StructureSpawns,
|
||||
}
|
||||
impl SpawnLevel for Column<'_> {
|
||||
fn state(&self, x: i32, y: i32, z: i32) -> BlockStateId {
|
||||
@@ -104,6 +111,73 @@ impl SpawnLevel for Column<'_> {
|
||||
})
|
||||
}
|
||||
}
|
||||
impl NaturalLevel for Column<'_> {
|
||||
fn min_y(&self) -> i32 {
|
||||
-64
|
||||
}
|
||||
fn max_y(&self) -> i32 {
|
||||
320
|
||||
}
|
||||
fn ambient_light(&self) -> f32 {
|
||||
0.0
|
||||
}
|
||||
fn has_ceiling(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn mob_type(&self, _entity: &str) -> Option<SelectionMobType> {
|
||||
None
|
||||
}
|
||||
fn biome_spawns(
|
||||
&self,
|
||||
_x: i32,
|
||||
_y: i32,
|
||||
_z: i32,
|
||||
) -> Option<&HashMap<String, Vec<horizon_client::biome::SpawnEntry>>> {
|
||||
None
|
||||
}
|
||||
fn spawn_cost(&self, _entity: &str, _x: i32, _y: i32, _z: i32) -> Option<SpawnCost> {
|
||||
None
|
||||
}
|
||||
fn creature_probability(&self, _x: i32, _y: i32, _z: i32) -> f32 {
|
||||
0.0
|
||||
}
|
||||
fn structures_at(
|
||||
&self,
|
||||
_x: i32,
|
||||
_y: i32,
|
||||
_z: i32,
|
||||
_wanted: &dyn Fn(&str) -> bool,
|
||||
) -> Vec<StructureAt> {
|
||||
Vec::new()
|
||||
}
|
||||
fn structure_spawns(&self) -> &StructureSpawns {
|
||||
&self.structure_spawns
|
||||
}
|
||||
fn nearest_player_positions(&self) -> &[[f64; 3]] {
|
||||
&[]
|
||||
}
|
||||
fn respawn(&self) -> Option<[i32; 3]> {
|
||||
None
|
||||
}
|
||||
fn can_spawn_in_chunk(&self, _cx: i32, _cz: i32) -> bool {
|
||||
true
|
||||
}
|
||||
fn no_collision(&self, _bounds: horizon_core::geometry::Aabb) -> bool {
|
||||
true
|
||||
}
|
||||
fn unobstructed(&self, _bounds: horizon_core::geometry::Aabb) -> bool {
|
||||
self.unobstructed
|
||||
}
|
||||
fn spawn_mobs(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn heightmap(&self, _kind: Heightmap, _x: i32, _z: i32) -> i32 {
|
||||
self.surface
|
||||
}
|
||||
fn pathfindable_land(&self, _x: i32, _y: i32, _z: i32) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// `name[key=value,...]`.
|
||||
fn parse_state(spec: &str) -> BlockState {
|
||||
@@ -213,7 +287,24 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
biome: get("biome", "minecraft:plains"),
|
||||
slime: get("slime", "0.0").parse()?,
|
||||
player,
|
||||
unobstructed: !get("unobstructed", "1").eq("0"),
|
||||
structure_spawns: StructureSpawns::default(),
|
||||
};
|
||||
if c.get("mode") == Some(&"obstruction") {
|
||||
let s = kind.species;
|
||||
let bounds = spawn_aabb(
|
||||
s,
|
||||
f64::from(x0) + 0.5,
|
||||
f64::from(y),
|
||||
f64::from(z0) + 0.5,
|
||||
kind.width as f32,
|
||||
kind.height as f32,
|
||||
);
|
||||
let obstruction =
|
||||
spawn_selection::spawn_obstruction(s, &level, bounds, Some(&[]), x0, y, z0);
|
||||
let _ = writeln!(out, "{name} {obstruction}");
|
||||
continue;
|
||||
}
|
||||
let mut random = LegacyRandom::new(get("seed", "0").parse()?);
|
||||
let s = kind.species;
|
||||
let ok = spawn_rules::is_spawn_position_ok(s, &level, x0, y, z0);
|
||||
|
||||
@@ -578,7 +578,7 @@ pub fn spawn_aabb(s: Species, x: f64, y: f64, z: f64, width: f32, height: f32) -
|
||||
/// `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>(
|
||||
pub fn spawn_obstruction<L: NaturalLevel>(
|
||||
s: Species,
|
||||
l: &L,
|
||||
bounds: Aabb,
|
||||
|
||||
@@ -14,6 +14,7 @@ import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.RegistryDataLoader;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.RegistryLayer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
@@ -21,6 +22,7 @@ import net.minecraft.server.packs.repository.ServerPacksSource;
|
||||
import net.minecraft.server.packs.resources.MultiPackResourceManager;
|
||||
import net.minecraft.tags.TagLoader;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.util.debug.ServerDebugSubscribers;
|
||||
import net.minecraft.world.Difficulty;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
@@ -61,6 +63,9 @@ public final class VanillaSpawnRulesOracle {
|
||||
Map<Integer, BlockState> column = new HashMap<>();
|
||||
int x0, z0, sky, block, darken, sea, surface;
|
||||
boolean thunder;
|
||||
boolean unobstructed = true;
|
||||
boolean liquid;
|
||||
MinecraftServer server;
|
||||
long seed;
|
||||
Difficulty difficulty;
|
||||
DimensionType dimension;
|
||||
@@ -70,6 +75,7 @@ public final class VanillaSpawnRulesOracle {
|
||||
WorldBorder border = new WorldBorder();
|
||||
int next = 1;
|
||||
@Override public RegistryAccess registryAccess() { return registries; }
|
||||
@Override public MinecraftServer getServer() { return server; }
|
||||
@Override public Difficulty getDifficulty() { return difficulty; }
|
||||
@Override public long getGameTime() { return 0L; }
|
||||
@Override public int getNextEntityId() { return next++; }
|
||||
@@ -79,6 +85,8 @@ public final class VanillaSpawnRulesOracle {
|
||||
return column.getOrDefault(pos.getY(), Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
@Override public FluidState getFluidState(BlockPos pos) { return getBlockState(pos).getFluidState(); }
|
||||
@Override public boolean isUnobstructed(net.minecraft.world.entity.Entity entity) { return unobstructed; }
|
||||
@Override public boolean containsAnyLiquid(net.minecraft.world.phys.AABB box) { return liquid; }
|
||||
@Override public int getBrightness(LightLayer layer, BlockPos pos) { return layer == LightLayer.SKY ? sky : block; }
|
||||
@Override public int getRawBrightness(BlockPos pos, int darkening) { return Math.max(block, sky - darkening); }
|
||||
@Override public int getSkyDarken() { return darken; }
|
||||
@@ -125,6 +133,8 @@ public final class VanillaSpawnRulesOracle {
|
||||
|
||||
Lvl level = (Lvl) alloc(Lvl.class);
|
||||
level.registries = access;
|
||||
level.server = (MinecraftServer) alloc(VanillaBlockUpdateOracle.Server.class);
|
||||
put(level.server, MinecraftServer.class, "debugSubscribers", new ServerDebugSubscribers(level.server));
|
||||
level.column = new HashMap<>();
|
||||
level.playerList = new ArrayList<>();
|
||||
level.border = new WorldBorder();
|
||||
@@ -162,6 +172,7 @@ public final class VanillaSpawnRulesOracle {
|
||||
level.column.put(y + offset, net.minecraft.commands.arguments.blocks.BlockStateParser.parseForBlock(blocks, e.getValue(), false).blockState());
|
||||
}
|
||||
}
|
||||
level.liquid = level.column.values().stream().anyMatch(state -> !state.getFluidState().isEmpty());
|
||||
level.sky = Integer.parseInt(c.getOrDefault("sky", "15"));
|
||||
level.block = Integer.parseInt(c.getOrDefault("light", "0"));
|
||||
level.darken = Integer.parseInt(c.getOrDefault("darken", "0"));
|
||||
@@ -187,10 +198,18 @@ public final class VanillaSpawnRulesOracle {
|
||||
EntityType<?> type = BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(c.get("type")));
|
||||
EntitySpawnReason reason = EntitySpawnReason.valueOf(c.getOrDefault("reason", "natural").toUpperCase(Locale.ROOT));
|
||||
BlockPos pos = new BlockPos(level.x0, y, level.z0);
|
||||
RandomSource random = RandomSource.create(Long.parseLong(c.getOrDefault("seed", "0")));
|
||||
boolean ok = SpawnPlacements.isSpawnPositionOk(type, level, pos);
|
||||
boolean rules = SpawnPlacements.checkSpawnRules(type, level, reason, pos, random);
|
||||
w.println(c.get("name") + " " + ok + " " + rules + " " + random.nextInt(1000));
|
||||
if (c.getOrDefault("mode", "").equals("obstruction")) {
|
||||
level.unobstructed = !c.getOrDefault("unobstructed", "1").equals("0");
|
||||
var entity = type.create(level, EntitySpawnReason.LOAD);
|
||||
if (entity != null) entity.setPosRaw(level.x0 + 0.5, y, level.z0 + 0.5);
|
||||
boolean obstruction = entity instanceof Mob mob && mob.checkSpawnObstruction(level);
|
||||
w.println(c.get("name") + " " + obstruction);
|
||||
} else {
|
||||
RandomSource random = RandomSource.create(Long.parseLong(c.getOrDefault("seed", "0")));
|
||||
boolean ok = SpawnPlacements.isSpawnPositionOk(type, level, pos);
|
||||
boolean rules = SpawnPlacements.checkSpawnRules(type, level, reason, pos, random);
|
||||
w.println(c.get("name") + " " + ok + " " + rules + " " + random.nextInt(1000));
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Throwable cause = t instanceof InvocationTargetException ite ? ite.getCause() : t;
|
||||
StringBuilder line = new StringBuilder(c.get("name") + " error " + cause);
|
||||
|
||||
@@ -125,9 +125,50 @@ def corpus(types):
|
||||
return cases
|
||||
|
||||
|
||||
def obstruction_cases(types):
|
||||
"""`Mob.checkSpawnObstruction` on the same column worlds: the default
|
||||
liquid/unobstructed answer and each class's override, with water and lava
|
||||
in the feet or head cell, Ocelot's sea-level and grass/leaves base, and
|
||||
the level's unobstructed flag both ways."""
|
||||
rng = random.Random(0x4F425354)
|
||||
cases = []
|
||||
scenarios = [
|
||||
dict(at0='minecraft:air', at1='minecraft:air', unobstructed=1),
|
||||
dict(at0='minecraft:water', at1='minecraft:water', unobstructed=1),
|
||||
dict(at0='minecraft:lava', at1='minecraft:lava', unobstructed=1),
|
||||
dict(at0='minecraft:water', at1='minecraft:air', unobstructed=1),
|
||||
dict(at0='minecraft:air', at1='minecraft:air', unobstructed=0),
|
||||
dict(at0='minecraft:water', at1='minecraft:water', unobstructed=0),
|
||||
dict(at0='minecraft:air', at1='minecraft:air', y=50, sea=63, **{'at-1': 'minecraft:grass_block'}, unobstructed=1),
|
||||
dict(at0='minecraft:air', at1='minecraft:air', y=50, sea=63, **{'at-1': 'minecraft:stone'}, unobstructed=1),
|
||||
dict(at0='minecraft:air', at1='minecraft:air', y=70, sea=63, **{'at-1': 'minecraft:grass_block'}, unobstructed=1),
|
||||
dict(at0='minecraft:air', at1='minecraft:air', y=70, sea=63, **{'at-1': 'minecraft:oak_leaves'}, unobstructed=1),
|
||||
]
|
||||
for t in types:
|
||||
n = t.split(':')[1]
|
||||
# These three need data components that a bare EntityType.create lacks.
|
||||
if n in ('wandering_trader', 'warden', 'wither', 'iron_golem'):
|
||||
continue
|
||||
for k, scenario in enumerate(scenarios):
|
||||
y = scenario.get('y', 64)
|
||||
fields = {key: value for key, value in scenario.items() if key.startswith('at')}
|
||||
fields['unobstructed'] = scenario['unobstructed']
|
||||
fields['sea'] = scenario.get('sea', 63)
|
||||
cases.append(f'{n}/obstruction/{k} type={t} mode=obstruction y={y} '
|
||||
+ ' '.join(f'{key}={value}' for key, value in fields.items()))
|
||||
# The Ocelot override's y >= sea path with both base block tags.
|
||||
for k, (y, below) in enumerate(((64, 'minecraft:grass_block'), (64, 'minecraft:oak_leaves'),
|
||||
(62, 'minecraft:grass_block'), (63, 'minecraft:stone'))):
|
||||
cases.append(f'{n}/ocelot/{k} type={t} mode=obstruction y={y} at-1={below} '
|
||||
f'at0=minecraft:air at1=minecraft:air unobstructed=1 sea=63')
|
||||
return cases
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--prepare', action='store_true')
|
||||
parser.add_argument('--no-obstruction', action='store_true',
|
||||
help='leave out the Mob.checkSpawnObstruction family, reproducing the previous corpus')
|
||||
args = parser.parse_args()
|
||||
build = lab.LOCAL / 'rust-spawn-rules-parity'
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
@@ -143,6 +184,8 @@ def main():
|
||||
tested = base.source_hashes(sources)
|
||||
inputs = build / 'inputs.txt'
|
||||
cases = corpus(types)
|
||||
if not args.no_obstruction:
|
||||
cases += obstruction_cases(types)
|
||||
inputs.write_text(''.join(line + '\n' for line in cases), encoding='utf-8')
|
||||
classes = build / 'classes'
|
||||
classes.mkdir(exist_ok=True)
|
||||
|
||||
Reference in new issue
Block a user