mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Fill Recipes grids without teaching the recipe
This commit is contained in:
1 parent
9d11d0c710
commit
c7d27a84a1
12 files changed
+337
-80
No files matched your search
@@ -2662,6 +2662,9 @@ impl Game<'_> {
|
||||
S::CustomPayload(p) if p.channel == horizon_client::lod_net::CHANNEL => {
|
||||
self.guest_far_terrain(host, conn, &p.data)
|
||||
}
|
||||
S::CustomPayload(p) if p.channel == horizon_client::lan::recipes::TRANSFER_CHANNEL => {
|
||||
self.guest_recipe_transfer(host, conn, &p.data);
|
||||
}
|
||||
S::ChatCommandSigned(p) => self.guest_signed_command(host, conn, p),
|
||||
S::ChangeDifficulty(p) if host.hub.guests[gi].permission >= 2 => {
|
||||
if !self.level.difficulty_locked && (0..=3).contains(&p.difficulty) {
|
||||
|
||||
@@ -10,7 +10,10 @@ use horizon_client::{
|
||||
furnace::FurnaceCooking,
|
||||
items::ItemStack,
|
||||
lan::{guest::Guest, menu::Slots},
|
||||
menu::{Env, Input, Kind, Menu},
|
||||
menu::{
|
||||
Env, Input, Kind, Menu,
|
||||
place::{PostPlace, TransferError},
|
||||
},
|
||||
};
|
||||
use horizon_protocol::packets::play::{self, Clientbound as C};
|
||||
|
||||
@@ -441,7 +444,6 @@ impl Game<'_> {
|
||||
conn: u32,
|
||||
p: &horizon_protocol::packets::recipes::PlaceRecipe,
|
||||
) {
|
||||
use horizon_client::menu::place::PostPlace;
|
||||
let Some(gi) = host.hub.guests.iter().position(|g| g.conn == conn) else {
|
||||
return;
|
||||
};
|
||||
@@ -470,7 +472,63 @@ impl Game<'_> {
|
||||
let (player, name) = (g.uuid, g.name.clone());
|
||||
self.native_player_recipe_book_click(player, &name, &id, p.use_max);
|
||||
let creative = g.creative();
|
||||
let placed = if p.container == 0 {
|
||||
let placed = self.lan_apply_recipe(host, gi, p.container, |menu, env| {
|
||||
menu.place_recipe(env, &id, p.use_max, creative, None)
|
||||
});
|
||||
if placed == Some(PostPlace::Ghost) {
|
||||
host.hub.send_ghost_recipe(conn, p.container, &id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn guest_recipe_transfer(&mut self, host: &mut LanHost, conn: u32, data: &[u8]) {
|
||||
use horizon_protocol::buf::{Reader, Writer};
|
||||
let Some(gi) = host.hub.guests.iter().position(|g| g.conn == conn) else {
|
||||
return;
|
||||
};
|
||||
let g = &host.hub.guests[gi];
|
||||
if g.brand != "horizon" || g.spectator() {
|
||||
return;
|
||||
}
|
||||
let mut reader = Reader::new(data);
|
||||
let (Ok(container), Ok(id)) = (reader.varint(), reader.string(512)) else {
|
||||
return;
|
||||
};
|
||||
if reader.remaining() != 0 {
|
||||
return;
|
||||
}
|
||||
let result = if container == 0 && (g.menu.is_some() || g.creative()) {
|
||||
Err(TransferError::Unavailable)
|
||||
} else {
|
||||
self.lan_apply_recipe(host, gi, container, |menu, env| {
|
||||
menu.transfer_recipe(env, &id)
|
||||
})
|
||||
.unwrap_or(Err(TransferError::Unavailable))
|
||||
};
|
||||
let mut reply = Writer::new();
|
||||
reply.varint(container);
|
||||
reply.u8(match result {
|
||||
Ok(()) => 0,
|
||||
Err(TransferError::Missing) => 1,
|
||||
Err(TransferError::NoRoom) => 2,
|
||||
Err(TransferError::Unavailable) => 3,
|
||||
});
|
||||
host.hub.guests[gi].send(&C::CustomPayload(
|
||||
horizon_protocol::packets::configuration::CustomPayload {
|
||||
channel: horizon_client::lan::recipes::TRANSFER_CHANNEL.into(),
|
||||
data: reply.data,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn lan_apply_recipe<R>(
|
||||
&mut self,
|
||||
host: &mut LanHost,
|
||||
gi: usize,
|
||||
container: i32,
|
||||
apply: impl FnOnce(&mut Menu, &Env) -> R,
|
||||
) -> Option<R> {
|
||||
let conn = host.hub.guests[gi].conn;
|
||||
let placed = if container == 0 {
|
||||
let crafting = host.play.entry(conn).or_default();
|
||||
let mut menu = crafting
|
||||
.crafting
|
||||
@@ -482,7 +540,7 @@ impl Game<'_> {
|
||||
for (k, s) in grid.into_iter().enumerate() {
|
||||
menu.set_slot(env, 1 + k, s);
|
||||
}
|
||||
menu.place_recipe(env, &id, p.use_max, creative, None)
|
||||
apply(menu, env)
|
||||
});
|
||||
let g = &mut host.hub.guests[gi];
|
||||
for k in 0..4 {
|
||||
@@ -493,23 +551,16 @@ impl Game<'_> {
|
||||
host.hub.send_inventory(gi);
|
||||
placed
|
||||
} else {
|
||||
let Some((window, state, mut menu, pos)) = host
|
||||
let (window, state, mut menu, pos) = host
|
||||
.play
|
||||
.get_mut(&conn)
|
||||
.and_then(|play| play.crafting.table.take_if(|(w, ..)| *w == p.container))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let placed = self.guest_menu_op(host, gi, &mut menu, |menu, env| {
|
||||
menu.place_recipe(env, &id, p.use_max, creative, None)
|
||||
});
|
||||
.and_then(|play| play.crafting.table.take_if(|(w, ..)| *w == container))?;
|
||||
let placed = self.guest_menu_op(host, gi, &mut menu, apply);
|
||||
host.play.entry(conn).or_default().crafting.table = Some((window, state, menu, pos));
|
||||
self.lan_send_table_content(host, conn);
|
||||
placed
|
||||
};
|
||||
if placed == PostPlace::Ghost {
|
||||
host.hub.send_ghost_recipe(conn, p.container, &id);
|
||||
}
|
||||
Some(placed)
|
||||
}
|
||||
|
||||
/// The guest opened a crafting table (`CraftingTableBlock.getMenuProvider`).
|
||||
|
||||
@@ -9980,6 +9980,11 @@ impl Game<'_> {
|
||||
pub(super) fn native_place_recipe(&mut self, book_id: i32, all: bool) -> Result<(), String> {
|
||||
self.recipe_place(book_id, all)
|
||||
}
|
||||
/// Move ingredients for a viewer recipe into the open crafting grid.
|
||||
/// Unlike the recipe book click, this doesn't require learning the recipe.
|
||||
pub(super) fn native_transfer_recipe(&mut self, book_id: i32) {
|
||||
self.transfer_viewer_recipe(book_id);
|
||||
}
|
||||
/// Creative mode only: gives the player `count` of `item`
|
||||
/// (`minecraft:diamond`) as taking it from the creative inventory does
|
||||
/// (`ServerboundSetCreativeModeSlot`): the selected slot first, then the
|
||||
|
||||
@@ -69,6 +69,7 @@ pub(super) struct Panel {
|
||||
searching: bool,
|
||||
/// The recipes shown instead of the items.
|
||||
pub(super) recipes: Option<super::native_panel_recipes::Pages>,
|
||||
pub(super) transfer_warning: Option<(&'static str, &'static str)>,
|
||||
}
|
||||
|
||||
impl Panel {
|
||||
@@ -116,6 +117,7 @@ impl Panel {
|
||||
/// nothing, except while filtering the item list with a non-empty query.
|
||||
fn native_changed(&mut self, keep_search: bool) {
|
||||
self.versions += 1;
|
||||
self.transfer_warning = None;
|
||||
if let Some(shown) = self.native.as_mut() {
|
||||
shown.version = self.versions;
|
||||
if shown.items.is_empty() && shown.recipes.is_none() && !keep_search {
|
||||
@@ -499,10 +501,20 @@ impl Game<'_> {
|
||||
}
|
||||
/// The container screen closed: the search box lets go of the keys.
|
||||
pub(super) fn mod_browser_closed(&mut self) {
|
||||
self.panel.transfer_warning = None;
|
||||
if let Some(search) = self.panel.search.as_mut() {
|
||||
search.focused = false;
|
||||
}
|
||||
}
|
||||
pub(super) fn close_transferred_recipe(&mut self) {
|
||||
if let Some(owner) = self.browser_sync()
|
||||
&& owner.native_id() == "recipes"
|
||||
&& self.panel.recipes.is_some()
|
||||
{
|
||||
self.mod_browser_closed();
|
||||
self.panel_event(&owner, BrowserEvent::Back);
|
||||
}
|
||||
}
|
||||
/// The item under the pointer on a container screen, for the mods' keys
|
||||
/// (the `Key` event): the panel's, else a slot's.
|
||||
pub(super) fn mod_browser_pointed(&mut self) -> Option<BrowserStack> {
|
||||
|
||||
@@ -453,6 +453,17 @@ impl Game<'_> {
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some((key, fallback)) = self.panel.transfer_warning {
|
||||
let warning = self.translate(key, fallback);
|
||||
let width = self.gui.atlas.font.width(&warning);
|
||||
horizon_client::widgets::record(&warning, [cursor.0, cursor.1, width, 9.0], false);
|
||||
self.draw_tooltip_lines(
|
||||
batch,
|
||||
&[(warning, rgba(255, 100, 100, 255))],
|
||||
cursor.0,
|
||||
cursor.1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The tabs, on slots; the selected one framed.
|
||||
|
||||
@@ -1019,6 +1019,100 @@ impl Game<'_> {
|
||||
self.recipe_ui.ghost.clear();
|
||||
self.place_recipe(BookType::Crafting, index, all);
|
||||
}
|
||||
pub(super) fn transfer_viewer_recipe(&mut self, display_id: i32) {
|
||||
self.panel.transfer_warning = None;
|
||||
if !self.mod_recipe_screen() || (self.creative && self.craft_size != 3) {
|
||||
self.show_transfer_result(3);
|
||||
return;
|
||||
}
|
||||
if self.remote.as_ref().is_some_and(|r| !r.recipe_transfer) {
|
||||
// Older Horizon and vanilla servers still accept book clicks.
|
||||
if self.recipe_place(display_id, false).is_err() {
|
||||
self.show_transfer_result(4);
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.ensure_book();
|
||||
let Some(id) = self
|
||||
.recipe_ui
|
||||
.book
|
||||
.as_ref()
|
||||
.and_then(|book| {
|
||||
book.entries
|
||||
.iter()
|
||||
.find(|entry| entry.display_id == display_id)
|
||||
})
|
||||
.map(|entry| entry.id.clone())
|
||||
else {
|
||||
self.show_transfer_result(3);
|
||||
return;
|
||||
};
|
||||
if self.remote.is_some() {
|
||||
let mut request = horizon_protocol::buf::Writer::new();
|
||||
request.varint(self.remote_menu_id());
|
||||
request.string(&id);
|
||||
self.send(horizon_protocol::packets::play::Serverbound::CustomPayload(
|
||||
horizon_protocol::packets::configuration::CustomPayload {
|
||||
channel: horizon_client::lan::recipes::TRANSFER_CHANNEL.into(),
|
||||
data: request.data,
|
||||
},
|
||||
));
|
||||
return;
|
||||
}
|
||||
use horizon_client::menu::place::TransferError;
|
||||
let mut result = Err(TransferError::Unavailable);
|
||||
self.menu_session(|menu, env, _| {
|
||||
result = menu.transfer_recipe(env, &id);
|
||||
Some(result.is_ok())
|
||||
});
|
||||
self.show_transfer_result(match result {
|
||||
Ok(()) => 0,
|
||||
Err(TransferError::Missing) => 1,
|
||||
Err(TransferError::NoRoom) => 2,
|
||||
Err(TransferError::Unavailable) => 3,
|
||||
});
|
||||
if result.is_ok() {
|
||||
self.recipe_book_changed();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn remote_recipe_transfer(&mut self, data: &[u8]) {
|
||||
if data.is_empty() {
|
||||
if let Some(remote) = &mut self.remote {
|
||||
remote.recipe_transfer = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut reader = horizon_protocol::buf::Reader::new(data);
|
||||
if let (Ok(container), Ok(result)) = (reader.varint(), reader.u8())
|
||||
&& reader.remaining() == 0
|
||||
&& container == self.remote_menu_id()
|
||||
&& self.mod_recipe_screen()
|
||||
&& self.panel.recipes.is_some()
|
||||
{
|
||||
self.show_transfer_result(result);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_transfer_result(&mut self, result: u8) {
|
||||
if result == 0 {
|
||||
self.close_transferred_recipe();
|
||||
}
|
||||
self.panel.transfer_warning = match result {
|
||||
1 => Some(("horizon.recipe_transfer.missing", "Missing Items")),
|
||||
2 => Some(("horizon.recipe_transfer.no_room", "Inventory full")),
|
||||
3 => Some((
|
||||
"horizon.recipe_transfer.unavailable",
|
||||
"Recipe does not fit this menu",
|
||||
)),
|
||||
4 => Some((
|
||||
"horizon.recipe_transfer.unsupported",
|
||||
"Server requires a known recipe",
|
||||
)),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
/// `ServerboundPlaceRecipePacket`: the server places the recipe
|
||||
/// (`ServerPlaceRecipe` on the menu engine) and answers with a ghost
|
||||
/// recipe when the items are missing.
|
||||
|
||||
@@ -498,6 +498,7 @@ enum OwnProfile {
|
||||
pub(super) struct Remote {
|
||||
pub session: Session,
|
||||
pub brand: String,
|
||||
pub(super) recipe_transfer: bool,
|
||||
/// This connection's configured feature flags, carried through reconfiguration.
|
||||
pub(super) features: Vec<String>,
|
||||
/// The server announced it runs mods' quiet commands.
|
||||
@@ -1088,6 +1089,7 @@ impl Game<'_> {
|
||||
self.remote = Some(Remote {
|
||||
session,
|
||||
brand: "unknown".into(),
|
||||
recipe_transfer: false,
|
||||
features: vec!["minecraft:vanilla".into()],
|
||||
quiet_commands: false,
|
||||
mod_net: false,
|
||||
@@ -3337,6 +3339,9 @@ impl Game<'_> {
|
||||
Err(e) => log::debug!("Horizon: a bad menu extra payload: {e}"),
|
||||
}
|
||||
}
|
||||
C::CustomPayload(p) if p.channel == horizon_client::lan::recipes::TRANSFER_CHANNEL => {
|
||||
self.remote_recipe_transfer(&p.data);
|
||||
}
|
||||
// The server's recipes its native mods asked to send.
|
||||
C::CustomPayload(p) if p.channel == horizon_client::lan::recipes::CONTENT_CHANNEL => {
|
||||
self.remote_recipe_content(&p.data);
|
||||
|
||||
@@ -1329,6 +1329,12 @@ impl Hub {
|
||||
// for, to a Horizon client only.
|
||||
if g.brand == "horizon" {
|
||||
send_recipe_content(g);
|
||||
g.send(&C::CustomPayload(
|
||||
horizon_protocol::packets::configuration::CustomPayload {
|
||||
channel: super::recipes::TRANSFER_CHANNEL.into(),
|
||||
data: Vec::new(),
|
||||
},
|
||||
));
|
||||
}
|
||||
// PlayerList.sendPlayerPermissionLevel, then the recipe book.
|
||||
self.send_permission_level(i);
|
||||
|
||||
@@ -66,6 +66,10 @@ fn recipes(dir: &Path, namespace: &str, out: &mut BTreeMap<String, Value>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Horizon's optional recipe viewer transfer. An empty clientbound payload
|
||||
/// advertises support. Requests carry a menu id and resource location.
|
||||
pub const TRANSFER_CHANNEL: &str = "horizon:recipe_transfer";
|
||||
|
||||
/// The recipes by id in vanilla's registration order (path, then namespace),
|
||||
/// the order `RecipeManager` numbers groups and lists the stonecutter's
|
||||
/// recipes in, and so the order the host's own tables keep.
|
||||
|
||||
@@ -298,6 +298,14 @@ pub enum PostPlace {
|
||||
Ghost,
|
||||
}
|
||||
|
||||
/// Why a viewer transfer could not move a complete set of ingredients.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TransferError {
|
||||
Missing,
|
||||
NoRoom,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
/// The menu slots a placement works on.
|
||||
struct Grid {
|
||||
width: usize,
|
||||
@@ -384,7 +392,8 @@ impl Menu {
|
||||
}
|
||||
let ingredients: Vec<&Ingredient> = placement.ingredients.iter().collect();
|
||||
let post = if stacked.try_pick(&ingredients, 1, None) {
|
||||
self.place(env, id, &grid, &placement, &mut stacked, use_max);
|
||||
let matches = self.recipe_in_grid(env, id);
|
||||
self.place(env, &grid, &placement, &mut stacked, use_max, matches);
|
||||
PostPlace::Nothing
|
||||
} else {
|
||||
self.clear_grid(env, &grid);
|
||||
@@ -395,6 +404,51 @@ impl Menu {
|
||||
post
|
||||
}
|
||||
|
||||
/// Move one set of ingredients from the player's real inventory and grid.
|
||||
/// The viewer leaves both alone on failure, including in creative mode.
|
||||
pub fn transfer_recipe(&mut self, env: &Env, id: &str) -> Result<(), TransferError> {
|
||||
if !matches!(self.kind, Kind::Inventory | Kind::Crafting) || self.carried.is_some() {
|
||||
return Err(TransferError::Unavailable);
|
||||
}
|
||||
let grid = self.place_grid().ok_or(TransferError::Unavailable)?;
|
||||
let placement = env
|
||||
.recipes
|
||||
.placement(id)
|
||||
.ok_or(TransferError::Unavailable)?;
|
||||
if placement.impossible()
|
||||
|| placement.ingredients.len() > grid.inputs.len()
|
||||
|| placement
|
||||
.shaped
|
||||
.is_some_and(|(w, h)| w > grid.width || h > grid.height)
|
||||
{
|
||||
return Err(TransferError::Unavailable);
|
||||
}
|
||||
let mut stacked = self.stacked_contents(env);
|
||||
let ingredients: Vec<_> = placement.ingredients.iter().collect();
|
||||
if !stacked.try_pick(&ingredients, 1, None) {
|
||||
return Err(TransferError::Missing);
|
||||
}
|
||||
// The vanilla placement can return leftover grid items as drops.
|
||||
// A viewer transfer must keep the original slots if it cannot move
|
||||
// every input without dropping anything.
|
||||
let previous = self.containers.clone();
|
||||
let (drops, dropped) = (self.drops.len(), self.dropped.len());
|
||||
self.place(env, &grid, &placement, &mut stacked, false, false);
|
||||
let no_room = self.dropped.len() != dropped;
|
||||
if no_room || !self.recipe_in_grid(env, id) {
|
||||
self.containers = previous;
|
||||
self.drops.truncate(drops);
|
||||
self.dropped.truncate(dropped);
|
||||
return Err(if no_room {
|
||||
TransferError::NoRoom
|
||||
} else {
|
||||
TransferError::Unavailable
|
||||
});
|
||||
}
|
||||
self.recompute(env);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `recipeMatches`: the recipe already fills the grid.
|
||||
fn recipe_in_grid(&self, env: &Env, id: &str) -> bool {
|
||||
match self.kind {
|
||||
@@ -417,14 +471,13 @@ impl Menu {
|
||||
fn place(
|
||||
&mut self,
|
||||
env: &Env,
|
||||
id: &str,
|
||||
grid: &Grid,
|
||||
placement: &Placement,
|
||||
stacked: &mut Stacked,
|
||||
use_max: bool,
|
||||
matches: bool,
|
||||
) {
|
||||
let ingredients: Vec<&Ingredient> = placement.ingredients.iter().collect();
|
||||
let matches = self.recipe_in_grid(env, id);
|
||||
let biggest = stacked.try_pick_all(&ingredients, i32::MAX, None);
|
||||
if matches {
|
||||
for &s in &grid.inputs {
|
||||
|
||||
+4
-4
@@ -71,10 +71,10 @@ Recipes mod and a matching `horizon-ctl`. This opt-in check fails if Recipes is
|
||||
LAN runs do not require it.
|
||||
|
||||
The runner installs the same original `tools/fixtures/recipes_hmod` package in both scratch game
|
||||
folders. It finds the output of an original recipe (flint to two echo shards), checks unknown recipes
|
||||
and missing ingredients, fills the
|
||||
crafting grid through Recipes' `+` button, takes the result, and checks authoritative inventory counts
|
||||
before and after Save and Quit/rejoin. Logs, screenshots and `recipes-hmod.json` go under `--out`.
|
||||
folders. It finds the output of an original recipe (flint to two echo shards) in Survival and Creative.
|
||||
With the recipe book empty, it verifies that missing ingredients show a warning and preserve the
|
||||
occupied grid. It then fills the grid through Recipes' `+`, takes the result, and checks authoritative
|
||||
inventory counts before and after Save and Quit/rejoin. Logs, screenshots and `recipes-hmod.json` go under `--out`.
|
||||
As an assertion canary, changing only the fixture's result count from two to one must fail the custom
|
||||
result check; restore it before running the passing case. This is a content scenario, not general
|
||||
Java-mod compatibility or custom-item registration coverage.
|
||||
|
||||
+71
-58
@@ -13215,15 +13215,9 @@ def prepare_recipes_hmod(*games):
|
||||
(settings / 'give').write_text('false')
|
||||
|
||||
|
||||
def check_recipes_hmod(host, guest, out):
|
||||
"""Recipes' real panel -> normal server menu -> custom output -> save/rejoin (#9647).
|
||||
|
||||
Opt in with --check recipes-hmod and --bin-dir pointing at a game compiled with the official
|
||||
native Recipes mod. The fixture is ours; no Java mod, copied assets or synthetic game replies.
|
||||
"""
|
||||
def exercise_recipes_hmod(game, out, mode, supplied):
|
||||
"""Use the real Recipes panel and the authoritative crafting table."""
|
||||
item = 'minecraft:echo_shard'
|
||||
recipe = 'recipe_pilot:flint_to_echo_shard'
|
||||
seen = {}
|
||||
|
||||
def widget(game, predicate):
|
||||
matches = [w for w in game.raw('ui')['widgets'] if predicate(w)]
|
||||
@@ -13256,70 +13250,89 @@ def check_recipes_hmod(host, guest, out):
|
||||
def contents(game):
|
||||
return {'flint': server_count(game, 'flint'), 'echo_shard': server_count(game, item)}
|
||||
|
||||
for game, supplied in ((host, 4), (guest, 5)):
|
||||
game('command', command='clear @s')
|
||||
server_reply(game, 'recipe take @s *', 'recipe')
|
||||
game('command', command=f'gamemode {mode}')
|
||||
game.wait(10)
|
||||
game.until(lambda: game('state')['gamemode'], lambda current: current == mode)
|
||||
game('command', command='clear @s')
|
||||
game('command', command='give @s minecraft:dirt 1')
|
||||
table = None
|
||||
if mode == 'creative':
|
||||
x, y, z = (math.floor(v) for v in pos_of(game))
|
||||
table = (x, y, z - 2)
|
||||
game('command', command=f'setblock {table[0]} {table[1]} {table[2]} minecraft:crafting_table')
|
||||
game.wait(10)
|
||||
game.raw('interact', *table)
|
||||
game.raw('wait-for', 'screen', 'inventory', '--timeout', 10)
|
||||
assert game('screen').get('craft_size') == 3
|
||||
else:
|
||||
game.raw('key', 'e', 'tap')
|
||||
game.raw('wait-for', 'screen', 'inventory', '--timeout', 10)
|
||||
assert game('screen').get('craft_size') == (3 if table else 2)
|
||||
|
||||
# The output exists in vanilla; its flint recipe is original .hmod content.
|
||||
search(game, 'recipe_pilot:no_such_item')
|
||||
assert not any(w.get('label') == 'Echo Shard' for w in game.raw('ui')['widgets'])
|
||||
search(game, item)
|
||||
game.shot(out, 'recipes-hmod-search')
|
||||
unknown = page(game)
|
||||
assert unknown['enabled'] is False, f'{game.name}: unknown recipe can be placed: {unknown}'
|
||||
game.shot(out, 'recipes-hmod-unknown')
|
||||
assert not any(i for i, _ in grid_of(game, 100, 109).values())
|
||||
assert not result_of(game)[0]
|
||||
# Put an unrelated item in the grid so a refused transfer cannot pass by
|
||||
# merely clearing an already empty grid.
|
||||
game.raw('click-slot', hotbar_slot(game, 'minecraft:dirt'))
|
||||
game.raw('click-slot', 100)
|
||||
game.wait(10)
|
||||
before = grid_of(game, 100, 109)
|
||||
assert before[100] == ('minecraft:dirt', 1), before
|
||||
assert not result_of(game)[0], f'{mode}: control ingredient must have no output'
|
||||
server_reply(game, 'recipe take @s *', 'recipe')
|
||||
game.until(lambda: game('screen')['recipe_book']['known'], lambda count: count == 0)
|
||||
|
||||
server_reply(game, f'recipe give @s {recipe}', 'recipe')
|
||||
game.wait(10)
|
||||
back = widget(game, lambda w: w.get('label') == 'Back')
|
||||
game.raw('click', back['id'])
|
||||
game.wait(5)
|
||||
known = page(game)
|
||||
assert known['enabled'] is True, f'{game.name}: learned custom recipe unavailable: {known}'
|
||||
game.raw('click', known['id'])
|
||||
game.wait(10)
|
||||
missing = contents(game)
|
||||
assert missing == {'flint': 0, 'echo_shard': 0}, missing
|
||||
assert not any(i for i, _ in grid_of(game, 100, 109).values())
|
||||
assert not result_of(game)[0], f'{game.name}: missing ingredients fabricated a result'
|
||||
game.shot(out, 'recipes-hmod-missing')
|
||||
search(game, 'recipe_pilot:no_such_item')
|
||||
assert not any(w.get('label') == 'Echo Shard' for w in game.raw('ui')['widgets'])
|
||||
search(game, item)
|
||||
transfer = page(game)
|
||||
assert transfer['enabled'], f'{mode}: + must be available before unlock'
|
||||
game.raw('click', transfer['id'])
|
||||
game.until(lambda: game.raw('ui')['widgets'],
|
||||
lambda widgets: any(w.get('label') == 'Missing Items' for w in widgets))
|
||||
assert grid_of(game, 100, 109) == before, f'{mode}: missing ingredients changed the grid'
|
||||
assert not result_of(game)[0], f'{mode}: missing ingredients fabricated a result'
|
||||
missing = contents(game)
|
||||
assert missing == {'flint': 0, 'echo_shard': 0}, missing
|
||||
game.shot(out, f'{mode}-missing')
|
||||
|
||||
# Supply the input through the server, then use only the panel's normal + action.
|
||||
game('command', command=f'give @s minecraft:flint {supplied}')
|
||||
game.wait(10)
|
||||
known = widget(game, lambda w: (w.get('label') or '').startswith('+ '))
|
||||
game.raw('click', known['id'])
|
||||
game.until(lambda: result_of(game), lambda r: bool(r[0]))
|
||||
grid = [s for s in grid_of(game, 100, 109).values() if s[0]]
|
||||
result = result_of(game)
|
||||
assert grid == [('minecraft:flint', 1)], f'{game.name}: filled grid {grid}'
|
||||
# Independent expected yield: changing the fixture to yield one must fail here.
|
||||
assert result == (item, 2), f'{game.name}: custom result {result}'
|
||||
game.shot(out, 'recipes-hmod-filled')
|
||||
game.raw('click-slot', 200, 'shift')
|
||||
game.wait(10)
|
||||
assert not result_of(game)[0], f'{game.name}: consumed input left another result'
|
||||
back_to_game(game)
|
||||
crafted = contents(game)
|
||||
assert crafted == {'flint': supplied - 1, 'echo_shard': 2}, crafted
|
||||
seen[game.name] = {'missing': missing, 'grid': grid, 'result': result, 'crafted': crafted}
|
||||
game('command', command=f'give @s minecraft:flint {supplied}')
|
||||
server_reply(game, 'recipe take @s *', 'recipe')
|
||||
game.until(lambda: game('screen')['recipe_book']['known'], lambda count: count == 0)
|
||||
transfer = widget(game, lambda w: (w.get('label') or '').startswith('+ '))
|
||||
game.raw('click', transfer['id'])
|
||||
game.until(lambda: result_of(game), lambda r: bool(r[0]))
|
||||
grid = [s for s in grid_of(game, 100, 109).values() if s[0]]
|
||||
result = result_of(game)
|
||||
assert grid == [('minecraft:flint', 1)], f'{mode}: filled grid {grid}'
|
||||
assert result == (item, 2), f'{mode}: custom result {result}'
|
||||
assert game('screen')['recipe_book']['known'] == 0, f'{mode}: transfer learned a recipe'
|
||||
assert server_count(game, 'dirt') == 1, f'{mode}: displaced ingredient was lost'
|
||||
assert server_count(game, 'flint') == supplied - 1, f'{mode}: transfer fabricated an input'
|
||||
assert not any(w.get('label') == 'Missing Items' for w in game.raw('ui')['widgets'])
|
||||
game.wait(140)
|
||||
game.shot(out, f'{mode}-filled')
|
||||
game.raw('click-slot', 200, 'shift')
|
||||
game.wait(10)
|
||||
assert not result_of(game)[0], f'{mode}: consumed input left another result'
|
||||
back_to_game(game)
|
||||
crafted = contents(game)
|
||||
assert crafted == {'flint': supplied - 1, 'echo_shard': 2}, crafted
|
||||
if table:
|
||||
game('command', command=f'setblock {table[0]} {table[1]} {table[2]} minecraft:air')
|
||||
return {'mode': mode, 'missing': missing, 'warning': 'Missing Items',
|
||||
'grid_unchanged_on_failure': True, 'known_before_and_after_fill': 0,
|
||||
'grid': grid, 'result': result, 'crafted': crafted}
|
||||
|
||||
|
||||
def check_recipes_hmod(host, guest, out):
|
||||
"""Exercise Survival and Creative through the real host/guest server."""
|
||||
seen = {
|
||||
host.name: exercise_recipes_hmod(host, out, 'survival', 4),
|
||||
guest.name: exercise_recipes_hmod(guest, out, 'creative', 5),
|
||||
}
|
||||
save_and_reopen(host, guest)
|
||||
for game in (host, guest):
|
||||
persisted = contents(game)
|
||||
persisted = {'flint': server_count(game, 'flint'), 'echo_shard': server_count(game, 'echo_shard')}
|
||||
assert persisted == seen[game.name]['crafted'], f'{game.name}: after rejoin {persisted}'
|
||||
seen[game.name]['rejoined'] = persisted
|
||||
game.shot(out, 'recipes-hmod-rejoined')
|
||||
game.shot(out, f"{seen[game.name]['mode']}-rejoined")
|
||||
(out / 'recipes-hmod.json').write_text(json.dumps(seen, indent=2) + '\n')
|
||||
return seen
|
||||
|
||||
|
||||
Reference in new issue
Block a user