Merge pull request #5520 from levzzz5154/mods/analog-output

Native mods: a mod block's comparator output
This commit is contained in:
levzzz authored and GitHub committed 2026-11-01 03:23:22 +05:00
commit 76dc5e3bd0
7 files changed
+181 -8

No files matched your search

+20
View File
@@ -1102,6 +1102,26 @@ impl LevelAccess for GameLevel<'_> {
}
signal.clamp(0, 15)
}
fn adjust_has_analog_output(&self, state: BlockStateId, vanilla: bool) -> bool {
let mut output = vanilla;
if self.native_authoritative {
self.native_has_analog_output(state, &mut output);
}
output
}
fn adjust_analog_output(
&self,
pos: BlockPos,
state: BlockStateId,
direction: Direction,
vanilla: i32,
) -> i32 {
let mut output = vanilla;
if self.native_authoritative {
self.native_analog_output(self.dimension, pos, state, direction, &mut output);
}
output.clamp(0, 15)
}
fn before_random_tick(
&mut self,
ticks: &mut Ticks,
@@ -364,6 +364,39 @@ impl GameLevel<'_> {
let _ = (state, source);
}
/// `BlockState.hasAnalogOutputSignal`: set `output` for a block of yours
/// that comparators read (how full or far along it is, as a composter's
/// level), so they ask `native_analog_output`. Asked from inside the
/// level's update: answer from `state` alone. Authoritative servers only.
/// API @API:analog_output@.
#[inline(always)]
pub(super) fn native_has_analog_output(&self, state: BlockStateId, output: &mut bool) {
let _ = (state, output);
}
/// `BlockState.getAnalogOutputSignal`: the signal a comparator reads from
/// the block `state` at `pos`, `face` the side that touches the comparator
/// (or the conductor in front of it). `output` holds vanilla's answer:
/// change it when the block is yours (0 to 15; anything else is clamped).
/// Asked only for blocks `native_has_analog_output` answered for, from
/// inside the level's update: don't change the world here. When the
/// answer changes without the block's state changing, call
/// `native_update_output_neighbors`; a change of state through
/// `native_set_block` or `native_set_block_property` (neighbour updates
/// on) tells the comparators itself, as vanilla's `Level.setBlock` does.
/// Authoritative servers only. API @API:analog_output@.
#[inline(always)]
pub(super) fn native_analog_output(
&self,
dimension: LevelDimension,
pos: BlockPos,
state: BlockStateId,
face: Direction,
output: &mut i32,
) {
let _ = (dimension, pos, state, face, output);
}
/// An authoritative server's `ServerLevel.tickChunk` is about to
/// random-tick the block `state` at `pos` (`BlockState.randomTick`):
/// only blocks that tick randomly, a mod's as its vanilla template block
@@ -1923,6 +1956,26 @@ impl Game<'_> {
let at = BlockPos::new(pos[0], pos[1], pos[2]);
self.with_level(|b, level, ticks| b.set_block(level, ticks, at, id, flags))
}
/// The block at `pos` changed its comparator output
/// (`native_analog_output`) without changing its state: the comparators
/// beside it, or behind a conductor there, read it again
/// (`Level.updateNeighbourForOutputSignal`). Acts on the level in place:
/// false (and nothing happens) when `dimension` is another, or this
/// window isn't a server. API @API:analog_output@.
pub(super) fn native_update_output_neighbors(
&mut self,
dimension: &str,
pos: [i32; 3],
) -> bool {
if (self.serving.is_none() && self.options.dedicated.is_none())
|| dimension != self.dim.id()
{
return false;
}
let at = BlockPos::new(pos[0], pos[1], pos[2]);
self.with_level(|b, level, ticks| b.update_neighbour_for_output_signal(level, ticks, at));
true
}
/// The block at `pos` changed the signal it gives: its neighbours, and
/// theirs (a block it powers passes the signal on), read it again, as
/// around a lever (`Level.updateNeighborsAt`). Call it when the answer
+17
View File
@@ -510,6 +510,23 @@ pub trait LevelAccess {
fn adjust_signal_source(&self, _state: BlockStateId, vanilla: bool) -> bool {
vanilla
}
/// `BlockState.hasAnalogOutputSignal` once the library answered `vanilla`:
/// whether comparators read the block. The default keeps it.
fn adjust_has_analog_output(&self, _state: BlockStateId, vanilla: bool) -> bool {
vanilla
}
/// `BlockState.getAnalogOutputSignal(level, pos, direction)` once the
/// library answered `vanilla`, `direction` pointing from the block to the
/// comparator reading it: the output to use. The default keeps it.
fn adjust_analog_output(
&self,
_pos: BlockPos,
_state: BlockStateId,
_direction: Direction,
vanilla: i32,
) -> i32 {
vanilla
}
/// `BlockState.getSignal` (or `getDirectSignal` when `direct`) once the
/// library answered `vanilla`: the signal to use. The default keeps it.
fn adjust_signal(
+21 -4
View File
@@ -514,13 +514,13 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
};
if b.instance_of(state, Class::Comparator) {
let target_state = self.state(target);
if b.has_flag(target_state, state_flags::ANALOG_OUTPUT) {
if self.has_analog_output(target_state) {
result = self.analog_output(target_state, target, facing.opposite());
} else if result < 15 && self.is_redstone_conductor(target_state, target) {
let further = rel(target, facing);
let further_state = self.state(further);
let block = b
.has_flag(further_state, state_flags::ANALOG_OUTPUT)
let block = self
.has_analog_output(further_state)
.then(|| self.analog_output(further_state, further, facing.opposite()));
// ComparatorBlock.getInputSignal: an item frame behind the
// conductor counts too, the larger signal winning.
@@ -667,12 +667,29 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
}
}
/// `getAnalogOutputSignal` for state-only sources; others ask the caller.
/// `BlockState.hasAnalogOutputSignal`, as the level answers it (a mod's block).
pub(crate) fn has_analog_output(&self, state: BlockStateId) -> bool {
let vanilla = self.b.has_flag(state, state_flags::ANALOG_OUTPUT);
self.level.adjust_has_analog_output(state, vanilla)
}
/// `BlockState.getAnalogOutputSignal`, as the level answers it (a mod's block), 0 to 15.
pub(crate) fn analog_output(
&self,
state: BlockStateId,
pos: BlockPos,
direction: Direction,
) -> i32 {
let vanilla = self.vanilla_analog_output(state, pos, direction);
self.level
.adjust_analog_output(pos, state, direction, vanilla)
.clamp(0, 15)
}
/// `getAnalogOutputSignal` for state-only sources; others ask the caller.
fn vanilla_analog_output(
&self,
state: BlockStateId,
pos: BlockPos,
direction: Direction,
) -> i32 {
let b = self.b;
let p = b.props;
+61
View File
@@ -55,6 +55,10 @@ pub(crate) struct TestLevel {
/// What `adjust_signal` answers at a position: the weak and direct signal,
/// and the only direction they go in, if any.
pub signals: HashMap<BlockPos, (i32, i32, Option<Direction>)>,
/// What `adjust_analog_output` answers at a position, for the states in
/// `analog_states` (`adjust_has_analog_output`).
pub analog: HashMap<BlockPos, i32>,
pub analog_states: HashSet<BlockStateId>,
}
impl TestLevel {
fn new(b: &Behaviours) -> Self {
@@ -63,6 +67,8 @@ impl TestLevel {
air: b.air(),
random: LegacyRandom::new(42),
signals: HashMap::new(),
analog: HashMap::new(),
analog_states: HashSet::new(),
time: 0,
day_time: 0,
events: Vec::new(),
@@ -73,6 +79,18 @@ impl TestLevel {
}
}
impl LevelAccess for TestLevel {
fn adjust_has_analog_output(&self, state: BlockStateId, vanilla: bool) -> bool {
vanilla || self.analog_states.contains(&state)
}
fn adjust_analog_output(
&self,
pos: BlockPos,
_state: BlockStateId,
_direction: Direction,
vanilla: i32,
) -> i32 {
self.analog.get(&pos).copied().unwrap_or(vanilla)
}
fn adjust_signal(
&self,
pos: BlockPos,
@@ -1971,6 +1989,49 @@ fn a_level_can_answer_for_a_blocks_signal() {
assert_eq!(read(&mut w, beside), 15);
}
#[test]
fn a_level_can_answer_for_a_blocks_comparator_output() {
let mut w = world!();
w.floor(0, 4, "stone");
let block = BlockPos::new(0, 1, 0);
let comparator = BlockPos::new(1, 1, 0);
let after = BlockPos::new(2, 1, 0);
// A comparator facing west reads the block west of it.
let facing_west = w.id("comparator", &[("facing", "west")]);
w.set(comparator, facing_west);
let read = |w: &mut World| {
for _ in 0..4 {
w.full_tick();
}
w.f.behaviours
.best_neighbor_signal(&mut w.level, &mut w.ticks, after)
};
let gold = w.id("gold_block", &[]);
w.level.analog.insert(block, 9);
w.set(block, gold);
assert_eq!(read(&mut w), 0, "gold has no output of its own");
// Placing a block that says it has one tells the comparator, as Level.setBlock does.
w.level.analog_states.insert(gold);
w.set(block, w.id("stone", &[]));
w.set(block, gold);
assert_eq!(read(&mut w), 9);
// An output that changes without the block: updateNeighbourForOutputSignal.
w.level.analog.insert(block, 4);
assert_eq!(read(&mut w), 9, "nothing told the comparator yet");
w.f.behaviours
.update_neighbour_for_output_signal(&mut w.level, &mut w.ticks, block);
assert_eq!(read(&mut w), 4);
// Behind a conductor, and out of range answers are clamped.
let further = BlockPos::new(-1, 1, 0);
w.set(block, w.id("stone", &[]));
w.level.analog.insert(further, 40);
w.set(further, gold);
assert_eq!(read(&mut w), 15);
}
#[test]
fn sculk_sensor_phases_and_signal() {
let mut w = world!();
+1 -4
View File
@@ -281,10 +281,7 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
if update_flags & flags::UPDATE_NEIGHBORS != 0 {
let old_block = self.block_of(old);
self.update_neighbors_at(pos, old_block);
if self
.b
.has_flag(state, super::data::state_flags::ANALOG_OUTPUT)
{
if self.has_analog_output(state) {
let block = self.block_of(state);
self.update_neighbour_for_output_signal(pos, block);
}
@@ -0,0 +1,8 @@
{
"hooks": [
{"name": "has_analog_output", "target": "horizon_client::game::native_hooks::GameLevel<'_>::native_has_analog_output"},
{"name": "analog_output", "target": "horizon_client::game::native_hooks::GameLevel<'_>::native_analog_output"}
],
"rust_summary": "comparator output from a mod's block (`native_has_analog_output`,\n`native_analog_output`, and the helper `native_update_output_neighbors`).",
"doc_summary": "a mod block's comparator output, as vanilla's `BlockBehaviour.hasAnalogOutputSignal` and `getAnalogOutputSignal`: `native_has_analog_output` says a block of yours has one,\nand `native_analog_output` gives it (0 to 15) for its state and position. A state change with neighbour updates tells the comparators itself; when the output\nchanges otherwise, `native_update_output_neighbors(dimension, pos)` makes them read it again (`Level.updateNeighbourForOutputSignal`). Bundles need `\"hooks\": @API:analog_output@`."
}