aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Metin2

Metin2 Offline-Shop-System: Verkaufen im Abwesenheitsmodus

Ein metin2 offline shop System sorgt dafür, dass der private Markt eines Spielers in der Welt bestehen bleibt, auch nachdem dieser Spieler sich ausgeloggt hat. Im Standard-Metin2 schließt der „Verkaufsstand", den du öffnest, in dem Moment, in dem du das Spiel verlässt; die Wirtschaft läuft nur, solange Spieler online sind. Ein Offline-Shop ändert das: Der Spieler stellt seinen Stand auf, loggt sich aus, und die Items werden weiter verkauft. In dieser Anleitung gehe ich die drei Säulen des Systems durch — Datenbank, Game-Source (C++) und Client — mit derselben Logik, der ich beim Aufbau auf meinem eigenen Server, Runa2, gefolgt bin.

Worin unterscheidet sich ein Offline-Shop von einem normalen Markt?

Der klassische private Shop lebt vollständig im Speicher. Während der Charakter in der Welt ist, wird ein CShop-Objekt erstellt, Items werden aus dem Inventar „gesperrt", und der Shop wird beim Verlassen des Spielers zerstört. Ein Offline-Shop hat drei zentrale Unterschiede:

  • Persistenz: der Shop und seine Items werden in MySQL geschrieben, sodass sie selbst nach einem Neustart des Game-Core wiederhergestellt werden.
  • Besitzerlose Transaktionen: wenn ein Käufer kauft, muss der Fluss von Gold und Items abgeschlossen werden, ohne dass der Besitzer online ist.
  • Visuelle Darstellung: der Shop erscheint in der Welt als Stand/Charakter, üblicherweise auf einer dedizierten „Offline-Shop"-Map geparkt.

Deshalb ist ein Offline-Shop kein Client-Mod, sondern ein Feature auf Source-Ebene.

1. Datenbankschema

Zuerst bauen wir die Persistenzschicht. Zwei Tabellen reichen aus: eine für den Shop selbst, eine für die Items darin. Erstelle sie in der player-Datenbank:

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 ist item_id die ID der Zeile in der echten item-Tabelle des Spielers. Wir kopieren das Item nicht; wir „verschieben" es vom Besitzer in den Shop. Das ist die wichtigste Regel, um Dupes (Duplikate) zu verhindern: ein Item darf niemals gleichzeitig im Inventar und im Shop existieren.

2. Source-Seite: die Klasse COfflineShop

In der Game-Source baust du einen COfflineShopManager auf Basis der bestehenden CShop-Logik. Der Ablauf dreht sich um drei Funktionen:

  • Create — der Spieler öffnet den Shop; die gewählten Items werden aus dem Inventar entfernt, in offline_shop_item geschrieben, und eine Stand-Entity (Mob) wird in der Welt gespawnt.
  • Load — beim Start des Game-Core werden alle Offline-Shop-Datensätze gelesen und auf ihrer Map neu gespawnt.
  • Buy — ein Käufer kauft ein Item; die Operation muss atomar sein.

Das Grundgerüst der Kauffunktion sieht so aus:

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, "Nicht genug Yang.");
        return false;
    }

    // 1) Gold des Käufers abziehen
    buyer->PointChange(POINT_GOLD, -item->price);
    // 2) Item dem Käufer geben (mit der vom Besitzer verschobenen item_id)
    LPITEM newItem = ITEM_MANAGER::instance().CreateItem(item->vnum, item->count);
    buyer->AutoGiveItem(newItem);
    // 3) Erlös zum Gold-Pool des Shops hinzufügen
    m_dwGold += item->price;
    // 4) MySQL aktualisieren (verkaufte Zeile löschen, Gold schreiben)
    RemoveItemFromDB(item->id);
    UpdateGoldInDB();
    return true;
}

Der kritische Punkt ist die Reihenfolge: zuerst Gold abziehen, dann das Item geben, dann in die Datenbank schreiben. Schlägt ein Schritt fehl, musst du die Operation zurückrollen; sonst kann ein Serverabsturz zu Verlust von Gold oder Items führen.

3. Netzwerk: neue Pakete

Zwischen Client und Core werden drei neue Paket-Header definiert: einen Shop öffnen, seinen Inhalt auflisten und kaufen. Du fügst Header-Konstanten in service.h und packet.h hinzu und verarbeitest die Pakete anschließend in input_main.cpp. Halte die Paketstruktur einfach:

typedef struct SPacketCGOfflineShopBuy
{
    BYTE    header;       // HEADER_CG_OFFLINE_SHOP_BUY
    DWORD   shop_vid;     // die VID des Stands
    BYTE    pos;          // der gekaufte Slot
} TPacketCGOfflineShopBuy;

Wenn der Server dieses Paket empfängt, findet er über die VID das passende COfflineShop-Objekt und ruft die obige Buy()-Funktion auf. Vergiss die Distanzprüfung nicht: ist der Käufer nicht nah genug am Stand, muss die Anfrage abgelehnt werden.

4. Client-Seite: das Shop-Fenster und die Suche

Der Client braucht zwei Dinge. Erstens eine Verwaltungs-UI, um den Shop zu öffnen und Items hinzuzufügen (du fügst einen neuen OfflineShopDialog zu den Python-UI-Dateien im Root hinzu). Zweitens ein Suchsystem, mit dem Spieler das gewünschte Item finden — das ist der eigentliche Wert eines Offline-Shops. Statt an jedem Stand vorbeizulaufen, tippt ein Spieler eine vnum oder einen Namen ein, und der Server gibt die passenden Shops zurück. Diese Suche lässt sich mit einem dedizierten Paket oder einer webbasierten Marktliste lösen.

Visuell schützt das Sammeln der Stände auf einer dedizierten Map die Performance: spawnen Hunderte Offline-Shops in der Hauptstadt, explodieren sowohl das visuelle Gedränge als auch der Paketverkehr. Die meisten Server nutzen einen NPC, der den Spieler in eine „Offline-Shop-Zone" teleportiert.

Sicherheit und Balance

  • Dupe-Schutz: ein Item darf nicht in den Shop geschrieben werden, bevor es das Inventar verlässt; beide Operationen müssen in derselben Transaktion liegen.
  • Zeitlimit: damit Shops nicht ewig stehen bleiben, füge eine automatische Schließung nach einer Periode (z. B. 7 Tage) basierend auf created_at hinzu.
  • Gebühr: eine kleine Yang-Gebühr fürs Öffnen eines Shops zieht Geld aus der Wirtschaft und bremst die Inflation.
  • Steuer: 1-5 % Steuer auf jeden Verkauf wirkt als Gold-Sink und gibt dem Server eine eigene Einnahmenlogik.

Häufige Fragen

Lässt sich ein Offline-Shop nur mit Source umsetzen?

In der Praxis ja. Items persistent zu machen, besitzerlose Transaktionen auszuführen und Stände neu zu spawnen erfordert alles C++-Code im Game-Core. Eine rein client-basierte Lösung, die stabil und dupe-sicher ist, ist nicht möglich.

Warum Items verschieben statt kopieren?

Beim Kopieren existiert dasselbe Item sowohl im Inventar als auch im Shop, und der Spieler kann es durch Nutzung beider duplizieren. Das Item physisch aus dem Inventar zu entfernen und in den Shop zu verschieben, beseitigt dieses Risiko an der Wurzel.

Beeinträchtigen Hunderte Shops die Performance?

Wenn du Stände auf einer separaten Map sammelst, Pakete nach Sichtweite verschickst und die Suche serverseitig löst, sind selbst Tausende Shops kein Problem. Die echte Last entsteht dadurch, sie alle in der Hauptstadt zu spawnen.

Möchtest du ein Offline-Shop-System zu deinem Server hinzufügen? Vom Datenbankschema über die Source-Einrichtung bis zur Client-UI können wir den ganzen Prozess gemeinsam planen. Kontaktiere mich und lass uns über dein Projekt sprechen.

Devamı için