A metin2 offline shop system lets a player's private market keep standing in the world even after that player logs out. In stock Metin2 the "sales shop" you open closes the moment you exit; the economy only turns while people are online. An offline shop changes that: the player sets up the stall, logs off, and the items keep selling. In this guide I walk through the three pillars of the system — database, game source (C++) and client — using the same logic I followed when building it on my own server, Runa2.
How an offline shop differs from a normal market
The classic private shop lives entirely in memory. While the character is in the world a CShop object is created, items are "locked" out of the player's inventory, and the shop is destroyed when the player leaves. An offline shop has three core differences:
- Persistence: the shop and its items are written to MySQL, so they are restored even if the game core restarts.
- Ownerless transactions: when a buyer purchases, the flow of gold and items must complete without the owner being online.
- Visual representation: the shop appears in the world as a stall/character, usually parked on a dedicated "offline shop" map.
That is why an offline shop is not a client mod but a source-level feature.
1. Database schema
We build the persistence layer first. Two tables are enough: one for the shop itself, one for the items inside it. Create them in the player database:
CREATE TABLE `offline_shop` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`owner_id` INT UNSIGNED NOT NULL,
`shop_name` VARCHAR(64) NOT NULL,
`map_index` INT NOT NULL,
`pos_x` INT NOT NULL,
`pos_y` INT NOT NULL,
`gold` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`created_at` INT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
KEY `owner_id` (`owner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `offline_shop_item` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`shop_id` INT UNSIGNED NOT NULL,
`item_id` BIGINT UNSIGNED NOT NULL,
`vnum` INT UNSIGNED NOT NULL,
`count` SMALLINT UNSIGNED NOT NULL,
`price` BIGINT UNSIGNED NOT NULL,
`display_pos` TINYINT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
KEY `shop_id` (`shop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Here item_id is the id of the row in the player's real item table. We do not copy the item; we "move" it from the owner into the shop. This is the single most important rule for preventing dupes: one item can never exist in both the inventory and the shop at the same time.
2. Source side: the COfflineShop class
In the game source you build a COfflineShopManager based on the existing CShop logic. The flow revolves around three functions:
- Create — the player opens the shop; the chosen items are removed from the inventory, written to
offline_shop_item, and a stall entity (mob) is spawned in the world. - Load — on game core startup all offline shop records are read and respawned on their map.
- Buy — a buyer purchases an item; the operation must be atomic.
The skeleton of the buy function looks like this:
bool COfflineShop::Buy(LPCHARACTER buyer, BYTE pos)
{
TShopItem* item = GetItem(pos);
if (!item || item->count == 0)
return false;
if (buyer->GetGold() < item->price)
{
buyer->ChatPacket(CHAT_TYPE_INFO, "Not enough yang.");
return false;
}
// 1) Deduct the buyer's gold
buyer->PointChange(POINT_GOLD, -item->price);
// 2) Give the item to the buyer (with the item_id moved from the owner)
LPITEM newItem = ITEM_MANAGER::instance().CreateItem(item->vnum, item->count);
buyer->AutoGiveItem(newItem);
// 3) Add the earnings to the shop's gold pool
m_dwGold += item->price;
// 4) Update MySQL (delete the sold row, write the gold)
RemoveItemFromDB(item->id);
UpdateGoldInDB();
return true;
}
The critical point is ordering: deduct gold first, then give the item, then write to the database. If any step fails you must roll the operation back; otherwise a server crash can cause loss of gold or items.
3. Network: new packets
Three new packet headers are defined between client and core: opening a shop, listing its contents, and buying. You add header constants in service.h and packet.h, then handle the packets in input_main.cpp. Keep the packet structure simple:
typedef struct SPacketCGOfflineShopBuy
{
BYTE header; // HEADER_CG_OFFLINE_SHOP_BUY
DWORD shop_vid; // the stall's VID
BYTE pos; // the purchased slot
} TPacketCGOfflineShopBuy;
When the server receives this packet it finds the matching COfflineShop object from the VID and calls the Buy() function above. Don't forget a distance check: if the buyer is not close enough to the stall, the request must be rejected.
4. Client side: the shop window and search
The client needs two things. First, a management UI to open the shop and add items (you add a new OfflineShopDialog to the Python UI files in root). Second, a search system that lets players find the item they want — this is the real value of an offline shop. Instead of walking past every stall, a player types a vnum or name and the server returns matching shops. This search can be solved with a dedicated packet or a web-based market listing.
Visually, gathering the stalls on a dedicated map protects performance: if hundreds of offline shops spawn in the main city, both visual clutter and packet traffic explode. Most servers use an NPC that teleports the player into an "offline shop zone".
Security and balance
- Dupe protection: an item must not be written to the shop before it leaves the inventory; both operations must sit inside the same transaction.
- Time limit: so shops do not stand forever, add an auto-close after a period (e.g. 7 days) based on
created_at. - Fee: charging a small yang fee to open a shop pulls money out of the economy and curbs inflation.
- Tax: taking 1-5% tax on each sale acts as a gold sink and gives the server its own revenue logic.
Frequently Asked Questions
Can an offline shop only be done with source?
In practice, yes. Persisting items, running ownerless transactions and respawning stalls all require C++ code in the game core. A purely client-based solution that is stable and dupe-proof is not possible.
Why move items instead of copying them?
If you copy, the same item exists in both the inventory and the shop, and the player can duplicate it by using both. Physically removing the item from the inventory and moving it into the shop eliminates that risk at the root.
Do hundreds of shops hurt performance?
If you gather stalls on a separate map, send packets by view range, and solve search on the server side, even thousands of shops are fine. The real load comes from spawning them all in the main city.
Want to add an offline shop system to your server? From the database schema to the source setup and the client UI, we can plan the whole process together. Get in touch with me and let's talk about your project.