Fix creative barrier and light block marker particles
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
//! Probe for tools/test_marker_particles_parity.py.
|
||||
use horizon_client::{
|
||||
Result,
|
||||
animate_tick::{AnimateContext, AnimateTables, animate_tick, marker_particle_target},
|
||||
assets::Assets,
|
||||
biome::Biomes,
|
||||
blocks::BlockFacts,
|
||||
chunk::Chunk,
|
||||
collisions,
|
||||
command::args::GameMode,
|
||||
items::ItemStack,
|
||||
particles::{ParticleEngine, ParticleOptions, ParticleType},
|
||||
world::World,
|
||||
};
|
||||
use horizon_core::random::RandomSource;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let root = Path::new(".local/horizon-client/26.3");
|
||||
let assets = Assets::load(root)?;
|
||||
let facts = BlockFacts::load(root, assets.registry)?;
|
||||
let biomes = Biomes::load(root)?;
|
||||
let collision = collisions::load(root, assets.registry)?;
|
||||
let tables = AnimateTables::load(root, assets.registry, &biomes)?;
|
||||
let mut world = World::new(facts.air, 0);
|
||||
let barrier = assets
|
||||
.registry
|
||||
.default_state("minecraft:barrier")
|
||||
.ok_or("barrier")?;
|
||||
for cz in -2..2 {
|
||||
for cx in -2..2 {
|
||||
world
|
||||
.chunks
|
||||
.insert((cx, cz), Arc::new(Chunk::empty(cx, cz)));
|
||||
for y in 33..96 {
|
||||
for z in 0..16 {
|
||||
for x in 0..16 {
|
||||
world.set(cx * 16 + x, y, cz * 16 + z, barrier)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut engine = ParticleEngine::new(root, &HashMap::new());
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for line in BufReader::new(File::open(&args[1])?).lines() {
|
||||
let case: Value = serde_json::from_str(&line?)?;
|
||||
let mode = GameMode::ALL
|
||||
.into_iter()
|
||||
.find(|mode| Some(mode.name()) == case["mode"].as_str())
|
||||
.ok_or("mode")?;
|
||||
let stack = ItemStack::new(
|
||||
&format!("minecraft:{}", case["main"].as_str().ok_or("main")?),
|
||||
case["count"].as_u64().ok_or("count")? as u32,
|
||||
);
|
||||
let target = marker_particle_target(mode, Some(&stack));
|
||||
writeln!(out, "{}", target.unwrap_or("none"))?;
|
||||
// Exercise the real ambient sampling loop in a dry barrier-filled world.
|
||||
let options =
|
||||
ParticleOptions::block(ParticleType::BlockMarker, "item/barrier".into(), 0xFFFFFF);
|
||||
let options = |_| Some(options.clone());
|
||||
let ctx = AnimateContext {
|
||||
world: &world,
|
||||
facts: &facts,
|
||||
collision: &collision,
|
||||
biomes: &biomes,
|
||||
registry: assets.registry,
|
||||
raining_at: &|_| false,
|
||||
marker: target.map(|name| (name, &options as &dyn Fn(_) -> _)),
|
||||
};
|
||||
engine.seed_randoms(1057, 0, 0);
|
||||
engine.requested = Some(Vec::new());
|
||||
animate_tick(&tables, &mut engine, &ctx, [0, 64, 0]);
|
||||
for (options, p, v) in engine.requested.take().ok_or("requests")? {
|
||||
assert_eq!(options.ty, ParticleType::BlockMarker);
|
||||
assert_eq!(v, [0.0; 3]);
|
||||
writeln!(out, "marker {} {} {}", p[0], p[1], p[2])?;
|
||||
}
|
||||
writeln!(out, "next {}", engine.random().next_int())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
use crate::{
|
||||
biome::Biomes,
|
||||
blocks::{BlockFacts, Fluid},
|
||||
command::args::GameMode,
|
||||
items::ItemStack,
|
||||
particles::{ParticleEngine, ParticleOptions, ParticleType as T},
|
||||
world::World,
|
||||
};
|
||||
@@ -254,6 +256,24 @@ pub struct AnimateContext<'a> {
|
||||
pub registry: &'a BlockRegistry,
|
||||
/// `Level.isRainingAt`.
|
||||
pub raining_at: &'a dyn Fn([i32; 3]) -> bool,
|
||||
/// Creative main-hand marker block and a resolver for each placed state's sprite.
|
||||
pub marker: Option<(&'a str, &'a dyn Fn(BlockStateId) -> Option<ParticleOptions>)>,
|
||||
}
|
||||
|
||||
/// `ClientLevel.getMarkerParticleTarget`: only the selected main-hand item counts.
|
||||
pub fn marker_particle_target(
|
||||
mode: GameMode,
|
||||
main_hand: Option<&ItemStack>,
|
||||
) -> Option<&'static str> {
|
||||
if mode != GameMode::Creative {
|
||||
return None;
|
||||
}
|
||||
let stack = main_hand.filter(|stack| stack.count > 0)?;
|
||||
match stack.item.as_ref() {
|
||||
"minecraft:barrier" => Some("minecraft:barrier"),
|
||||
"minecraft:light" => Some("minecraft:light"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
impl AnimateContext<'_> {
|
||||
fn state(&self, p: [i32; 3]) -> BlockStateId {
|
||||
@@ -358,6 +378,12 @@ fn do_animate_tick(
|
||||
try_spawn_drip(tables, engine, ctx, [x, y - 1, z], drip, top_solid);
|
||||
}
|
||||
}
|
||||
if let Some((block, options)) = ctx.marker
|
||||
&& ctx.registry.state(state).is_some_and(|s| s.name == block)
|
||||
&& let Some(options) = options(state)
|
||||
{
|
||||
engine.add(&options, pos.map(|v| f64::from(v) + 0.5), [0.0; 3]);
|
||||
}
|
||||
if !facts.full_collision {
|
||||
let biome = ctx
|
||||
.world
|
||||
|
||||
@@ -1720,8 +1720,14 @@ impl Game<'_> {
|
||||
Some(stack)
|
||||
}
|
||||
fn particle_sprite(&self, state: BlockStateId) -> Option<(String, u32)> {
|
||||
let models = self
|
||||
.pack
|
||||
Self::block_particle_sprite(&self.pack, self.res.facts(), state)
|
||||
}
|
||||
fn block_particle_sprite(
|
||||
pack: &PackResources,
|
||||
facts: &BlockFacts,
|
||||
state: BlockStateId,
|
||||
) -> Option<(String, u32)> {
|
||||
let models = pack
|
||||
.render
|
||||
.model_catalog
|
||||
.collect(state, horizon_core::position::BlockPos::new(0, 0, 0))
|
||||
@@ -1735,9 +1741,9 @@ impl Game<'_> {
|
||||
let layer = face
|
||||
.map(|face| face.layer)
|
||||
.or_else(|| models.iter().find_map(|(_, model)| model.particle))?;
|
||||
let name = self.pack.layer_names.get(&layer)?.clone();
|
||||
let name = pack.layer_names.get(&layer)?.clone();
|
||||
let tint = if face.is_some_and(|face| face.quad.tint_index >= 0) {
|
||||
let n = &self.res.facts().get(state).name;
|
||||
let n = &facts.get(state).name;
|
||||
if n.contains("leaves") || n.contains("vine") {
|
||||
0x48B518
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use super::{Dim, Game};
|
||||
use horizon_client::fog::{FogData, FogInputs, FogType, fog_type, update_rain_fog};
|
||||
use horizon_client::{
|
||||
animate_tick::{AnimateContext, animate_tick, campfire_particles},
|
||||
animate_tick::{AnimateContext, animate_tick, campfire_particles, marker_particle_target},
|
||||
biome::Biomes,
|
||||
blocks::{BlockFacts, Fluid},
|
||||
chunk::{HEIGHT, MIN_Y},
|
||||
@@ -290,6 +290,15 @@ impl Game<'_> {
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
let marker = marker_particle_target(self.mode, self.stack_in(self.selected));
|
||||
let marker_options = |state| {
|
||||
let (sprite, tint) = Self::block_particle_sprite(&self.pack, self.res.facts(), state)?;
|
||||
Some(ParticleOptions::block(
|
||||
ParticleType::BlockMarker,
|
||||
sprite,
|
||||
tint,
|
||||
))
|
||||
};
|
||||
let world = &self.world;
|
||||
let facts = self.res.facts();
|
||||
let biomes = self.res.biomes();
|
||||
@@ -306,6 +315,7 @@ impl Game<'_> {
|
||||
biomes,
|
||||
registry: self.pack.assets.registry,
|
||||
raining_at: &raining_at,
|
||||
marker: marker.map(|name| (name, &marker_options as &dyn Fn(_) -> _)),
|
||||
};
|
||||
if let Some(e) = eye {
|
||||
let block = [
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package dev.horizonlab.parity;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.multiplayer.MultiPlayerGameMode;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.particles.ParticleOptions;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.material.FluidState;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
|
||||
/** Invoke shipped marker selection and ambient sampling in a dry barrier world. */
|
||||
public final class VanillaMarkerParticlesOracle {
|
||||
static Field field(Class<?> type, String name) throws Exception {
|
||||
var field = type.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
}
|
||||
public static final class Player extends LocalPlayer {
|
||||
ItemStack main, off;
|
||||
Player() { super(null, null, null, null, null, null, false, null, null); }
|
||||
@Override public ItemStack getMainHandItem() { return main; }
|
||||
@Override public ItemStack getItemInHand(InteractionHand hand) {
|
||||
return hand == InteractionHand.MAIN_HAND ? main : off;
|
||||
}
|
||||
}
|
||||
public static final class Mode extends MultiPlayerGameMode {
|
||||
GameType mode;
|
||||
Mode() { super(null, null); }
|
||||
@Override public GameType getPlayerMode() { return mode; }
|
||||
}
|
||||
public static final class MarkerLevel extends ClientLevel {
|
||||
java.io.BufferedWriter out;
|
||||
MarkerLevel() { super(null, null, null, null, 0, 0, null, false, 0L, 0); }
|
||||
@Override public BlockState getBlockState(BlockPos pos) { return Blocks.BARRIER.defaultBlockState(); }
|
||||
@Override public FluidState getFluidState(BlockPos pos) { return Fluids.EMPTY.defaultFluidState(); }
|
||||
@Override public void addParticle(ParticleOptions options, double x, double y, double z,
|
||||
double dx, double dy, double dz) {
|
||||
if (dx != 0 || dy != 0 || dz != 0) throw new AssertionError("marker velocity");
|
||||
try {
|
||||
out.write("marker " + x + " " + y + " " + z);
|
||||
out.newLine();
|
||||
} catch (java.io.IOException e) { throw new RuntimeException(e); }
|
||||
}
|
||||
}
|
||||
public static void main(String[] args) throws Exception {
|
||||
net.minecraft.SharedConstants.tryDetectVersion();
|
||||
net.minecraft.server.Bootstrap.bootStrap();
|
||||
for (var item : BuiltInRegistries.ITEM) {
|
||||
item.builtInRegistryHolder().bindComponents(net.minecraft.core.component.DataComponentMap.EMPTY);
|
||||
}
|
||||
var allocator = Class.forName("sun.misc.Unsafe");
|
||||
var unsafe = field(allocator, "theUnsafe").get(null);
|
||||
var allocate = allocator.getMethod("allocateInstance", Class.class);
|
||||
var mc = (Minecraft) allocate.invoke(unsafe, Minecraft.class);
|
||||
var level = (MarkerLevel) allocate.invoke(unsafe, MarkerLevel.class);
|
||||
var player = (Player) allocate.invoke(unsafe, Player.class);
|
||||
var mode = (Mode) allocate.invoke(unsafe, Mode.class);
|
||||
field(ClientLevel.class, "minecraft").set(level, mc);
|
||||
mc.player = player;
|
||||
mc.gameMode = mode;
|
||||
var select = ClientLevel.class.getDeclaredMethod("getMarkerParticleTarget");
|
||||
select.setAccessible(true);
|
||||
try (var out = Files.newBufferedWriter(Path.of(args[1]))) {
|
||||
level.out = out;
|
||||
for (var line : Files.readAllLines(Path.of(args[0]))) {
|
||||
var c = JsonParser.parseString(line).getAsJsonObject();
|
||||
mode.mode = GameType.byName(c.get("mode").getAsString());
|
||||
player.main = new ItemStack(BuiltInRegistries.ITEM.getValue(
|
||||
Identifier.parse("minecraft:" + c.get("main").getAsString())), c.get("count").getAsInt());
|
||||
player.off = new ItemStack(BuiltInRegistries.ITEM.getValue(
|
||||
Identifier.parse("minecraft:" + c.get("off").getAsString())));
|
||||
var block = (Block) select.invoke(level);
|
||||
out.write(block == null ? "none" : BuiltInRegistries.BLOCK.getKey(block).toString());
|
||||
out.newLine();
|
||||
var random = RandomSource.create(1057);
|
||||
field(Level.class, "random").set(level, random);
|
||||
level.animateTick(0, 64, 0);
|
||||
out.write("next " + random.nextInt());
|
||||
out.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare creative marker selection and ambient sampling with the pinned ClientLevel."""
|
||||
import itertools
|
||||
import json
|
||||
|
||||
try:
|
||||
from . import lab, oracle_harness as harness
|
||||
except ImportError:
|
||||
import lab
|
||||
import oracle_harness as harness
|
||||
|
||||
|
||||
def make_input(build):
|
||||
items = ['air', 'barrier', 'light', 'stone', 'structure_void', 'apple']
|
||||
cases = [dict(mode=mode, main=main, off=off, count=count)
|
||||
for mode, main, off, count in itertools.product(
|
||||
['survival', 'creative', 'adventure', 'spectator'], items, items, [0, 1, 64])]
|
||||
path = build / 'cases.jsonl'
|
||||
path.write_text(''.join(json.dumps(case) + '\n' for case in cases))
|
||||
return path, len(cases)
|
||||
|
||||
|
||||
def main():
|
||||
harness.differential(
|
||||
'marker-particles-parity', 'VanillaMarkerParticlesOracle', 'marker_particles_probe',
|
||||
make_input, 'Creative main-hand marker selection and barrier marker positions, velocities and random draws',
|
||||
sources=[lab.ROOT / 'tools/test_marker_particles_parity.py',
|
||||
lab.ROOT / 'crates/horizon-client/src/animate_tick.rs'], expected_lines=16872)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user