Een metin2 gift code-systeem is een eenvoudig maar bijzonder handig mechanisme dat een speler een eenmalige code geeft en in ruil daarvoor een in-game beloning toekent (yang, items, EXP-drankjes of een exclusief kostuum). Verspreid tijdens Discord-evenementen, in YouTube-video's of om nieuwe spelers aan te trekken, houden deze codes spelers tevreden terwijl ze, mits goed gebouwd, bestand blijven tegen misbruik. In dit artikel loop ik de hele flow door: van het database-ontwerp en de codegeneratie tot het inwisselen op het webpaneel en het afleveren van de beloning bij de speler.
Hoe het systeem werkt
In Metin2 staan account- (account) en spelersgegevens (player) in MySQL. De gift code-flow verloopt doorgaans zo:
- Een beheerder definieert een beloningsset in het paneel (bijvoorbeeld 1.000.000 yang + 50 EXP-drankjes).
- Het systeem genereert een of meerdere unieke codes die aan die beloning gekoppeld zijn.
- De speler voert de code in het veld "Code inwisselen" op de website in.
- De server valideert de code, controleert of die al gebruikt is en zet de beloning over naar het account van de speler.
Bij het afleveren van de beloning is de veiligste aanpak om Metin2's mail / cadeaubox-logica te gebruiken in plaats van items rechtstreeks in de inventory te schrijven; zo gaat de beloning nooit verloren, zelfs niet wanneer de speler offline is.
De databasetabellen ontwerpen
Het hart van het hele systeem zijn twee tabellen: gift_rewards die de beloningen bevat en gift_codes die de codes bevat. Het onderstaande schema voegt ook een relatietabel toe zodat één code meerdere item-beloningen kan dragen.
CREATE TABLE gift_codes (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(32) NOT NULL UNIQUE,
reward_id INT NOT NULL,
max_uses INT NOT NULL DEFAULT 1,
used_count INT NOT NULL DEFAULT 0,
expires_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX (reward_id)
);
CREATE TABLE gift_rewards (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
yang BIGINT NOT NULL DEFAULT 0
);
CREATE TABLE gift_reward_items (
reward_id INT NOT NULL,
item_vnum INT NOT NULL,
count SMALLINT NOT NULL DEFAULT 1
);
CREATE TABLE gift_redemptions (
id INT AUTO_INCREMENT PRIMARY KEY,
code_id INT NOT NULL,
account_id INT NOT NULL,
redeemed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_code_account (code_id, account_id)
);
De UNIQUE KEY uq_code_account is hier cruciaal: die voorkomt op databaseniveau dat hetzelfde account dezelfde code twee keer gebruikt. max_uses bepaalt hoeveel verschillende personen één code mogen gebruiken (iets als "100 personen" voor evenementcodes).
Unieke codes genereren
Codes moeten onvoorspelbaar en goed leesbaar zijn. Een goede praktijk is om dubbelzinnige tekens als 0/O en 1/I/L weg te laten en een alfabet in hoofdletters te gebruiken. Veilige generatie met PHP:
function generateGiftCode(int $length = 12): string {
$alphabet = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
$code = '';
$max = strlen($alphabet) - 1;
for ($i = 0; $i < $length; $i++) {
$code .= $alphabet[random_int(0, $max)];
}
// groeperen voor leesbaarheid: ABCD-EFGH-JKMN
return implode('-', str_split($code, 4));
}
Omdat random_int() cryptografisch veilig is, gebruik je die altijd in plaats van rand(). Wanneer je de gegenereerde code opslaat, vangt de UNIQUE-beperking in de database botsingen op; bij een botsing genereer je simpelweg opnieuw en probeer je het nog eens.
De inwisselflow (webpaneel)
Na het inloggen verstuurt de speler de code. De serverlogica moet binnen één transactie draaien zodat de beloning niet twee keer wordt toegekend wanneer er twee verzoeken tegelijk binnenkomen:
$pdo->beginTransaction();
$stmt = $pdo->prepare(
'SELECT * FROM gift_codes WHERE code = ? FOR UPDATE'
);
$stmt->execute([$code]);
$gc = $stmt->fetch();
if (!$gc) {
throw new Exception('Ongeldige code.');
}
if ($gc['expires_at'] !== null && strtotime($gc['expires_at']) < time()) {
throw new Exception('Deze code is verlopen.');
}
if ($gc['used_count'] >= $gc['max_uses']) {
throw new Exception('Deze code is opgebruikt.');
}
// heeft dit account hem al ingewisseld?
$ins = $pdo->prepare(
'INSERT INTO gift_redemptions (code_id, account_id) VALUES (?, ?)'
);
try {
$ins->execute([$gc['id'], $accountId]);
} catch (PDOException $e) {
throw new Exception('Je hebt deze code al gebruikt.');
}
$pdo->prepare('UPDATE gift_codes SET used_count = used_count + 1 WHERE id = ?')
->execute([$gc['id']]);
grantReward($pdo, $gc['reward_id'], $accountId);
$pdo->commit();
De regel SELECT ... FOR UPDATE vergrendelt de betreffende coderij voor de duur van de transactie en voorkomt zo race-condities (twee keer tegelijk inwisselen). De "al ingewisseld"-controle overlaten aan een UNIQUE-fout die met try/catch wordt opgevangen, is veiliger dan een aparte SELECT, die er een open tijdvenster tussenuit laat.
De beloning bij de speler afleveren
Yang toekennen is eenvoudig; je verhoogt de gold-kolom van de speler. Maar als de speler op dat moment in het spel is, kan de wijziging die de webkant maakt worden overschreven door de in-memory kopie op de game-server en verloren gaan. Daarom is het wegschrijven van item- en yang-beloningen naar Metin2's cadeau-/mailsysteem de juiste methode; wanneer de speler de box vanuit het spel opent, belandt de beloning veilig in de inventory.
function grantReward(PDO $pdo, int $rewardId, int $accountId): void {
$reward = $pdo->query(
"SELECT * FROM gift_rewards WHERE id = $rewardId"
)->fetch();
// de yang-beloning aan de cadeaubox toevoegen
if ($reward['yang'] > 0) {
addMailMoney($accountId, (int)$reward['yang']);
}
// de items aan de cadeaubox toevoegen
$items = $pdo->prepare(
'SELECT * FROM gift_reward_items WHERE reward_id = ?'
);
$items->execute([$rewardId]);
foreach ($items as $it) {
addMailItem($accountId, (int)$it['item_vnum'], (int)$it['count']);
}
}
De functies addMailMoney en addMailItem voegen hier rijen toe aan de cadeau-/mailtabellen van jouw serverdistributie. Welke tabellen en kolommen je gebruikt, hangt af van je serverbron (revisie); de meest robuuste aanpak is daarom om de bestaande "item shop-aflevering"-logica in je eigen bron als referentie te nemen.
Beveiliging en misbruikpreventie
- Rate limiting: beperk pogingen tot enkele per minuut per IP en per account; anders kunnen bots de codepool met brute force aftasten.
- Lange, willekeurige codes: codes van 12 tekens gegenereerd met
random_intzijn praktisch onmogelijk te raden. - Logging: registreer elke inwisseling met IP, account en tijdstempel; zo betrap je verdachte massapogingen.
- Vervaldatum- en aantallimieten: geef evenementcodes altijd een
expires_aten een redelijkemax_uses.
Veelgestelde vragen
Kan ik de beloning rechtstreeks in de inventory van de speler schrijven?
Technisch wel, maar als de speler op dat moment online is, overschrijven de in-memory gegevens van de game-server de webwijziging en gaat de beloning verloren. Het gebruik van de cadeau-/mailbox lost dit probleem volledig op.
Hoeveel personen kunnen één code gebruiken?
Dat bepaalt de kolom max_uses. Je zet 1 voor persoonlijke codes en bijvoorbeeld 100 voor algemene evenementcodes. Dankzij de unieke beperking in de tabel gift_redemptions kan hetzelfde account dezelfde code toch maar één keer gebruiken.
Kan ik codes in bulk genereren?
Ja. Als je de generatorfunctie in een lus aanroept en elke code in de tabel gift_codes invoegt, vangt de UNIQUE-beperking eventuele botsingen op. Je genereert de botsende code gewoon opnieuw en voegt hem in.
Wil je een veilig gift code-systeem op je server opzetten? Van database-ontwerp tot het webpaneel en de logica voor het afleveren van beloningen kan ik een oplossing bouwen die bij jouw Metin2-infrastructuur past. Neem contact op om over je project te praten.