Een metin2 offline shop systeem zorgt ervoor dat de privémarkt van een speler in de wereld blijft staan, zelfs nadat die speler is uitgelogd. In het standaard Metin2 sluit de "verkoopwinkel" die je opent op het moment dat je het spel verlaat; de economie draait alleen zolang spelers online zijn. Een offline shop verandert dat: de speler zet zijn kraam op, logt uit, en de items blijven verkopen. In deze gids loop ik door de drie pijlers van het systeem — database, game source (C++) en client — met dezelfde logica die ik volgde toen ik het op mijn eigen server, Runa2, bouwde.
Waarin verschilt een offline shop van een gewone markt?
De klassieke privéwinkel leeft volledig in het geheugen. Terwijl het personage in de wereld is, wordt een CShop-object aangemaakt, worden items uit de inventaris "vergrendeld", en wordt de winkel vernietigd wanneer de speler vertrekt. Een offline shop heeft drie kernverschillen:
- Persistentie: de winkel en zijn items worden naar MySQL geschreven, zodat ze worden hersteld zelfs als de game core herstart.
- Transacties zonder eigenaar: wanneer een koper iets koopt, moet de stroom van goud en items voltooid worden zonder dat de eigenaar online is.
- Visuele weergave: de winkel verschijnt in de wereld als een kraam/personage, meestal geparkeerd op een speciale "offline shop"-map.
Daarom is een offline shop geen client-mod maar een functie op source-niveau.
1. Databaseschema
We bouwen eerst de persistentielaag. Twee tabellen volstaan: één voor de winkel zelf, één voor de items erin. Maak ze aan in de 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;
Hier is item_id het id van de rij in de echte item-tabel van de speler. We kopiëren het item niet; we "verplaatsen" het van de eigenaar naar de winkel. Dit is de allerbelangrijkste regel om dupes te voorkomen: één item kan nooit tegelijk in de inventaris én in de winkel bestaan.
2. Source-kant: de COfflineShop-klasse
In de game source bouw je een COfflineShopManager op basis van de bestaande CShop-logica. De flow draait om drie functies:
- Create — de speler opent de winkel; de gekozen items worden uit de inventaris gehaald, naar
offline_shop_itemgeschreven, en een kraam-entity (mob) verschijnt in de wereld. - Load — bij het opstarten van de game core worden alle offline-shop-records gelezen en opnieuw gespawnd op hun map.
- Buy — een koper koopt een item; de operatie moet atomair zijn.
Het skelet van de koopfunctie ziet er zo uit:
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, "Niet genoeg yang.");
return false;
}
// 1) Trek het goud van de koper af
buyer->PointChange(POINT_GOLD, -item->price);
// 2) Geef het item aan de koper (met de van de eigenaar verplaatste item_id)
LPITEM newItem = ITEM_MANAGER::instance().CreateItem(item->vnum, item->count);
buyer->AutoGiveItem(newItem);
// 3) Voeg de opbrengst toe aan de goudpot van de winkel
m_dwGold += item->price;
// 4) Werk MySQL bij (verwijder de verkochte rij, schrijf het goud)
RemoveItemFromDB(item->id);
UpdateGoldInDB();
return true;
}
Het cruciale punt is de volgorde: trek eerst het goud af, geef dan het item, schrijf daarna naar de database. Als een stap mislukt, moet je de operatie terugdraaien; anders kan een servercrash verlies van goud of items veroorzaken.
3. Netwerk: nieuwe pakketten
Er worden drie nieuwe pakketheaders tussen client en core gedefinieerd: een winkel openen, de inhoud opsommen en kopen. Je voegt headerconstanten toe in service.h en packet.h, en verwerkt de pakketten daarna in input_main.cpp. Houd de pakketstructuur simpel:
typedef struct SPacketCGOfflineShopBuy
{
BYTE header; // HEADER_CG_OFFLINE_SHOP_BUY
DWORD shop_vid; // de VID van de kraam
BYTE pos; // de gekochte slot
} TPacketCGOfflineShopBuy;
Wanneer de server dit pakket ontvangt, zoekt hij het bijbehorende COfflineShop-object op via de VID en roept de bovenstaande Buy()-functie aan. Vergeet de afstandscontrole niet: als de koper niet dicht genoeg bij de kraam staat, moet het verzoek worden geweigerd.
4. Client-kant: het winkelvenster en zoeken
De client heeft twee dingen nodig. Ten eerste een beheerinterface om de winkel te openen en items toe te voegen (je voegt een nieuwe OfflineShopDialog toe aan de Python-UI-bestanden in root). Ten tweede een zoeksysteem waarmee spelers het gewenste item kunnen vinden — dit is de echte waarde van een offline shop. In plaats van langs elke kraam te lopen, typt een speler een vnum of naam en geeft de server de overeenkomende winkels terug. Dit zoeken kan worden opgelost met een speciaal pakket of een webgebaseerde marktlijst.
Visueel beschermt het verzamelen van de kramen op een speciale map de prestaties: als er honderden offline shops in de hoofdstad spawnen, exploderen zowel de visuele drukte als het pakketverkeer. De meeste servers gebruiken een NPC die de speler naar een "offline-shop-zone" teleporteert.
Beveiliging en balans
- Dupe-bescherming: een item mag niet naar de winkel worden geschreven voordat het de inventaris verlaat; beide operaties moeten binnen dezelfde transactie zitten.
- Tijdslimiet: zodat winkels niet eeuwig blijven staan, voeg een automatische sluiting toe na een periode (bijv. 7 dagen) op basis van
created_at. - Kosten: een kleine yang-vergoeding vragen om een winkel te openen haalt geld uit de economie en remt inflatie af.
- Belasting: 1-5% belasting op elke verkoop heffen werkt als een gold sink en geeft de server een eigen inkomstenlogica.
Veelgestelde vragen
Kan een offline shop alleen met source worden gemaakt?
In de praktijk wel. Het persistent maken van items, transacties zonder eigenaar uitvoeren en kramen opnieuw spawnen vereisen allemaal C++-code in de game core. Een puur client-gebaseerde oplossing die stabiel en dupe-bestendig is, is niet mogelijk.
Waarom items verplaatsen in plaats van kopiëren?
Als je kopieert, bestaat hetzelfde item in zowel de inventaris als de winkel, en kan de speler het dupliceren door beide te gebruiken. Het item fysiek uit de inventaris halen en naar de winkel verplaatsen elimineert dat risico bij de wortel.
Schaden honderden winkels de prestaties?
Als je kramen op een aparte map verzamelt, pakketten op basis van kijkbereik verstuurt en het zoeken aan de serverkant oplost, zijn zelfs duizenden winkels geen probleem. De echte last komt van ze allemaal in de hoofdstad spawnen.
Wil je een offline shop systeem aan je server toevoegen? Van het databaseschema tot de source-installatie en de client-UI kunnen we het hele proces samen plannen. Neem contact met me op en laten we het over je project hebben.