mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 10:39:46 -10:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23293b86b4 | ||
|
|
4fa9c5b082 | ||
|
|
a790d406c7 | ||
|
|
1d12ab67f5 | ||
|
|
98c8498c8b | ||
|
|
507e48073a | ||
|
|
85514bb934 |
No files matched your search
@@ -307,6 +307,7 @@ mod native_menu_stacks;
|
||||
mod native_menus;
|
||||
mod native_mob_draw;
|
||||
mod native_mobs;
|
||||
mod native_mod_screen;
|
||||
mod native_motion;
|
||||
mod native_mounts;
|
||||
mod native_net;
|
||||
@@ -2835,8 +2836,8 @@ struct Game<'a> {
|
||||
native_player_fx: native_player_fx::NativePlayerFx,
|
||||
/// Native mods' kicks waiting for the tick's end (`game/native_packets.rs`).
|
||||
native_packets: native_packets::NativePackets,
|
||||
/// The mod whose own screen is open (`Screen::Mod`).
|
||||
native_mod_screen: Option<String>,
|
||||
/// The mod whose own screen is open (`Screen::Mod`), and what it replaced.
|
||||
native_mod_screen: native_mod_screen::ModScreen,
|
||||
/// Meshed sections waiting for an upload budget (Horizon settings).
|
||||
pending_uploads: Vec<((i32, i32, i32), horizon_client::mesher::SectionMesh)>,
|
||||
/// This frame's view frustum, for culling entities and block entities.
|
||||
@@ -6246,56 +6247,7 @@ impl Game<'_> {
|
||||
} else {
|
||||
self.screen
|
||||
};
|
||||
match screen {
|
||||
Screen::Paused if self.pause_menu => self.draw_pause_menu(&mut batch),
|
||||
// PauseScreen(false): only its title, at the top.
|
||||
Screen::Paused => {
|
||||
let title = self.translate("menu.paused", "Game Paused");
|
||||
horizon_client::widgets::begin_screen(|| {
|
||||
horizon_client::widgets::ScreenInfo::new("paused", title.clone())
|
||||
});
|
||||
let tw = HudBatch::text_width(&self.gui.atlas, &title);
|
||||
batch.text(
|
||||
&self.gui.atlas,
|
||||
&title,
|
||||
((batch.width / 2.0).floor() - (tw / 2.0).floor()).floor(),
|
||||
10.0,
|
||||
u32::MAX,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Screen::GameModeSwitcher => self.draw_gamemode_switcher(&mut batch),
|
||||
Screen::EmoteWheel => self.draw_emote_wheel(&mut batch),
|
||||
Screen::Dead => self.draw_death_screen(&mut batch),
|
||||
Screen::Stats => self.draw_stats(&mut batch),
|
||||
Screen::Advancements => self.draw_advancements(&mut batch),
|
||||
Screen::Mod => {}
|
||||
Screen::WorldOptions => self.draw_world_options(&mut batch),
|
||||
Screen::Friends => self.draw_friends_screen(&mut batch),
|
||||
Screen::Social => self.draw_social_screen(&mut batch),
|
||||
Screen::ServerLinks => self.draw_server_links(&mut batch),
|
||||
Screen::ConfirmLink => self.draw_confirm_link(&mut batch),
|
||||
Screen::DialogWarning => self.draw_dialog_warning(&mut batch),
|
||||
Screen::CommandBlock => self.draw_command_block(&mut batch),
|
||||
Screen::BlockEditor => self.draw_block_editor(&mut batch),
|
||||
Screen::DebugOptions => self.draw_debug_options(&mut batch),
|
||||
Screen::HudEditor => self.draw_hud_editor(&mut batch),
|
||||
Screen::Studio => {}
|
||||
Screen::Inventory => {
|
||||
let from = batch.vertices.len();
|
||||
self.draw_inventory(&mut batch);
|
||||
self.draw_mod_browser(&mut batch);
|
||||
self.draw_pad_menu_guide(&mut batch, from);
|
||||
}
|
||||
Screen::Creative => {
|
||||
let from = batch.vertices.len();
|
||||
self.draw_creative(&mut batch);
|
||||
self.draw_mod_browser(&mut batch);
|
||||
self.draw_pad_menu_guide(&mut batch, from);
|
||||
}
|
||||
Screen::Chat => self.draw_chat_screen(&mut batch),
|
||||
Screen::Playing | Screen::Options | Screen::Win => {}
|
||||
}
|
||||
self.draw_screen(&mut batch, screen);
|
||||
// ScreenEvent.Render.Post: the open screen has drawn itself and its
|
||||
// tooltips, so a native mod draws over it here. Never `Playing`: the
|
||||
// prompts, books and sign editors drawn over gameplay keep it, draw
|
||||
@@ -12205,7 +12157,7 @@ impl<'a> Game<'a> {
|
||||
native_net: Default::default(),
|
||||
native_player_fx: Default::default(),
|
||||
native_packets: Default::default(),
|
||||
native_mod_screen: None,
|
||||
native_mod_screen: Default::default(),
|
||||
sky_darken_memo: Default::default(),
|
||||
water_plane_memo: Default::default(),
|
||||
water_plane_scan: Default::default(),
|
||||
|
||||
@@ -11300,25 +11300,12 @@ impl Game<'_> {
|
||||
/// dedicated server). A gamepad can close it (B, Start) but not click on
|
||||
/// it: its clicks don't reach the mouse hooks. API 111.
|
||||
pub(super) fn native_mod_screen_open(&mut self, mod_id: &str) -> bool {
|
||||
// What `screen_hooks_open` would refuse over the world: the mod's
|
||||
// screen would be up with no hook to draw or click it.
|
||||
let blocked = self.title.is_some()
|
||||
|| self.renderer.is_none()
|
||||
|| self.lan_acting.is_some()
|
||||
|| self.player.is_none()
|
||||
|| self.screen_over_playing()
|
||||
|| self.remote_loading();
|
||||
let blocked = self.mod_screen_blocked();
|
||||
let owner = self.native_mod_screen();
|
||||
if let Some(open) = mod_screen_opens(self.screen, blocked, owner, mod_id) {
|
||||
return open;
|
||||
}
|
||||
self.native_mod_screen = Some(mod_id.to_owned());
|
||||
self.screen = Screen::Mod;
|
||||
self.keys.clear();
|
||||
self.left = false;
|
||||
self.right = false;
|
||||
self.mining = None;
|
||||
self.capture(false);
|
||||
self.open_mod_screen(mod_id, None);
|
||||
true
|
||||
}
|
||||
/// Closes mod `mod_id`'s own screen, if it is open. API 111.
|
||||
@@ -11331,17 +11318,10 @@ impl Game<'_> {
|
||||
/// replacing it, closes it). API 111.
|
||||
pub(super) fn native_mod_screen(&self) -> Option<&str> {
|
||||
self.native_mod_screen
|
||||
.id
|
||||
.as_deref()
|
||||
.filter(|_| self.screen == Screen::Mod)
|
||||
}
|
||||
/// A mod's own screen closes (Escape, or `native_mod_screen_close`).
|
||||
pub(super) fn close_mod_screen(&mut self) {
|
||||
self.native_mod_screen = None;
|
||||
if self.screen == Screen::Mod {
|
||||
self.screen = Screen::Playing;
|
||||
self.capture(true);
|
||||
}
|
||||
}
|
||||
/// The open menu's title (the block's name, or the one it was given),
|
||||
/// as plain text. API 20.
|
||||
pub(super) fn native_screen_title(&self) -> Option<String> {
|
||||
|
||||
@@ -685,6 +685,29 @@ fn catalog_matches_hook_points() {
|
||||
let conn_hooks = free_hooks(include_str!("../lan/conn.rs"));
|
||||
// The log writer's (`horizon_client_lib::native_client_hooks`).
|
||||
let client_hooks = free_hooks(include_str!("../native_client_hooks.rs"));
|
||||
// Hook points kept out of native_hooks.rs, in another module under game/: each is declared in
|
||||
// the file its target names.
|
||||
let mut game_hooks = std::collections::BTreeMap::new();
|
||||
for hook in &hooks {
|
||||
let target = hook["target"].as_str().unwrap();
|
||||
let Some((module, name)) = target
|
||||
.strip_prefix("horizon_client::game::")
|
||||
.and_then(|rest| rest.split_once("::Game<'_>::native_"))
|
||||
.filter(|(module, _)| *module != "native_hooks")
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let file = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src/game")
|
||||
.join(format!("{}.rs", module.replace("::", "/")));
|
||||
let source = std::fs::read_to_string(&file).unwrap();
|
||||
assert!(
|
||||
source.contains(&format!("fn native_{name}(")),
|
||||
"{} does not declare native_{name} (target {target})",
|
||||
file.display()
|
||||
);
|
||||
game_hooks.insert(name.to_owned(), target.to_owned());
|
||||
}
|
||||
let declared: BTreeSet<_> = section
|
||||
.lines()
|
||||
.chain(level_section.lines())
|
||||
@@ -709,6 +732,7 @@ fn catalog_matches_hook_points() {
|
||||
.chain(mob_hooks.iter().copied())
|
||||
.chain(conn_hooks.iter().copied())
|
||||
.chain(client_hooks.iter().copied())
|
||||
.chain(game_hooks.keys().map(String::as_str))
|
||||
.collect();
|
||||
let mut names = BTreeSet::new();
|
||||
let mut contracts = BTreeSet::new();
|
||||
@@ -720,7 +744,9 @@ fn catalog_matches_hook_points() {
|
||||
"invalid or duplicate hook: {name} v{version}"
|
||||
);
|
||||
names.insert(name);
|
||||
let target = if render_hooks.contains(name) {
|
||||
let target = if let Some(target) = game_hooks.get(name) {
|
||||
target.clone()
|
||||
} else if render_hooks.contains(name) {
|
||||
format!("horizon_client_render::gfx::native_render::native_{name}")
|
||||
} else if item_hooks.contains(name) {
|
||||
format!("horizon_world::item_defaults::native_{name}")
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
//! A native mod's own screen (`Screen::Mod`): opening it over the world or over a screen it
|
||||
//! replaces and gives back, drawing that screen under it, closing it (docs/native-mods.md).
|
||||
//! `native_mod_screen_open` and the screen hooks stay in `native_hooks`.
|
||||
use super::{Game, Screen};
|
||||
use horizon_client::hud::HudBatch;
|
||||
|
||||
/// The open mod screen (`Screen::Mod`): whose it is, and what it replaced.
|
||||
#[derive(Default)]
|
||||
pub(super) struct ModScreen {
|
||||
/// The mod whose own screen is open.
|
||||
pub(super) id: Option<String>,
|
||||
/// The screen the open mod screen replaced (None: it was over the world).
|
||||
under: Option<ModScreenUnder>,
|
||||
}
|
||||
|
||||
/// The screen a native mod's own screen replaced
|
||||
/// (`native_mod_screen_open_over`): what Escape returns to.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct ModScreenUnder {
|
||||
screen: Screen,
|
||||
/// Drawn under the mod screen (the mod asked, and the screen is one that
|
||||
/// can be drawn without its input).
|
||||
drawn: bool,
|
||||
/// The panel it drew this frame (`screen_rect`), for the mod to place
|
||||
/// what it draws over it by.
|
||||
rect: Option<[f32; 4]>,
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// Opens a screen of mod `mod_id`'s own like `native_mod_screen_open`, but
|
||||
/// over the options page (Options > Mods), the pause menu or an open
|
||||
/// inventory or container too, as vanilla's `setScreen` would replace
|
||||
/// them. The replaced screen comes back when the mod screen closes
|
||||
/// (Escape or `native_mod_screen_close`), as `lastScreen` does; a server
|
||||
/// that closes the container meanwhile closes it for good. With
|
||||
/// `keep_drawing_previous` the pause menu's title or the container's
|
||||
/// panel is still drawn under the mod screen, with no pointer over it (no
|
||||
/// slot highlight, no tooltip), and `native_mod_screen_under_rect` says
|
||||
/// where; the container takes no clicks or keys. A single-player game
|
||||
/// stays paused under a mod screen over a pausing screen. True when it is
|
||||
/// open (`mod_id`'s was already), false for the same screens as
|
||||
/// `native_mod_screen_open` and for the chat, the creative inventory and
|
||||
/// the other screens of the game's own. Bundles need "hooks": @API:mod_screen_over@.
|
||||
pub(super) fn native_mod_screen_open_over(
|
||||
&mut self,
|
||||
mod_id: &str,
|
||||
keep_drawing_previous: bool,
|
||||
) -> bool {
|
||||
let blocked = self.mod_screen_blocked();
|
||||
let owner = self.native_mod_screen();
|
||||
if let Some(open) = mod_screen_opens_over(self.screen, blocked, owner, mod_id) {
|
||||
return open;
|
||||
}
|
||||
let under = (self.screen != Screen::Playing).then_some(ModScreenUnder {
|
||||
screen: self.screen,
|
||||
drawn: keep_drawing_previous && mod_screen_draws_under(self.screen),
|
||||
rect: None,
|
||||
});
|
||||
self.open_mod_screen(mod_id, under);
|
||||
true
|
||||
}
|
||||
/// What `screen_hooks_open` would refuse over the world: the mod's screen
|
||||
/// would be up with no hook to draw or click it.
|
||||
pub(super) fn mod_screen_blocked(&self) -> bool {
|
||||
self.title.is_some()
|
||||
|| self.renderer.is_none()
|
||||
|| self.lan_acting.is_some()
|
||||
|| self.player.is_none()
|
||||
|| self.screen_over_playing()
|
||||
|| self.remote_loading()
|
||||
}
|
||||
pub(super) fn open_mod_screen(&mut self, mod_id: &str, under: Option<ModScreenUnder>) {
|
||||
self.native_mod_screen = ModScreen {
|
||||
id: Some(mod_id.to_owned()),
|
||||
under,
|
||||
};
|
||||
self.screen = Screen::Mod;
|
||||
self.keys.clear();
|
||||
self.left = false;
|
||||
self.right = false;
|
||||
self.mining = None;
|
||||
self.capture(false);
|
||||
}
|
||||
/// The panel of the screen under the open mod screen, in GUI pixels
|
||||
/// (`NativeScreenView.rect` as that screen has it), as the last frame drew
|
||||
/// it; None when the mod screen is over the world, or did not ask for the
|
||||
/// screen under it to be drawn. A container's panel is where its slots
|
||||
/// and overlays are. Bundles need "hooks": @API:mod_screen_over@.
|
||||
pub(super) fn native_mod_screen_under_rect(&self) -> Option<[f32; 4]> {
|
||||
if self.screen != Screen::Mod {
|
||||
return None;
|
||||
}
|
||||
self.native_mod_screen.under.filter(|u| u.drawn)?.rect
|
||||
}
|
||||
/// A mod's own screen closes (Escape, or `native_mod_screen_close`).
|
||||
pub(super) fn close_mod_screen(&mut self) {
|
||||
let under = std::mem::take(&mut self.native_mod_screen).under;
|
||||
if self.screen == Screen::Mod {
|
||||
// `lastScreen`: the screen it replaced comes back; the mouse
|
||||
// stays free, as that screen had it.
|
||||
self.screen = mod_screen_returns_to(under.map(|u| u.screen));
|
||||
if self.screen == Screen::Playing {
|
||||
self.capture(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Draws the screen under the open mod screen, if the mod asked
|
||||
/// (`native_mod_screen_open_over`): as itself, with the pointer far off so
|
||||
/// no slot lights up and no tooltip shows (its clicks are the mod
|
||||
/// screen's), then remembers its panel.
|
||||
pub(super) fn draw_mod_screen_under(&mut self, batch: &mut HudBatch) {
|
||||
let Some(under) = self.native_mod_screen.under.filter(|u| u.drawn) else {
|
||||
return;
|
||||
};
|
||||
let (screen, cursor) = (self.screen, self.cursor);
|
||||
self.screen = under.screen;
|
||||
self.cursor = (-1.0e6, -1.0e6);
|
||||
self.draw_screen(batch, under.screen);
|
||||
let rect = self.screen_rect(batch, under.screen);
|
||||
self.screen = screen;
|
||||
self.cursor = cursor;
|
||||
if let Some(under) = self.native_mod_screen.under.as_mut() {
|
||||
under.rect = Some(rect);
|
||||
}
|
||||
}
|
||||
/// A mod screen is up over the inventory or a container (a server's
|
||||
/// closing it closes the mod screen with it).
|
||||
fn mod_screen_covers_inventory(&self) -> bool {
|
||||
mod_screen_over_inventory(self.screen, self.native_mod_screen.under.map(|u| u.screen))
|
||||
}
|
||||
/// The inventory or a container is open, or a mod screen is up over it:
|
||||
/// a server's closing the container closes the mod screen with it.
|
||||
pub(super) fn screen_shows_inventory(&self) -> bool {
|
||||
self.screen == Screen::Inventory || self.mod_screen_covers_inventory()
|
||||
}
|
||||
/// The open screen, or the one an open mod screen replaced: a mod screen
|
||||
/// over a pausing screen keeps the game paused.
|
||||
pub(super) fn screen_under_mod(&self) -> Screen {
|
||||
match self.native_mod_screen.under {
|
||||
Some(under) if self.screen == Screen::Mod => under.screen,
|
||||
_ => self.screen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What `native_mod_screen_open_over(mod_id)` does over `screen` (as
|
||||
/// `mod_screen_opens`, which it widens): the options page, the pause menu, and
|
||||
/// the inventory or a container (a server's or a block's) are replaced and
|
||||
/// come back; the others are not.
|
||||
fn mod_screen_opens_over(
|
||||
screen: Screen,
|
||||
blocked: bool,
|
||||
owner: Option<&str>,
|
||||
mod_id: &str,
|
||||
) -> Option<bool> {
|
||||
if screen == Screen::Mod {
|
||||
return Some(owner == Some(mod_id));
|
||||
}
|
||||
// No `matches!`: horizon-mixins refuses macros in a hook point's body.
|
||||
let covered = screen == Screen::Playing
|
||||
|| screen == Screen::Options
|
||||
|| screen == Screen::Paused
|
||||
|| screen == Screen::Inventory;
|
||||
if !covered || blocked {
|
||||
return Some(false);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether the open `screen`, with `under` the screen a mod screen replaced,
|
||||
/// is the inventory or a container shown by a mod screen over it.
|
||||
fn mod_screen_over_inventory(screen: Screen, under: Option<Screen>) -> bool {
|
||||
screen == Screen::Mod && under == Some(Screen::Inventory)
|
||||
}
|
||||
|
||||
/// The screen a mod screen gives back when it closes: the one it replaced, or
|
||||
/// the world.
|
||||
fn mod_screen_returns_to(under: Option<Screen>) -> Screen {
|
||||
under.unwrap_or(Screen::Playing)
|
||||
}
|
||||
|
||||
/// Whether `screen` can be drawn under a mod screen without its input: the
|
||||
/// pause menu and the inventory or container panel, which draw from state
|
||||
/// alone. The options page is built of widgets that need their own input.
|
||||
fn mod_screen_draws_under(screen: Screen) -> bool {
|
||||
screen == Screen::Paused || screen == Screen::Inventory
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
Screen, mod_screen_draws_under, mod_screen_opens_over, mod_screen_over_inventory,
|
||||
mod_screen_returns_to,
|
||||
};
|
||||
|
||||
/// A mod's own screen can also replace the options page, the pause menu
|
||||
/// and the inventory or a container (they come back), but still not
|
||||
/// another mod's screen, an overlay or any other screen of the game's.
|
||||
#[test]
|
||||
fn a_mod_screen_opens_over_a_screen_it_can_give_back() {
|
||||
for covered in [
|
||||
Screen::Playing,
|
||||
Screen::Options,
|
||||
Screen::Paused,
|
||||
Screen::Inventory,
|
||||
] {
|
||||
assert_eq!(mod_screen_opens_over(covered, false, None, "a"), None);
|
||||
assert_eq!(mod_screen_opens_over(covered, true, None, "a"), Some(false));
|
||||
}
|
||||
for other in [
|
||||
Screen::Chat,
|
||||
Screen::Creative,
|
||||
Screen::Dead,
|
||||
Screen::Stats,
|
||||
Screen::CommandBlock,
|
||||
] {
|
||||
assert_eq!(mod_screen_opens_over(other, false, None, "a"), Some(false));
|
||||
}
|
||||
assert_eq!(
|
||||
mod_screen_opens_over(Screen::Mod, false, Some("a"), "a"),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
mod_screen_opens_over(Screen::Mod, false, Some("b"), "a"),
|
||||
Some(false)
|
||||
);
|
||||
// A server closing the container closes the mod screen over it; the
|
||||
// screen it gives back is the replaced one, or the world.
|
||||
assert!(mod_screen_over_inventory(
|
||||
Screen::Mod,
|
||||
Some(Screen::Inventory)
|
||||
));
|
||||
assert!(!mod_screen_over_inventory(
|
||||
Screen::Mod,
|
||||
Some(Screen::Paused)
|
||||
));
|
||||
assert!(!mod_screen_over_inventory(Screen::Mod, None));
|
||||
assert!(!mod_screen_over_inventory(
|
||||
Screen::Inventory,
|
||||
Some(Screen::Inventory)
|
||||
));
|
||||
assert!(mod_screen_returns_to(Some(Screen::Options)) == Screen::Options);
|
||||
assert!(mod_screen_returns_to(None) == Screen::Playing);
|
||||
// Only what draws from state alone is kept on screen.
|
||||
assert!(mod_screen_draws_under(Screen::Inventory));
|
||||
assert!(mod_screen_draws_under(Screen::Paused));
|
||||
assert!(!mod_screen_draws_under(Screen::Options));
|
||||
assert!(!mod_screen_draws_under(Screen::Playing));
|
||||
}
|
||||
}
|
||||
@@ -45,11 +45,13 @@ impl Game<'_> {
|
||||
}
|
||||
/// `Minecraft.pause`: a single-player game with a pausing screen open.
|
||||
pub(super) fn pauses_game(&self) -> bool {
|
||||
// A mod screen over a pausing screen keeps the game paused.
|
||||
let screen = self.screen_under_mod();
|
||||
self.has_singleplayer_server()
|
||||
&& !self.is_published()
|
||||
&& !self.has_split_players()
|
||||
&& self.title.is_none()
|
||||
&& (self.screen.is_pause_screen() || self.book.is_some() || self.win_screen_pauses())
|
||||
&& (screen.is_pause_screen() || self.book.is_some() || self.win_screen_pauses())
|
||||
}
|
||||
/// A screen open over gameplay while `screen` stays `Playing`: a book or
|
||||
/// lectern, a sign, a server dialog, the code of conduct, a link or
|
||||
|
||||
@@ -5205,7 +5205,7 @@ impl Game<'_> {
|
||||
self.book = None;
|
||||
self.sign_edit = None;
|
||||
}
|
||||
if !self.remote_lectern_closed() && (self.screen == Screen::Inventory || dialog) {
|
||||
if !self.remote_lectern_closed() && (self.screen_shows_inventory() || dialog) {
|
||||
self.remote_close_local();
|
||||
self.screen = Screen::Playing;
|
||||
self.capture(true);
|
||||
|
||||
@@ -34,6 +34,59 @@ impl Game<'_> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// What `screen` draws of its own, once a frame (the match `hud_layers`
|
||||
/// runs after the HUD): `Screen::Mod` draws only the screen it replaced,
|
||||
/// when `native_mod_screen_open_over` asked for it.
|
||||
pub(super) fn draw_screen(&mut self, batch: &mut HudBatch, screen: Screen) {
|
||||
match screen {
|
||||
Screen::Paused if self.pause_menu => self.draw_pause_menu(batch),
|
||||
// PauseScreen(false): only its title, at the top.
|
||||
Screen::Paused => {
|
||||
let title = self.translate("menu.paused", "Game Paused");
|
||||
widgets::begin_screen(|| ScreenInfo::new("paused", title.clone()));
|
||||
let tw = HudBatch::text_width(&self.gui.atlas, &title);
|
||||
batch.text(
|
||||
&self.gui.atlas,
|
||||
&title,
|
||||
((batch.width / 2.0).floor() - (tw / 2.0).floor()).floor(),
|
||||
10.0,
|
||||
u32::MAX,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Screen::GameModeSwitcher => self.draw_gamemode_switcher(batch),
|
||||
Screen::EmoteWheel => self.draw_emote_wheel(batch),
|
||||
Screen::Dead => self.draw_death_screen(batch),
|
||||
Screen::Stats => self.draw_stats(batch),
|
||||
Screen::Advancements => self.draw_advancements(batch),
|
||||
Screen::Mod => self.draw_mod_screen_under(batch),
|
||||
Screen::WorldOptions => self.draw_world_options(batch),
|
||||
Screen::Friends => self.draw_friends_screen(batch),
|
||||
Screen::Social => self.draw_social_screen(batch),
|
||||
Screen::ServerLinks => self.draw_server_links(batch),
|
||||
Screen::ConfirmLink => self.draw_confirm_link(batch),
|
||||
Screen::DialogWarning => self.draw_dialog_warning(batch),
|
||||
Screen::CommandBlock => self.draw_command_block(batch),
|
||||
Screen::BlockEditor => self.draw_block_editor(batch),
|
||||
Screen::DebugOptions => self.draw_debug_options(batch),
|
||||
Screen::HudEditor => self.draw_hud_editor(batch),
|
||||
Screen::Studio => {}
|
||||
Screen::Inventory => {
|
||||
let from = batch.vertices.len();
|
||||
self.draw_inventory(batch);
|
||||
self.draw_mod_browser(batch);
|
||||
self.draw_pad_menu_guide(batch, from);
|
||||
}
|
||||
Screen::Creative => {
|
||||
let from = batch.vertices.len();
|
||||
self.draw_creative(batch);
|
||||
self.draw_mod_browser(batch);
|
||||
self.draw_pad_menu_guide(batch, from);
|
||||
}
|
||||
Screen::Chat => self.draw_chat_screen(batch),
|
||||
Screen::Playing | Screen::Options | Screen::Win => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
|
||||
@@ -590,6 +590,65 @@ fn free_function_source(target: &str) -> Option<(&'static str, String)> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The `Game` method a hook point `target` names. Most are in native_hooks.rs (`hooks`), where
|
||||
/// `cargo run -p xtask -- mod-api` fills a new one's dispatch call. One may live in any module under
|
||||
/// game/ (`game`, the target naming that module as a helper's does), to keep a big file from
|
||||
/// growing: there it makes its `dispatch_<name>` call itself and holds no macro or `cfg`, which
|
||||
/// horizon-mixins could not patch (what `hook_points_stay_hookable` checks in native_hooks.rs).
|
||||
fn game_hook<'a>(
|
||||
target: &str,
|
||||
name: &str,
|
||||
hooks: &'a [syn::ItemFn],
|
||||
game: &'a [(String, syn::ItemFn)],
|
||||
) -> Result<&'a syn::ItemFn, String> {
|
||||
let native = format!("native_{name}");
|
||||
if let Some(rest) = target.strip_prefix("horizon_client::game::native_hooks::") {
|
||||
if !rest.ends_with(&format!("::{native}")) {
|
||||
return Err(format!("wrong target for {name}"));
|
||||
}
|
||||
return one_function(hooks.iter(), &native);
|
||||
}
|
||||
let found: Vec<_> = game
|
||||
.iter()
|
||||
.filter(|(module, method)| {
|
||||
method.sig.ident == native && !module.ends_with("::native_hooks")
|
||||
})
|
||||
.collect();
|
||||
let [(module, method)] = found[..] else {
|
||||
return Err(format!(
|
||||
"wrong target for {name}: expected one {native} under game/"
|
||||
));
|
||||
};
|
||||
let expected = format!("{module}::Game<'_>::{native}");
|
||||
if target != expected {
|
||||
return Err(format!(
|
||||
"wrong target for {name}: {target}, it is {expected}"
|
||||
));
|
||||
}
|
||||
if !has_call(&method.block, &format!("dispatch_{name}")) {
|
||||
return Err(format!(
|
||||
"{native}: a hook point outside native_hooks.rs makes its dispatch_{name} call itself"
|
||||
));
|
||||
}
|
||||
struct Unhookable(bool);
|
||||
impl<'ast> Visit<'ast> for Unhookable {
|
||||
fn visit_macro(&mut self, _: &'ast syn::Macro) {
|
||||
self.0 = true;
|
||||
}
|
||||
fn visit_attribute(&mut self, attr: &'ast syn::Attribute) {
|
||||
self.0 |= attr.path().is_ident("cfg") || attr.path().is_ident("cfg_attr");
|
||||
}
|
||||
}
|
||||
let mut unhookable = Unhookable(false);
|
||||
unhookable.visit_item_fn(method);
|
||||
if unhookable.0 {
|
||||
return Err(format!(
|
||||
"{native}: a macro or cfg in a hook point, which horizon-mixins cannot patch"
|
||||
));
|
||||
}
|
||||
Ok(method)
|
||||
}
|
||||
|
||||
/// Whether a hook function needs its dispatch `call` filled in: false when it
|
||||
/// makes the call; true when it is a `Game` or `GameLevel` method whose body is
|
||||
/// only the placeholder a new hook starts with, `let _ = (every, parameter);`
|
||||
@@ -721,9 +780,17 @@ pub fn fill_hooks(root: &Path) -> Result<(), String> {
|
||||
fn render_hooks_current(root: &Path, render: &str) -> Result<(), String> {
|
||||
let path = root.join(RENDER_HOOKS);
|
||||
let committed = fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
// rustfmt adds a trailing comma where it wraps a list (`fill_hooks` formats the file), which
|
||||
// the generator's one-line code lacks: the same code either way.
|
||||
let tokens = |code: &str| {
|
||||
syn::parse_file(code)
|
||||
.map(|file| file.into_token_stream().to_string())
|
||||
.map(|file| {
|
||||
file.into_token_stream()
|
||||
.to_string()
|
||||
.replace(" ,)", ")")
|
||||
.replace(" , }", " }")
|
||||
.replace(" ,]", "]")
|
||||
})
|
||||
.map_err(|e| format!("{}: {e}", path.display()))
|
||||
};
|
||||
if tokens(&committed)? == tokens(render)? {
|
||||
@@ -758,6 +825,13 @@ fn sources(root: &Path, fill: FillMode) -> Result<Sources, String> {
|
||||
methods.extend(impl_functions(item));
|
||||
}
|
||||
}
|
||||
// Every `impl Game` method under game/: helpers, and hook points kept out of native_hooks.rs.
|
||||
let mut game = Vec::new();
|
||||
game_methods(
|
||||
&root.join("crates/horizon-client/src/game"),
|
||||
"horizon_client::game",
|
||||
&mut game,
|
||||
)?;
|
||||
let mut traits = String::new();
|
||||
let mut dispatch = String::new();
|
||||
let mut render_traits = String::new();
|
||||
@@ -806,13 +880,8 @@ fn sources(root: &Path, fill: FillMode) -> Result<Sources, String> {
|
||||
},
|
||||
)
|
||||
} else {
|
||||
if !target.starts_with("horizon_client::game::native_hooks::")
|
||||
|| !target.ends_with(&format!("::{native}"))
|
||||
{
|
||||
return Err(format!("wrong target for {name}"));
|
||||
}
|
||||
(
|
||||
one_function(methods.iter(), &native)?.clone(),
|
||||
game_hook(target, name, &methods, &game)?.clone(),
|
||||
format!("dispatch_{name}"),
|
||||
)
|
||||
};
|
||||
@@ -1026,12 +1095,6 @@ fn sources(root: &Path, fill: FillMode) -> Result<Sources, String> {
|
||||
let api_source = fs::read_to_string(root.join("crates/horizon-client/src/mod_api.rs"))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let types = api_types(&api_source, &code)?;
|
||||
let mut game = Vec::new();
|
||||
game_methods(
|
||||
&root.join("crates/horizon-client/src/game"),
|
||||
"horizon_client::game",
|
||||
&mut game,
|
||||
)?;
|
||||
let mut host_traits = String::new();
|
||||
let mut host_impl = String::new();
|
||||
let mut names = HashSet::new();
|
||||
@@ -1237,6 +1300,73 @@ mod tests {
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// A hook point may live in any module under game/ when it makes its own dispatch call and holds
|
||||
/// nothing horizon-mixins cannot patch; in native_hooks.rs it may still wait to be filled.
|
||||
#[test]
|
||||
fn hook_points_outside_native_hooks() {
|
||||
let method = |source: &str| -> syn::ItemFn { syn::parse_str(source).unwrap() };
|
||||
let module = |name: &str| format!("horizon_client::game::{name}");
|
||||
let game = vec![
|
||||
(
|
||||
module("native_net"),
|
||||
method(
|
||||
"fn native_sent(&self, cancel: &mut bool) { mod_api::dispatch_sent(self, mod_api::mods(), cancel); }",
|
||||
),
|
||||
),
|
||||
(
|
||||
module("native_menus"),
|
||||
method("fn native_bare(&self) { let _ = (); }"),
|
||||
),
|
||||
(
|
||||
module("native_menus"),
|
||||
method(
|
||||
"fn native_logged(&self) { log::info!(\"x\"); mod_api::dispatch_logged(self, mod_api::mods()); }",
|
||||
),
|
||||
),
|
||||
(
|
||||
module("native_menus"),
|
||||
method(
|
||||
"fn native_gated(&self) { #[cfg(test)] mod_api::dispatch_gated(self, mod_api::mods()); }",
|
||||
),
|
||||
),
|
||||
];
|
||||
let hooks = [method("fn native_shown(&self) { let _ = (); }")];
|
||||
let found = |target: &str, name: &str| {
|
||||
super::game_hook(
|
||||
&format!("horizon_client::game::{target}"),
|
||||
name,
|
||||
&hooks,
|
||||
&game,
|
||||
)
|
||||
.map(|m| m.sig.ident.to_string())
|
||||
};
|
||||
assert_eq!(
|
||||
found("native_net::Game<'_>::native_sent", "sent").unwrap(),
|
||||
"native_sent"
|
||||
);
|
||||
assert_eq!(
|
||||
found("native_hooks::Game<'_>::native_shown", "shown").unwrap(),
|
||||
"native_shown"
|
||||
);
|
||||
assert_eq!(
|
||||
found("native_menus::Game<'_>::native_sent", "sent").unwrap_err(),
|
||||
"wrong target for sent: horizon_client::game::native_menus::Game<'_>::native_sent, \
|
||||
it is horizon_client::game::native_net::Game<'_>::native_sent"
|
||||
);
|
||||
assert_eq!(
|
||||
found("native_menus::Game<'_>::native_bare", "bare").unwrap_err(),
|
||||
"native_bare: a hook point outside native_hooks.rs makes its dispatch_bare call itself"
|
||||
);
|
||||
for name in ["logged", "gated"] {
|
||||
assert!(
|
||||
found(&format!("native_menus::Game<'_>::native_{name}"), name)
|
||||
.unwrap_err()
|
||||
.ends_with("which horizon-mixins cannot patch")
|
||||
);
|
||||
}
|
||||
assert!(found("native_net::Game<'_>::native_missing", "missing").is_err());
|
||||
}
|
||||
|
||||
/// The committed render hooks pass while they hold the catalog's code in any layout, and fail
|
||||
/// the build once a hook changes.
|
||||
#[test]
|
||||
@@ -1253,6 +1383,16 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
super::render_hooks_current(&root, code).unwrap();
|
||||
// rustfmt's trailing commas, where it wraps a long list, change nothing.
|
||||
let wrapped = "/// Drawn.\npub trait RenderHooks { fn drawn(&self, a: u8, b: u8) {} }\n\
|
||||
pub fn go() { let e = E { a: 1, b: [2, 3] }; }\n";
|
||||
std::fs::write(
|
||||
&path,
|
||||
"/// Drawn.\npub trait RenderHooks {\n fn drawn(&self, a: u8, b: u8,) {}\n}\n\
|
||||
pub fn go() {\n let e = E {\n a: 1,\n b: [2, 3,],\n };\n}\n",
|
||||
)
|
||||
.unwrap();
|
||||
super::render_hooks_current(&root, wrapped).unwrap();
|
||||
let changed = code.replace("drawn(&self)", "drawn(&self, partial: f32)");
|
||||
assert!(
|
||||
super::render_hooks_current(&root, &changed)
|
||||
|
||||
+5
-1
@@ -5350,7 +5350,11 @@ A new hook needs its fragment in `tools/native-hook-fragments/` with its `mod_ap
|
||||
`{"contract": "cancel", "out": "cancel"}`), its `native_*` function with the placeholder body
|
||||
`let _ = (every, parameter);` (all its parameters, in order), and a run of `cargo run -p xtask -- mod-api`.
|
||||
That command only replaces the placeholder in `native_hooks.rs` with the `dispatch_<hook>` call; commit
|
||||
that source change and the fragment. The `horizon-mod-api-gen` build dependency generates the hook traits,
|
||||
that source change and the fragment. `native_hooks.rs` is over the 3000 lines a file may have
|
||||
([layers.md](layers.md#file-size)), so it may not grow: a new hook's function can instead go in a small module
|
||||
under `game/` (`native_net.rs`, `native_menu_stacks.rs`…), its fragment's `target` naming that module. There it
|
||||
writes its `dispatch_<hook>` call itself (the command fills only `native_hooks.rs`) and, like every hook point,
|
||||
holds no macro or `cfg`, which horizon-mixins cannot patch; the build refuses it otherwise. The `horizon-mod-api-gen` build dependency generates the hook traits,
|
||||
`Host` methods and dispatch code into Cargo's `OUT_DIR` when the game builds, including release builds.
|
||||
These generated Rust files are not kept in the source tree or committed, and no manual regeneration is
|
||||
needed after a merge. The one exception is the renderer's (`horizon-client-render`, an engine crate, which
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"hooks": [],
|
||||
"helpers": [
|
||||
{
|
||||
"name": "mod_screen_open_over",
|
||||
"target": "horizon_client::game::native_mod_screen::Game<'_>::native_mod_screen_open_over"
|
||||
},
|
||||
{
|
||||
"name": "mod_screen_under_rect",
|
||||
"target": "horizon_client::game::native_mod_screen::Game<'_>::native_mod_screen_under_rect"
|
||||
}
|
||||
],
|
||||
"rust_summary": "a mod's own screen over the options page, the pause menu or an open container (`native_mod_screen_open_over`, `native_mod_screen_under_rect`).",
|
||||
"doc_summary": "`native_mod_screen_open_over(mod_id, keep_drawing_previous)` opens a mod's own screen over the options page (Options > Mods), the pause menu or the inventory or a container, not only over the world: Escape or `native_mod_screen_close` gives the replaced screen back, a server closing the container closes it for good, and a single-player game stays paused under a pausing screen. With `keep_drawing_previous` the pause menu's title or the container's panel is still drawn under the mod screen with no pointer over it, and `native_mod_screen_under_rect()` gives its panel (`[left, top, width, height]` in GUI pixels) for the mod to place its overlays by. It takes no clicks or keys while the mod screen is up. Bundles need \"hooks\": @API:mod_screen_over@.",
|
||||
"comments": ["crates/horizon-client/src/game/native_mod_screen.rs"]
|
||||
}
|
||||
Reference in new issue
Block a user