Un système metin2 offline shop permet à la boutique privée d'un joueur de rester ouverte dans le monde même après sa déconnexion. Dans le Metin2 d'origine, la « boutique de vente » que vous ouvrez se ferme dès que vous quittez le jeu ; l'économie ne tourne que tant que les joueurs sont en ligne. La boutique offline change cela : le joueur installe son étal, se déconnecte, et les objets continuent de se vendre. Dans ce guide, je détaille les trois piliers du système — base de données, source du jeu (C++) et client — avec la logique que j'ai suivie en le mettant en place sur mon propre serveur, Runa2.
En quoi une boutique offline diffère d'un marché normal
La boutique privée classique vit entièrement en mémoire. Tant que le personnage est dans le monde, un objet CShop est créé, les objets sont « verrouillés » hors de l'inventaire, et la boutique est détruite au départ du joueur. Une boutique offline présente trois différences fondamentales :
- Persistance : la boutique et ses objets sont écrits en MySQL, donc restaurés même si le game core redémarre.
- Transactions sans propriétaire : lors d'un achat, le flux d'or et d'objets doit se conclure sans que le propriétaire soit en ligne.
- Représentation visuelle : la boutique apparaît dans le monde comme un étal/personnage, généralement garé sur une carte « offline shop » dédiée.
C'est pourquoi une boutique offline n'est pas un mod client mais une fonctionnalité au niveau de la source.
1. Schéma de base de données
Nous construisons d'abord la couche de persistance. Deux tables suffisent : une pour la boutique, une pour les objets qu'elle contient. Créez-les dans la base player :
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;
Ici item_id est l'identifiant de la ligne dans la vraie table item du joueur. On ne copie pas l'objet ; on le « déplace » du propriétaire vers la boutique. C'est la règle la plus importante pour empêcher les duplications : un objet ne peut jamais exister à la fois dans l'inventaire et dans la boutique.
2. Côté source : la classe COfflineShop
Dans la source du jeu, vous construisez un COfflineShopManager basé sur la logique CShop existante. Le flux tourne autour de trois fonctions :
- Create — le joueur ouvre la boutique ; les objets choisis sont retirés de l'inventaire, écrits dans
offline_shop_item, et un étal (mob) apparaît dans le monde. - Load — au démarrage du game core, tous les enregistrements de boutiques offline sont lus et réapparaissent sur leur carte.
- Buy — un acheteur achète un objet ; l'opération doit être atomique.
Le squelette de la fonction d'achat ressemble à ceci :
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, "Pas assez de yang.");
return false;
}
// 1) Déduire l'or de l'acheteur
buyer->PointChange(POINT_GOLD, -item->price);
// 2) Donner l'objet à l'acheteur (avec l'item_id déplacé du propriétaire)
LPITEM newItem = ITEM_MANAGER::instance().CreateItem(item->vnum, item->count);
buyer->AutoGiveItem(newItem);
// 3) Ajouter les gains à la réserve d'or de la boutique
m_dwGold += item->price;
// 4) Mettre à jour MySQL (supprimer la ligne vendue, écrire l'or)
RemoveItemFromDB(item->id);
UpdateGoldInDB();
return true;
}
Le point critique est l'ordre : déduire l'or d'abord, puis donner l'objet, puis écrire en base. Si une étape échoue, vous devez annuler l'opération ; sinon un plantage du serveur peut entraîner une perte d'or ou d'objets.
3. Réseau : nouveaux paquets
Trois nouveaux en-têtes de paquet sont définis entre le client et le core : ouvrir une boutique, lister son contenu et acheter. Vous ajoutez des constantes d'en-tête dans service.h et packet.h, puis traitez les paquets dans input_main.cpp. Gardez la structure du paquet simple :
typedef struct SPacketCGOfflineShopBuy
{
BYTE header; // HEADER_CG_OFFLINE_SHOP_BUY
DWORD shop_vid; // le VID de l'étal
BYTE pos; // l'emplacement acheté
} TPacketCGOfflineShopBuy;
Lorsque le serveur reçoit ce paquet, il retrouve l'objet COfflineShop correspondant à partir du VID et appelle la fonction Buy() ci-dessus. N'oubliez pas un contrôle de distance : si l'acheteur n'est pas assez proche de l'étal, la requête doit être rejetée.
4. Côté client : la fenêtre de boutique et la recherche
Le client a besoin de deux choses. D'abord, une interface de gestion pour ouvrir la boutique et ajouter des objets (vous ajoutez un nouveau OfflineShopDialog aux fichiers d'UI Python du root). Ensuite, un système de recherche qui permet aux joueurs de trouver l'objet souhaité — c'est la vraie valeur d'une boutique offline. Au lieu de passer devant chaque étal, le joueur saisit un vnum ou un nom et le serveur renvoie les boutiques correspondantes. Cette recherche peut se résoudre avec un paquet dédié ou une liste de marché web.
Visuellement, rassembler les étals sur une carte dédiée protège les performances : si des centaines de boutiques offline apparaissent dans la ville principale, l'encombrement visuel et le trafic de paquets explosent. La plupart des serveurs utilisent un PNJ qui téléporte le joueur dans une « zone de boutiques offline ».
Sécurité et équilibre
- Protection anti-dupe : un objet ne doit pas être écrit dans la boutique avant de quitter l'inventaire ; les deux opérations doivent tenir dans la même transaction.
- Limite de temps : pour que les boutiques ne restent pas éternellement, ajoutez une fermeture automatique après une période (par ex. 7 jours) basée sur
created_at. - Frais : facturer un petit frais en yang pour ouvrir une boutique retire de l'argent de l'économie et freine l'inflation.
- Taxe : prélever 1 à 5 % de taxe sur chaque vente agit comme un puits d'or et donne au serveur sa propre logique de revenus.
Questions fréquentes
Une boutique offline ne se fait-elle qu'avec la source ?
En pratique, oui. Persister les objets, exécuter des transactions sans propriétaire et réapparaître les étals exigent tous du code C++ dans le game core. Une solution purement côté client, stable et inviolable, n'est pas possible.
Pourquoi déplacer les objets au lieu de les copier ?
Si vous copiez, le même objet existe dans l'inventaire et dans la boutique, et le joueur peut le dupliquer en utilisant les deux. Retirer physiquement l'objet de l'inventaire pour le déplacer dans la boutique élimine ce risque à la racine.
Des centaines de boutiques nuisent-elles aux performances ?
Si vous rassemblez les étals sur une carte séparée, envoyez les paquets selon la portée de vue et résolvez la recherche côté serveur, même des milliers de boutiques ne posent aucun problème. La vraie charge vient de les faire toutes apparaître dans la ville principale.
Vous voulez ajouter un système de boutique offline à votre serveur ? Du schéma de base de données à l'installation de la source et à l'UI du client, nous pouvons planifier tout le processus ensemble. Contactez-moi et parlons de votre projet.