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

Metin2 Gift Code System: Building Redeemable Codes

A metin2 gift code system is a simple but highly useful mechanism that hands a player a single-use code and grants an in-game reward (yang, items, EXP potions or an exclusive costume) in return. Distributed during Discord events, in YouTube videos or to attract new players, these codes keep players happy while staying resistant to abuse when built correctly. In this article I walk through the entire flow: from the database design and code generation to redemption on the web panel and delivering the reward to the player.

How the system works

In Metin2, account and player data live in MySQL. The gift code flow typically goes like this:

  • An admin defines a reward set in the panel (for example 1,000,000 yang + 50 EXP potions).
  • The system generates one or many unique codes tied to that reward.
  • The player enters the code in the "Redeem Code" field on the website.
  • The server validates the code, checks whether it has already been used and transfers the reward to the player's account.

When delivering the reward, the safest approach is to use Metin2's mail / gift box logic rather than writing items straight into the inventory; that way the reward is never lost even while the player is offline.

Designing the database tables

The heart of the whole system is two tables: gift_rewards that holds the rewards and gift_codes that holds the codes. The schema below also adds a relation table so a single code can carry multiple item rewards.

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)
);

The UNIQUE KEY uq_code_account here is critical: it stops the same account from using the same code twice at the database level. max_uses defines how many different people can use a single code (something like "100 people" for event codes).

Generating unique codes

Codes should be unpredictable and easy to read. A good practice is to drop ambiguous characters like 0/O and 1/I/L and use an uppercase alphabet. Secure generation with 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)];
    }
    // group for readability: ABCD-EFGH-JKMN
    return implode('-', str_split($code, 4));
}

Because random_int() is cryptographically secure, always use it instead of rand(). When you save the generated code, the UNIQUE constraint in the database catches collisions; if one occurs you simply regenerate and try again.

The redemption flow (web panel)

After logging in, the player submits the code. The server-side logic must run inside a single transaction so the reward isn't granted twice when two requests arrive at once:

$pdo->beginTransaction();

$stmt = $pdo->prepare(
    'SELECT * FROM gift_codes WHERE code = ? FOR UPDATE'
);
$stmt->execute([$code]);
$gc = $stmt->fetch();

if (!$gc) {
    throw new Exception('Invalid code.');
}
if ($gc['expires_at'] !== null && strtotime($gc['expires_at']) < time()) {
    throw new Exception('This code has expired.');
}
if ($gc['used_count'] >= $gc['max_uses']) {
    throw new Exception('This code is used up.');
}

// has this account already redeemed it?
$ins = $pdo->prepare(
    'INSERT INTO gift_redemptions (code_id, account_id) VALUES (?, ?)'
);
try {
    $ins->execute([$gc['id'], $accountId]);
} catch (PDOException $e) {
    throw new Exception('You have already used this code.');
}

$pdo->prepare('UPDATE gift_codes SET used_count = used_count + 1 WHERE id = ?')
    ->execute([$gc['id']]);

grantReward($pdo, $gc['reward_id'], $accountId);
$pdo->commit();

The SELECT ... FOR UPDATE line locks the relevant code row for the duration of the transaction, preventing race conditions (using it twice simultaneously). Leaving the "already redeemed" check to a UNIQUE error caught with try/catch is safer than doing a separate SELECT, which leaves an open time window in between.

Delivering the reward to the player

Granting yang is simple; you increase the player's gold column. But if the player is in the game at that moment, the change made by the web side can be overwritten by the in-memory copy on the game server and lost. That's why writing item and yang rewards into Metin2's gift/mail system is the correct method; when the player opens the box from inside the game, the reward safely lands in the inventory.

function grantReward(PDO $pdo, int $rewardId, int $accountId): void {
    $reward = $pdo->query(
        "SELECT * FROM gift_rewards WHERE id = $rewardId"
    )->fetch();

    // add the yang reward to the gift box
    if ($reward['yang'] > 0) {
        addMailMoney($accountId, (int)$reward['yang']);
    }

    // add the items to the gift box
    $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']);
    }
}

The addMailMoney and addMailItem functions here insert rows into your server distribution's gift/mail tables. Which tables and columns you use depends on your server source (revision); so the most robust approach is to reference the existing "item shop delivery" logic in your own source.

Security and abuse prevention

  • Rate limiting: limit attempts to a few per minute per IP and account; otherwise bots can brute-force scan the code pool.
  • Long, random codes: codes of 12 characters generated with random_int are practically impossible to guess.
  • Logging: record every redemption with IP, account and timestamp; that's how you catch suspicious bulk attempts.
  • Expiry and quantity limits: always give event codes an expires_at and a sensible max_uses.

Frequently Asked Questions

Can I write the reward straight into the player's inventory?

Technically yes, but if the player is online at that moment the game server's in-memory data overwrites the web change and the reward is lost. Using the gift/mail box removes this problem entirely.

How many people can use a single code?

The max_uses column decides this. You set 1 for personal codes and, say, 100 for general event codes. Thanks to the unique constraint in the gift_redemptions table, the same account can still use the same code only once.

Can I generate codes in bulk?

Yes. If you call the generator function in a loop and insert each one into the gift_codes table, the UNIQUE constraint catches any collisions. You just regenerate and insert the conflicting code.

Want to set up a secure gift code system on your server? From database design to the web panel and reward delivery logic, I can build a solution that fits your Metin2 infrastructure. Get in touch to talk about your project.

Bu kategorideki tüm yazılar →

Devamı için