Files
voxelforge/core/src/SlotAction.cpp
T

466 lines
17 KiB
C++

#include "vf/SlotAction.h"
#include <algorithm>
namespace vf {
namespace {
// The half of a menu a shift-click aims at. Craft inputs and result slots are
// deliberately not part of it: shift-clicking in a player inventory moves items
// between the rows and the hotbar, it does not stuff the 2x2 grid.
bool isTransferSection(MenuSlot::Role role) {
switch (role) {
case MenuSlot::Role::Storage:
case MenuSlot::Role::FuelInput:
case MenuSlot::Role::SmeltInput:
case MenuSlot::Role::SmeltOutput:
case MenuSlot::Role::BrewBottle:
case MenuSlot::Role::BrewIngredient:
case MenuSlot::Role::EnchantItem:
case MenuSlot::Role::EnchantLapis:
case MenuSlot::Role::AnvilInput:
case MenuSlot::Role::AnvilResult:
case MenuSlot::Role::TradeInput:
case MenuSlot::Role::TradeResult:
return true;
case MenuSlot::Role::Inventory:
case MenuSlot::Role::Hotbar:
case MenuSlot::Role::OffHand:
case MenuSlot::Role::CraftInput:
case MenuSlot::Role::CraftResult:
case MenuSlot::Role::Count:
return false;
}
return false;
}
std::vector<int> collectRoles(const ContainerMenu& menu, bool wantTransferSection) {
std::vector<int> found;
for (int i = 0; i < menu.slotCount(); ++i) {
if (isTransferSection(menu.slot(i).role) == wantTransferSection) found.push_back(i);
}
return found;
}
// Where a shift-click from `index` should land. Container half to player half
// and back; in a menu that has no container half, the player's rows and the
// hotbar swap roles instead.
std::vector<int> transferTargetsFor(const ContainerMenu& menu, int index) {
std::vector<int> container = collectRoles(menu, true);
MenuSlot::Role role = menu.slot(index).role;
if (isTransferSection(role)) return menu.playerSectionSlots();
if (!container.empty()) return container;
std::vector<int> targets;
MenuSlot::Role wanted =
role == MenuSlot::Role::Hotbar ? MenuSlot::Role::Inventory : MenuSlot::Role::Hotbar;
for (int i = 0; i < menu.slotCount(); ++i) {
if (menu.slot(i).role == wanted) targets.push_back(i);
}
return targets;
}
bool isResultSlot(const ContainerMenu& menu, int index) {
return menu.layout().resultSlot >= 0 && index == menu.layout().resultSlot;
}
void touchCraftInput(ContainerMenu& menu, int index) {
if (menu.slot(index).role == MenuSlot::Role::CraftInput) menu.refreshResult();
}
// Every removal from a slot goes through here so the crafting grid can never
// hand out a result without paying for it.
MetaStack takeFromSlot(ContainerMenu& menu, int index, int amount, SlotActionResult& out) {
if (isResultSlot(menu, index)) {
CraftingView view = menu.crafting();
MetaStack crafted = view.takeResult();
if (!crafted.empty()) {
out.craftedResult = true;
out.craftedCount += crafted.count;
}
return crafted;
}
MetaStack taken = menu.takeFrom(index, amount);
touchCraftInput(menu, index);
return taken;
}
int putIntoSlot(ContainerMenu& menu, int index, MetaStack& incoming) {
int moved = menu.insertAt(index, incoming);
if (moved > 0) touchCraftInput(menu, index);
return moved;
}
bool swapWithSlot(ContainerMenu& menu, int index, CursorStack& cursor) {
MenuSlot& target = menu.slot(index);
if (target.takeOnly) return false;
if (!menu.canPlace(index, cursor.held)) return false;
if (target.cell.limitFor(cursor.held) < cursor.held.count) return false;
cursor.held = target.cell.swapWith(cursor.held);
touchCraftInput(menu, index);
return true;
}
SlotActionResult leftClick(ContainerMenu& menu, CursorStack& cursor, const SlotAction& action) {
SlotActionResult out;
int index = action.slotIndex;
if (!menu.validIndex(index)) return out;
if (cursor.empty()) {
MetaStack taken = takeFromSlot(menu, index, menu.stackAt(index).count, out);
if (taken.empty()) return out;
out.moved = taken.count;
cursor.held = taken;
out.cursorChanged = true;
out.applied = true;
return out;
}
if (isResultSlot(menu, index)) {
const MetaStack& pending = menu.stackAt(index);
if (pending.empty() || !stacksMergeable(cursor.held, pending)) return out;
if (cursor.held.room() < pending.count) return out;
MetaStack crafted = takeFromSlot(menu, index, pending.count, out);
out.moved = crafted.count;
transferAll(crafted, cursor.held);
out.cursorChanged = true;
out.applied = true;
return out;
}
int moved = putIntoSlot(menu, index, cursor.held);
if (moved > 0) {
out.moved = moved;
out.cursorChanged = true;
out.applied = true;
return out;
}
if (menu.stackAt(index).empty() || stacksMergeable(menu.stackAt(index), cursor.held)) return out;
if (!swapWithSlot(menu, index, cursor)) return out;
out.moved = cursor.held.count + menu.stackAt(index).count;
out.cursorChanged = true;
out.applied = true;
return out;
}
SlotActionResult rightClick(ContainerMenu& menu, CursorStack& cursor, const SlotAction& action) {
SlotActionResult out;
int index = action.slotIndex;
if (!menu.validIndex(index)) return out;
if (cursor.empty()) {
if (menu.stackAt(index).empty()) return out;
int half = (menu.stackAt(index).count + 1) / 2;
MetaStack taken = takeFromSlot(menu, index, half, out);
if (taken.empty()) return out;
out.moved = taken.count;
cursor.held = taken;
out.cursorChanged = true;
out.applied = true;
return out;
}
if (isResultSlot(menu, index)) return out;
MetaStack single = splitOff(cursor.held, 1);
int moved = putIntoSlot(menu, index, single);
if (moved == 0) {
transferAll(single, cursor.held);
return out;
}
out.moved = moved;
out.cursorChanged = true;
out.applied = true;
return out;
}
SlotActionResult shiftClick(ContainerMenu& menu, const SlotAction& action) {
SlotActionResult out;
int index = action.slotIndex;
if (!menu.validIndex(index) || menu.stackAt(index).empty()) return out;
if (isResultSlot(menu, index)) {
CraftingView view = menu.crafting();
int rounds = view.craftAll(view.maxCrafts());
if (rounds <= 0) return out;
out.craftedResult = true;
out.craftedCount = rounds;
out.applied = true;
return out;
}
std::vector<int> targets = transferTargetsFor(menu, index);
MetaStack moving = menu.slot(index).cell.takeAll();
int moved = menu.distributeInto(targets, moving);
if (!moving.empty()) {
// Whatever did not fit goes straight back where it came from.
menu.slot(index).cell.insert(moving);
}
touchCraftInput(menu, index);
if (moved == 0) return out;
out.moved = moved;
out.applied = true;
return out;
}
SlotActionResult hotbarSwap(ContainerMenu& menu, const SlotAction& action) {
SlotActionResult out;
int index = action.slotIndex;
const MenuLayout& layout = menu.layout();
if (!menu.validIndex(index) || layout.hotbarFirst < 0) return out;
if (action.hotbarButton < 0 || action.hotbarButton >= layout.hotbarCount) return out;
int hotbarIndex = layout.hotbarFirst + action.hotbarButton;
if (hotbarIndex == index) return out;
if (isResultSlot(menu, index)) {
if (!menu.stackAt(hotbarIndex).empty()) return out;
MetaStack crafted = takeFromSlot(menu, index, menu.stackAt(index).count, out);
if (crafted.empty()) return out;
out.moved = crafted.count;
menu.slot(hotbarIndex).cell.stack = crafted;
out.applied = true;
return out;
}
MenuSlot& source = menu.slot(index);
MenuSlot& hotbar = menu.slot(hotbarIndex);
if (source.takeOnly) return out;
if (source.cell.stack.empty() && hotbar.cell.stack.empty()) return out;
if (!hotbar.cell.stack.empty()) {
if (!menu.canPlace(index, hotbar.cell.stack)) return out;
if (source.cell.limitFor(hotbar.cell.stack) < hotbar.cell.stack.count) return out;
}
out.moved = source.cell.stack.count + hotbar.cell.stack.count;
MetaStack carried = source.cell.stack;
source.cell.stack = hotbar.cell.stack;
hotbar.cell.stack = carried;
touchCraftInput(menu, index);
out.applied = true;
return out;
}
SlotActionResult middleClick(ContainerMenu& menu, CursorStack& cursor, const SlotAction& action) {
SlotActionResult out;
int index = action.slotIndex;
if (!menu.validIndex(index) || !menu.creativeMode()) return out;
if (!cursor.empty() || menu.stackAt(index).empty()) return out;
MetaStack clone = menu.stackAt(index);
clone.count = clone.maxCount();
out.created = clone.count;
cursor.held = clone;
out.cursorChanged = true;
out.applied = true;
return out;
}
SlotActionResult doubleClick(ContainerMenu& menu, CursorStack& cursor) {
SlotActionResult out;
if (cursor.empty() || cursor.held.room() == 0) return out;
// Partial stacks first: a double click is meant to tidy up leftovers before
// it starts breaking apart full stacks.
for (int pass = 0; pass < 2; ++pass) {
for (int i = 0; i < menu.slotCount(); ++i) {
if (cursor.held.room() == 0) break;
MenuSlot& source = menu.slot(i);
if (source.takeOnly || source.cell.stack.empty()) continue;
if (!stacksMergeable(source.cell.stack, cursor.held)) continue;
bool partial = source.cell.stack.count < source.cell.stack.maxCount();
if ((pass == 0) != partial) continue;
int before = cursor.held.count;
transferAll(source.cell.stack, cursor.held);
int gained = cursor.held.count - before;
if (gained > 0) {
out.moved += gained;
touchCraftInput(menu, i);
}
}
}
if (out.moved == 0) return out;
out.cursorChanged = true;
out.applied = true;
return out;
}
SlotActionResult dragStart(ContainerMenu& menu, CursorStack& cursor, DragState& drag,
const SlotAction& action, bool rightButton) {
SlotActionResult out;
if (cursor.empty()) return out;
drag.reset();
drag.active = true;
drag.rightButton = rightButton;
drag.payload = cursor.held;
if (menu.validIndex(action.slotIndex) && menu.canPlace(action.slotIndex, cursor.held)) {
drag.paintedSlots.push_back(action.slotIndex);
}
out.applied = true;
return out;
}
SlotActionResult dragAdd(ContainerMenu& menu, CursorStack& cursor, DragState& drag,
const SlotAction& action, bool rightButton) {
SlotActionResult out;
if (!drag.active || drag.rightButton != rightButton) return out;
int index = action.slotIndex;
if (!menu.validIndex(index) || drag.alreadyPainted(index)) return out;
if (!menu.canPlace(index, cursor.held)) return out;
const MetaStack& existing = menu.stackAt(index);
if (!existing.empty() && !stacksMergeable(existing, cursor.held)) return out;
drag.paintedSlots.push_back(index);
out.applied = true;
return out;
}
SlotActionResult dragEnd(ContainerMenu& menu, CursorStack& cursor, DragState& drag,
bool rightButton) {
SlotActionResult out;
if (!drag.active || drag.rightButton != rightButton) return out;
int painted = static_cast<int>(drag.paintedSlots.size());
if (painted == 0 || cursor.empty()) {
drag.reset();
out.applied = true;
return out;
}
int share = rightButton ? 1 : drag.payload.count / painted;
if (share > 0) {
for (int index : drag.paintedSlots) {
if (cursor.empty()) break;
MetaStack portion = splitOff(cursor.held, share);
int moved = putIntoSlot(menu, index, portion);
out.moved += moved;
if (!portion.empty()) transferAll(portion, cursor.held);
}
}
drag.reset();
out.cursorChanged = out.moved > 0;
out.applied = true;
return out;
}
SlotActionResult dropAction(ContainerMenu& menu, CursorStack& cursor, const SlotAction& action,
bool wholeStack) {
SlotActionResult out;
if (!cursor.empty()) {
if (menu.validIndex(action.slotIndex)) return out;
MetaStack thrown = wholeStack ? splitOff(cursor.held, cursor.held.count)
: splitOff(cursor.held, 1);
out.noteDrop(thrown);
out.cursorChanged = true;
out.applied = true;
return out;
}
if (!menu.validIndex(action.slotIndex)) return out;
int index = action.slotIndex;
if (menu.stackAt(index).empty()) return out;
int amount = wholeStack ? menu.stackAt(index).count : 1;
// A result slot always hands over a whole craft: there is no such thing as
// crafting one third of a recipe, so Q on a result drops the whole batch.
MetaStack thrown = takeFromSlot(menu, index, amount, out);
if (thrown.empty()) return out;
out.noteDrop(thrown);
out.applied = true;
return out;
}
} // namespace
const char* clickKindName(ClickKind kind) {
switch (kind) {
case ClickKind::Left: return "left";
case ClickKind::Right: return "right";
case ClickKind::ShiftLeft: return "shiftLeft";
case ClickKind::ShiftRight: return "shiftRight";
case ClickKind::HotbarSwap: return "hotbarSwap";
case ClickKind::Middle: return "middle";
case ClickKind::Double: return "double";
case ClickKind::DragLeftStart: return "dragLeftStart";
case ClickKind::DragLeftAdd: return "dragLeftAdd";
case ClickKind::DragLeftEnd: return "dragLeftEnd";
case ClickKind::DragRightStart: return "dragRightStart";
case ClickKind::DragRightAdd: return "dragRightAdd";
case ClickKind::DragRightEnd: return "dragRightEnd";
case ClickKind::DropOne: return "dropOne";
case ClickKind::DropStack: return "dropStack";
case ClickKind::Count: break;
}
return "unknown";
}
void DragState::reset() {
active = false;
rightButton = false;
paintedSlots.clear();
payload.clear();
}
bool DragState::alreadyPainted(int index) const {
return std::find(paintedSlots.begin(), paintedSlots.end(), index) != paintedSlots.end();
}
SlotAction clickAt(ClickKind kind, int slotIndex) {
SlotAction action;
action.kind = kind;
action.slotIndex = slotIndex;
return action;
}
SlotAction hotbarSwapAt(int slotIndex, int hotbarButton) {
SlotAction action;
action.kind = ClickKind::HotbarSwap;
action.slotIndex = slotIndex;
action.hotbarButton = hotbarButton;
return action;
}
void SlotActionResult::noteDrop(const MetaStack& stack) {
if (stack.empty()) return;
dropped += stack.count;
droppedStacks.push_back(stack);
}
SlotActionResult applySlotAction(ContainerMenu& menu, CursorStack& cursor, DragState& drag,
const SlotAction& action) {
switch (action.kind) {
case ClickKind::Left: return leftClick(menu, cursor, action);
case ClickKind::Right: return rightClick(menu, cursor, action);
case ClickKind::ShiftLeft:
case ClickKind::ShiftRight: return shiftClick(menu, action);
case ClickKind::HotbarSwap: return hotbarSwap(menu, action);
case ClickKind::Middle: return middleClick(menu, cursor, action);
case ClickKind::Double: return doubleClick(menu, cursor);
case ClickKind::DragLeftStart: return dragStart(menu, cursor, drag, action, false);
case ClickKind::DragLeftAdd: return dragAdd(menu, cursor, drag, action, false);
case ClickKind::DragLeftEnd: return dragEnd(menu, cursor, drag, false);
case ClickKind::DragRightStart: return dragStart(menu, cursor, drag, action, true);
case ClickKind::DragRightAdd: return dragAdd(menu, cursor, drag, action, true);
case ClickKind::DragRightEnd: return dragEnd(menu, cursor, drag, true);
case ClickKind::DropOne: return dropAction(menu, cursor, action, false);
case ClickKind::DropStack: return dropAction(menu, cursor, action, true);
case ClickKind::Count: break;
}
return SlotActionResult{};
}
int totalAcrossMenuAndCursor(const ContainerMenu& menu, const CursorStack& cursor, ItemId item) {
int total = menu.totalOf(item);
if (!cursor.empty() && cursor.held.item == item) total += cursor.held.count;
return total;
}
int totalItemsAcrossMenuAndCursor(const ContainerMenu& menu, const CursorStack& cursor) {
return menu.totalItems() + cursor.held.count;
}
} // namespace vf