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

ComponentResponsibility
PortalConnectorOutbound WebSocket connection, bearer authentication, handshake, heartbeat, reconnect, and message dispatch
PortalConfigLoads config/portal.properties
GameBridgePack-specific database reads, live-world access, credential operations, and grant application
GrantResultReports 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.

FieldPropertyMeaning
ENABLEDEnabledMaster switch; false leaves the connector off
GATEWAY_URLGatewayUrlWebSocket URL of the Forgeport gateway
SERVER_TOKENTokenBearer credential, gps_<game-server-id>.<secret>
GAME_SERVER_IDParsed out of the token; identifies this server on the wire
HEARTBEAT_SECONDSHeartbeatSecondsHeartbeat interval, default 15
DELIVERY_WINDOWSDeliveryWindow.<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_ZUnstuck.X/Y/ZWhere 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

  1. PortalConfig.load() reads config/portal.properties.
  2. PortalConnector.start(bridge) receives the bridge instance and runs its init().
  3. The connector opens the configured WebSocket URL with the server token as a bearer credential.
  4. The connector sends a protocol v1 handshake.
  5. Heartbeats report connection state and online-player count.
  6. 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 → APPLIED

The aCis and Mobius code examples store this journal in the portal_command table.

  • A replay of APPLIED returns ALREADY_APPLIED.
  • An offline character returns RETRYABLE_FAILURE / CHARACTER_OFFLINE; the command waits without polling until the matching PLAYER_ONLINE_EVENT.
  • Input that cannot succeed — an unknown item, a taken name, a skill already known — returns REFUSED from a check that ran before anything was written; the portal releases the player's reservation on it. PERMANENT_FAILURE keeps meaning something went wrong, and a person looks.
  • A replay left in APPLYING returns RECONCILIATION_REQUIRED; it is not applied again automatically.

This journal is required for item delivery, skill delivery, rename, and gender change.

On this page