From 3639670fae78cee86f29f0592c89912d19711b47 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 25 Aug 2026 11:19:36 +0200 Subject: [PATCH] feat(core): add dispenser item ejection triggered by circuit signal --- core/include/vf/Dispenser.h | 51 ++++++++++++++++++++++ core/src/Dispenser.cpp | 87 +++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 core/include/vf/Dispenser.h create mode 100644 core/src/Dispenser.cpp diff --git a/core/include/vf/Dispenser.h b/core/include/vf/Dispenser.h new file mode 100644 index 0000000..5bfd6bd --- /dev/null +++ b/core/include/vf/Dispenser.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include "Container.h" +#include "World.h" +#include "Circuit.h" +#include "Block.h" +#include "Math.h" + +namespace vf { + +constexpr int dispenserSlots = 9; + +enum class DispenseAction : uint8_t { + None, + PlacedBlock, + InsertedToContainer, + EjectedToWorld, +}; + +struct DispenseResult { + DispenseAction action = DispenseAction::None; + ItemSlot item; + Vec3i target; +}; + +struct DispenserState { + Face facing = Face::PosX; + bool lastPowered = false; + std::uint32_t rng = 0x9e3779b9u; +}; + +// Fires on a rising redstone edge. A dispenser places block items into the air +// cell it faces, or ejects non-block items (into a container in front when one +// exists, otherwise into the world). A dropper never places: it always ejects. +// The slot to draw from is chosen with the deterministic per-dispenser rng. +DispenseResult tickDispenser(World& world, const Vec3i& pos, DispenserState& state, + ContainerStore& store, int powerLevel); +DispenseResult tickDropper(World& world, const Vec3i& pos, DispenserState& state, + ContainerStore& store, int powerLevel); + +DispenseResult tickDispenser(World& world, const Vec3i& pos, DispenserState& state, + ContainerStore& store, const RedstoneCircuit& circuit); +DispenseResult tickDropper(World& world, const Vec3i& pos, DispenserState& state, + ContainerStore& store, const RedstoneCircuit& circuit); + +// Deterministic non-empty slot pick used by dispensers/droppers. Returns -1 when +// the inventory holds nothing. Exposed for testing the selection stream. +int chooseNonEmptySlot(const Container& inventory, std::uint32_t& rng); + +} // namespace vf diff --git a/core/src/Dispenser.cpp b/core/src/Dispenser.cpp new file mode 100644 index 0000000..12b7e0e --- /dev/null +++ b/core/src/Dispenser.cpp @@ -0,0 +1,87 @@ +#include "vf/Dispenser.h" + +namespace vf { + +namespace { + +std::uint32_t nextRandom(std::uint32_t& state) { + state = state * 1664525u + 1013904223u; + return state; +} + +DispenseResult ejectInto(ContainerStore& store, const Vec3i& front, const ItemSlot& one) { + Container* dest = store.at(front); + if (dest) { + ItemSlot copy = one; + if (dest->insert(copy) > 0) return {DispenseAction::InsertedToContainer, one, front}; + } + return {DispenseAction::EjectedToWorld, one, front}; +} + +bool risingEdge(DispenserState& state, int powerLevel) { + bool powered = powerLevel > 0; + bool rising = powered && !state.lastPowered; + state.lastPowered = powered; + return rising; +} + +DispenseResult fire(World& world, const Vec3i& pos, DispenserState& state, + ContainerStore& store, bool placeBlocks) { + Container& inventory = store.ensure(pos, dispenserSlots); + int index = chooseNonEmptySlot(inventory, state.rng); + if (index < 0) return {}; + + ItemSlot one = inventory.extractSlot(index, 1); + if (one.empty()) return {}; + + Vec3i front = pos + faceDelta(state.facing); + if (placeBlocks && isBlockItem(one.item)) { + Block placed = blockForItem(one.item); + if (placed != Block::Air && world.blockAt(front.x, front.y, front.z) == Block::Air) { + world.setBlock(front.x, front.y, front.z, placed); + return {DispenseAction::PlacedBlock, one, front}; + } + } + return ejectInto(store, front, one); +} + +} // namespace + +int chooseNonEmptySlot(const Container& inventory, std::uint32_t& rng) { + int candidates = 0; + for (int i = 0; i < inventory.size(); ++i) + if (!inventory.slot(i).empty()) ++candidates; + if (candidates == 0) return -1; + + int pick = static_cast((nextRandom(rng) >> 16) % static_cast(candidates)); + for (int i = 0; i < inventory.size(); ++i) { + if (inventory.slot(i).empty()) continue; + if (pick == 0) return i; + --pick; + } + return -1; +} + +DispenseResult tickDispenser(World& world, const Vec3i& pos, DispenserState& state, + ContainerStore& store, int powerLevel) { + if (!risingEdge(state, powerLevel)) return {}; + return fire(world, pos, state, store, true); +} + +DispenseResult tickDropper(World& world, const Vec3i& pos, DispenserState& state, + ContainerStore& store, int powerLevel) { + if (!risingEdge(state, powerLevel)) return {}; + return fire(world, pos, state, store, false); +} + +DispenseResult tickDispenser(World& world, const Vec3i& pos, DispenserState& state, + ContainerStore& store, const RedstoneCircuit& circuit) { + return tickDispenser(world, pos, state, store, circuit.powerAt(pos.x, pos.y, pos.z)); +} + +DispenseResult tickDropper(World& world, const Vec3i& pos, DispenserState& state, + ContainerStore& store, const RedstoneCircuit& circuit) { + return tickDropper(world, pos, state, store, circuit.powerAt(pos.x, pos.y, pos.z)); +} + +} // namespace vf