Deliver purchases through L2Off
Queue items, skills, services, and Premium without writing live game tables
An L2Off delivery is a handoff to the running game server. The connector writes a queue job, your extender applies it to the live character, and the connector reports the server's final decision. Item delivery is required; the other paths are optional.
Player buys an item
→ connector inserts one queue row
→ portal shows the purchase as queued
→ extender reads the row for the player
→ extender applies the item to the live character
→ extender marks the row consumed or refused
→ connector settles the purchaseWhy direct game-table writes do not deliver
Cached.exe keeps loaded character and item state in memory. Inventory, skill,
mail, name, gender, or position writes made behind it can look correct in SQL
and remain invisible until a cache restart. Delivery must reach the live game
object through the extender.
Configure item and skill jobs
The generated profile uses one queue with kind = 0 for items and kind = 1
for skills. Each statement includes @commandId and a WHERE NOT EXISTS guard,
so replaying one purchase line cannot insert a second job.
[delivery.item]
window = "both"
sql = """
INSERT INTO portal_delivery (char_id, kind, target_id, amount, enchant, command_id)
SELECT @characterId, 0, @itemId, @count, @enchant, @commandId
WHERE NOT EXISTS (SELECT 1 FROM portal_delivery WHERE command_id = @commandId)
"""A bundle shares one order id but has one command id per grant. Deduplicate by
command id, never by order id. If your extender delivers items but not skills,
remove [delivery.skill]; leaving it present enables skill sales that cannot
arrive.
Settlement and expiry
consumed = 1 means the game server made a final decision. A null
refused_code means applied; a code means the server declined the job.
reported_at belongs to the connector and is stamped only after Forgeport
acknowledges the result.
An item can remain pending while inventory is full, a trade is open, or a
private store is active. Jobs older than [queue].expiryDays close as
DELIVERY_EXPIRED and release the player's coins. The default is 30 days.
Expiry protects players from jobs that can never finish
A bad item id can look like a temporarily full inventory to an extender. Do not set an excessive expiry while testing; it controls how long the player's coins remain held.
Character services and unstuck
Rename (kind = 3), gender change (kind = 2), and unstuck (kind = 4) use
the same queue only when your extender implements them. Rename should call the
pack's normal lin_ChangeCharacterName2 path so uniqueness and prohibited-name
rules remain authoritative. @gender is the final value: 0 male, 1 female.
Unstuck has no purchase command id. Its SQL must verify account ownership and
use the configured @locX, @locY, and @locZ. Remove any service section
that the extender does not consume.
Account Premium
[delivery.premium] is account-scoped and optional. Configure a read-only
status_sql, an explicit mode = "extend" or "replace", and delivery SQL
using both @account and @durationSeconds. The status read and declared mode
let checkout explain the result before charging.
Premium SQL and the connector's replay journal commit in one transaction.
extend should preserve remaining future time; replace starts a new period at
delivery. The mode describes your SQL—it does not rewrite it.
Verify
- Insert or purchase one low-value item for an online test character.
- Confirm the extender consumes the row and the item appears once.
- Repeat with the character offline, then log in.
- Confirm
reported_atis set after the order settles. - Test each optional job separately before enabling its portal feature.
See Extender integration for the queue schema and the three supported extender situations.