L2J

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:

ForgeportIntegration.java
.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 result

The handler prepares the game change; it must not call addItem(...) before returning Delivery.apply(...).

ForgeportIntegration.java
.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:

EnterWorld.java
Forgeport.playerOnline(player.getAccountName(), player.getObjectId());

Call this only after the player is fully present in the live world.

Verify

  1. In Admin, search the item picker for Adena and confirm item 57 appears.
  2. Run Connector check with a designated test character.
  3. Buy one low-value item with a test account.
  4. Confirm it appears once on the selected character.
  5. 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.

ForgeportIntegration.java
.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.

On this page