A metin2 belt system adds a new equipment slot (the belt slot) to the player's gear window plus a separate small inventory (the belt inventory) that opens when a belt is equipped. You can drop consumables like potions or health flasks into the belt for quick access during play, and the number of usable cells grows as the belt is upgraded. In this guide I walk through how I get a belt working across three layers — item_proto, the server source and the client (Python UI).
Architecture of the belt system
The belt is not a single script; it works only when three layers stay in sync. They must all share the same constants (the slot range), otherwise items "vanish" or the client crashes:
- item_proto: defines the belt item's type, the slot it equips into, and how many cells it unlocks at each grade.
- Server source: a dedicated slot range for the belt inventory, item-placement validation, and persistence logic.
- Client (root/Python): the belt slot in the gear window and the grid UI that opens when a belt is worn.
In most open-source releases the system sits behind a compile flag, e.g. ENABLE_BELT_INVENTORY_SYSTEM. If you don't build both server and client with this define enabled, the packet layouts won't match and players will get disconnected on login.
Adding a belt item to item_proto
The belt is its own item type and equips into a dedicated gear slot. Three fields in the item_proto entry are critical: the type (ITEM_BELT), the wear flag (WEARABLE_BELT) and which slot it sits in (WEAR_BELT). The grade (how many cells it unlocks) is usually stored in a value field (value0):
-- if you edit the proto directly in MySQL the logic is:
-- type = ITEM_BELT (belt type)
-- subtype = 0
-- wearflags = WEARABLE_BELT (can be worn in the belt slot)
-- value0 = belt grade (1,2,3,4 → unlocked cell tier)
UPDATE item_proto
SET type = 'ITEM_BELT',
wearflags = 'WEARABLE_BELT',
value0 = 1 -- starting grade
WHERE vnum = 18000;
In Turkish/multi-source builds the fields can also be managed via item_names.txt and item_proto.txt. The key point: the belt's type is not weapon or armor but a separate belt type, so it fits the WEAR_BELT slot and doesn't occupy normal inventory. After adding the item, recompile the proto from item_proto.txt (or update the live table) and restart the game core.
Server source: the belt inventory slot range
The belt inventory lives in a separate address range from the normal inventory and equipment slots. In the source this range is defined with constants (file and names vary by source, but the logic is the same):
// in a header like service.h / item_length
#define BELT_INVENTORY_SLOT_START (INVENTORY_AND_EQUIP_SLOT_END)
#define BELT_INVENTORY_SLOT_COUNT 16
#define BELT_INVENTORY_SLOT_END (BELT_INVENTORY_SLOT_START + BELT_INVENTORY_SLOT_COUNT)
A helper function that recognises this range is used during item-placement validation. When the game core decides which cell an item may go into, it checks whether the slot falls inside the belt range:
bool CHARACTER::IsBeltInventorySlot(WORD wCell) const
{
return (wCell >= BELT_INVENTORY_SLOT_START &&
wCell < BELT_INVENTORY_SLOT_END);
}
// inside the item-move validation (char_item.cpp-like):
if (IsBeltInventorySlot(wDestCell))
{
// 1) Does the player have a belt equipped?
LPITEM belt = GetWear(WEAR_BELT);
if (!belt)
return false; // no belt → cells unusable
// 2) Is the target cell unlocked by the belt's grade?
if (!IsValidBeltCell(belt, wDestCell))
return false; // can't drop into a locked cell
}
Both checks are critical: items must not go into belt cells when no belt is worn, and only the cells unlocked by the belt's value0 grade may be used. Skip this logic and players exploit locked cells for free extra inventory. When the belt is removed, the game core must move its items back to the normal inventory (or reject the action if there's no room); otherwise the items become unreachable.
Database and persistence
Items in the belt inventory are stored just like any other item in the player.item table via the window and pos fields; only the pos value falls into the belt range. Watch out for Metin2's classic trap here: while the player is online the inventory is held in memory in the DB-cache layer. If you try to write belt items straight to MySQL, the player overwrites that with their in-memory copy on logout and the items are lost. Item moves must always go through the game-core API, never raw SQL. The storage schema doesn't change; the only difference is that belt slots are written into the new pos range.
Client: the belt slot and grid UI
There are two additions on the client side. First, the belt slot in the gear window; second, the belt inventory grid that opens when a belt is worn. In the Python UI files (uiInventory.py and the related window script) the slot range must be defined identically to the server:
# in playerSettingModule / player.py
BELT_INVENTORY_SLOT_START = 0
BELT_INVENTORY_SLOT_COUNT = 16
BELT_INVENTORY_SLOT_END = BELT_INVENTORY_SLOT_START + BELT_INVENTORY_SLOT_COUNT
# show/hide the grid when the belt is equipped
def OnEquipBelt(self, isEquipped):
if isEquipped:
self.beltInventoryGrid.Show()
else:
self.beltInventoryGrid.Hide()
An economical rule for the grid cells: show as many cells "active" as the belt's grade (the value from the server) unlocks, and the rest "locked". Drag-and-drop into locked cells must be blocked. Don't forget to add the UI's .tga graphics and cell coordinates either; most sources keep these in a belt_inventory_window.py file. Finally, the grid's open/close animation is purely cosmetic — item validation always happens on the server, never trust the client.
Upgrading the belt: unlocking cells
The belt's appeal is that it is upgradeable. The general approach is to raise the belt's value0 grade through an upgrade NPC or a quest. A simple grade bump on the quest side looks like this:
quest belt_upgrade begin
state start begin
when 18000.use begin
local belt = item.get_value(0) -- current grade
if belt >= 4 then
say("Your belt is already at the top grade.")
return
end
-- upgrade material / yang check goes here
item.set_value(0, belt + 1)
say("Your belt was upgraded to grade "..(belt+1).."!")
end
end
end
When the grade rises, the server counts more belt cells as "unlocked" based on the equipped belt's new value0 and updates the client. Keep the grade cap (4 in the example) consistent both in the quest and in the source's IsValidBeltCell logic; if the two disagree you either leave cells needlessly locked or open an exploitable hole.
Frequently Asked Questions
The grid opens when I equip a belt, but items vanish on logout — why?
The classic DB-cache trap. If you write belt items straight to MySQL, the player overwrites them with their memory copy on logout. Always move items through the game-core API and make sure the pos value is written correctly into the belt range.
Players can use belt cells without wearing a belt.
The server-side GetWear(WEAR_BELT) check is missing or weak. On every placement into the belt range, validate first that a belt is equipped, then that the target cell is unlocked by the grade. The lock on the client is cosmetic only.
I enabled the system but players disconnect on login.
Almost always a mismatch in the ENABLE_BELT_INVENTORY_SYSTEM flag or the slot constants between server and client. Recompile both with the same definitions; if packet sizes don't match, the session drops.
Want the belt system installed cleanly on your server? I set up the belt inventory end to end — from item_proto to source and client integration, including graded cell unlocking. Let's discuss your project — get in touch with me.