Fix creative barrier and light block marker particles

This commit is contained in:
rdbtCVS
2026-10-20 16:30:34 +01:00
parent defaa7cd2a
commit cadc20cd58
6 changed files with 267 additions and 5 deletions
@@ -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(())
}
+26
View File
@@ -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
+10 -4
View File
@@ -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 {
+11 -1
View File
@@ -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();
}
}
}
}
+32
View File
@@ -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()