Guides

Delivering items on L2Off without touching the game tables

Why an SQL INSERT into a live L2Off world doesn't reach the player, and what does

If you run an L2Off (retail-binary) server and your donation panel writes a purchased item straight into the inventory or mail tables, there is a good chance it doesn't actually work — it just looks like it does until someone is online when the row lands.

Why the write disappears

Cached.exe builds its item registry in memory at startup, and keeps every loaded character's inventory there too. It never re-reads either table while it runs. That means no external write reaches a player on a live server — not the inventory table, not the skill table, and not mail.

Mail looks like the safe path because it doesn't touch a live inventory directly, but it isn't: an attachment is three coordinated rows (post, a user_item row with warehouse = 4, and the post_item_admin link), and the in-memory item registry still has no such object. On a High Five pack tested this way, 7 out of 7 mails inserted by SQL were listed by the client and could be neither opened nor deleted. Restarting CacheD made them work — which is not a window a running server has, and not something you can ask your players to wait for.

"The player was offline" doesn't save you either. Logging out is not the same event as being evicted from CacheD's cache, and the item registry itself was already built before your server opened for the day. There is no reliable moment where an SQL write is safe.

What actually works: a queue, not a write

The fix is to stop writing game state directly and hand the job to something running inside the game server process, which already holds the live objects:

  1. Insert a job into a queue table. Report the purchase as queued, not delivered — because it isn't yet.
  2. An extender running inside L2Server.exe polls that queue on a timer, for each online player.
  3. It grants the item or skill to the live player object, then marks the job consumed — clearing a refused_code column if it applied, or writing the reason if it declined.
  4. Whatever wrote the job reads consumed rows back and only then settles the purchase as delivered.

The distinction matters because a booking reported as delivered has to be undone later — you end up telling a player their item arrived while it's still sitting in a table waiting for them to log in. Done right, it reaches them within one poll interval if they're online, and immediately at login if it was waiting.

A queue table for this needs, at minimum: the character and item/skill to grant, a consumed flag, a refused_code the extender can write a reason into, and a way to dedupe — key it on the originating command, not the order, because a bundle purchase is several commands on one order and keying on the order silently drops every item after the first. Expire jobs nobody ever collects (a departed player, a mistyped item id) after a configurable window, and refund rather than hold the value forever.

What this costs you

An extender that already polls a delivery queue (a few common L2Off extender builds do). If yours doesn't, you need its source and roughly an evening to add polling, the two extra columns, and the expiry job. There's no way around modifying the extender — nothing outside the game server process can hand an item to a character who might be logged in right now.

This is the exact mechanism Forgeport's L2Off connector uses — a portal_delivery queue table with two NULL-safe stored procedures your extender calls. The full schema, the SQL, and every edge case (full inventory, open trade, a skill grant that fails permanently instead of retrying) are in PTS / L2Off servers.

On this page