Expose block clone stacks to native mods

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
Delete228andCodex committed 2026-11-06 14:53:55 +03:00
1 parent a1ff75a4a5
commit fd61fa626e
6 files changed
+136 -5

No files matched your search

+20 -5
View File
@@ -125,11 +125,26 @@ impl Game<'_> {
Some(t) if t == "sticky" => "minecraft:sticky_piston",
_ => "minecraft:piston",
});
let item = clone_item(&facts.name, head.unwrap_or(&facts.item));
// FlowerPotBlock.getCloneItemStack: a potted plant picks as that plant
// (`new ItemStack(potted)`), an empty pot as the pot itself.
let item = picked_item(self.res.behaviours(), state, item)?;
let mut stack = ItemStack::new(item, 1);
let mut custom = None;
self.native_block_clone_item(&facts.name, pos, state, &mut custom);
let mut stack = match custom {
Some(stack) => {
if stack.count == 0
|| stack.item.as_ref() == "minecraft:air"
|| self.items().get(&stack.item).is_none()
{
return None;
}
stack
}
None => {
let item = clone_item(&facts.name, head.unwrap_or(&facts.item));
// FlowerPotBlock.getCloneItemStack: a potted plant picks as
// that plant (`new ItemStack(potted)`), an empty pot as itself.
let item = picked_item(self.res.behaviours(), state, item)?;
ItemStack::new(item, 1)
}
};
let mut patch = Compound::new();
// BeehiveBlock.getCloneItemStack: the honey level as the item's block state.
if include_data
@@ -4638,6 +4638,21 @@ impl Game<'_> {
cancel,
);
}
/// A block's clone item before Ctrl+Pick adds block-entity components.
/// The first supplied stack replaces the block's normal clone item;
/// no response keeps vanilla behavior. Empty or unregistered replacements
/// produce no picked item, including for a block without a primary item.
/// @API:block_clone_item@
pub(super) fn native_block_clone_item(
&self,
block: &str,
pos: [i32; 3],
state: BlockStateId,
stack: &mut Option<ItemStack>,
) {
mod_api::dispatch_block_clone_item(self, mod_api::mods(), block, pos, state, stack);
}
/// A server player picked an item from a block or entity, before it enters
/// the hotbar. `pos` is zero for an entity; `entity_id` is zero for a
/// block. The player's state is swapped in, though helpers that look them
@@ -0,0 +1,52 @@
mod block_clone_item_tests {
use super::*;
struct CloneItem {
responds: bool,
calls: Arc<Mutex<Vec<bool>>>,
}
impl Hooks for CloneItem {
fn block_clone_item(
&self,
_: &dyn Host,
event: &mut BlockCloneItemEvent<'_>,
) -> Option<ItemStack> {
assert_eq!(event.block, "fixture:heater");
assert_eq!(event.pos, [2, 64, 2]);
self.calls.lock().unwrap().push(self.responds);
self.responds.then_some(ItemStack)
}
}
#[test]
fn first_clone_response_owns_the_result_and_unanswered_stays_empty() {
let calls = Arc::new(Mutex::new(Vec::new()));
let mods: Vec<Box<dyn Hooks>> = [false, true, true]
.into_iter()
.map(|responds| {
Box::new(CloneItem {
responds,
calls: calls.clone(),
}) as Box<dyn Hooks>
})
.collect();
let mut result = None;
dispatch_block_clone_item(
&TestHost,
&[],
"fixture:heater",
[2, 64, 2],
BlockStateId,
&mut result,
);
assert!(result.is_none());
dispatch_block_clone_item(
&TestHost,
&mods,
"fixture:heater",
[2, 64, 2],
BlockStateId,
&mut result,
);
assert!(result.is_some());
assert_eq!(*calls.lock().unwrap(), [false, true]);
}
}
@@ -1,5 +1,6 @@
#[cfg(test)]
mod tests {
include!("block_clone_item_tests.rs");
use std::sync::{Arc, Mutex};
use std::time::Duration;
+37
View File
@@ -0,0 +1,37 @@
# Native block clone items (pending API)
`Game::native_block_clone_item(block, pos, state, &mut stack)` supplies the
equivalent of a block's `getCloneItemStack` before the ordinary Ctrl+Pick path
copies block-entity components. The callback receives the actual state and
position. The first supplied stack owns the result; no response preserves the
existing vanilla clone logic. Empty and unregistered replacements produce no
picked item. A replacement can represent a block with no primary block item.
The supplied stack's components survive. Ctrl+Pick adds the ordinary canonical
block-entity component patch afterward, including native mod payloads; it does
not return early from that path. This differs from the existing cancellable
player-pick event, which controls whether the final result enters the hotbar.
This extension has no global API number. Its generated hook fragment is stamped
after an upstream merge.
## Validation
The focused generated-dispatch regression passes, including first-response
ownership. The explicit prepared-resource `actual_state_factory_lifecycle` Game
fixture passes all nine groups. Its clone group checks actual state-dependent
items, a block with `item: false`, prototype components, Ctrl+Pick native entity
data, explicit empty/unknown refusal and unchanged vanilla fallback. The earlier
eight groups retain their placement, command, piston and synchronization checks.
Run the dispatch case with `cargo test -p horizon-mod-api-gen-tests
block_clone_item_tests`. The ignored Game fixture requires its own process and
private game/launcher/XDG homes, `HORIZON_TEST_RESOURCES=<prepared-26.3-root>`
and a local-only `HORIZON_API_DISCOVERY=http://127.0.0.1:9/` endpoint; run
`cargo test -p horizon-client --lib --no-default-features
actual_state_factory_lifecycle -- --ignored --nocapture`.
Strict no-default-features Clippy passes all targets of `horizon-client`,
`horizon-client-lib`, `horizon-mod-api-gen` and `horizon-mod-api-gen-tests`.
The final tests and Clippy used an unchanged source snapshot. These focused
checks do not claim a full `pr_ready` run, Java input comparison or UI parity.
@@ -0,0 +1,11 @@
{
"hooks": [
{
"name": "block_clone_item",
"target": "horizon_client::game::native_hooks::Game<'_>::native_block_clone_item",
"mod_api": {"contract": "replace", "out": "stack"}
}
],
"rust_summary": "state-dependent block clone items before Ctrl+Pick data.",
"doc_summary": "`native_block_clone_item` supplies a block's clone stack from its actual state before ordinary Ctrl+Pick block-entity component copying. First response owns the result; no response retains vanilla. An empty or unregistered response suppresses picking. Bundles need \"hooks\": @API:block_clone_item@."
}