Add optional minimap outline styles and coordinate display

Preserve upstream defaults and existing settings. Include the supplied screenshot, settings regression checks, and the native tool pins and split control-target support needed to validate this renderer integration.

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
eltanschauungandCodex committed 2026-11-03 23:16:37 -05:00
1 parent ee1620f438
commit 13d6b9fbaa
17 files changed
+302 -23

No files matched your search

+1 -1
View File
@@ -4,5 +4,5 @@
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "4baa571566e39bfd6594988d3bb2bc85440c30e7",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
"horizon_mixins_revision": "e8cd0aa38cffef49cb2e088ef716baa107148c02"
}
+1 -1
View File
@@ -4,5 +4,5 @@
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
"horizon_mixins_revision": "e8cd0aa38cffef49cb2e088ef716baa107148c02"
}
+1 -1
View File
@@ -4,5 +4,5 @@
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
"horizon_mixins_revision": "e8cd0aa38cffef49cb2e088ef716baa107148c02"
}
+18 -6
View File
@@ -1,6 +1,6 @@
# Minimap (Horizon native mod)
Horizon's official Minimap: a map of the terrain around you in a corner, in map colours, square or round, north up or turning with you, drawn by Horizon's own minimap. Its settings page (Options > Mods > Minimap > Settings) picks the corner, size, shape, zoom and turning.
Horizon's official Minimap: a map of the terrain around you in a corner, in map colours, square or round, north up or turning with you, drawn by Horizon's own minimap. Its settings page (Options > Mods > Minimap > Settings) picks the corner, size, shape, zoom, turning, coordinates and outline style.
It is a [horizon-mixins](https://github.com/horizon-rust/horizon-mixins) bundle (`mixins/minimap/`) that the Horizon
launcher builds into the game. It keeps the settings of earlier versions: the same files (`horizon-mods/.horizon-mod-data/minimap/corner`, `size`, `shape`, `zoom`, `rotate`).
@@ -14,14 +14,12 @@ launcher downloads its own Rust and builds the game with your native mods; see H
## Made on the hook points
The bundle hooks only Horizon's native hook points (`crates/horizon-client/src/game/native_hooks.rs`), which keep
their names and signatures across releases. `compatibility.json` names the version it needs (`"hooks": 1`), so the
launcher builds Minimap on any Horizon with that version or a later one, the player's own game by default, together with any other
mod. `horizon_revision` is only the commit it was last tested on.
The bundle uses Horizon's native hook points (`crates/horizon-client/src/game/native_hooks.rs`), which keep
their names and signatures across releases. `compatibility.json` records the required native API baseline (`"hooks": 359`) and the tested Horizon revision. The outline and coordinate drawing also inject into the internal HUD renderer, so they depend on its `draw_minimap` function and corner layout; the checks below verify that integration on the tested revision and newest Nightly.
| File | What it is |
|---|---|
| `mixins/minimap/horizon-mixin.toml` | The bundle: one module and the HEAD hooks |
| `mixins/minimap/horizon-mixin.toml` | The bundle: settings/game hooks and minimap HUD drawing injections |
| `mixins/minimap/runtime.rs` | The mod's logic and state, added to the game as `game::official_minimap`; also this crate's library, for its tests |
| `mixins/minimap/*.rs` | One small helper per hook |
| `../tools/native.py` | Shared by the native mods: apply on Horizon (from GitHub, by commit), check, build, package the `.hnmod`, run the proof |
@@ -42,3 +40,17 @@ The proof needs resources prepared from your own Minecraft 26.3 (`horizon-prepar
It seeds an earlier version's settings (bottom right, round, 2 blocks per pixel) in a temporary game folder, then checks that Options > Mods shows them, and that the Corner button moves the map to the top left and writes the same file. CI runs `cargo test`, packages the `.hnmod` and checks the game on the tested commit, and every Monday on the newest Nightly.
License: MIT.
## Outline styles and coordinates
**Minimap Outline** cycles through Black (the existing frame), Stone (a textured grey bevel with mineral flecks and a black outer edge), and Bamboo (a textured beige frame). Stone and Bamboo draw four half-GUI-pixel bands inside the map's existing bounds, for square and round maps in every corner.
Enable **Show Coordinates** to display the player's floored `X Y Z` block position, including negative coordinates. The text is centered below the map, white with a shadow and no background box. HUD layout reserves space for the strip in every corner so it does not overlap adjacent widgets.
Existing defaults stay the same: top right, medium, square, one block per pixel, north up, and a black outline. Coordinates are off by default. Saved preferences are respected; the new values persist as `outline` and `show_coordinates` alongside the existing settings.
The game hook supplies position and style state. Two additional injections around `extras_hud::draw_minimap` reserve the coordinate strip and draw it and the optional outline; these internal rendering injections also depend on Horizon's HUD layout, beyond the stable native hook point used for the map itself.
![Stone outline and coordinate strip, shown in a multiplayer game](docs/minimap-outline-coordinates.png)
Screenshot supplied by eltanschauung. It shows the Stone outline and coordinates enabled; those are optional settings, and the other mods visible in the scene are separate from this change.
+3 -3
View File
@@ -1,8 +1,8 @@
{
"game": "26.3",
"hooks": 1,
"hooks": 359,
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a",
"horizon_revision": "eaa7da4fb1e1c580ed618bc7cfd1758858c0b878",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
"horizon_mixins_revision": "e8cd0aa38cffef49cb2e088ef716baa107148c02"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

@@ -0,0 +1,6 @@
thread_local! {
pub static COORDINATES: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
}
thread_local! {
pub static OUTLINE: std::cell::Cell<(usize, bool)> = const { std::cell::Cell::new((0, false)) };
}
@@ -0,0 +1,125 @@
fn coordinates_after(
_result: (),
corners: &mut Corners,
look: &MinimapLook,
batch: &mut HudBatch,
atlas: &HudAtlas,
) {
// Draw inside the map's reserved square, so larger frames cannot clip at screen edges.
minimap_coordinates::OUTLINE.with(|style| {
let (style, round) = style.get();
if style == 0 {
return;
} // Preserve the original Black appearance exactly.
let i = Corner::ALL
.iter()
.position(|c| *c == look.corner)
.unwrap_or(0);
let x = if look.corner.left() {
2.0
} else {
corners.width - 2.0 - look.size
};
let y = if look.corner.top() {
corners.used[i] - look.size
} else {
corners.height - corners.used[i]
};
// Keep four texture-pixel layers, each half a GUI pixel wide.
const PIXEL: f32 = 0.5;
let side = (look.size / PIXEL).round() as i32;
let radius = side as f32 / 2.0;
for py in 0..side {
for px in 0..side {
let dx = px as f32 + 0.5 - radius;
let dy = py as f32 + 0.5 - radius;
let edge = if round {
radius - dx.hypot(dy)
} else {
(px.min(py).min(side - 1 - px).min(side - 1 - py)) as f32
};
if !(0.0..4.0).contains(&edge) {
continue;
}
let color = if style == 1 {
// From content outward: dark grey, two textured greys, pure black.
if edge < 1.0 {
rgba(0, 0, 0, 255)
} else if edge >= 3.0 {
rgba(72, 72, 72, 255)
} else {
let hash = |x: i32, y: i32| {
let n = (x as u32)
.wrapping_mul(374761393)
.wrapping_add((y as u32).wrapping_mul(668265263));
let n = (n ^ (n >> 13)).wrapping_mul(1274126177);
n ^ (n >> 16)
};
let fine = hash(px, py);
// Small mineral flecks over broader patches avoid uniform noise.
let grain = (fine % 9) as i32 - 4 + (hash(px / 3, py / 2) % 13) as i32 - 6;
let fleck = match fine & 31 {
0 => -9,
1 => 7,
_ => 0,
};
let corner_distance = if round {
(dx.abs() - dy.abs()).abs()
} else {
(px.min(side - 1 - px) as f32).hypot(py.min(side - 1 - py) as f32)
};
let highlight = (1.0 - corner_distance / 16.0).clamp(0.0, 1.0) * 12.0;
let bevel = if dx + dy < 0.0 { 3 } else { -3 };
let base = if edge < 2.0 { 186 } else { 134 };
let grey = (base + grain + fleck + bevel + highlight.round() as i32)
.clamp(0, 255) as u8;
rgba(grey, grey, grey, 255)
}
} else if edge < 1.0 || edge >= 3.0 {
rgba(8, 8, 8, 255)
} else {
// Keep Bamboo's existing beige frame.
let grain = ((px * 17 + py * 31 + (px / 3) * (py / 3) * 7) & 15) as i32 - 8;
let light = if dx + dy < 0.0 { 24 } else { -20 };
let c = [192, 171, 119].map(|v| (v + grain + light).clamp(0, 255) as u8);
rgba(c[0], c[1], c[2], 255)
};
batch.fill(
atlas,
x + px as f32 * PIXEL,
y + py as f32 * PIXEL,
PIXEL,
PIXEL,
color,
);
}
}
});
minimap_coordinates::COORDINATES.with(|c| {
let text = c.borrow();
let Some(text) = text.as_deref() else {
return;
};
let width = HudBatch::text_width(atlas, text) * 0.5;
let (x, y) = if look.corner.top() {
corners.place(look.corner, look.size, 7.0)
} else {
let i = Corner::ALL
.iter()
.position(|c| *c == look.corner)
.unwrap_or(0);
let y = corners.height - corners.used[i] + look.size + 2.0;
let x = if look.corner.left() {
2.0
} else {
corners.width - 2.0 - look.size
};
(x, y)
};
let tx = (x + (look.size - width) / 2.0).round();
let ty = y + 2.0;
let start = batch.vertices.len();
batch.text(atlas, text, tx, ty, rgba(255, 255, 255, 255), true);
batch.scale_since(start, tx, ty, 0.5, 0.5);
});
}
@@ -0,0 +1,5 @@
fn coordinates_before(corners: &mut Corners, look: &MinimapLook) {
if !look.corner.top() && minimap_coordinates::COORDINATES.with(|c| c.borrow().is_some()) {
corners.place(look.corner, look.size, 7.0);
}
}
@@ -3,15 +3,16 @@ id = "minimap"
version = "2.0.0"
priority = 100
# The commit it was tested on. It uses only the native hook points (hooks 1 in
# compatibility.json): the launcher builds it on any Horizon that has them.
# Tested renderer layout. The map uses a stable hook; its outline/coordinate
# drawing also injects around extras_hud::draw_minimap.
[compatibility]
horizon_revision = "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a"
horizon_revision = "eaa7da4fb1e1c580ed618bc7cfd1758858c0b878"
[[module]]
id = "runtime"
file = "crates/horizon-client/src/game.rs"
name = "official_minimap"
visibility = "pub(crate)"
source = "runtime.rs"
[[injection]]
@@ -54,3 +55,33 @@ bindings = [
{ name = "game", ty = "&crate::game::Game<'_>", value = "&*self" },
{ name = "minimap", ty = "&mut Option<crate::game::native_hooks::NativeMinimap>", value = "&mut *minimap" },
]
[[injection]]
id = "coordinates-before"
kind = "HEAD"
target = "horizon_client_lib::extras_hud::draw_minimap"
helper = "coordinates_before.rs"
bindings = [
{ name = "corners", ty = "&mut Corners", value = "&mut *corners" },
{ name = "look", ty = "&MinimapLook", value = "look" },
]
[[injection]]
id = "coordinates-after"
kind = "RETURN"
target = "horizon_client_lib::extras_hud::draw_minimap"
helper = "coordinates_after.rs"
bindings = [
{ name = "_result", ty = "()", value = "$value" },
{ name = "corners", ty = "&mut Corners", value = "&mut *corners" },
{ name = "look", ty = "&MinimapLook", value = "look" },
{ name = "batch", ty = "&mut HudBatch", value = "&mut *batch" },
{ name = "atlas", ty = "&HudAtlas", value = "atlas" },
]
[[module]]
id = "coordinate-state"
file = "crates/horizon-client/src/extras_hud.rs"
name = "minimap_coordinates"
visibility = "pub"
source = "coordinate_state.rs"
+29 -2
View File
@@ -1,15 +1,42 @@
// The minimap Horizon draws for the mod, unless another mod's came first.
fn minimap_view(game: &crate::game::Game<'_>, minimap: &mut Option<crate::game::native_hooks::NativeMinimap>) {
fn minimap_view(
game: &crate::game::Game<'_>,
minimap: &mut Option<crate::game::native_hooks::NativeMinimap>,
) {
use crate::game::official_minimap as m;
use horizon_client::extras::Corner;
use horizon_client::extras_hud::minimap_coordinates::COORDINATES;
COORDINATES.with(|c| *c.borrow_mut() = None);
horizon_client::extras_hud::minimap_coordinates::OUTLINE.with(|s| s.set((0, false)));
// Turned off in Options > Mods: nothing until the next build leaves it out.
if minimap.is_some() || !game.native_on(m::ID) {
return;
}
let settings = m::settings(|key| game.native_setting(m::ID, key));
COORDINATES.with(|c| {
*c.borrow_mut() = if settings.show_coordinates {
game.native_player().map(|p| {
format!(
"{} {} {}",
p.x.floor() as i64,
p.y.floor() as i64,
p.z.floor() as i64
)
})
} else {
None
}
});
horizon_client::extras_hud::minimap_coordinates::OUTLINE
.with(|s| s.set((settings.outline, settings.round)));
let (size, blocks_per_pixel) = settings.view();
*minimap = Some(crate::game::native_hooks::NativeMinimap {
corner: [Corner::TopLeft, Corner::TopRight, Corner::BottomLeft, Corner::BottomRight][settings.corner],
corner: [
Corner::TopLeft,
Corner::TopRight,
Corner::BottomLeft,
Corner::BottomRight,
][settings.corner],
size,
blocks_per_pixel,
rotate: settings.rotate,
+71 -2
View File
@@ -31,6 +31,8 @@ pub struct Settings {
pub round: bool,
pub zoom: usize,
pub rotate: bool,
pub show_coordinates: bool,
pub outline: usize,
}
impl Settings {
@@ -52,6 +54,12 @@ impl Settings {
round: get("shape").as_deref() == Some("round"),
zoom: find("zoom", &zooms, 1),
rotate: get("rotate").as_deref() == Some("true"),
show_coordinates: get("show_coordinates").as_deref() == Some("true"),
outline: find(
"outline",
&["black".into(), "stone".into(), "bamboo".into()],
0,
),
}
}
pub fn labels(&self) -> Vec<String> {
@@ -72,10 +80,26 @@ impl Settings {
"Turn with the player: {}",
if self.rotate { "ON" } else { "OFF" }
),
format!(
"Show Coordinates: {}",
if self.show_coordinates { "ON" } else { "OFF" }
),
format!(
"Minimap Outline: {}",
["Black", "Stone", "Bamboo"][self.outline]
),
]
}
pub fn click(&mut self, i: usize) {
let next = |i: usize, n: usize| (i + 1) % n;
if i == 5 {
self.show_coordinates = !self.show_coordinates;
return;
}
if i == 6 {
self.outline = next(self.outline, 3);
return;
}
match i {
0 => self.corner = next(self.corner, CORNERS.len()),
1 => self.size = next(self.size, SIZES.len()),
@@ -85,7 +109,7 @@ impl Settings {
_ => {}
}
}
pub fn stored(&self) -> [(&'static str, String); 5] {
pub fn stored(&self) -> [(&'static str, String); 7] {
[
("corner", CORNERS[self.corner].0.to_owned()),
("size", SIZES[self.size].0.to_string()),
@@ -98,6 +122,11 @@ impl Settings {
"rotate",
if self.rotate { "true" } else { "false" }.to_owned(),
),
("show_coordinates", self.show_coordinates.to_string()),
(
"outline",
["black", "stone", "bamboo"][self.outline].to_owned(),
),
]
}
/// What Horizon's minimap draws: (side, blocks per pixel).
@@ -154,7 +183,9 @@ mod tests {
"Size: Large",
"Shape: Round",
"Zoom: ½ block per pixel",
"Turn with the player: ON"
"Turn with the player: ON",
"Show Coordinates: OFF",
"Minimap Outline: Black"
]
);
s.click(3);
@@ -164,4 +195,42 @@ mod tests {
let back = Settings::read(|k: &str| round.iter().find(|f| f.0 == k).map(|f| f.1.clone()));
assert_eq!(back, s);
}
#[test]
fn optional_features_keep_the_original_appearance_by_default() {
let settings = Settings::read(|_| None);
assert_eq!(settings.view(), (96.0, 1.0));
assert!(!settings.show_coordinates);
assert_eq!(settings.outline, 0);
let invalid = Settings::read(|key| match key {
"outline" => Some("unknown".into()),
"show_coordinates" => Some("invalid".into()),
_ => None,
});
assert_eq!(invalid, settings);
}
#[test]
fn coordinates_and_each_outline_toggle_and_round_trip() {
let mut settings = Settings::read(|_| None);
for expected in ["stone", "bamboo", "black"] {
settings.click(5);
settings.click(6);
let stored = settings.stored();
assert_eq!(
stored.iter().find(|(key, _)| *key == "outline").unwrap().1,
expected
);
let restored = Settings::read(|key| {
stored
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| format!("{v}\n"))
});
assert_eq!(restored, settings);
}
assert!(settings.show_coordinates);
settings.click(5);
assert!(!settings.show_coordinates);
}
}
+1 -1
View File
@@ -4,5 +4,5 @@
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "4baa571566e39bfd6594988d3bb2bc85440c30e7",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
"horizon_mixins_revision": "e8cd0aa38cffef49cb2e088ef716baa107148c02"
}
+1 -1
View File
@@ -4,5 +4,5 @@
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "48c63bc09d9acc82f2074e75ae59720514796a9d",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
"horizon_mixins_revision": "e8cd0aa38cffef49cb2e088ef716baa107148c02"
}
+4
View File
@@ -159,6 +159,10 @@ def cargo(stage_dir, action):
jobs = os.environ.get("FLEET_LOAD_JOBS") or os.environ.get("CARGO_BUILD_JOBS")
command = ["cargo", action, "--locked", "-p", "horizon-client", "--bin", "horizon-client"]
if action == "build":
# Newer Horizon moved the control tool into the reusable client library.
library = stage_dir / "crates/horizon-client-lib/Cargo.toml"
if library.is_file() and any(b.get("name") == "horizon-ctl" for b in tomllib.loads(library.read_text()).get("bin", [])):
command += ["-p", "horizon-client-lib"]
command += ["--bin", "horizon-ctl", "--profile", "fast"]
if os.environ.get("NATIVE_NO_DEFAULT_FEATURES"):
command.append("--no-default-features")
+1 -1
View File
@@ -4,5 +4,5 @@
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "f4833621f9037e08c487b1779e8c030487e65cc2",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
"horizon_mixins_revision": "e8cd0aa38cffef49cb2e088ef716baa107148c02"
}
+1 -1
View File
@@ -4,5 +4,5 @@
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
"horizon_mixins_revision": "e8cd0aa38cffef49cb2e088ef716baa107148c02"
}