Lineage 2
Overview
Reward your Lineage 2 private server players for voting on topgservers. This guide covers a custom Java handler for L2J/L2jMobius emulators that polls the vote-check API and grants adena, items, or noblesse status. A Python sidecar handles real-time webhooks.
Prerequisites
- A Lineage 2 server running L2J, L2jMobius, or compatible Java emulator
- Java 11 or newer (17 recommended)
- A topgservers API key — generate one in My Servers → API
- Your webhook secret (optional, for real-time rewards)
- Port 443 outbound access from your server for HTTPS calls
Installation
1 Create the handler class
Add a new Java class to your game server source. The handler registers a voice command (.topgvote) and a background polling task.
gameserver/java/org/l2j/gameserver/custom/
├── TopGVoteHandler.java
├── TopGVoteTask.java
└── TopGWebhookReceiver.java (optional) 2 Implement the vote handler
The handler exposes a voiced command for players to manually check their vote status and claim rewards.
package org.l2j.gameserver.custom;
import org.l2j.gameserver.handler.IVoicedCommandHandler;
import org.l2j.gameserver.model.actor.instance.PlayerInstance;
import java.net.URI;
import java.net.http.*;
public class TopGVoteHandler implements IVoicedCommandHandler {
private static final String API_KEY = "tgs_your_api_key_here";
private static final String[] COMMANDS = { "topgvote" };
private final HttpClient http = HttpClient.newHttpClient();
@Override
public boolean useVoicedCommand(String command, PlayerInstance player,
String params) {
if (!"topgvote".equals(command)) return false;
try {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(
"https://topgservers.net/api/v1/vote-check?username="
+ player.getName()))
.header("Authorization", "Bearer " + API_KEY)
.GET().build();
http.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenAccept(res -> {
if (res.statusCode() == 200
&& res.body().contains("\"voted\":true")) {
grantReward(player);
player.sendMessage(
"Thanks for voting on topgservers! Rewards granted.");
} else {
player.sendMessage(
"No vote found. Visit topgservers.net to vote!");
}
});
} catch (Exception e) {
player.sendMessage("Vote check failed. Try again later.");
}
return true;
}
private void grantReward(PlayerInstance player) {
// 500k adena
player.addAdena("TopGVote", 500000, null, true);
// 5x Scroll of Enchant Weapon (D)
player.addItem("TopGVote", 955, 5, null, true);
}
@Override
public String[] getVoicedCommandList() {
return COMMANDS;
}
} 3 Add background polling task
Schedule a task that automatically checks votes for all online players every 60 seconds.
package org.l2j.gameserver.custom;
import org.l2j.gameserver.model.World;
import org.l2j.gameserver.model.actor.instance.PlayerInstance;
import java.net.URI;
import java.net.http.*;
import java.util.*;
import java.util.concurrent.*;
public class TopGVoteTask implements Runnable {
private static final String API_KEY = "tgs_your_api_key_here";
private final HttpClient http = HttpClient.newHttpClient();
private final Set<String> rewarded =
ConcurrentHashMap.newKeySet();
public void schedule() {
ScheduledExecutorService exec =
Executors.newSingleThreadScheduledExecutor();
exec.scheduleAtFixedRate(this, 30, 60, TimeUnit.SECONDS);
}
@Override
public void run() {
for (PlayerInstance player : World.getInstance().getPlayers()) {
String name = player.getName();
if (rewarded.contains(name)) continue;
try {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(
"https://topgservers.net/api/v1/vote-check?username="
+ name))
.header("Authorization", "Bearer " + API_KEY)
.GET().build();
HttpResponse<String> res =
http.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() == 200
&& res.body().contains("\"voted\":true")) {
rewarded.add(name);
player.addAdena("TopGVote", 500000, null, true);
player.addItem("TopGVote", 955, 5, null, true);
player.sendMessage(
"Thanks for voting on topgservers! Rewards granted.");
}
} catch (Exception e) {
// Silently skip — will retry next cycle
}
}
}
/** Call at midnight to allow re-voting */
public void resetDaily() {
rewarded.clear();
}
} Configuration
// Configuration
private static final String API_KEY = "tgs_your_api_key_here";
private static final String WEBHOOK_SECRET = "whsec_your_secret_here";
// Rewards
private static final int REWARD_ADENA = 500000; // adena
private static final int REWARD_ITEM_ID = 955; // EWD scroll
private static final int REWARD_ITEM_COUNT = 5;
// Polling
private static final int CHECK_INTERVAL = 60; // seconds Vote Check
Call the /api/v1/vote-check endpoint to determine if a player has voted today.
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(
"https://topgservers.net/api/v1/vote-check?username=" + playerName))
.header("Authorization", "Bearer " + API_KEY)
.GET().build();
HttpResponse<String> res =
http.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() == 200 && res.body().contains("\"voted\":true")) {
// Player voted today — grant reward
} Webhook Receiver
Verify the X-TopG-Signature header using HMAC-SHA256 to ensure webhook payloads are authentic.
"""
Webhook receiver for Lineage 2 servers.
Run alongside your game server. Inserts into topg_votes table
so the Java handler can pick up rewards on next check.
"""
import hmac, hashlib, json, mysql.connector
from http.server import HTTPServer, BaseHTTPRequestHandler
WEBHOOK_SECRET = "whsec_your_secret_here"
DB_CONFIG = {
"host": "127.0.0.1",
"user": "l2j",
"password": "password",
"database": "l2jgs"
}
class WebhookHandler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode()
sig = self.headers.get('X-TopG-Signature', '')
if not verify_signature(body, sig):
self.send_response(401)
self.end_headers()
return
data = json.loads(body)
username = data.get('username', '')
if username:
db = mysql.connector.connect(**DB_CONFIG)
cursor = db.cursor()
cursor.execute(
"INSERT IGNORE INTO topg_votes (name, vote_date) "
"VALUES (%s, CURDATE())",
(username,)
)
db.commit()
db.close()
self.send_response(200)
self.end_headers()
def verify_signature(body, sig_header):
parts = dict(p.split('=', 1) for p in sig_header.split(',') if '=' in p)
timestamp = parts.get('t', '')
received = parts.get('v1', '')
expected = hmac.new(
WEBHOOK_SECRET.encode(),
f"{timestamp}.{body}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received)
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 8090), WebhookHandler)
print("Webhook receiver listening on :8090")
server.serve_forever() Reward Examples
// Adena
player.addAdena("TopGVote", 500000, null, true);
// Items — addItem(process, itemId, count, reference, sendMessage)
player.addItem("TopGVote", 955, 5, null, true); // EWD scroll x5
player.addItem("TopGVote", 6673, 10, null, true); // Festival Adena x10
// Noblesse status
player.setNoble(true);
player.sendMessage("You have been granted Noblesse status!");
// Class-specific buff
player.getEffectList().stopAllEffects();
// or use SkillTable for custom buffs:
// SkillTable.getInstance().getSkill(1204, 2).applyEffects(player, player);
// Announce server-wide
Announcements.getInstance().announceToAll(
player.getName() + " voted on topgservers and received rewards!"); Notes & Tips
L2J and L2jMobius class names may differ slightly between versions. Adjust package paths and class names to match your source. For the webhook approach, create the votes table with: `CREATE TABLE topg_votes (name VARCHAR(50), vote_date DATE, rewarded TINYINT DEFAULT 0, PRIMARY KEY(name, vote_date));` Register the handler in your GameServer init or custom loader.