feat(mods): allow cancelling targeted block highlights

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
[email protected]andCodex committed 2026-11-01 09:21:41 +01:00
1 parent 5edb339a76
commit f4d6ebab0c
4 files changed
+80

No files matched your search

+6
View File
@@ -14529,6 +14529,12 @@ impl Game<'_> {
|| !self.block_action_restricted(h.pos)
|| self.can_place_on_in_adventure(h.pos)
})
.filter(|h| {
let state = self.world.block(h.pos[0], h.pos[1], h.pos[2]);
let mut canceled = false;
self.native_block_highlight(h.pos, state, alpha as f32, &mut canceled);
!canceled
})
.map(|h| {
let state = self.world.block(h.pos[0], h.pos[1], h.pos[2]);
let f = self.res.facts().get(state);
@@ -1084,6 +1084,17 @@ impl Game<'_> {
/// and on servers), and the server's own (`native_window` is false
/// there: the single player server thread, a dedicated server).
pub(super) fn native_tick(&mut self) {}
/// Before an eligible targeted block's selection outline is built.
/// Cancel skips only the default block highlight, after normal HUD and game-mode gates.
pub(super) fn native_block_highlight(
&self,
pos: [i32; 3],
state: BlockStateId,
partial: f32,
canceled: &mut bool,
) {
let _ = (pos, state, partial, canceled);
}
/// The world frame's local visual sources. Push requests; Horizon validates
/// and limits them before rendering. Called only while a world is visible.
pub(super) fn native_visuals(
+51
View File
@@ -0,0 +1,51 @@
"""Run the actual outline filter with a fake mod, without a renderer."""
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class BlockHighlightHookTests(unittest.TestCase):
@unittest.skipUnless(shutil.which("rustc"), "requires Rust")
def test_cancel_filters_only_the_target_outline(self):
source = (ROOT / "crates/horizon-client/src/game.rs").read_text()
call = source.index("self.native_block_highlight(")
start = source.rindex(".filter(|h| {", 0, call)
end = source.index(".map(|h| {", call)
self.assertLess(source.index("let outline = if !self.ui.hide_gui"), start)
self.assertLess(source.index("self.can_place_on_in_adventure(h.pos)"), start)
fixture = r"""
use std::cell::Cell;
#[derive(Clone, Copy, Debug, PartialEq)]
struct Hit { pos: [i32; 3] }
#[derive(Clone, Copy, Debug, PartialEq)]
struct BlockStateId(u32);
struct World;
impl World { fn block(&self, x: i32, y: i32, z: i32) -> BlockStateId { assert_eq!([x,y,z], [1,2,3]); BlockStateId(42) } }
struct Game { world: World, cancel: bool, calls: Cell<usize> }
impl Game {
fn native_block_highlight(&self, pos: [i32; 3], state: BlockStateId, partial: f32, canceled: &mut bool) {
assert_eq!(pos, [1,2,3]); assert_eq!(state, BlockStateId(42)); assert_eq!(partial, 0.5);
self.calls.set(self.calls.get()+1); *canceled = self.cancel;
}
fn filter(&self, hit: Option<Hit>) -> Option<Hit> { let alpha = 0.5; hit FILTER }
}
#[test]
fn cancelled_default_and_missing_target() {
for cancel in [false, true] {
let game = Game { world: World, cancel, calls: Cell::new(0) };
let hit = Hit { pos: [1,2,3] };
assert_eq!(game.filter(Some(hit)), if cancel { None } else { Some(hit) });
assert_eq!(game.filter(None), None);
assert_eq!(game.calls.get(), 1);
}
}
""".replace("FILTER", source[start:end])
with tempfile.TemporaryDirectory() as folder:
src, exe = Path(folder) / "outline.rs", Path(folder) / "outline.exe"
src.write_text(fixture)
subprocess.run(["rustc", "--edition=2024", "--test", str(src), "-o", str(exe)], check=True)
subprocess.run([str(exe)], check=True)
@@ -0,0 +1,12 @@
{
"hooks": [
{
"name": "block_highlight",
"target": "horizon_client::game::native_hooks::Game<'_>::native_block_highlight",
"side": "client",
"cancellable": true
}
],
"rust_summary": "a cancellable targeted-block highlight hook (`native_block_highlight`).",
"doc_summary": "`native_block_highlight(pos, state, partial, canceled)` runs before building the eligible targeted block's selection outline, after hidden-HUD and game-mode restrictions. Set `canceled` to suppress only that outline; default behavior is unchanged otherwise. Position is block coordinates and state is the current block state ID. No entity outlines are affected. Bundles need \"hooks\": @API:block_highlight@."
}