mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 08:07:53 -10:00
Native mods: the item panel and recipe pages for native mods
The item panel beside container screens and its recipe pages move to game/native_panel.rs and game/native_panel_recipes.rs, built in every build, with their state in Game::panel and native types (NativeStack, NativeRecipeView, NativeRecipeLayout, NativePanelEvent). A native mod shows them with native_panel_items and native_panel_recipes and hears the player's clicks, searches, page turns and keys over items through the new hook point native_panel_event: NATIVE_HOOKS_API is 2. A WebAssembly mod's panel feeds the same state through game/mod_panel.rs (feature `mods`) and hears its events as before; a native mod's panel wins when both show one. Players see no change. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e6bb2f74c7
commit
11f22e26fa
8 files changed
+630
-139
No files matched your search
@@ -108,10 +108,9 @@ mod minecarts;
|
||||
mod mob_blocks;
|
||||
mod mob_gear;
|
||||
mod mob_sounds;
|
||||
mod mod_browser;
|
||||
#[cfg(feature = "mods")]
|
||||
mod mod_browser_recipes;
|
||||
mod mod_input;
|
||||
#[cfg(feature = "mods")]
|
||||
mod mod_panel;
|
||||
mod mod_render;
|
||||
mod mods;
|
||||
mod mount_menu;
|
||||
@@ -121,6 +120,8 @@ mod moving_pistons;
|
||||
mod music;
|
||||
mod names;
|
||||
mod native_hooks;
|
||||
mod native_panel;
|
||||
mod native_panel_recipes;
|
||||
mod native_recipes;
|
||||
mod native_shaders;
|
||||
mod natural_spawning;
|
||||
@@ -1823,6 +1824,8 @@ struct Game<'a> {
|
||||
mods: mods::ModsState,
|
||||
/// Every item and recipe, for mods (`game/native_recipes.rs`), made once.
|
||||
recipe_tables: native_recipes::RecipeTables,
|
||||
/// The item panel beside container screens (`game/native_panel.rs`).
|
||||
panel: native_panel::Panel,
|
||||
/// Horizon's own shader pack, while one is picked (`game/native_shaders.rs`).
|
||||
native_shaders: native_shaders::NativeShaders,
|
||||
/// Meshed sections waiting for an upload budget (Horizon settings).
|
||||
@@ -14090,6 +14093,7 @@ impl<'a> Game<'a> {
|
||||
extras: Default::default(),
|
||||
mods: Default::default(),
|
||||
recipe_tables: Default::default(),
|
||||
panel: Default::default(),
|
||||
native_shaders: Default::default(),
|
||||
sky_darken_memo: Default::default(),
|
||||
persist: Default::default(),
|
||||
|
||||
@@ -130,6 +130,7 @@ impl Game<'_> {
|
||||
&& let Some(name) = horizon_client::keybinds::key_name(key)
|
||||
&& let Some(stack) = self.mod_browser_pointed()
|
||||
{
|
||||
let stack = super::mod_panel::browser_stack(stack);
|
||||
self.mods.mods.browser_key(name, &stack);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
//! Capability `ui`'s item panel of a WebAssembly mod (WIT `interface
|
||||
//! browser`, docs/mods.md): its items and recipe page as the native panel's
|
||||
//! (`native_panel.rs`), and the panel's events back in the runtime's types.
|
||||
use super::{
|
||||
Game,
|
||||
native_hooks::{
|
||||
NativePanelEvent as BrowserEvent, NativeRecipeLayout, NativeRecipeView,
|
||||
NativeStack as BrowserStack,
|
||||
},
|
||||
native_panel::{Owner, Shown},
|
||||
native_panel_recipes::RecipePage,
|
||||
};
|
||||
use horizon_mods as m;
|
||||
|
||||
/// A WebAssembly mod's panel, converted once per `set-items` or
|
||||
/// `set-recipes`.
|
||||
impl Game<'_> {
|
||||
pub(super) fn panel_wasm_sync(&mut self) {
|
||||
let Some((index, version, items)) = self.mods.mods.browser_items() else {
|
||||
self.panel.wasm = None;
|
||||
return;
|
||||
};
|
||||
let owner = Owner::Wasm(index);
|
||||
let wasm = self.panel.wasm.as_ref();
|
||||
if wasm.is_some_and(|s| s.owner == owner && s.version == version) {
|
||||
return;
|
||||
}
|
||||
let items = items.iter().map(stack).collect();
|
||||
let recipes = self.mods.mods.browser_recipes(index).map(page);
|
||||
self.panel.wasm = Some(Shown {
|
||||
owner,
|
||||
version,
|
||||
items,
|
||||
recipes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn stack(s: &m::BrowserStack) -> BrowserStack {
|
||||
BrowserStack {
|
||||
item: s.item.clone(),
|
||||
count: s.count,
|
||||
components: s.components.clone(),
|
||||
}
|
||||
}
|
||||
/// A native stack as the runtime's (`browser_key`).
|
||||
pub(super) fn browser_stack(s: BrowserStack) -> m::BrowserStack {
|
||||
m::BrowserStack {
|
||||
item: s.item,
|
||||
count: s.count,
|
||||
components: s.components,
|
||||
}
|
||||
}
|
||||
fn layout(l: m::BrowserLayout) -> NativeRecipeLayout {
|
||||
match l {
|
||||
m::BrowserLayout::Grid => NativeRecipeLayout::Grid,
|
||||
m::BrowserLayout::Cooking => NativeRecipeLayout::Cooking,
|
||||
m::BrowserLayout::Single => NativeRecipeLayout::Single,
|
||||
m::BrowserLayout::Smithing => NativeRecipeLayout::Smithing,
|
||||
m::BrowserLayout::Brewing => NativeRecipeLayout::Brewing,
|
||||
}
|
||||
}
|
||||
fn stacks(list: &[m::BrowserStack]) -> Vec<BrowserStack> {
|
||||
list.iter().map(stack).collect()
|
||||
}
|
||||
pub(super) fn page(p: &m::RecipePage) -> RecipePage {
|
||||
RecipePage {
|
||||
title: p.title.clone(),
|
||||
tabs: stacks(&p.tabs),
|
||||
tab: p.tab,
|
||||
recipes: p
|
||||
.recipes
|
||||
.iter()
|
||||
.map(|r| NativeRecipeView {
|
||||
layout: layout(r.layout),
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
slots: r.slots.iter().map(|s| stacks(s)).collect(),
|
||||
results: stacks(&r.results),
|
||||
note: r.note.clone(),
|
||||
action: r.action,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
pub(super) fn event(e: BrowserEvent) -> m::BrowserEvent {
|
||||
match e {
|
||||
BrowserEvent::Clicked { index, right } => {
|
||||
m::BrowserEvent::Clicked(m::BrowserClick { index, right })
|
||||
}
|
||||
BrowserEvent::Search(text) => m::BrowserEvent::Search(text),
|
||||
// Not sent to WebAssembly mods: they hear their own keys
|
||||
// (`browser_key`).
|
||||
BrowserEvent::Key { key, stack } => m::BrowserEvent::Key(m::BrowserItemKey {
|
||||
key,
|
||||
stack: browser_stack(stack),
|
||||
}),
|
||||
BrowserEvent::SlotClicked {
|
||||
recipe,
|
||||
slot,
|
||||
stack,
|
||||
right,
|
||||
} => m::BrowserEvent::SlotClicked(m::BrowserSlotClick {
|
||||
recipe,
|
||||
slot,
|
||||
stack: browser_stack(stack),
|
||||
right,
|
||||
}),
|
||||
BrowserEvent::Action(i) => m::BrowserEvent::Action(i),
|
||||
BrowserEvent::Page(i) => m::BrowserEvent::Page(i),
|
||||
BrowserEvent::Back => m::BrowserEvent::Back,
|
||||
BrowserEvent::Tab(i) => m::BrowserEvent::Tab(i),
|
||||
}
|
||||
}
|
||||
@@ -92,9 +92,6 @@ pub(super) struct ModsState {
|
||||
/// Capability `recipes`'s tables, made once a mod reads them.
|
||||
#[cfg(feature = "mods")]
|
||||
pub(super) recipes: Option<std::sync::Arc<horizon_mods::Recipes>>,
|
||||
/// Capability `ui`'s item panel beside container screens.
|
||||
#[cfg(feature = "mods")]
|
||||
pub(super) browser: super::mod_browser::Browser,
|
||||
/// Extras that are official mods now, being installed.
|
||||
#[cfg(feature = "mods")]
|
||||
pub(super) moved: super::moved_extras::MovedState,
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
//! Horizon's own HUD extras do, and keep a mod's settings where the
|
||||
//! WebAssembly mod of the same id kept them.
|
||||
//!
|
||||
//! API 2 adds the item panel beside container screens and its recipe pages
|
||||
//! (`native_panel_items`, `native_panel_recipes`, `native_panel_event`),
|
||||
//! and the game's items and recipes (`native_recipes`,
|
||||
//! `native_known_recipes`, `native_place_recipe`, `native_give`): what a
|
||||
//! recipe viewer needs.
|
||||
//!
|
||||
//! Names, parameters and call sites are kept from release to release, and
|
||||
//! hook points are only ever added: each addition raises [`NATIVE_HOOKS_API`].
|
||||
//! A bundle that hooks only these names the API it needs (`hooks`), and the
|
||||
@@ -13,7 +19,7 @@
|
||||
//! that would break such a bundle is not made.
|
||||
//! Bodies have no `cfg` and no macros: horizon-mixins refuses a target with
|
||||
//! either (`tests::hook_points_stay_hookable`).
|
||||
use super::{Game, Screen, execution_role::ExecutionRole};
|
||||
use super::{Game, Screen, execution_role::ExecutionRole, native_panel_recipes::RecipePage};
|
||||
use horizon_client::{
|
||||
chat::ChatSource,
|
||||
extras::Corner,
|
||||
@@ -29,7 +35,7 @@ use winit::{event::MouseButton, keyboard::KeyCode};
|
||||
/// The hook points' API version. The launcher reads this line from the
|
||||
/// source it builds; keep its form.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const NATIVE_HOOKS_API: u32 = 1;
|
||||
pub(crate) const NATIVE_HOOKS_API: u32 = 2;
|
||||
|
||||
/// The zoom a native mod asks for (`native_zoom`): the factor (1 is none),
|
||||
/// eased through the field of view like the spyglass when `smooth`, and the
|
||||
@@ -80,6 +86,83 @@ pub(crate) struct NativeMod {
|
||||
pub(crate) keys: Vec<[String; 3]>,
|
||||
}
|
||||
|
||||
/// An item the panel shows (`native_panel_items`): its id
|
||||
/// (`minecraft:diamond`), a count (1 draws none) and its components beyond
|
||||
/// the item's own, in vanilla's item syntax, or "".
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub(crate) struct NativeStack {
|
||||
pub(crate) item: String,
|
||||
pub(crate) count: u32,
|
||||
pub(crate) components: String,
|
||||
}
|
||||
|
||||
/// How the panel lays out a recipe of `native_panel_recipes`.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum NativeRecipeLayout {
|
||||
/// A crafting grid of `width` x `height` slots, an arrow and the result.
|
||||
Grid,
|
||||
/// One input over a flame, an arrow and the result (furnaces, smokers,
|
||||
/// campfires).
|
||||
Cooking,
|
||||
/// One input, an arrow and the result (the stonecutter).
|
||||
Single,
|
||||
/// Template, base and addition, an arrow and the result.
|
||||
Smithing,
|
||||
/// A potion and an ingredient, an arrow and the result.
|
||||
Brewing,
|
||||
}
|
||||
|
||||
/// A recipe the panel shows (`native_panel_recipes`). Each slot, and the
|
||||
/// result, cycles through its stacks, one a second; an empty slot shows
|
||||
/// nothing.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct NativeRecipeView {
|
||||
pub(crate) layout: NativeRecipeLayout,
|
||||
/// The grid's size (`Grid` only), at most 3 x 3.
|
||||
pub(crate) width: u32,
|
||||
pub(crate) height: u32,
|
||||
pub(crate) slots: Vec<Vec<NativeStack>>,
|
||||
pub(crate) results: Vec<NativeStack>,
|
||||
/// A short line under the recipe (a cooking time), or "".
|
||||
pub(crate) note: String,
|
||||
/// The action button beside it: none, enabled, or shown greyed.
|
||||
pub(crate) action: Option<bool>,
|
||||
}
|
||||
|
||||
/// What the player did on a native mod's panel (`native_panel_event`).
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum NativePanelEvent {
|
||||
/// A click on an item of the panel, by its index in
|
||||
/// `native_panel_items`' list.
|
||||
Clicked { index: u32, right: bool },
|
||||
/// The search box's text, each time the player changes it.
|
||||
Search(String),
|
||||
/// A key (`key.keyboard.r`; compare it with `native_mod_key`) went down
|
||||
/// while the pointer was on an item: a slot of the screen's, or the
|
||||
/// panel's.
|
||||
Key { key: String, stack: NativeStack },
|
||||
/// A click on a slot of a recipe the panel shows: the recipe's index in
|
||||
/// `native_panel_recipes`' list and the slot's in it (the results come
|
||||
/// after the slots), and the stack it showed then.
|
||||
SlotClicked {
|
||||
recipe: u32,
|
||||
slot: u32,
|
||||
stack: NativeStack,
|
||||
right: bool,
|
||||
},
|
||||
/// The action button of the recipe at this index.
|
||||
Action(u32),
|
||||
/// The player turned the recipes' page: the index of the first recipe
|
||||
/// now shown.
|
||||
Page(u32),
|
||||
/// The recipe page's back button.
|
||||
Back,
|
||||
/// The player picked the tab at this index.
|
||||
Tab(u32),
|
||||
}
|
||||
|
||||
/// An item of [`NativeRecipes::items`], by its index there, with a count
|
||||
/// and the components it has beyond the item's own, in vanilla's item
|
||||
/// syntax (`[minecraft:potion_contents={potion:"minecraft:swiftness"}]`),
|
||||
@@ -230,6 +313,11 @@ impl Game<'_> {
|
||||
pub(super) fn native_settings_click(&mut self, mod_id: &str, button: usize) {
|
||||
let _ = (mod_id, button);
|
||||
}
|
||||
/// The player did `event` on mod `mod_id`'s panel (`native_panel_items`):
|
||||
/// act on it when it is yours. API 2.
|
||||
pub(super) fn native_panel_event(&mut self, mod_id: &str, event: &NativePanelEvent) {
|
||||
let _ = (mod_id, event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helpers for native mods, kept as stable as the hook points.
|
||||
@@ -351,6 +439,30 @@ impl Game<'_> {
|
||||
let dir = self.native_data_dir(mod_id);
|
||||
let _ = fs::create_dir_all(&dir).and_then(|()| fs::write(dir.join(key), value));
|
||||
}
|
||||
/// Shows the panel beside container screens with these items (at most
|
||||
/// 8192), as the game's own slots in pages, with a search box; the
|
||||
/// player's clicks, searches and keys over items come back as
|
||||
/// `native_panel_event`. The panel is the last mod's to show one; empty
|
||||
/// items and no recipe page take yours away.
|
||||
pub(super) fn native_panel_items(&mut self, mod_id: &str, items: Vec<NativeStack>) {
|
||||
self.panel.show_items(mod_id, items);
|
||||
}
|
||||
/// Shows these recipes in the panel instead of its items, as many as
|
||||
/// fit a page, under `title` and a back button, with a row of `tabs`
|
||||
/// (each an icon; `tab` is the selected one) between them. At most 256
|
||||
/// recipes and 16 tabs; no recipes shows the items again. The same title
|
||||
/// and tab again keep the page the player turned to.
|
||||
pub(super) fn native_panel_recipes(
|
||||
&mut self,
|
||||
mod_id: &str,
|
||||
title: &str,
|
||||
tabs: Vec<NativeStack>,
|
||||
tab: u32,
|
||||
views: Vec<NativeRecipeView>,
|
||||
) {
|
||||
let page = RecipePage::new(title, tabs, tab, views);
|
||||
self.panel.show_recipes(mod_id, page);
|
||||
}
|
||||
/// Every item and recipe the game knows, made once (the same for every
|
||||
/// call); none outside a world. They are the window's own: the world's
|
||||
/// in single player, vanilla's on a server, which sends a client no
|
||||
@@ -434,6 +546,9 @@ impl Game<'_> {
|
||||
return;
|
||||
}
|
||||
if let Some(name) = keybinds::key_name(key) {
|
||||
if pressed {
|
||||
self.native_panel_key(name);
|
||||
}
|
||||
self.native_key(name, pressed);
|
||||
}
|
||||
}
|
||||
|
||||
+313
-104
@@ -1,64 +1,144 @@
|
||||
//! Capability `ui`'s panel beside container screens (WIT `interface
|
||||
//! browser`, docs/mods.md): the items a recipe viewer mod lists, drawn as
|
||||
//! the game's own item slots in pages right of the screen's window, with a
|
||||
//! search box under them. The mod says what the panel holds; the game pages
|
||||
//! it and tells the mod what the player clicked, typed, and which of the
|
||||
//! mod's keys went down over an item. With no mod showing a panel, each
|
||||
//! hook here returns after one check.
|
||||
use super::Game;
|
||||
#[cfg(feature = "mods")]
|
||||
use {
|
||||
super::Screen,
|
||||
horizon_client::{
|
||||
hud::{HudBatch, rgba},
|
||||
items::ItemStack,
|
||||
ui::EditBox,
|
||||
},
|
||||
horizon_mods::{BrowserClick, BrowserEvent, BrowserStack},
|
||||
//! The item panel beside container screens: the items a recipe viewer mod
|
||||
//! lists (a native mod's `native_panel_items`, or WIT `interface browser`
|
||||
//! of a WebAssembly mod, docs/mods.md), drawn as the game's own item slots
|
||||
//! in pages right of the screen's window, with a search box under them. The
|
||||
//! mod says what the panel holds; the game pages it and tells the mod what
|
||||
//! the player clicked, typed, and which keys went down over an item
|
||||
//! (`native_panel_event`). With no mod showing a panel, each hook here
|
||||
//! returns after one check.
|
||||
use super::{
|
||||
Game, Screen,
|
||||
native_hooks::{NativePanelEvent as BrowserEvent, NativeStack as BrowserStack},
|
||||
native_panel_recipes::RecipePage,
|
||||
};
|
||||
use horizon_client::{
|
||||
hud::{HudBatch, rgba},
|
||||
items::ItemStack,
|
||||
ui::EditBox,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "mods"))]
|
||||
impl Game<'_> {
|
||||
pub(super) fn draw_mod_browser(&mut self, _batch: &mut horizon_client::hud::HudBatch) {}
|
||||
pub(super) fn mod_browser_press(&mut self, _right: bool) -> bool {
|
||||
false
|
||||
}
|
||||
pub(super) fn mod_browser_typing(&self) -> bool {
|
||||
false
|
||||
}
|
||||
pub(super) fn mod_browser_key(&mut self, _event: &winit::event::KeyEvent) {}
|
||||
pub(super) fn mod_browser_text(&mut self, _text: Option<&str>, _backspace: bool) {}
|
||||
pub(super) fn mod_browser_closed(&mut self) {}
|
||||
}
|
||||
|
||||
/// One slot of the panel, in GUI pixels.
|
||||
#[cfg(feature = "mods")]
|
||||
const SLOT: f32 = 18.0;
|
||||
/// The most columns the panel takes, and the fewest it shows with.
|
||||
#[cfg(feature = "mods")]
|
||||
const COLUMNS: (usize, usize) = (12, 2);
|
||||
/// The gap between the window and the panel, and the screen's edges.
|
||||
#[cfg(feature = "mods")]
|
||||
const MARGIN: f32 = 8.0;
|
||||
|
||||
/// Items a panel may hold.
|
||||
const ITEMS: usize = 8192;
|
||||
|
||||
/// Whose panel it is: a native mod's id, or a running WebAssembly mod's
|
||||
/// index.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(super) enum Owner {
|
||||
Native(String),
|
||||
#[cfg(feature = "mods")]
|
||||
Wasm(usize),
|
||||
}
|
||||
impl Owner {
|
||||
fn native_id(&self) -> Option<&str> {
|
||||
match self {
|
||||
Owner::Native(id) => Some(id),
|
||||
#[cfg(feature = "mods")]
|
||||
Owner::Wasm(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a mod shows on the panel: its items, and the recipes shown in
|
||||
/// their place.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(super) struct Shown {
|
||||
pub(super) owner: Owner,
|
||||
/// Which change the panel is from, so the game converts it once.
|
||||
pub(super) version: u64,
|
||||
pub(super) items: Vec<BrowserStack>,
|
||||
pub(super) recipes: Option<RecipePage>,
|
||||
}
|
||||
|
||||
/// The panel's state in the game.
|
||||
#[cfg(feature = "mods")]
|
||||
#[derive(Default)]
|
||||
pub(super) struct Browser {
|
||||
pub(super) struct Panel {
|
||||
/// A native mod's panel: the last one to show something.
|
||||
native: Option<Shown>,
|
||||
/// A WebAssembly mod's, converted once per change.
|
||||
#[cfg(feature = "mods")]
|
||||
pub(super) wasm: Option<Shown>,
|
||||
/// The last native change.
|
||||
versions: u64,
|
||||
/// The items' owner and version they were made from.
|
||||
of: Option<(usize, u64)>,
|
||||
of: Option<(Owner, u64)>,
|
||||
/// The mod's items as stacks; `None` for an item the game doesn't have.
|
||||
stacks: Vec<Option<ItemStack>>,
|
||||
page: usize,
|
||||
search: Option<EditBox>,
|
||||
/// The search text the mod last heard.
|
||||
sent: String,
|
||||
/// `set-recipes`' recipes, shown instead of the items.
|
||||
pub(super) recipes: Option<super::mod_browser_recipes::Pages>,
|
||||
/// The recipes shown instead of the items.
|
||||
pub(super) recipes: Option<super::native_panel_recipes::Pages>,
|
||||
}
|
||||
|
||||
impl Panel {
|
||||
/// What the panel shows: a native mod's (one `on`), else a
|
||||
/// WebAssembly mod's.
|
||||
pub(super) fn shown(&self, on: impl Fn(&str) -> bool) -> Option<&Shown> {
|
||||
let native = self
|
||||
.native
|
||||
.as_ref()
|
||||
.filter(|s| s.owner.native_id().is_some_and(&on));
|
||||
#[cfg(feature = "mods")]
|
||||
let native = native.or(self.wasm.as_ref());
|
||||
native
|
||||
}
|
||||
/// `native_panel_items`.
|
||||
pub(super) fn show_items(&mut self, mod_id: &str, mut items: Vec<BrowserStack>) {
|
||||
items.truncate(ITEMS);
|
||||
if let Some(shown) = self.native_of(mod_id, !items.is_empty()) {
|
||||
shown.items = items;
|
||||
self.native_changed();
|
||||
}
|
||||
}
|
||||
/// `native_panel_recipes`.
|
||||
pub(super) fn show_recipes(&mut self, mod_id: &str, page: Option<RecipePage>) {
|
||||
if let Some(shown) = self.native_of(mod_id, page.is_some()) {
|
||||
shown.recipes = page;
|
||||
self.native_changed();
|
||||
}
|
||||
}
|
||||
/// Mod `mod_id`'s panel, made when it shows something on another's;
|
||||
/// none when it clears a panel that isn't its own.
|
||||
fn native_of(&mut self, mod_id: &str, showing: bool) -> Option<&mut Shown> {
|
||||
let own = self
|
||||
.native
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.owner.native_id() == Some(mod_id));
|
||||
if !own {
|
||||
if !showing {
|
||||
return None;
|
||||
}
|
||||
self.native = Some(Shown {
|
||||
owner: Owner::Native(mod_id.to_owned()),
|
||||
version: 0,
|
||||
items: Vec::new(),
|
||||
recipes: None,
|
||||
});
|
||||
}
|
||||
self.native.as_mut()
|
||||
}
|
||||
/// A native panel changed: a new version, and no panel once it shows
|
||||
/// nothing.
|
||||
fn native_changed(&mut self) {
|
||||
self.versions += 1;
|
||||
if let Some(shown) = self.native.as_mut() {
|
||||
shown.version = self.versions;
|
||||
if shown.items.is_empty() && shown.recipes.is_none() {
|
||||
self.native = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the panel's parts are this frame.
|
||||
#[cfg(feature = "mods")]
|
||||
pub(super) struct Layout {
|
||||
/// The panel's whole area, for clicks that hit none of its parts.
|
||||
pub(super) area: [f32; 4],
|
||||
@@ -70,7 +150,6 @@ pub(super) struct Layout {
|
||||
pub(super) arrows_y: f32,
|
||||
pub(super) search: [f32; 4],
|
||||
}
|
||||
#[cfg(feature = "mods")]
|
||||
impl Layout {
|
||||
fn per_page(&self) -> usize {
|
||||
self.columns * self.rows
|
||||
@@ -93,48 +172,62 @@ impl Layout {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mods")]
|
||||
pub(super) fn inside(p: (f32, f32), [x, y, w, h]: [f32; 4]) -> bool {
|
||||
p.0 >= x && p.1 >= y && p.0 < x + w && p.1 < y + h
|
||||
}
|
||||
|
||||
#[cfg(feature = "mods")]
|
||||
impl Game<'_> {
|
||||
fn browser_batch(&self) -> Option<HudBatch> {
|
||||
let r = self.renderer.as_ref()?;
|
||||
Some(HudBatch::new([r.config.width, r.config.height]))
|
||||
}
|
||||
/// The mod showing a panel, with its items made into stacks once per
|
||||
/// `set-items`; none while no mod shows one or no container screen is up.
|
||||
fn browser_sync(&mut self) -> Option<usize> {
|
||||
/// change; none while no mod shows one or no container screen is up.
|
||||
fn browser_sync(&mut self) -> Option<Owner> {
|
||||
if self.screen != Screen::Inventory || self.title.is_some() {
|
||||
return None;
|
||||
}
|
||||
let Some((owner, version)) = self.mods.mods.browser_items().map(|(o, v, _)| (o, v)) else {
|
||||
self.mods.browser.of = None;
|
||||
self.mods.browser.stacks.clear();
|
||||
self.panel_wasm_sync();
|
||||
let disabled = &self.settings.mods.disabled;
|
||||
let Some(shown) = self.panel.shown(|id| !disabled.contains(id)) else {
|
||||
self.panel.of = None;
|
||||
self.panel.stacks.clear();
|
||||
return None;
|
||||
};
|
||||
if self.mods.browser.of != Some((owner, version)) {
|
||||
let of = (shown.owner.clone(), shown.version);
|
||||
if self.panel.of.as_ref() != Some(&of) {
|
||||
let items = self.res.items();
|
||||
let stacks = self
|
||||
.mods
|
||||
.mods
|
||||
.browser_items()
|
||||
.map_or_else(Vec::new, |(_, _, list)| {
|
||||
list.iter().map(|s| browser_stack(items, s)).collect()
|
||||
});
|
||||
self.mods.browser.stacks = stacks;
|
||||
let stacks = shown
|
||||
.items
|
||||
.iter()
|
||||
.map(|s| browser_stack(items, s))
|
||||
.collect();
|
||||
let shown = shown.recipes.clone();
|
||||
self.panel.stacks = stacks;
|
||||
// The same title again (the mod updating its buttons) keeps the page.
|
||||
let old = self.mods.browser.recipes.take();
|
||||
self.mods.browser.recipes = self
|
||||
let old = self.panel.recipes.take();
|
||||
self.panel.recipes = shown
|
||||
.as_ref()
|
||||
.map(|shown| super::native_panel_recipes::Pages::new(items, shown, old));
|
||||
self.panel.of = Some(of.clone());
|
||||
}
|
||||
Some(of.0)
|
||||
}
|
||||
/// What the panel shows now.
|
||||
fn panel_shown(&self) -> Option<&Shown> {
|
||||
let disabled = &self.settings.mods.disabled;
|
||||
self.panel.shown(|id| !disabled.contains(id))
|
||||
}
|
||||
/// The player did `event` on `owner`'s panel.
|
||||
pub(super) fn panel_event(&mut self, owner: &Owner, event: BrowserEvent) {
|
||||
match owner {
|
||||
Owner::Native(id) => self.native_panel_event(id, &event),
|
||||
#[cfg(feature = "mods")]
|
||||
Owner::Wasm(i) => self
|
||||
.mods
|
||||
.mods
|
||||
.browser_recipes(owner)
|
||||
.map(|shown| super::mod_browser_recipes::Pages::new(items, shown, old));
|
||||
self.mods.browser.of = Some((owner, version));
|
||||
.browser_event(*i, super::mod_panel::event(event)),
|
||||
}
|
||||
Some(owner)
|
||||
}
|
||||
/// The panel's parts: right of the window, from the top of the screen to
|
||||
/// the search box at its foot. None when the screen has no room for it.
|
||||
@@ -161,17 +254,12 @@ impl Game<'_> {
|
||||
})
|
||||
}
|
||||
fn browser_pages(&self, layout: &Layout) -> usize {
|
||||
self.mods
|
||||
.browser
|
||||
.stacks
|
||||
.len()
|
||||
.div_ceil(layout.per_page())
|
||||
.max(1)
|
||||
self.panel.stacks.len().div_ceil(layout.per_page()).max(1)
|
||||
}
|
||||
/// The stack under the pointer on the panel, an item's or a recipe's,
|
||||
/// as the mod sent it and as a stack.
|
||||
fn browser_pointed_stack(&self, layout: &Layout) -> Option<(BrowserStack, ItemStack)> {
|
||||
if self.mods.browser.recipes.is_some() {
|
||||
if self.panel.recipes.is_some() {
|
||||
if let Some((_, sent, stack)) = self.recipe_tab_hovered(layout) {
|
||||
return Some((sent, stack));
|
||||
}
|
||||
@@ -179,26 +267,26 @@ impl Game<'_> {
|
||||
return Some((sent, stack));
|
||||
}
|
||||
let (index, stack) = self.browser_hovered(layout)?;
|
||||
let (_, _, items) = self.mods.mods.browser_items()?;
|
||||
let items = &self.panel_shown()?.items;
|
||||
Some((items.get(index)?.clone(), stack))
|
||||
}
|
||||
/// The panel's item under the pointer: its index and stack.
|
||||
fn browser_hovered(&self, layout: &Layout) -> Option<(usize, ItemStack)> {
|
||||
if self.mods.browser.recipes.is_some() {
|
||||
if self.panel.recipes.is_some() {
|
||||
return None;
|
||||
}
|
||||
let first = self.mods.browser.page * layout.per_page();
|
||||
let first = self.panel.page * layout.per_page();
|
||||
(0..layout.per_page()).find_map(|n| {
|
||||
let (x, y) = layout.slot(n);
|
||||
if !inside(self.cursor, [x, y, SLOT, SLOT]) {
|
||||
return None;
|
||||
}
|
||||
let stack = self.mods.browser.stacks.get(first + n)?.clone()?;
|
||||
let stack = self.panel.stacks.get(first + n)?.clone()?;
|
||||
Some((first + n, stack))
|
||||
})
|
||||
}
|
||||
fn browser_search(&mut self, layout: &Layout) -> &mut EditBox {
|
||||
let search = self.mods.browser.search.get_or_insert_with(|| {
|
||||
let search = self.panel.search.get_or_insert_with(|| {
|
||||
let mut edit = EditBox::new(0.0, 0.0, 0.0, 0.0);
|
||||
edit.max_length = 50;
|
||||
edit.focused = false;
|
||||
@@ -211,8 +299,8 @@ impl Game<'_> {
|
||||
/// The items' page arrows and grid.
|
||||
fn draw_browser_items(&mut self, batch: &mut HudBatch, layout: &Layout) {
|
||||
let pages = self.browser_pages(layout);
|
||||
self.mods.browser.page = self.mods.browser.page.min(pages - 1);
|
||||
let page = self.mods.browser.page;
|
||||
self.panel.page = self.panel.page.min(pages - 1);
|
||||
let page = self.panel.page;
|
||||
let cursor = self.cursor;
|
||||
if pages > 1 {
|
||||
let labels = [
|
||||
@@ -247,7 +335,7 @@ impl Game<'_> {
|
||||
}
|
||||
let first = page * layout.per_page();
|
||||
for n in 0..layout.per_page() {
|
||||
let Some(stack) = self.mods.browser.stacks.get(first + n) else {
|
||||
let Some(stack) = self.panel.stacks.get(first + n) else {
|
||||
break;
|
||||
};
|
||||
let Some(stack) = stack.clone() else {
|
||||
@@ -285,7 +373,7 @@ impl Game<'_> {
|
||||
return;
|
||||
};
|
||||
let cursor = self.cursor;
|
||||
if self.mods.browser.recipes.is_some() {
|
||||
if self.panel.recipes.is_some() {
|
||||
self.draw_browser_recipes(batch, &layout);
|
||||
} else {
|
||||
self.draw_browser_items(batch, &layout);
|
||||
@@ -296,7 +384,7 @@ impl Game<'_> {
|
||||
search.suggestion = (search.value.is_empty() && !search.focused).then_some(hint);
|
||||
let mut search = search.clone();
|
||||
search.draw(batch, &self.gui.atlas, None);
|
||||
self.mods.browser.search = Some(search);
|
||||
self.panel.search = Some(search);
|
||||
if self.carried.is_some() {
|
||||
self.draw_carried(batch);
|
||||
} else if let Some(stack) = self.browser_pointed_stack(&layout) {
|
||||
@@ -323,7 +411,7 @@ impl Game<'_> {
|
||||
let cursor = self.cursor;
|
||||
let shift = self.has_shift_down();
|
||||
let atlas = &self.gui.atlas;
|
||||
let search = self.mods.browser.search.get_or_insert_with(|| {
|
||||
let search = self.panel.search.get_or_insert_with(|| {
|
||||
let mut edit = EditBox::new(0.0, 0.0, 0.0, 0.0);
|
||||
edit.max_length = 50;
|
||||
edit
|
||||
@@ -336,12 +424,12 @@ impl Game<'_> {
|
||||
return true;
|
||||
}
|
||||
search.focused = false;
|
||||
if self.mods.browser.recipes.is_some() {
|
||||
return self.browser_recipes_press(&layout, owner, right)
|
||||
if self.panel.recipes.is_some() {
|
||||
return self.browser_recipes_press(&layout, &owner, right)
|
||||
|| inside(cursor, layout.area);
|
||||
}
|
||||
let pages = self.browser_pages(&layout);
|
||||
let page = self.mods.browser.page;
|
||||
let page = self.panel.page;
|
||||
let [back, forward] = layout.arrows();
|
||||
let step = if pages > 1 && page > 0 && inside(cursor, [back.0, back.1, 12.0, 17.0]) {
|
||||
Some(page - 1)
|
||||
@@ -354,17 +442,17 @@ impl Game<'_> {
|
||||
None
|
||||
};
|
||||
if let Some(to) = step {
|
||||
self.mods.browser.page = to;
|
||||
self.panel.page = to;
|
||||
self.audio
|
||||
.play("minecraft:ui.button.click", None, 0.25, 1.0);
|
||||
return true;
|
||||
}
|
||||
if let Some((index, _)) = self.browser_hovered(&layout) {
|
||||
let event = BrowserEvent::Clicked(BrowserClick {
|
||||
let event = BrowserEvent::Clicked {
|
||||
index: index as u32,
|
||||
right,
|
||||
});
|
||||
self.mods.mods.browser_event(owner, event);
|
||||
};
|
||||
self.panel_event(&owner, event);
|
||||
return true;
|
||||
}
|
||||
inside(cursor, layout.area)
|
||||
@@ -373,8 +461,8 @@ impl Game<'_> {
|
||||
/// Whether keys go to the panel's search box.
|
||||
pub(super) fn mod_browser_typing(&self) -> bool {
|
||||
self.screen == Screen::Inventory
|
||||
&& self.mods.browser.of.is_some()
|
||||
&& self.mods.browser.search.as_ref().is_some_and(|s| s.focused)
|
||||
&& self.panel.of.is_some()
|
||||
&& self.panel.search.as_ref().is_some_and(|s| s.focused)
|
||||
}
|
||||
/// A key press while the search box has focus.
|
||||
pub(super) fn mod_browser_key(&mut self, event: &winit::event::KeyEvent) {
|
||||
@@ -382,7 +470,7 @@ impl Game<'_> {
|
||||
let shortcut = self.edit_shortcut(event);
|
||||
let ctrl = self.has_control_down();
|
||||
let shift = self.has_shift_down();
|
||||
let Some(search) = self.mods.browser.search.as_mut() else {
|
||||
let Some(search) = self.panel.search.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if let Some(shortcut) = shortcut {
|
||||
@@ -405,7 +493,7 @@ impl Game<'_> {
|
||||
}
|
||||
/// Text typed (or one backspace) into the search box (`horizon-ctl type`).
|
||||
pub(super) fn mod_browser_text(&mut self, text: Option<&str>, backspace: bool) {
|
||||
let Some(search) = self.mods.browser.search.as_mut() else {
|
||||
let Some(search) = self.panel.search.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if backspace {
|
||||
@@ -417,27 +505,24 @@ impl Game<'_> {
|
||||
}
|
||||
/// The mod hears the search text when it changed.
|
||||
fn browser_searched(&mut self) {
|
||||
let (Some((owner, _)), Some(search)) = (self.mods.browser.of, &self.mods.browser.search)
|
||||
else {
|
||||
let (Some((owner, _)), Some(search)) = (&self.panel.of, &self.panel.search) else {
|
||||
return;
|
||||
};
|
||||
if search.value != self.mods.browser.sent {
|
||||
let value = search.value.clone();
|
||||
self.mods.browser.sent.clone_from(&value);
|
||||
self.mods.browser.page = 0;
|
||||
self.mods
|
||||
.mods
|
||||
.browser_event(owner, BrowserEvent::Search(value));
|
||||
if search.value != self.panel.sent {
|
||||
let (owner, value) = (owner.clone(), search.value.clone());
|
||||
self.panel.sent.clone_from(&value);
|
||||
self.panel.page = 0;
|
||||
self.panel_event(&owner, BrowserEvent::Search(value));
|
||||
}
|
||||
}
|
||||
/// The container screen closed: the search box lets go of the keys.
|
||||
pub(super) fn mod_browser_closed(&mut self) {
|
||||
if let Some(search) = self.mods.browser.search.as_mut() {
|
||||
if let Some(search) = self.panel.search.as_mut() {
|
||||
search.focused = false;
|
||||
}
|
||||
}
|
||||
/// The item under the pointer on a container screen, for the mods' keys
|
||||
/// (`browser`'s `key` event): the panel's, else a slot's.
|
||||
/// (the `Key` event): the panel's, else a slot's.
|
||||
pub(super) fn mod_browser_pointed(&mut self) -> Option<BrowserStack> {
|
||||
let batch = self.browser_batch()?;
|
||||
let stack = match self
|
||||
@@ -459,11 +544,24 @@ impl Game<'_> {
|
||||
components: super::native_recipes::components(&stack),
|
||||
})
|
||||
}
|
||||
/// A key `key` (`key.keyboard.r`) went down: over an item on a container
|
||||
/// screen, a native mod's panel hears it as a `Key` event.
|
||||
pub(super) fn native_panel_key(&mut self, key: &str) {
|
||||
if self.screen != Screen::Inventory || self.title.is_some() || self.mod_browser_typing() {
|
||||
return;
|
||||
}
|
||||
let Some(owner) = self.browser_sync().filter(|o| o.native_id().is_some()) else {
|
||||
return;
|
||||
};
|
||||
if let Some(stack) = self.mod_browser_pointed() {
|
||||
let key = key.to_owned();
|
||||
self.panel_event(&owner, BrowserEvent::Key { key, stack });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A mod's item as a stack: an item the game has, its components read as
|
||||
/// `/give` reads them (ones it can't read are left out).
|
||||
#[cfg(feature = "mods")]
|
||||
pub(super) fn browser_stack(
|
||||
items: &horizon_client::items::Items,
|
||||
s: &BrowserStack,
|
||||
@@ -483,3 +581,114 @@ pub(super) fn browser_stack(
|
||||
extra,
|
||||
})
|
||||
}
|
||||
|
||||
/// No WebAssembly mods: no panel of theirs (`mod_panel.rs` with them).
|
||||
#[cfg(not(feature = "mods"))]
|
||||
impl Game<'_> {
|
||||
fn panel_wasm_sync(&mut self) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{BrowserStack, Owner, Panel, RecipePage};
|
||||
use crate::game::native_hooks::{NativeRecipeLayout, NativeRecipeView};
|
||||
|
||||
fn stack(item: &str) -> BrowserStack {
|
||||
BrowserStack {
|
||||
item: item.into(),
|
||||
count: 1,
|
||||
components: String::new(),
|
||||
}
|
||||
}
|
||||
fn view() -> NativeRecipeView {
|
||||
NativeRecipeView {
|
||||
layout: NativeRecipeLayout::Grid,
|
||||
width: 5,
|
||||
height: 1,
|
||||
slots: vec![vec![stack("minecraft:oak_log")]; 12],
|
||||
results: vec![stack("minecraft:oak_planks")],
|
||||
note: String::new(),
|
||||
action: Some(true),
|
||||
}
|
||||
}
|
||||
|
||||
/// A native mod's items and recipe page are what the panel holds, and
|
||||
/// its owner (who hears the panel's events) is that mod.
|
||||
#[test]
|
||||
fn native_owner_holds_the_panel() {
|
||||
let mut panel = Panel::default();
|
||||
let items = vec![stack("minecraft:stone"), stack("minecraft:dirt")];
|
||||
panel.show_items("recipes", items.clone());
|
||||
let shown = panel.shown(|_| true).expect("a panel");
|
||||
assert_eq!(shown.owner, Owner::Native("recipes".into()));
|
||||
assert_eq!(shown.items, items);
|
||||
assert_eq!(shown.recipes, None);
|
||||
let first = shown.version;
|
||||
|
||||
let tabs = vec![stack("minecraft:crafting_table")];
|
||||
let page = RecipePage::new("Oak Planks", tabs.clone(), 0, vec![view()]);
|
||||
panel.show_recipes("recipes", page);
|
||||
let shown = panel.shown(|_| true).expect("a panel");
|
||||
assert_ne!(shown.version, first, "the game converts each change");
|
||||
assert_eq!(shown.items, items);
|
||||
let page = shown.recipes.as_ref().expect("a recipe page");
|
||||
assert_eq!(
|
||||
(page.title.as_str(), &page.tabs, page.tab),
|
||||
("Oak Planks", &tabs, 0)
|
||||
);
|
||||
// Cut as `set-recipes` cuts it: a 3 x 3 grid at most, 9 slots.
|
||||
let r = &page.recipes[0];
|
||||
assert_eq!((r.width, r.height, r.slots.len()), (3, 1, 9));
|
||||
assert_eq!(r.results, [stack("minecraft:oak_planks")]);
|
||||
|
||||
// Another mod clearing its own (empty) panel leaves this one.
|
||||
panel.show_items("other", Vec::new());
|
||||
let owner = panel.shown(|_| true).map(|s| s.owner.clone());
|
||||
assert_eq!(owner, Some(Owner::Native("recipes".into())));
|
||||
// A mod the player turned off shows nothing.
|
||||
assert!(panel.shown(|id| id != "recipes").is_none());
|
||||
|
||||
// No recipes: the items again; then no items: no panel.
|
||||
panel.show_recipes("recipes", RecipePage::new("", Vec::new(), 0, Vec::new()));
|
||||
let shown = panel.shown(|_| true).expect("a panel");
|
||||
assert_eq!((shown.items.len(), shown.recipes.is_none()), (2, true));
|
||||
panel.show_items("recipes", Vec::new());
|
||||
assert!(panel.shown(|_| true).is_none());
|
||||
}
|
||||
|
||||
/// A native mod's panel wins over a WebAssembly mod's, and events go
|
||||
/// back to a WebAssembly owner in the runtime's form.
|
||||
#[cfg(feature = "mods")]
|
||||
#[test]
|
||||
fn native_owner_wins_over_webassembly() {
|
||||
use super::{BrowserEvent, Shown};
|
||||
use crate::game::mod_panel as wasm;
|
||||
let mut panel = Panel {
|
||||
wasm: Some(Shown {
|
||||
owner: Owner::Wasm(0),
|
||||
version: 7,
|
||||
items: vec![stack("minecraft:stone")],
|
||||
recipes: None,
|
||||
}),
|
||||
..Panel::default()
|
||||
};
|
||||
let owner = |p: &Panel| p.shown(|_| true).map(|s| s.owner.clone());
|
||||
assert_eq!(owner(&panel), Some(Owner::Wasm(0)));
|
||||
panel.show_items("recipes", vec![stack("minecraft:dirt")]);
|
||||
assert_eq!(owner(&panel), Some(Owner::Native("recipes".into())));
|
||||
panel.show_items("recipes", Vec::new());
|
||||
assert_eq!(owner(&panel), Some(Owner::Wasm(0)));
|
||||
|
||||
let event = BrowserEvent::SlotClicked {
|
||||
recipe: 2,
|
||||
slot: 9,
|
||||
stack: stack("minecraft:oak_planks"),
|
||||
right: true,
|
||||
};
|
||||
let horizon_mods::BrowserEvent::SlotClicked(c) = wasm::event(event) else {
|
||||
panic!("a slot click");
|
||||
};
|
||||
assert_eq!((c.recipe, c.slot, c.right), (2, 9, true));
|
||||
assert_eq!(c.stack.item, "minecraft:oak_planks");
|
||||
}
|
||||
}
|
||||
+72
-27
@@ -1,17 +1,20 @@
|
||||
//! `browser.set-recipes` (docs/mods.md): a mod's recipes shown in its item
|
||||
//! panel's place, laid out by the game with its own slots, arrows and
|
||||
//! A mod's recipes shown in its item panel's place (`native_panel_recipes`,
|
||||
//! or `browser.set-recipes` of a WebAssembly mod, docs/mods.md), laid out by the game with its own slots, arrows and
|
||||
//! buttons, paged to the panel's height, under a title and a back button.
|
||||
//! The mod hears clicks on their slots, their action buttons, the back
|
||||
//! button and page turns.
|
||||
use super::{
|
||||
Game,
|
||||
mod_browser::{Layout, browser_stack, inside},
|
||||
native_hooks::{
|
||||
NativePanelEvent as BrowserEvent, NativeRecipeLayout as BrowserLayout,
|
||||
NativeRecipeView as BrowserRecipe, NativeStack as BrowserStack,
|
||||
},
|
||||
native_panel::{Layout, Owner, browser_stack, inside},
|
||||
};
|
||||
use horizon_client::{
|
||||
hud::{HudBatch, rgba},
|
||||
items::ItemStack,
|
||||
};
|
||||
use horizon_mods::{BrowserEvent, BrowserLayout, BrowserSlotClick, BrowserStack, RecipePage};
|
||||
use std::time::Instant;
|
||||
|
||||
const SLOT: f32 = 18.0;
|
||||
@@ -21,6 +24,54 @@ const TITLE: f32 = 18.0;
|
||||
const TAB: f32 = 20.0;
|
||||
const GAP: f32 = 6.0;
|
||||
|
||||
/// Recipes a page may show, slots in one, stacks a slot cycles, and tabs.
|
||||
const RECIPES: usize = 256;
|
||||
const RECIPE_SLOTS: usize = 9;
|
||||
const SLOT_STACKS: usize = 64;
|
||||
const TABS: usize = 16;
|
||||
|
||||
/// What the panel shows instead of its items: a title, tabs (`tab` the
|
||||
/// selected one) and the recipes.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(super) struct RecipePage {
|
||||
pub(super) title: String,
|
||||
pub(super) tabs: Vec<BrowserStack>,
|
||||
pub(super) tab: u32,
|
||||
pub(super) recipes: Vec<BrowserRecipe>,
|
||||
}
|
||||
impl RecipePage {
|
||||
/// `native_panel_recipes`' page, cut as `set-recipes` cuts one; none
|
||||
/// without recipes (the items again).
|
||||
pub(super) fn new(
|
||||
title: &str,
|
||||
tabs: Vec<BrowserStack>,
|
||||
tab: u32,
|
||||
recipes: Vec<BrowserRecipe>,
|
||||
) -> Option<Self> {
|
||||
let cycle = |mut list: Vec<BrowserStack>| {
|
||||
list.truncate(SLOT_STACKS);
|
||||
list
|
||||
};
|
||||
let recipes: Vec<BrowserRecipe> = recipes
|
||||
.into_iter()
|
||||
.take(RECIPES)
|
||||
.map(|r| BrowserRecipe {
|
||||
width: r.width.clamp(1, 3),
|
||||
height: r.height.clamp(1, 3),
|
||||
slots: r.slots.into_iter().take(RECIPE_SLOTS).map(cycle).collect(),
|
||||
results: cycle(r.results),
|
||||
..r
|
||||
})
|
||||
.collect();
|
||||
(!recipes.is_empty()).then(|| Self {
|
||||
title: title.to_owned(),
|
||||
tabs: tabs.into_iter().take(TABS).collect(),
|
||||
tab,
|
||||
recipes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// One recipe as the game draws it.
|
||||
pub(super) struct Shown {
|
||||
layout: BrowserLayout,
|
||||
@@ -180,7 +231,7 @@ impl Game<'_> {
|
||||
}
|
||||
/// The recipes on the open page, each with its corner.
|
||||
fn recipes_shown(&self, layout: &Layout) -> Vec<(usize, (f32, f32))> {
|
||||
let Some(pages) = &self.mods.browser.recipes else {
|
||||
let Some(pages) = &self.panel.recipes else {
|
||||
return Vec::new();
|
||||
};
|
||||
let starts = Self::recipe_pages(pages, layout);
|
||||
@@ -202,7 +253,7 @@ impl Game<'_> {
|
||||
}
|
||||
/// What a slot of a shown recipe holds now: its stacks cycle once a second.
|
||||
fn recipe_slot_now(&self, recipe: usize, slot: usize) -> Option<&(BrowserStack, ItemStack)> {
|
||||
let pages = self.mods.browser.recipes.as_ref()?;
|
||||
let pages = self.panel.recipes.as_ref()?;
|
||||
let stacks = pages.recipes.get(recipe)?.slots.get(slot)?;
|
||||
let n = pages.since.elapsed().as_secs() as usize;
|
||||
stacks.get(n % stacks.len().max(1))
|
||||
@@ -212,7 +263,7 @@ impl Game<'_> {
|
||||
&self,
|
||||
layout: &Layout,
|
||||
) -> Option<(usize, BrowserStack, ItemStack)> {
|
||||
let pages = self.mods.browser.recipes.as_ref()?;
|
||||
let pages = self.panel.recipes.as_ref()?;
|
||||
let places = tab_places(pages, layout);
|
||||
places.iter().enumerate().find_map(|(i, &(x, y))| {
|
||||
if !inside(self.cursor, [x, y, SLOT, SLOT]) {
|
||||
@@ -227,7 +278,7 @@ impl Game<'_> {
|
||||
&self,
|
||||
layout: &Layout,
|
||||
) -> Option<(usize, usize, BrowserStack, ItemStack)> {
|
||||
let pages = self.mods.browser.recipes.as_ref()?;
|
||||
let pages = self.panel.recipes.as_ref()?;
|
||||
self.recipes_shown(layout)
|
||||
.into_iter()
|
||||
.find_map(|(i, (x, y))| {
|
||||
@@ -244,7 +295,7 @@ impl Game<'_> {
|
||||
|
||||
/// Draws the title row, the page's recipes and the page arrows.
|
||||
pub(super) fn draw_browser_recipes(&mut self, batch: &mut HudBatch, layout: &Layout) {
|
||||
let Some(pages) = &self.mods.browser.recipes else {
|
||||
let Some(pages) = &self.panel.recipes else {
|
||||
return;
|
||||
};
|
||||
let cursor = self.cursor;
|
||||
@@ -324,7 +375,7 @@ impl Game<'_> {
|
||||
batch.text(atlas, &text, x, layout.arrows_y + 5.0, u32::MAX, true);
|
||||
}
|
||||
for (i, (x, y)) in self.recipes_shown(layout) {
|
||||
let Some(r) = self.mods.browser.recipes.as_ref().map(|p| &p.recipes[i]) else {
|
||||
let Some(r) = self.panel.recipes.as_ref().map(|p| &p.recipes[i]) else {
|
||||
break;
|
||||
};
|
||||
let (at, arrow, action) = r.places(layout.width());
|
||||
@@ -405,7 +456,7 @@ impl Game<'_> {
|
||||
|
||||
/// The tabs, on slots; the selected one framed.
|
||||
fn draw_recipe_tabs(&mut self, batch: &mut HudBatch, layout: &Layout) {
|
||||
let Some(pages) = &self.mods.browser.recipes else {
|
||||
let Some(pages) = &self.panel.recipes else {
|
||||
return;
|
||||
};
|
||||
let places = tab_places(pages, layout);
|
||||
@@ -451,10 +502,10 @@ impl Game<'_> {
|
||||
pub(super) fn browser_recipes_press(
|
||||
&mut self,
|
||||
layout: &Layout,
|
||||
owner: usize,
|
||||
owner: &Owner,
|
||||
right: bool,
|
||||
) -> bool {
|
||||
let Some(pages) = &self.mods.browser.recipes else {
|
||||
let Some(pages) = &self.panel.recipes else {
|
||||
return false;
|
||||
};
|
||||
let cursor = self.cursor;
|
||||
@@ -469,38 +520,34 @@ impl Game<'_> {
|
||||
None
|
||||
};
|
||||
if let Some(to) = turn {
|
||||
if let Some(p) = self.mods.browser.recipes.as_mut() {
|
||||
if let Some(p) = self.panel.recipes.as_mut() {
|
||||
p.page = to;
|
||||
}
|
||||
self.audio
|
||||
.play("minecraft:ui.button.click", None, 0.25, 1.0);
|
||||
self.mods
|
||||
.mods
|
||||
.browser_event(owner, BrowserEvent::Page(starts[to] as u32));
|
||||
self.panel_event(owner, BrowserEvent::Page(starts[to] as u32));
|
||||
return true;
|
||||
}
|
||||
if let Some((tab, _, _)) = self.recipe_tab_hovered(layout) {
|
||||
self.audio
|
||||
.play("minecraft:ui.button.click", None, 0.25, 1.0);
|
||||
self.mods
|
||||
.mods
|
||||
.browser_event(owner, BrowserEvent::Tab(tab as u32));
|
||||
self.panel_event(owner, BrowserEvent::Tab(tab as u32));
|
||||
return true;
|
||||
}
|
||||
if inside(cursor, self.back_button(layout).0) {
|
||||
self.audio
|
||||
.play("minecraft:ui.button.click", None, 0.25, 1.0);
|
||||
self.mods.mods.browser_event(owner, BrowserEvent::Back);
|
||||
self.panel_event(owner, BrowserEvent::Back);
|
||||
return true;
|
||||
}
|
||||
if let Some((recipe, slot, stack, _)) = self.recipe_slot_hovered(layout) {
|
||||
let event = BrowserEvent::SlotClicked(BrowserSlotClick {
|
||||
let event = BrowserEvent::SlotClicked {
|
||||
recipe: recipe as u32,
|
||||
slot: slot as u32,
|
||||
stack,
|
||||
right,
|
||||
});
|
||||
self.mods.mods.browser_event(owner, event);
|
||||
};
|
||||
self.panel_event(owner, event);
|
||||
return true;
|
||||
}
|
||||
let action = self.recipes_shown(layout).into_iter().find(|&(i, (x, y))| {
|
||||
@@ -512,9 +559,7 @@ impl Game<'_> {
|
||||
if pages.recipes[i].action == Some(true) {
|
||||
self.audio
|
||||
.play("minecraft:ui.button.click", None, 0.25, 1.0);
|
||||
self.mods
|
||||
.mods
|
||||
.browser_event(owner, BrowserEvent::Action(i as u32));
|
||||
self.panel_event(owner, BrowserEvent::Action(i as u32));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -32,6 +32,12 @@ mod was tested on. In its own copy of the mod it sets `horizon_revision` to the
|
||||
official mods (Zoom, FPS counter, Keystrokes, Minimap, Recipes) are made this way, so any of them build together
|
||||
with no re-release for each Nightly. The `mods` feature (WebAssembly mods) is passed to the build only when the source has it.
|
||||
|
||||
API 2 adds the item panel beside container screens and its recipe pages (`native_panel_items`,
|
||||
`native_panel_recipes`, and the hook `native_panel_event` for the player's clicks, searches and keys over items), and
|
||||
the game's items and recipes (`native_recipes`, `native_known_recipes`, `native_place_recipe`, `native_give`): what a
|
||||
recipe viewer needs, in builds with or without the `mods` feature. A WebAssembly mod's panel shows only while no native
|
||||
mod shows one.
|
||||
|
||||
## What the launcher does
|
||||
|
||||
In the window: install a native mod from the Mods page, turn on Settings → Game → **Native mods (experimental)**,
|
||||
|
||||
Reference in new issue
Block a user