Maintainer notes¶
Why it exists¶
Two independent systems now want to raise the same Pixelmon allowances: durable tickets
(skintickets) and the donor rank ladder. BoxCountCalculateEvent is a single event carrying a
single number, so two mods each doing event.setBoxCount(event.getBoxCount() + mine) either
double-count or overwrite each other depending on listener order, and neither outcome logs
anything. One owner collecting every claim and resolving it once is the only way that stays
correct as consumers are added.
The second reason is that the two allowances behave nothing alike, and a caller cannot tell:
| PC boxes | Day Care slots | |
|---|---|---|
| Stored as | recomputed from config + event on every PCStorage load |
persisted integer DayCareBoxCount in party NBT |
| Lowering it | DELETES the Pokemon above the new count at the next save | non-destructive; boxes stay in the save and stop passing hasBox |
| Re-raising it | boxes come back empty | boxes come back with their contents |
PCStorage.readFromNBT iterates i < this.boxes.length, and that array was sized by the
constructor from the number the event produced. Boxes past that index are never read, and
writeToNBT then persists only the boxes it has. Verified against the 9.3.16 decompile.
Any consumer reasoning about "the storage cap" as one idea gets one of those two wrong.
Writing a consumer¶
// once, in your @Mod constructor
StorageCapacity.register(myProvider);
// the provider answers per player, cheaply, on the server thread
public Contribution contribute(UUID player, CapacityType type) {
int owned = myDatabase.countFor(player, type);
return owned < 0 ? Contribution.unavailable(name()) // never guess 0 on an outage
: Contribution.extra(name(), owned);
}
That is the whole integration. The bridge hooks Pixelmon's event, reconciles on every login, and
applies changes to live storage. A consumer never touches PCStorage, PlayerDayCare or
BoxCountCalculateEvent.
After committing an entitlement, call StorageCapacity.applyLive(player, type) to have it take
effect immediately. It recomputes from every provider rather than adding to the current value, so
calling it twice cannot grant twice.
Floors and extras are not interchangeable¶
- Floor: an entitlement that LAPSES with whatever grants it. A donor rank. Floors from different providers resolve to the highest, never the sum, so rank 2 plus rank 3 is rank 3.
- Extra: something the player OWNS. A redeemed ticket. Extras add up.
The rule is min(cap, max(baseline, highestFloor) + sumOfExtras), deliberately the same rule GTS
uses for listing slots and durations.
Expressing a rank as an extra double-counts it on every upgrade. Expressing a purchase as a floor confiscates it on every downgrade, which is the chargeback path. Neither error is visible in a single reading.
Safety rules it enforces¶
- An unavailable provider never costs a player storage. A database outage that read as
0would look exactly like "not entitled", soContribution.unavailablepoisons the resolution and nothing shrinks. On the event path there is no way to ask what the player had (the storage being sized is the only record and it does not exist yet), so the fallback there is the configured ceiling: over-granting for one session is reversible, deleting Pokemon is not. - Nothing shrinks below occupancy. A lowered allowance removes only empty trailing boxes.
- A provider that throws is treated as unavailable, so one broken mod degrades instead of throwing through Pixelmon's PC construction.
- A ceiling below Pixelmon's own default is clamped up, so a misconfiguration cannot turn this mod into something that removes storage on sight.
A lowering needs a registered provider. With no providers the bridge has no opinion about anyone,
and a partial deployment would otherwise reset every bought Day Care slot on the first login
(CapacityResolver.mayLower, unit-tested). On the PC-construction path the fallback is the
ceiling, not the baseline: over-granting for one session can be undone, deleting Pokemon cannot.
There is no Pixelmon maximum¶
PlayerDayCare.setAllowedBoxes(int)has no clamp and no callers in Pixelmon. Pixelmon ships no way to raise the allowance, which is why this mod exists.DayCareUpgradeItemis an empty stub.default-day-care-amountonly sets the starting value for new players. A player with a savedDayCareBoxCountnever sees a change to it, so per-player writes are the only route.- The Day Care GUI has no cap either, but every box is sent on each update, so very large numbers have a cost. The GUI treats N as N boxes, which is the unit this mod uses.
Traps¶
- Listeners register on
Pixelmon.EVENT_BUS, not NeoForge's. A Pixelmon listener on the wrong bus is never called and never errors. - Reading Pixelmon's configured box count means posting the event we listen to (the field
behind
StorageConfig.getComputerBoxesis private).StorageCapacitysets a thread-local so the listener stands aside for that one call; without it the value comes back with our own contributions already in it and every entitlement is counted twice. The thread-local is safe only becauseEVENT_BUS.postis synchronous on the calling thread. PlayerDayCare.hasBoxisboxId >= 0 && boxId <= allowedBoxes, so an allowance of N admits N+1 box ids, whilegetFirstEmptyBoxandisFulltreat N as the count. This mod works in Pixelmon's units and does not try to correct that.- Occupancy is probed with
getBox, nevergetOrCreateBox: the latter creates the box it is asked about, so using it to inspect occupancy would populate the day care as a side effect of reading it. - The whole storage surface this mod touches (
PCStorage,PlayerDayCare,BoxCountCalculateEvent,StorageConfig,StorageProxy) is byte-identical in the 9.3.16 and 9.4.0 decompiles, so one build serves both Pixelmon lines. - Mod-to-mod class access is direct; NeoForge mods share a classloader. That is not true of a
Bukkit plugin implementing a mod interface, which fails at
defineClassbeforeonEnable. Do not generalise from one to the other. BoxCountCalculateEventfires only from thePCStorage(UUID)constructor, so a rank change mid-session needsStorageCapacity.applyLive.- The listener is additive: it takes the value the event carries as its baseline, so it works with any other listener in either order.
- Inter-mod event order is not guaranteed (
ordering="AFTER"orders loading, not events), so the ceiling config loads lazily on first use.