mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Expose block clone stacks to native mods
Co-Authored-By: Codex <[email protected]>
This commit is contained in:
1 parent
a1ff75a4a5
commit
fd61fa626e
6 files changed
+136
-5
No files matched your search
@@ -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;
|
||||
|
||||
|
||||
@@ -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@."
|
||||
}
|
||||
Reference in new issue
Block a user