Give purchased items to characters
Expose the item catalog and apply item purchases through pack APIs
Item support has two parts. The catalog lets an admin find "Adena" or a Blessed
Scroll while creating content. Delivery turns a completed purchase into the
pack's normal addItem(...) call. Both are required.
Expose the item catalog
Register a fresh stream of the item templates already loaded by the game
server. Put this inside the Bridge.builder() call in
ForgeportIntegration.java; add the java.util.Arrays and java.util.Objects
imports if the file does not already have them:
.items(() -> Arrays.stream(ItemData.getInstance().getTemplates())
.filter(Objects::nonNull)
.map(item -> new GameItem(item.getItemId(), item.getName())))Your code maps pack objects to GameItem. Forgeport normalizes the admin's
query, ranks exact/prefix/substring matches, sorts the result, removes duplicate
ids, applies the limit, and serializes the response.
Deliver an item
If a player buys 10 Blessed Scrolls of Escape, the flow is:
Purchase completes
→ Forgeport calls .itemDelivery(...) with the selected character and item
→ your code validates the live player, account, and item template
→ your code returns a prepared Delivery
→ Forgeport records APPLYING
→ the pack's addItem(...) runs
→ Forgeport records APPLIED and reports the resultThe handler prepares the game change; it must not call addItem(...) before
returning Delivery.apply(...).
.itemDelivery((target, item) -> {
Player player = World.getInstance()
.getPlayer(target.requiredCharacterId());
if (player == null)
return Delivery.offline();
if (!player.getAccountName().equalsIgnoreCase(target.account()))
return Delivery.invalidTarget();
if (ItemData.getInstance().getTemplate(item.itemId()) == null)
return Delivery.invalidItem();
return Delivery.apply(() -> grantItems(player, item));
})grantItems(...) is your pack-specific helper that calls the normal item API.
Preserve enchant level and create non-stackable quantities one unit at a time;
the complete examples show the exact handling required by their revisions.
Never mutate before returning Delivery.apply
Forgeport must write the delivery journal before the actual game change. An early mutation can be repeated after a reconnect and bypasses the safety guarantees of the connector core.
Offline characters
The reference handlers return Delivery.offline(). Forgeport holds the
delivery, and the explicit EnterWorld hook resumes it:
Forgeport.playerOnline(player.getAccountName(), player.getObjectId());Call this only after the player is fully present in the live world.
Verify
- In Admin, search the item picker for
Adenaand confirm item57appears. - Run Connector check with a designated test character.
- Buy one low-value item with a test account.
- Confirm it appears once on the selected character.
- Repeat once with the character offline, then log in and confirm delivery resumes.
For the exact journal state machine, see L2J architecture.
Enable marketplace escrow
Marketplace adds two registrations to the same bridge: a live inventory
snapshot and a prepared item withdrawal. Return every slot as an
InventoryItem; classify bound or attributed assets explicitly and use
INVENTORY, EQUIPPED, WAREHOUSE, or OTHER for the location. The core
normalizes location casing and treats unknown locations as OTHER.
.inventory((account, characterId) -> inventoryOf(account, characterId).stream()
.map(slot -> new InventoryItem(
slot.getObjectId(), slot.getItemId(), slot.getCount(), slot.isStackable(),
slot.getEnchantLevel(), slot.isEquipped(), slot.getLocation(),
slot.isTransferable()
? InventoryItem.Fidelity.TRANSFERABLE
: InventoryItem.Fidelity.BOUND,
slot.getIcon(), slot.getName()))
.toList())
.itemWithdrawal((target, item) -> {
Player player = World.getInstance().getPlayer(target.requiredCharacterId());
if (player == null)
return Withdrawal.offline();
if (!player.getAccountName().equalsIgnoreCase(target.account()))
return Withdrawal.characterNotOnAccount();
return Withdrawal.apply(() -> {
removeExactItem(player, item);
return new WithdrawalReceipt(
item.itemId(), item.count(), item.enchantLevel(), item.objectId());
});
})itemWithdrawal(...) is preparation, just like delivery: do not remove the
item before returning Withdrawal.apply(...). The core serializes grants and withdrawals
for the same account (including grants without a character target), records APPLYING, reads inventory before
and after the mutation, compares the actual delta and returned receipt, then
records APPLIED with the verified escrow evidence. Any ambiguous mutation is
stopped in reconciliation and is never blindly replayed.
This core lock covers connector commands only. Gameplay inventory changes must be coordinated by the integration; concurrent game activity during readback can still require reconciliation. Do not disable the independent inventory checks.
Inventory fidelity is part of the escrow proof
Do not mark an augmented, bound, timed, equipped, warehouse, or otherwise
non-fungible item as TRANSFERABLE. Marketplace accepts only an exact,
unequipped INVENTORY row and verifies that same classification after the
mutation.