L2J

Complete Mobius integration

Complete type-safe Forgeport integration for Mobius CT 2.6 High Five

This is a single-file integration for stock Mobius CT 2.6 (High Five). It owns only Mobius SQL, SHA-1/Base64 credentials, template/player lookup and concrete mutations. The connector core owns capabilities, JSON, command parsing and the JDBC replay journal.

Stock CT 2.6 does not expose the complete skill map publicly, so this example does not register .skills(...), and SEARCH_SKILL_CATALOG is not active. Skill delivery still works through the public exact skill lookup. If the target revision has a public skill enumeration, register a Stream<GameSkill> and the capability appears automatically.

Copy this as one file, then change only the paths, columns, password rules, and pack calls that differ in your revision. Keep the Bridge domain types and the prepared Delivery.apply(...) boundary intact.

ForgeportIntegration.java

l2j/mobius-ct26-highfive/ForgeportIntegration.java on GitHub

Create java/org/l2jmobius/gameserver/portal/ForgeportIntegration.java:

ForgeportIntegration.java
package org.l2jmobius.gameserver.portal;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLIntegrityConstraintViolationException;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import org.l2jmobius.commons.database.DatabaseFactory;

import org.l2jmobius.gameserver.data.sql.CharInfoTable;
import org.l2jmobius.gameserver.data.xml.ItemData;
import org.l2jmobius.gameserver.data.xml.NpcData;
import org.l2jmobius.gameserver.data.xml.SkillData;
import org.l2jmobius.gameserver.model.World;
import org.l2jmobius.gameserver.model.actor.Player;
import org.l2jmobius.gameserver.model.actor.enums.creature.Race;
import org.l2jmobius.gameserver.model.item.ItemTemplate;
import org.l2jmobius.gameserver.model.item.enums.ItemGrade;
import org.l2jmobius.gameserver.model.item.enums.ItemProcessType;
import org.l2jmobius.gameserver.model.skill.Skill;

import net.forgeport.connector.Bridge;
import net.forgeport.connector.CharacterServices;
import net.forgeport.connector.Delivery;
import net.forgeport.connector.Forgeport;
import net.forgeport.connector.PortalConfig;
import net.forgeport.connector.model.CharacterSkill;
import net.forgeport.connector.model.CreateAccountResult;
import net.forgeport.connector.model.CredentialsResult;
import net.forgeport.connector.model.DeliveryTarget;
import net.forgeport.connector.model.GameCharacter;
import net.forgeport.connector.model.GameItem;
import net.forgeport.connector.model.GameRace;
import net.forgeport.connector.model.GameSex;
import net.forgeport.connector.model.GenderGrant;
import net.forgeport.connector.model.ItemGrant;
import net.forgeport.connector.model.RankingMetric;
import net.forgeport.connector.model.RenameGrant;
import net.forgeport.connector.model.SkillGrant;
import net.forgeport.connector.model.UnstuckResult;

public final class ForgeportIntegration
{
	private static final String CHARACTER_COLUMNS = "c.charId, c.char_name, c.level, c.classid, c.race, c.sex, c.online, c.pvpkills, c.pkkills, cl.clan_name";
	private static final String CHARACTER_JOIN = "characters c LEFT JOIN clan_data cl ON cl.clan_id = c.clanid";
	private static final GameRace[] RACES =
	{
		GameRace.HUMAN,
		GameRace.ELF,
		GameRace.DARK_ELF,
		GameRace.ORC,
		GameRace.DWARF,
		GameRace.KAMAEL
	};

	public static void start()
	{
		Forgeport.start(Bridge.builder()
			.database(DatabaseFactory::getConnection)
			.onlineCount(() -> World.getInstance().getPlayers().size())
			.credentials(ForgeportIntegration::verifyCredentials)
			.accountCreation(ForgeportIntegration::createAccount)
			.characters(ForgeportIntegration::listCharacters)
			.characterSkills(ForgeportIntegration::listCharacterSkills)
			.rankings(ForgeportIntegration::listRankings)
			.items(ForgeportIntegration::items)
			.itemDelivery(ForgeportIntegration::prepareItemDelivery)
			.skillDelivery(ForgeportIntegration::prepareSkillDelivery)
			.characterServices(CharacterServices.of(
				ForgeportIntegration::prepareRename,
				ForgeportIntegration::prepareGenderChange))
			.unstuck(ForgeportIntegration::unstuck)
			.playerOnlineEvents()
			.build());
	}

	public static void shutdown()
	{
		Forgeport.shutdown();
	}

	public static void playerOnline(Player player)
	{
		Forgeport.playerOnline(player.getAccountName(), player.getObjectId());
	}

	private static CredentialsResult verifyCredentials(String account, String password) throws Exception
	{
		try (Connection con = DatabaseFactory.getConnection();
			PreparedStatement ps = con.prepareStatement("SELECT password, accessLevel FROM accounts WHERE login = ?"))
		{
			ps.setString(1, account);
			try (ResultSet rs = ps.executeQuery())
			{
				if (!rs.next()) return CredentialsResult.NOT_FOUND;
				if (rs.getInt("accessLevel") < 0) return CredentialsResult.BLOCKED;
				return hash(password).equals(rs.getString("password"))
					? CredentialsResult.VALID
					: CredentialsResult.INVALID;
			}
		}
	}

	private static CreateAccountResult createAccount(String account, String password) throws Exception
	{
		if (account == null || !account.matches("^[a-z0-9]{4,14}$"))
			return CreateAccountResult.INVALID_NAME;
		try (Connection con = DatabaseFactory.getConnection();
			PreparedStatement ps = con.prepareStatement("INSERT INTO accounts (login, password) VALUES (?, ?)"))
		{
			ps.setString(1, account);
			ps.setString(2, hash(password));
			ps.executeUpdate();
			return CreateAccountResult.CREATED;
		}
		catch (SQLIntegrityConstraintViolationException e)
		{
			return CreateAccountResult.ALREADY_EXISTS;
		}
	}

	private static String hash(String password) throws Exception
	{
		final MessageDigest digest = MessageDigest.getInstance("SHA");
		return Base64.getEncoder().encodeToString(digest.digest(password.getBytes(StandardCharsets.UTF_8)));
	}

	private static List<GameCharacter> listCharacters(String account) throws Exception
	{
		try (Connection con = DatabaseFactory.getConnection();
			PreparedStatement ps = con.prepareStatement("SELECT " + CHARACTER_COLUMNS + " FROM " + CHARACTER_JOIN + " WHERE c.account_name = ? ORDER BY c.level DESC LIMIT 30"))
		{
			ps.setString(1, account);
			try (ResultSet rs = ps.executeQuery())
			{
				final Stream.Builder<GameCharacter> result = Stream.builder();
				while (rs.next()) result.add(character(rs));
				return result.build().toList();
			}
		}
	}

	private static Optional<List<CharacterSkill>> listCharacterSkills(String account, int characterId) throws Exception
	{
		final Player online = World.getInstance().getPlayer(characterId);
		if (online != null)
		{
			if (!account.equalsIgnoreCase(online.getAccountName())) return Optional.empty();
			final Stream.Builder<CharacterSkill> result = Stream.builder();
			for (Skill skill : online.getAllSkills())
				result.add(new CharacterSkill(skill.getId(), skill.getLevel()));
			return Optional.of(result.build().toList());
		}

		try (Connection con = DatabaseFactory.getConnection())
		{
			if (!ownsCharacter(con, account, characterId)) return Optional.empty();
			try (PreparedStatement ps = con.prepareStatement("SELECT s.skill_id, s.skill_level FROM character_skills s JOIN characters c ON c.charId = s.charId LEFT JOIN character_subclasses sub ON sub.charId = c.charId AND sub.class_id = c.classid WHERE s.charId = ? AND c.account_name = ? AND s.class_index = CASE WHEN c.classid = c.base_class THEN 0 ELSE COALESCE(sub.class_index, 0) END"))
			{
				ps.setInt(1, characterId);
				ps.setString(2, account);
				try (ResultSet rs = ps.executeQuery())
				{
					final Stream.Builder<CharacterSkill> result = Stream.builder();
					while (rs.next()) result.add(new CharacterSkill(rs.getInt("skill_id"), rs.getInt("skill_level")));
					return Optional.of(result.build().toList());
				}
			}
		}
	}

	private static List<GameCharacter> listRankings(RankingMetric metric, int limit) throws Exception
	{
		final String order = switch (metric)
		{
			case PVP -> "c.pvpkills DESC, c.level DESC";
			case PK -> "c.pkkills DESC, c.level DESC";
			case LEVEL -> "c.level DESC, c.pvpkills DESC";
		};
		try (Connection con = DatabaseFactory.getConnection();
			PreparedStatement ps = con.prepareStatement("SELECT " + CHARACTER_COLUMNS + " FROM " + CHARACTER_JOIN + " WHERE c.accesslevel = 0 ORDER BY " + order + " LIMIT ?"))
		{
			ps.setInt(1, limit);
			try (ResultSet rs = ps.executeQuery())
			{
				final Stream.Builder<GameCharacter> result = Stream.builder();
				while (rs.next()) result.add(character(rs));
				return result.build().toList();
			}
		}
	}

	private static GameCharacter character(ResultSet rs) throws Exception
	{
		final int characterId = rs.getInt("charId");
		final int raceId = Math.max(0, Math.min(RACES.length - 1, rs.getInt("race")));
		return new GameCharacter(
			characterId,
			rs.getString("char_name"),
			Math.max(1, Math.min(99, rs.getInt("level"))),
			rs.getInt("classid"),
			RACES[raceId],
			rs.getInt("sex") == 1 ? GameSex.FEMALE : GameSex.MALE,
			(World.getInstance().getPlayer(characterId) != null) || rs.getInt("online") == 1,
			Math.max(0, rs.getInt("pvpkills")),
			Math.max(0, rs.getInt("pkkills")),
			rs.getString("clan_name"));
	}

	private static boolean ownsCharacter(Connection con, String account, int characterId) throws Exception
	{
		try (PreparedStatement ps = con.prepareStatement("SELECT 1 FROM characters WHERE charId = ? AND account_name = ?"))
		{
			ps.setInt(1, characterId);
			ps.setString(2, account);
			try (ResultSet rs = ps.executeQuery()) { return rs.next(); }
		}
	}

	private static Stream<GameItem> items()
	{
		final Stream.Builder<GameItem> result = Stream.builder();
		for (ItemTemplate item : ItemData.getInstance().getAllItems())
		{
			if (item == null || item.getName() == null || item.getName().isEmpty()) continue;
			final ItemGrade grade = item.getItemGrade();
			result.add(new GameItem(
				item.getId(),
				item.getName(),
				item.getClass().getSimpleName(),
				grade == ItemGrade.NONE ? null : grade.name(),
				item.isStackable()));
		}
		return result.build();
	}

	private static Delivery prepareItemDelivery(DeliveryTarget target, ItemGrant grant)
	{
		final Player player = World.getInstance().getPlayer(target.requiredCharacterId());
		if (player == null) return Delivery.offline();
		if (!target.account().equalsIgnoreCase(player.getAccountName())) return Delivery.invalidTarget();
		final ItemTemplate template = ItemData.getInstance().getTemplate(grant.itemId());
		if (template == null) return Delivery.invalidItem();
		return Delivery.apply(() ->
		{
			if (template.isStackable())
			{
				player.addItem(ItemProcessType.REWARD, grant.itemId(), grant.count(), grant.enchantLevel(), player, true);
			}
			else
			{
				for (int i = 0; i < grant.count(); i++)
					player.addItem(ItemProcessType.REWARD, grant.itemId(), 1, grant.enchantLevel(), player, true);
			}
		});
	}

	private static Delivery prepareSkillDelivery(DeliveryTarget target, SkillGrant grant)
	{
		final Player player = World.getInstance().getPlayer(target.requiredCharacterId());
		if (player == null) return Delivery.offline();
		if (!target.account().equalsIgnoreCase(player.getAccountName())) return Delivery.invalidTarget();
		final int maxLevel = SkillData.getInstance().getMaxLevel(grant.skillId());
		if (maxLevel == 0 || grant.level() > maxLevel) return Delivery.invalidSkill();
		final Skill skill = SkillData.getInstance().getSkill(grant.skillId(), grant.level());
		if (skill == null) return Delivery.invalidSkill();
		final Skill known = player.getKnownSkill(grant.skillId());
		if (known != null && known.getLevel() >= grant.level())
			return refuse("SKILL_ALREADY_KNOWN");
		return Delivery.apply(() ->
		{
			player.addSkill(skill, true);
			player.sendSkillList();
		});
	}

	private static Delivery prepareRename(DeliveryTarget target, RenameGrant grant)
	{
		final Player player = World.getInstance().getPlayer(target.requiredCharacterId());
		if (player == null) return Delivery.offline();
		if (!target.account().equalsIgnoreCase(player.getAccountName())) return Delivery.invalidTarget();
		if (NpcData.getInstance().getTemplateByName(grant.newName()) != null)
			return refuse("RESERVED_CHARACTER_NAME");
		if (CharInfoTable.getInstance().doesCharNameExist(grant.newName()))
			return refuse("CHARACTER_NAME_TAKEN");
		return Delivery.apply(() ->
		{
			player.setName(grant.newName());
			CharInfoTable.getInstance().addName(player);
			player.storeMe();
			player.broadcastUserInfo();
		});
	}

	private static Delivery prepareGenderChange(DeliveryTarget target, GenderGrant grant)
	{
		final Player player = World.getInstance().getPlayer(target.requiredCharacterId());
		if (player == null) return Delivery.offline();
		if (!target.account().equalsIgnoreCase(player.getAccountName())) return Delivery.invalidTarget();
		if (player.getRace() == Race.KAMAEL)
			return refuse("KAMAEL_GENDER_LOCKED");
		final boolean female = grant.newSex() == GameSex.FEMALE;
		if (player.getAppearance().isFemale() == female)
			return refuse("GENDER_UNCHANGED");
		return Delivery.apply(() ->
		{
			if (female) player.getAppearance().setFemale();
			else player.getAppearance().setMale();
			player.storeMe();
			player.broadcastUserInfo();
		});
	}

	private static UnstuckResult unstuck(String account, int characterId) throws Exception
	{
		final boolean offlineInDb;
		try (Connection con = DatabaseFactory.getConnection();
			PreparedStatement ps = con.prepareStatement("SELECT online FROM characters WHERE charId = ? AND account_name = ?"))
		{
			ps.setInt(1, characterId);
			ps.setString(2, account);
			try (ResultSet rs = ps.executeQuery())
			{
				if (!rs.next()) return UnstuckResult.NOT_FOUND;
				offlineInDb = rs.getInt("online") == 0;
			}
		}
		if (World.getInstance().getPlayer(characterId) != null || !offlineInDb)
			return UnstuckResult.CHARACTER_ONLINE;
		try (Connection con = DatabaseFactory.getConnection();
			PreparedStatement ps = con.prepareStatement("UPDATE characters SET x = ?, y = ?, z = ? WHERE charId = ? AND account_name = ?"))
		{
			ps.setInt(1, PortalConfig.UNSTUCK_X);
			ps.setInt(2, PortalConfig.UNSTUCK_Y);
			ps.setInt(3, PortalConfig.UNSTUCK_Z);
			ps.setInt(4, characterId);
			ps.setString(5, account);
			return ps.executeUpdate() == 1 ? UnstuckResult.UNSTUCK : UnstuckResult.FAILED;
		}
	}

	private static Delivery refuse(String code)
	{
		return Delivery.refused(code);
	}

	private ForgeportIntegration()
	{
	}
}

The integration cannot execute a prepared mutation before the core writes APPLYING: Delivery.apply(...) only returns a closure. If that closure throws, or if APPLIED cannot be persisted afterwards, the result is reconciliation — never a false success.

Hooks

After game data and the world are ready:

ForgeportIntegration.start();

After player.setEnteredWorld() in EnterWorld:

ForgeportIntegration.playerOnline(player);

During shutdown:

ForgeportIntegration.shutdown();

portal.properties

game/config/portal.properties
Enabled=True
GatewayUrl=wss://wss.forgeport.net/
Token=gps_<game-server-id>.<secret>

#HeartbeatSeconds=15
#DeliveryWindow.DELIVER_ITEM_GRANT=online
#DeliveryWindow.DELIVER_SKILL_GRANT=online
#DeliveryWindow.CHARACTER_SERVICES=online
#DeliveryWindow.UNSTUCK_CHARACTER=offline
#Unstuck.X=82698
#Unstuck.Y=148638
#Unstuck.Z=-3473

Grant delivery requires the database user to create/migrate and read/write the core-owned portal_command table. Keep the server token out of version control.

Verify

Build and restart the GameServer, confirm Connected, and run Connector check. Then link a real test account and deliver one item online and one after login from the offline state.

On this page