16 std::vector<InventoryChangeEvent>
events;
39bool tagsIntersect(
const std::vector<std::string> &
a,
const std::vector<std::string> &
b) {
40 for (
const auto &
t :
a) {
41 if (std::find(
b.begin(),
b.end(),
t) !=
b.end())
return true;
46bool propsEqual(
const std::unordered_map<std::string, std::string> &
a,
47 const std::unordered_map<std::string, std::string> &
b) {
53std::unordered_map<std::string, InventorySystem::AcceptFn> &InventorySystem::acceptRules() {
54 static std::unordered_map<std::string, AcceptFn>
t;
58std::unordered_map<std::string, InventorySystem::CapacityFn> &InventorySystem::capacityPolicies() {
59 static std::unordered_map<std::string, CapacityFn>
t;
63std::unordered_map<std::string, InventorySystem::StackFn> &InventorySystem::stackRules() {
64 static std::unordered_map<std::string, StackFn>
t;
68std::unordered_map<std::string, InventorySystem::ChangeHook> &InventorySystem::changeHooks() {
69 static std::unordered_map<std::string, ChangeHook>
t;
73std::vector<InventoryChangeEvent> &InventorySystem::eventQueue() {
74 static std::vector<InventoryChangeEvent>
q;
78int &InventorySystem::instanceCounter() {
83bool &InventorySystem::builtinsReady() {
84 static bool ready =
false;
88bool &InventorySystem::changeHooksSuppressed() {
89 static bool suppressed =
false;
94 if (
name.empty() || !
fn)
return;
95 acceptRules()[
name] = std::move(
fn);
102 return acceptRules().count(
name) > 0;
106 if (
name.empty() || !
fn)
return;
107 capacityPolicies()[
name] = std::move(
fn);
111 capacityPolicies().erase(
name);
116 return capacityPolicies().count(
name) > 0;
120 if (
name.empty() || !
fn)
return;
121 stackRules()[
name] = std::move(
fn);
128 return stackRules().count(
name) > 0;
132 if (
name.empty() || !
fn)
return;
133 changeHooks()[
name] = std::move(
fn);
139 return changeHooks().count(
name) > 0;
143 if (builtinsReady())
return;
144 builtinsReady() =
true;
151 std::string *reason) {
153 if (reason) *reason =
"rejected_tag";
157 if (reason) *reason =
"accept_tag_mismatch";
163 auto slotsOk = [](
const Bag &bag,
const ItemDefinition &def,
int quantity, std::string *reason) {
165 for (
int i = 0; i < bag.
getSlotCount() && need > 0; ++i) {
166 int space = freeSpaceInSlot(bag, i, def);
167 if (space > 0) need -= space;
170 if (reason) *reason =
"no_slot";
176 auto weightOk = [](
const Bag &bag,
const ItemDefinition &def,
int quantity, std::string *reason) {
178 float add = def.
weight * float(quantity);
180 if (reason) *reason =
"over_weight";
186 auto volumeOk = [](
const Bag &bag,
const ItemDefinition &def,
int quantity, std::string *reason) {
188 float add = def.
volume * float(quantity);
190 if (reason) *reason =
"over_volume";
203 int quantity, std::string *reason) {
204 return slotsOk(bag, def, quantity, reason) && weightOk(bag, def, quantity, reason);
208 int quantity, std::string *reason) {
209 return slotsOk(bag, def, quantity, reason) &&
210 weightOk(bag, def, quantity, reason) &&
211 volumeOk(bag, def, quantity, reason);
216 return !
a.empty() && !
b.empty() &&
a.itemId ==
b.itemId;
220 return !
a.empty() && !
b.empty() &&
a.itemId ==
b.itemId &&
221 propsEqual(
a.props,
b.props) &&
222 std::fabs(
a.durability -
b.durability) < 1e-6f;
231void InventorySystem::ensureNextInstanceIdAbove(
int usedInstanceId)
noexcept {
232 if (instanceCounter() <= usedInstanceId) instanceCounter() = usedInstanceId + 1;
235std::unique_ptr<Bag> InventorySystem::cloneBag(
const Bag &
source) {
236 auto clone = std::make_unique<Bag>(
source.getSlotCount());
238 clone->kind_ =
source.kind_;
239 clone->maxWeight_ =
source.maxWeight_;
240 clone->maxVolume_ =
source.maxVolume_;
241 clone->acceptRule_ =
source.acceptRule_;
242 clone->capacityPolicy_ =
source.capacityPolicy_;
243 clone->stackRule_ =
source.stackRule_;
244 clone->acceptTags_ =
source.acceptTags_;
245 clone->rejectTags_ =
source.rejectTags_;
246 clone->slots_ =
source.slots_;
247 clone->extra_ =
source.extra_;
251bool InventorySystem::bagsEqual(
const Bag &
target,
const Bag &baseline) {
252 if (
target.id_ != baseline.id_ ||
target.kind_ != baseline.kind_ ||
253 target.maxWeight_ != baseline.maxWeight_ ||
target.maxVolume_ != baseline.maxVolume_ ||
254 target.acceptRule_ != baseline.acceptRule_ ||
255 target.capacityPolicy_ != baseline.capacityPolicy_ ||
target.stackRule_ != baseline.stackRule_ ||
256 target.acceptTags_ != baseline.acceptTags_ ||
target.rejectTags_ != baseline.rejectTags_ ||
257 target.extra_ != baseline.extra_ ||
target.slots_.size() != baseline.slots_.size())
259 for (std::size_t i = 0; i <
target.slots_.size(); ++i) {
260 const auto &
a =
target.slots_[i];
261 const auto &
b = baseline.slots_[i];
262 if (
a.instanceId !=
b.instanceId ||
a.itemId !=
b.itemId ||
a.quantity !=
b.quantity ||
263 a.durability !=
b.durability ||
a.props !=
b.props ||
a.tags !=
b.tags)
269void InventorySystem::emit(InventoryChangeEvent ev) {
270 if (!changeHooksSuppressed()) {
271 for (
auto &kv : changeHooks()) {
272 if (!kv.second)
continue;
282 eventQueue().push_back(std::move(ev));
289 eventQueue().clear();
300 if (it == stackRules().
end() || !it->second) {
301 return !
a.empty() && !
b.empty() &&
a.itemId ==
b.itemId;
303 return it->second(
a,
b, def);
306bool InventorySystem::checkAccept(
const Bag &bag,
const ItemDefinition &def,
int quantity,
307 std::string *reason) {
309 auto it = acceptRules().find(bag.getAcceptRule());
310 if (it == acceptRules().
end() || !it->second) {
311 if (reason) *reason =
"unknown_accept_rule";
314 return it->second(bag, def, quantity, reason);
317bool InventorySystem::checkCapacity(
const Bag &bag,
const ItemDefinition &def,
int quantity,
318 std::string *reason) {
320 auto it = capacityPolicies().find(bag.getCapacityPolicy());
321 if (it == capacityPolicies().
end() || !it->second) {
322 if (reason) *reason =
"unknown_capacity_policy";
325 return it->second(bag, def, quantity, reason);
328int InventorySystem::freeSpaceInSlot(
const Bag &bag,
int slot,
const ItemDefinition &def) {
329 if (slot < 0 || slot >= bag.getSlotCount())
return 0;
330 const auto &
s = bag.slots()[size_t(slot)];
331 int maxStack = def.maxStack > 0 ? def.maxStack : 1;
336 probeA.itemId = def.id;
338 ItemStack probeB = probeA;
339 probeB.instanceId = 1;
340 if (!canStackTogether(bag, probeA, probeB, def)) maxStack = 1;
342 if (
s.empty())
return maxStack;
344 probe.itemId = def.id;
346 if (!canStackTogether(bag,
s, probe, def))
return 0;
347 return std::max(0, maxStack -
s.quantity);
351 std::string *reason) {
352 if (!bag || quantity <= 0) {
353 if (reason) *reason =
"invalid_args";
358 if (reason) *reason =
"unknown_item";
361 if (!checkAccept(*bag, *def, quantity, reason))
return false;
362 if (!checkCapacity(*bag, *def, quantity, reason))
return false;
367 return addItemImpl(bag,
itemId, quantity,
true);
370int InventorySystem::addItemImpl(
Bag *bag,
const std::string &
itemId,
int quantity,
372 if (!bag || quantity <= 0)
return 0;
376 if (!checkAccept(*bag, *def, 1,
nullptr))
return 0;
378 int remaining = quantity;
381 auto maxFittable = [&](
int space) ->
int {
382 int lo = 0, hi = std::min(space, remaining);
384 int mid = lo + (hi - lo + 1) / 2;
385 if (checkCapacity(*bag, *def,
mid,
nullptr))
394 for (
int i = 0; i < bag->
getSlotCount() && remaining > 0; ++i) {
395 int space = freeSpaceInSlot(*bag, i, *def);
396 auto &
s = bag->
slots()[size_t(i)];
397 if (
s.empty() || space <= 0)
continue;
398 int put = maxFittable(space);
399 if (put <= 0)
continue;
406 for (
int i = 0; i < bag->
getSlotCount() && remaining > 0; ++i) {
407 auto &
s = bag->
slots()[size_t(i)];
408 if (!
s.empty())
continue;
409 int space = freeSpaceInSlot(*bag, i, *def);
410 int put = maxFittable(space);
411 if (put <= 0)
continue;
422 if (publish && added > 0) {
423 InventoryChangeEvent ev;
425 ev.bagId = bag->
getId();
443 prepared.state_ = std::make_unique<PreparedInventoryAdd::State>();
444 prepared.state_->target = bag;
445 prepared.state_->baseline = cloneBag(*bag);
446 prepared.state_->candidate = cloneBag(*bag);
447 prepared.state_->events.reserve(grants.size());
449 const auto &grant = grants[
index];
450 if (grant.itemId.empty() || grant.quantity <= 0)
453 "grants[" + std::to_string(
index) +
"]"));
454 const int added = addItemImpl(prepared.state_->candidate.get(), grant.itemId, grant.quantity,
false);
455 if (added != grant.quantity)
460 :
"inventory reward references an unknown item",
461 "grants[" + std::to_string(
index) +
"].itemId", {{
"itemId", grant.itemId}},
462 "inventory.prepare-add-batch"));
463 if (prepared.state_->totalAdded > std::numeric_limits<int>::max() - added)
466 prepared.state_->totalAdded += added;
469 event.bagId = bag->
getId();
470 event.itemId = grant.itemId;
471 event.quantity = added;
472 prepared.state_->events.push_back(std::move(event));
479 if (!prepared.state_ || !prepared.state_->target || !prepared.state_->baseline ||
480 !prepared.state_->candidate)
484 const Bag &baseline = *prepared.state_->baseline;
485 if (!bagsEqual(
target, baseline))
489 target.slots_.swap(prepared.state_->candidate->slots_);
490 const int totalAdded = prepared.state_->totalAdded;
491 for (
auto &event : prepared.state_->events) emit(std::move(event));
492 prepared.state_.reset();
498 if (!bag ||
itemId.empty() || quantity <= 0)
501 "inventory removal requires a Bag, item id, and positive quantity",
"removal"));
505 {{
"itemId",
itemId}},
"inventory.prepare-remove"));
509 "inventory does not contain the complete removal quantity",
"quantity",
510 {{
"itemId",
itemId}},
"inventory.prepare-remove"));
513 prepared.state_ = std::make_unique<PreparedInventoryRemove::State>();
514 prepared.state_->target = bag;
515 prepared.state_->baseline = cloneBag(*bag);
516 prepared.state_->candidate = cloneBag(*bag);
517 int remaining = quantity;
518 for (
auto &stack : prepared.state_->candidate->slots_) {
519 if (remaining <= 0)
break;
520 if (stack.empty() || stack.itemId !=
itemId)
continue;
521 const int take = std::min(stack.quantity, remaining);
522 stack.quantity -= take;
524 if (stack.quantity <= 0) stack.clear();
526 prepared.state_->totalRemoved = quantity;
527 prepared.state_->event.action =
"remove";
528 prepared.state_->event.bagId = bag->
getId();
529 prepared.state_->event.itemId =
itemId;
530 prepared.state_->event.quantity = quantity;
536 if (!prepared.state_ || !prepared.state_->target || !prepared.state_->baseline ||
537 !prepared.state_->candidate)
541 if (!bagsEqual(
target, *prepared.state_->baseline))
544 target.slots_.swap(prepared.state_->candidate->slots_);
545 const int removed = prepared.state_->totalRemoved;
546 emit(std::move(prepared.state_->event));
547 prepared.state_.reset();
552 if (!bag || quantity <= 0 ||
itemId.empty())
return 0;
553 int remaining = quantity;
555 for (
int i = 0; i < bag->
getSlotCount() && remaining > 0; ++i) {
556 auto &
s = bag->
slots()[size_t(i)];
557 if (
s.empty() ||
s.itemId !=
itemId)
continue;
558 int take = std::min(
s.quantity, remaining);
562 if (
s.quantity <= 0)
s.clear();
576 if (!bag || slot < 0 || slot >= bag->
getSlotCount() || quantity <= 0)
return 0;
577 auto &
s = bag->
slots()[size_t(slot)];
578 if (
s.empty())
return 0;
579 int take = std::min(
s.quantity, quantity);
582 if (
s.quantity <= 0)
s.clear();
594 if (!bag || slotA < 0 || slotB < 0 || slotA >= bag->
getSlotCount() ||
597 std::swap(bag->
slots()[
size_t(slotA)], bag->
slots()[
size_t(slotB)]);
608 if (!bag || fromSlot < 0 || toSlot < 0 || fromSlot >= bag->
getSlotCount() ||
611 auto &
from = bag->
slots()[size_t(fromSlot)];
612 auto &
to = bag->
slots()[size_t(toSlot)];
613 if (
from.empty())
return false;
630 if (!def)
return false;
631 if (canStackTogether(*bag,
from,
to, *def)) {
633 int space = maxStack -
to.quantity;
634 if (space <= 0)
return false;
635 int put = std::min(space,
from.quantity);
637 from.quantity -= put;
638 if (
from.quantity <= 0)
from.clear();
653 if (!bag || slot < 0 || toSlot < 0 || slot >= bag->
getSlotCount() ||
654 toSlot >= bag->
getSlotCount() || slot == toSlot || quantity <= 0)
657 auto &
to = bag->
slots()[size_t(toSlot)];
658 if (
from.empty() || !
to.empty())
return false;
659 if (quantity >=
from.quantity)
return false;
663 to.quantity = quantity;
664 from.quantity -= quantity;
678 if (!
from || !
to || quantity <= 0 ||
itemId.empty())
return 0;
680 int want = std::min(available, quantity);
681 if (want <= 0)
return 0;
683 for (
int n = want;
n >= 1; --
n) {
688 for (
int i = 0; i <
from->getSlotCount() && remaining > 0; ++i) {
689 auto &
s =
from->slots()[size_t(i)];
690 if (
s.empty() ||
s.itemId !=
itemId)
continue;
691 int take = std::min(
s.quantity, remaining);
694 if (
s.quantity <= 0)
s.clear();
700 int needRestore =
n - added;
718 if (!
from || !
to || fromSlot < 0 || fromSlot >=
from->getSlotCount() || quantity <= 0)
720 auto &
s =
from->slots()[size_t(fromSlot)];
721 if (
s.empty())
return 0;
722 int want = std::min(
s.quantity, quantity);
725 for (
int n = want;
n >= 1; --
n) {
729 if (
n ==
s.quantity) {
731 for (
int i = 0; i <
to->getSlotCount(); ++i) {
732 if (
to->slots()[
size_t(i)].empty()) {
738 bool canPlaceWhole = empty >= 0 && def !=
nullptr;
742 for (
int i = 0; i <
to->getSlotCount(); ++i) {
743 auto &ts =
to->slots()[size_t(i)];
744 if (ts.empty())
continue;
745 if (!canStackTogether(*
to,
s, ts, *def))
continue;
747 int space = maxStack - ts.quantity;
748 if (space <
n)
continue;
749 if (!checkCapacity(*
to, *def,
n,
nullptr))
continue;
764 if (merged)
return n;
766 if (checkCapacity(*
to, *def,
n,
nullptr) && checkAccept(*
to, *def,
n,
nullptr)) {
767 to->slots()[size_t(empty)] =
s;
784 if (
s.quantity <= 0)
s.clear();
788 auto &fs =
from->slots()[size_t(fromSlot)];
791 fs.quantity =
n - added;
793 }
else if (fs.itemId ==
itemId) {
794 fs.quantity += (
n - added);
817 for (
const auto &
s : bag->
slots()) {
818 if (!
s.empty() &&
s.itemId ==
itemId)
n +=
s.quantity;
826 const auto &
s = bag->
slots()[size_t(i)];
827 if (!
s.empty() &&
s.itemId ==
itemId)
return i;
833 if (!bag ||
tag.empty())
return -1;
835 const auto &
s = bag->
slots()[size_t(i)];
836 if (
s.empty())
continue;
837 if (
s.hasTag(
tag))
return i;
845 if (!bag)
return 0.f;
847 for (
const auto &
s : bag->
slots()) {
848 if (
s.empty())
continue;
850 if (def)
w += def->
weight * float(
s.quantity);
856 if (!bag)
return 0.f;
858 for (
const auto &
s : bag->
slots()) {
859 if (
s.empty())
continue;
861 if (def)
v += def->
volume * float(
s.quantity);
869 for (
const auto &
s : bag->
slots()) {
877 for (
auto &
s : bag->
slots())
s.clear();
887 if (!eq || !bag)
return false;
889 if (!eq->
canEquipFromBag(bag, bagSlot, equipSlot, &reason))
return false;
891 auto &bs = bag->
slots()[size_t(bagSlot)];
896 auto *dest = eq->
stackAt(equipSlot);
899 bag->
slots()[size_t(bagSlot)] = moving;
904 if (!dest->empty()) {
905 bag->
slots()[size_t(bagSlot)] = *dest;
921 if (!eq || !bag)
return false;
922 auto *src = eq->
stackAt(equipSlot);
923 if (!src || src->empty())
return false;
928 if (bag->
slots()[
size_t(i)].empty()) {
935 if (!
canAdd(bag, src->itemId, src->quantity,
nullptr))
return false;
937 int qty = src->quantity;
943 src->quantity = qty - added;
956 bag->
slots()[size_t(empty)] = *src;
957 std::string
itemId = src->itemId;
958 int qty = src->quantity;
std::array< double, 10 > q
HexCoordinates to
Cell the unit walks towards on this segment.
const UnitySourceAsset & source
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
std::string getId() const
容器 id(变更事件定位用)。
float getMaxVolume() const
Returns the max volume.
const std::vector< ItemStack > & slots() const
Slots.
const std::vector< std::string > & rejectTags() const
Reject tags.
float getMaxWeight() const
重量 / 体积上限(容量策略用)。
const std::vector< std::string > & acceptTags() const
Accept tags.
std::string getStackRule() const
Returns the stack rule.
int getSlotCount() const
格数。
EVENGINE_API_FOUNDATION public API.
const ItemStack * stackAt(const std::string &slotName) const
Stack at.
bool canEquipFromBag(Bag *bag, int bagSlot, const std::string &equipSlot, std::string *reason=nullptr) const
Can equip from bag.
static void unregisterStackRule(const std::string &name)
Unregisters stack rule.
static int addItem(Bag *bag, const std::string &itemId, int quantity)
返回实际放入数量(可能部分成功)。
static void registerStackRule(const std::string &name, StackFn fn)
Registers stack rule.
static bool equipFromBag(EquipmentSet *eq, const std::string &equipSlot, Bag *bag, int bagSlot)
Equip from bag.
static void ensureBuiltins()
确保内置规则已注册(模块首次使用时自动调用)。
static const std::vector< InventoryChangeEvent > & events()
Events.
static bool unequipToBag(EquipmentSet *eq, const std::string &equipSlot, Bag *bag)
Unequip to bag.
static bool canAdd(Bag *bag, const std::string &itemId, int quantity, std::string *reason=nullptr)
Can add.
static bool swapSlots(Bag *bag, int slotA, int slotB)
Swap slots.
static float usedWeight(const Bag *bag)
Used weight.
static bool hasChangeHook(const std::string &name)
True when change hook.
static eve::Result< int > commitRemove(PreparedInventoryRemove prepared)
Commit a prepared removal and publish its event after the no-fail slot swap.
std::function< void(const InventoryChangeEvent &ev)> ChangeHook
static void registerCapacityPolicy(const std::string &name, CapacityFn fn)
Registers capacity policy.
static float usedVolume(const Bag *bag)
Used volume.
std::function< bool(const Bag &bag, const ItemDefinition &def, int quantity, std::string *reason)> CapacityFn
static eve::Result< int > commitAddBatch(PreparedInventoryAdd prepared)
Commit one prepared addition and then publish its change events.
static void unregisterCapacityPolicy(const std::string &name)
Unregisters capacity policy.
static int nextInstanceId()
Next instance id.
static void pushEvent(InventoryChangeEvent ev)
Pushes event.
static eve::Result< PreparedInventoryAdd > prepareAddBatch(Bag *bag, const std::vector< InventoryItemGrant > &grants)
Validate a multi-item addition against a private candidate Bag.
static bool splitStack(Bag *bag, int slot, int quantity, int toSlot)
Split stack.
std::function< bool(const Bag &bag, const ItemDefinition &def, int quantity, std::string *reason)> AcceptFn
static void registerAcceptRule(const std::string &name, AcceptFn fn)
Registers accept rule.
static int countItem(const Bag *bag, const std::string &itemId)
Returns the number of item.
static void registerChangeHook(const std::string &name, ChangeHook fn)
Registers change hook.
static int usedSlotCount(const Bag *bag)
Used slot count.
static bool hasStackRule(const std::string &name)
True when stack rule.
static void clearBag(Bag *bag)
Clears bag.
static int transfer(Bag *from, Bag *to, const std::string &itemId, int quantity)
Transfer.
static bool hasAcceptRule(const std::string &name)
True when accept rule.
static void unregisterAcceptRule(const std::string &name)
Unregisters accept rule.
std::function< bool(const ItemStack &a, const ItemStack &b, const ItemDefinition &def)> StackFn
static void unregisterChangeHook(const std::string &name)
Unregisters change hook.
static int removeAt(Bag *bag, int slot, int quantity)
Removes at.
static int removeItem(Bag *bag, const std::string &itemId, int quantity)
static bool moveSlot(Bag *bag, int fromSlot, int toSlot)
Moves slot.
static bool hasCapacityPolicy(const std::string &name)
True when capacity policy.
static int transferSlot(Bag *from, int fromSlot, Bag *to, int quantity)
Transfer slot.
static int findItemByTag(const Bag *bag, const std::string &tag)
Finds item by tag.
static void clearEvents()
Clears events.
static void pollEvents(std::vector< InventoryChangeEvent > &out)
Polls events.
static eve::Result< PreparedInventoryRemove > prepareRemove(Bag *bag, const std::string &itemId, int quantity)
Prepare removal of one exact item quantity without mutation or events.
static int findItem(const Bag *bag, const std::string &itemId)
Finds item.
static const ItemDefinition * find(const std::string &id)
Finds find.
Move-only, validated inventory state prepared for one Bag.
PreparedInventoryAdd()
Prepared inventory add.
~PreparedInventoryAdd()
Prepared inventory add.
Move-only, validated inventory removal prepared for one Bag.
运行时容器:固定格数的背包 / 箱子 / 商店栏等。 行为由 InventorySystem 提供;本类暴露便于脚本绑定的薄封装方法。
SettlementPipeline::Stage fn
一次成功库存变更的事件(供脚本 poll / C++ hook)。
std::string action
add/remove/move/swap/split/merge/transfer/equip/unequip
物品模板(进程级注册表中的定义,不含运行时数量)。
std::vector< std::string > tags
bool hasTag(const std::string &tag) const
True when tag.
容器中的一格堆叠(空槽:itemId 为空或 quantity <= 0)。
void clear()
Clears clear.
std::unique_ptr< Bag > baseline
std::vector< InventoryChangeEvent > events
std::unique_ptr< Bag > candidate
std::unique_ptr< Bag > baseline
InventoryChangeEvent event
std::unique_ptr< Bag > candidate