Bir metin2 offline shop sistemi, oyuncunun açtığı özel pazarın oyuncu çevrimdışı olduktan sonra da dünyada durmaya devam etmesini sağlar. Standart Metin2'de açtığınız "satış pazarı" siz çıkış yapınca kapanır; ekonomi yalnızca insanlar online iken döner. Offline shop bunu değiştirir: oyuncu pazarını kurar, çıkış yapar ve eşyaları satılmaya devam eder. Bu rehberde sistemin üç ayağını — veritabanı, game source (C++) ve client — kendi sunucum Runa2'de kurarken izlediğim mantıkla anlatıyorum.
Offline shop normal pazardan ne ile ayrılır?
Klasik özel pazar (private shop) tamamen bellekte yaşar. Karakter dünyadayken bir CShop nesnesi oluşturulur, eşyalar oyuncunun envanterinden "kilitlenir" ve oyuncu çıkınca pazar yok olur. Offline shop'ta üç temel fark vardır:
- Kalıcılık: pazar ve içindeki eşyalar MySQL'e yazılır, böylece game core yeniden başlasa bile geri yüklenir.
- Sahipsiz işlem: alıcı satın aldığında para ve eşya akışı, sahibi online olmadan tamamlanmalıdır.
- Görsel temsil: pazar dünyada bir tezgâh/karakter olarak görünür ve genelde özel bir "offline shop" haritasında park edilir.
Bu yüzden offline shop bir client modu değil, source seviyesinde bir özelliktir.
1. Veritabanı şeması
Önce kalıcılık katmanını kuruyoruz. İki tablo yeterlidir: biri pazarın kendisi, diğeri içindeki eşyalar. player veritabanında oluşturun:
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;
Burada item_id, oyuncunun gerçek item tablosundaki satırın kimliğidir. Eşyayı kopyalamıyoruz; sahibinden alıp pazara "taşıyoruz". Bu, dupe (çoğaltma) hatalarını önlemenin en önemli kuralıdır: bir eşya aynı anda hem envanterde hem pazarda olamaz.
2. Source tarafı: COfflineShop sınıfı
Game source'ta mevcut CShop mantığını taban alan bir COfflineShopManager kurgusu kullanılır. Akış üç fonksiyon etrafında döner:
- Create — oyuncu pazarı açar, seçtiği eşyalar envanterden çıkarılıp
offline_shop_item'a yazılır, dünyada bir tezgâh entity'si (mob) spawn edilir. - Load — game core açılışında tüm offline shop kayıtları okunur ve ilgili haritada yeniden spawn edilir.
- Buy — alıcı bir eşyayı satın alır; işlem atomik olmalıdır.
Satın alma fonksiyonunun iskeleti şöyle görünür:
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, "Yeterli yang yok.");
return false;
}
// 1) Alıcının parasını düş
buyer->PointChange(POINT_GOLD, -item->price);
// 2) Eşyayı alıcıya ver (sahibinden taşınmış item_id ile)
LPITEM newItem = ITEM_MANAGER::instance().CreateItem(item->vnum, item->count);
buyer->AutoGiveItem(newItem);
// 3) Kazancı pazarın gold havuzuna ekle
m_dwGold += item->price;
// 4) MySQL'i güncelle (satılan satırı sil, gold'u yaz)
RemoveItemFromDB(item->id);
UpdateGoldInDB();
return true;
}
Kritik nokta sıralamadır: önce para düşülür, sonra eşya verilir, en son veritabanı yazılır. Herhangi bir adım başarısız olursa işlemi geri almalısınız; aksi halde server çökerse para veya eşya kaybı yaşanır.
3. Network: yeni paketler
Client ile core arasında üç yeni paket başlığı tanımlanır: pazarı açma isteği, pazar içeriğini listeleme ve satın alma. service.h ve packet.h içinde header sabitleri eklenir, ardından input_main.cpp tarafında bu paketler işlenir. Paket yapısı sade tutulmalıdır:
typedef struct SPacketCGOfflineShopBuy
{
BYTE header; // HEADER_CG_OFFLINE_SHOP_BUY
DWORD shop_vid; // tezgâhın VID'i
BYTE pos; // satın alınan slot
} TPacketCGOfflineShopBuy;
Sunucu bu paketi aldığında VID'den ilgili COfflineShop nesnesini bulur ve yukarıdaki Buy() fonksiyonunu çağırır. Mesafe kontrolü unutulmamalı: alıcı tezgâha yeterince yakın değilse istek reddedilmelidir.
4. Client tarafı: pazar penceresi ve arama
Client'ta iki şey gerekir. Birincisi pazarı açıp eşya ekleyen yönetim arayüzü (root'taki Python UI dosyalarına yeni bir OfflineShopDialog eklersiniz). İkincisi, oyuncuların aradıkları eşyayı bulmasını sağlayan bir arama sistemidir — offline shop'ın asıl değeri budur. Tüm tezgâhları tek tek dolaşmak yerine, oyuncu bir vnum ya da isim girer ve sunucu eşleşen pazarları döndürür. Bu arama, ayrı bir paketle ya da bir web tabanlı pazar listesiyle çözülebilir.
Görsel olarak tezgâhları özel bir haritada toplamak performansı korur: yüzlerce offline shop ana şehirde spawn olursa hem görsel kalabalık hem paket trafiği artar. Çoğu sunucu, oyuncuyu "offline shop bölgesine" ışınlayan bir NPC kullanır.
Pratikte arama sistemini sunucu tarafında çözmek en sağlıklı yoldur: oyuncu bir vnum girdiğinde core, bellekteki offline shop listesini tarar ve eşleşen tezgâhların adını, konumunu ve fiyatını döndürür. Böylece oyuncu doğrudan ilgili pazara ışınlanabilir veya yönlendirilebilir. Bu yaklaşım hem istemciyi hafif tutar hem de fiyat karşılaştırmasını gerçek bir pazar deneyimine dönüştürür.
Güvenlik ve denge
Offline shop, kalıcı eşya ve sahipsiz para akışı içerdiği için sunucunuzdaki en hassas sistemlerden biridir. Aşağıdaki kuralları baştan uygularsanız sonradan dupe ve eşya kaybı kâbuslarından kurtulursunuz:
- Dupe koruması: eşya envanterden çıkmadan pazara yazılmamalı; her iki işlem aynı transaction içinde olmalı.
- Süre limiti: pazarların sonsuza dek durmaması için
created_atüzerinden bir süre (örn. 7 gün) sonra otomatik kapanma ekleyin. - Ücret: pazar açarken küçük bir yang ücreti almak, ekonomiden para çekerek enflasyonu dengeler.
- Vergi: satıştan %1-5 vergi kesmek hem para sink'i sağlar hem sunucuya gelir mantığı katar.
Sık Sorulan Sorular
Offline shop yalnızca source ile mi yapılır?
Pratikte evet. Eşyaların kalıcı olması, sahipsiz işlem yapılması ve tezgâhların yeniden spawn edilmesi game core'da C++ kodu gerektirir. Tamamen client tabanlı, kararlı ve dupe'a kapalı bir çözüm mümkün değildir.
Eşyaları kopyalamak yerine neden taşımalıyım?
Kopyalarsanız aynı eşya hem envanterde hem pazarda var olur ve oyuncu ikisini birden kullanarak çoğaltma yapabilir. Eşyayı envanterden fiziksel olarak çıkarıp pazara taşımak bu riski kökten ortadan kaldırır.
Yüzlerce pazar performansı düşürür mü?
Tezgâhları ayrı bir haritada toplar, görüş alanına göre paket gönderir ve arama sistemini sunucu tarafında çözerseniz binlerce pazar bile sorun çıkarmaz. Asıl yük, hepsini ana şehirde spawn etmekten gelir.
Sunucunuza offline shop sistemi mi eklemek istiyorsunuz? Veritabanı şemasından source kurulumuna ve client arayüzüne kadar tüm süreci birlikte planlayabiliriz. Benimle iletişime geçin ve projenizi konuşalım.