Architecture
Connector components, connection lifecycle, and delivery guarantees
This page is for L2J sources
Everything below describes the L2J connector core, which runs inside the game server process. An
L2Off server's connector is a separate process configured through portal.toml — its
architecture is described in PTS / L2Off servers.
Components
| Component | Responsibility |
|---|---|
PortalConnector | Outbound WebSocket connection, bearer authentication, handshake, heartbeat, reconnect, and message dispatch |
PortalConfig | Loads config/portal.properties |
GameBridge | Pack-specific database reads, live-world access, credential operations, and grant application |
GrantResult | Reports durable success, a refusal that releases the player's coins, retryable failure, permanent failure, or an ambiguous result requiring reconciliation |
All four are published in the package net.forgeport.connector and require
Java 17 or newer. The core uses the JDK HTTP/WebSocket client,
java.util.logging, java.util.Properties, and minimal-json. It imports no
game pack.
PortalConfig fields
PortalConfig.load() parses config/portal.properties once, at startup, and publishes every
value as a public static field. A bridge reads the fields; it never parses the file itself.
| Field | Property | Meaning |
|---|---|---|
ENABLED | Enabled | Master switch; false leaves the connector off |
GATEWAY_URL | GatewayUrl | WebSocket URL of the Forgeport gateway |
SERVER_TOKEN | Token | Bearer credential, gps_<game-server-id>.<secret> |
GAME_SERVER_ID | — | Parsed out of the token; identifies this server on the wire |
HEARTBEAT_SECONDS | HeartbeatSeconds | Heartbeat interval, default 15 |
DELIVERY_WINDOWS | DeliveryWindow.<CAPABILITY> | When each delivery reaches a character on this pack — online, offline, or both, keyed by capability name; empty means the defaults every bridge has always had |
UNSTUCK_X / UNSTUCK_Y / UNSTUCK_Z | Unstuck.X/Y/Z | Where UNSTUCK_CHARACTER sends a character; Giran by default |
The bridge-facing ones are UNSTUCK_X/Y/Z — the core acts on the rest itself. DELIVERY_WINDOWS
travels in the handshake, so an owner who tunes a window edits the file and restarts, without a
rebuild.
Forgeport ships this core but no pack-specific GameBridge. The game server
administrator or developer writes the bridge, in their own source tree and
their own package, and passes an instance to
PortalConnector.start(bridge).
The wiring is an ordinary constructor call in the game server's own startup
code, so the compiler checks it: a renamed or deleted bridge fails the build
rather than producing a server that starts with the connector quietly switched
off. If init() fails, the connector logs the reason and stays disabled
without affecting the rest of the game server.
The GameBridge interface
public interface GameBridge
{
String[] capabilities();
default void init() {}
int onlineCount();
JsonArray listCharacters(String accountName) throws Exception;
default JsonArray listCharacterSkills(String accountName, int characterId) throws Exception
{
throw new UnsupportedOperationException("listCharacterSkills");
}
JsonArray listRankings(String metric, int limit) throws Exception;
String verifyCredentials(String accountName, String password) throws Exception;
default String createGameAccount(String accountName, String password) throws Exception
{
return "TEMPORARY_FAILURE";
}
String unstuck(String accountName, int characterId) throws Exception;
JsonArray searchCatalog(String term, int limit);
JsonArray searchSkillCatalog(String term, int limit);
GrantResult applyGrant(String commandId, JsonObject payload);
}The class must implement every abstract interface method. Methods that are not available in the target source must return a safe, non-mutating result and their capability must be omitted. The capability reference defines each method, result, and payload.
Connection lifecycle
PortalConfig.load()readsconfig/portal.properties.PortalConnector.start(bridge)receives the bridge instance and runs itsinit().- The connector opens the configured WebSocket URL with the server token as a bearer credential.
- The connector sends a protocol v1 handshake.
- Heartbeats report connection state and online-player count.
- A closed or failed connection is retried with exponential backoff from 5 to 60 seconds.
The default heartbeat interval is 15 seconds. The gateway considers a connection stale when heartbeats stop arriving.
Handshake
The protocol v1 handshake contains the game type, connector version, and advertised capabilities:
{
"gameType": "LINEAGE_2",
"adapterVersion": "1.0.0",
"capabilities": ["VERIFY_GAME_CREDENTIALS", "GET_ONLINE_COUNT", "..."],
"deliveryWindows": { "DELIVER_ITEM_GRANT": "both" }
}deliveryWindows appears only when portal.properties declares
DeliveryWindow.* overrides; a handshake without it means the defaults every
bridge has always had.
The protocol does not transmit pack, chronicle, or revision metadata. The
protocol field remains named adapterVersion, but it identifies the Forgeport
connector core version, not an aCis or Mobius adapter. Forgeport compares it
with the connector release catalog.
Grant idempotency
applyGrant receives a stable commandId. The bridge must persist command
state before changing game state:
new command → APPLYING → APPLIEDThe aCis and Mobius code examples store this journal in the portal_command
table.
- A replay of
APPLIEDreturnsALREADY_APPLIED. - An offline character returns
RETRYABLE_FAILURE / CHARACTER_OFFLINE; the command waits without polling until the matchingPLAYER_ONLINE_EVENT. - Input that cannot succeed — an unknown item, a taken name, a skill already
known — returns
REFUSEDfrom a check that ran before anything was written; the portal releases the player's reservation on it.PERMANENT_FAILUREkeeps meaning something went wrong, and a person looks. - A replay left in
APPLYINGreturnsRECONCILIATION_REQUIRED; it is not applied again automatically.
This journal is required for item delivery, skill delivery, rename, and gender change.