diff --git a/native/coords-hud/compatibility.json b/native/coords-hud/compatibility.json index f4eebcb..7e96373 100644 --- a/native/coords-hud/compatibility.json +++ b/native/coords-hud/compatibility.json @@ -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" } diff --git a/native/fps-overlay/compatibility.json b/native/fps-overlay/compatibility.json index 7337dd0..0060643 100644 --- a/native/fps-overlay/compatibility.json +++ b/native/fps-overlay/compatibility.json @@ -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" } diff --git a/native/keystrokes/compatibility.json b/native/keystrokes/compatibility.json index 7337dd0..0060643 100644 --- a/native/keystrokes/compatibility.json +++ b/native/keystrokes/compatibility.json @@ -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" } diff --git a/native/minimap/README.md b/native/minimap/README.md index 9655381..15396ed 100644 --- a/native/minimap/README.md +++ b/native/minimap/README.md @@ -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. diff --git a/native/minimap/compatibility.json b/native/minimap/compatibility.json index 7337dd0..f2cf243 100644 --- a/native/minimap/compatibility.json +++ b/native/minimap/compatibility.json @@ -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" } diff --git a/native/minimap/docs/minimap-outline-coordinates.png b/native/minimap/docs/minimap-outline-coordinates.png new file mode 100644 index 0000000..2cbd2fa Binary files /dev/null and b/native/minimap/docs/minimap-outline-coordinates.png differ diff --git a/native/minimap/mixins/minimap/coordinate_state.rs b/native/minimap/mixins/minimap/coordinate_state.rs new file mode 100644 index 0000000..4dc39bd --- /dev/null +++ b/native/minimap/mixins/minimap/coordinate_state.rs @@ -0,0 +1,6 @@ +thread_local! { + pub static COORDINATES: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; +} +thread_local! { + pub static OUTLINE: std::cell::Cell<(usize, bool)> = const { std::cell::Cell::new((0, false)) }; +} diff --git a/native/minimap/mixins/minimap/coordinates_after.rs b/native/minimap/mixins/minimap/coordinates_after.rs new file mode 100644 index 0000000..065a6b3 --- /dev/null +++ b/native/minimap/mixins/minimap/coordinates_after.rs @@ -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); + }); +} diff --git a/native/minimap/mixins/minimap/coordinates_before.rs b/native/minimap/mixins/minimap/coordinates_before.rs new file mode 100644 index 0000000..0b4f2c7 --- /dev/null +++ b/native/minimap/mixins/minimap/coordinates_before.rs @@ -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); + } +} diff --git a/native/minimap/mixins/minimap/horizon-mixin.toml b/native/minimap/mixins/minimap/horizon-mixin.toml index e450dd1..49b2afe 100644 --- a/native/minimap/mixins/minimap/horizon-mixin.toml +++ b/native/minimap/mixins/minimap/horizon-mixin.toml @@ -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", 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" diff --git a/native/minimap/mixins/minimap/minimap.rs b/native/minimap/mixins/minimap/minimap.rs index f234ff4..1841aa8 100644 --- a/native/minimap/mixins/minimap/minimap.rs +++ b/native/minimap/mixins/minimap/minimap.rs @@ -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) { +fn minimap_view( + game: &crate::game::Game<'_>, + minimap: &mut Option, +) { 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, diff --git a/native/minimap/mixins/minimap/runtime.rs b/native/minimap/mixins/minimap/runtime.rs index d86f68a..523bc0c 100644 --- a/native/minimap/mixins/minimap/runtime.rs +++ b/native/minimap/mixins/minimap/runtime.rs @@ -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 { @@ -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); + } } diff --git a/native/recipes/compatibility.json b/native/recipes/compatibility.json index 1987b1f..c74a2bd 100644 --- a/native/recipes/compatibility.json +++ b/native/recipes/compatibility.json @@ -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" } diff --git a/native/smp/compatibility.json b/native/smp/compatibility.json index 9de5795..3a98a50 100644 --- a/native/smp/compatibility.json +++ b/native/smp/compatibility.json @@ -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" } diff --git a/native/tools/native.py b/native/tools/native.py index a84474d..742f6ed 100644 --- a/native/tools/native.py +++ b/native/tools/native.py @@ -174,9 +174,11 @@ 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": - # horizon-ctl is a bin of horizon-client-lib (its path is the client's src/bin), so it - # cannot be named under `-p horizon-client`. - command += ["-p", "horizon-client-lib", "--bin", "horizon-ctl", "--profile", "fast"] + # 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") if jobs: diff --git a/native/voicechat-server/compatibility.json b/native/voicechat-server/compatibility.json index 4f8e21c..4c0af22 100644 --- a/native/voicechat-server/compatibility.json +++ b/native/voicechat-server/compatibility.json @@ -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" } diff --git a/native/zoom/compatibility.json b/native/zoom/compatibility.json index 7337dd0..0060643 100644 --- a/native/zoom/compatibility.json +++ b/native/zoom/compatibility.json @@ -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" }