Complete aCis integration
Complete type-safe Forgeport integration for aCis 409
This is a source example for a stock aCis 409. It contains only aCis database,
password, data-table and live-player knowledge. Forgeport protocol JSON,
capabilities, command replay and the portal_command journal are owned by the
connector core.
Verify every SQL column and game API against the exact source revision before deploying it.
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/acis-409/ForgeportIntegration.java on GitHub
Create
aCis_gameserver/java/net/sf/l2j/gameserver/portal/ForgeportIntegration.java:
package net.sf.l2j.gameserver.portal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import net.sf.l2j.commons.crypt.BCrypt;
import net.sf.l2j.commons.pool.ConnectionPool;
import net.sf.l2j.gameserver.data.SkillTable;
import net.sf.l2j.gameserver.data.sql.PlayerInfoTable;
import net.sf.l2j.gameserver.data.xml.ItemData;
import net.sf.l2j.gameserver.data.xml.NpcData;
import net.sf.l2j.gameserver.model.World;
import net.sf.l2j.gameserver.model.actor.Player;
import net.sf.l2j.gameserver.model.item.instance.ItemInstance;
import net.sf.l2j.gameserver.model.item.kind.Armor;
import net.sf.l2j.gameserver.model.item.kind.EtcItem;
import net.sf.l2j.gameserver.model.item.kind.Item;
import net.sf.l2j.gameserver.model.item.kind.Weapon;
import net.sf.l2j.gameserver.network.SystemMessageId;
import net.sf.l2j.gameserver.network.serverpackets.SkillList;
import net.sf.l2j.gameserver.network.serverpackets.SystemMessage;
import net.sf.l2j.gameserver.skills.L2Skill;
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.GameSkill;
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.obj_Id, c.char_name, c.level, c.classid, c.sex, c.race, 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 String SELECT_SKILLS = "SELECT s.skill_id, s.skill_level FROM character_skills s JOIN characters c ON c.obj_Id = s.char_obj_id LEFT JOIN character_subclasses sub ON sub.char_obj_id = c.obj_Id AND sub.class_id = c.classid WHERE s.char_obj_id = ? AND c.account_name = ? AND s.class_index = CASE WHEN c.classid = c.base_class THEN 0 ELSE COALESCE(sub.class_index, 0) END";
private static final Pattern VALID_LOGIN = Pattern.compile("^[a-z0-9]{4,45}$");
private static final GameRace[] RACES =
{
GameRace.HUMAN,
GameRace.ELF,
GameRace.DARK_ELF,
GameRace.ORC,
GameRace.DWARF
};
public static void start()
{
Forgeport.start(Bridge.builder()
.database(ConnectionPool::getConnection)
.onlineCount(() -> World.getInstance().getPlayers().size())
.credentials(ForgeportIntegration::verifyCredentials)
.accountCreation(ForgeportIntegration::createAccount)
.characters(ForgeportIntegration::listCharacters)
.characterSkills(ForgeportIntegration::listCharacterSkills)
.rankings(ForgeportIntegration::listRankings)
.items(ForgeportIntegration::items)
.skills(ForgeportIntegration::skills)
.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 = ConnectionPool.getConnection();
PreparedStatement ps = con.prepareStatement("SELECT password, access_level FROM accounts WHERE login = ?"))
{
ps.setString(1, account);
try (ResultSet rs = ps.executeQuery())
{
if (!rs.next()) return CredentialsResult.NOT_FOUND;
if (rs.getInt("access_level") < 0) return CredentialsResult.BLOCKED;
return BCrypt.checkPw(password, rs.getString("password"))
? CredentialsResult.VALID
: CredentialsResult.INVALID;
}
}
}
private static CreateAccountResult createAccount(String account, String password) throws Exception
{
if (!VALID_LOGIN.matcher(account).matches()) return CreateAccountResult.INVALID_NAME;
try (Connection con = ConnectionPool.getConnection();
PreparedStatement ps = con.prepareStatement("INSERT INTO accounts (login, password) VALUES (?, ?)"))
{
ps.setString(1, account);
ps.setString(2, BCrypt.hashPw(password));
ps.executeUpdate();
return CreateAccountResult.CREATED;
}
catch (SQLException e)
{
if ("23000".equals(e.getSQLState()) || e.getErrorCode() == 1062)
return CreateAccountResult.ALREADY_EXISTS;
throw e;
}
}
private static List<GameCharacter> listCharacters(String account) throws Exception
{
try (Connection con = ConnectionPool.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();
return Optional.of(online.getSkills().values().stream()
.map(skill -> new CharacterSkill(skill.getId(), skill.getLevel()))
.toList());
}
try (Connection con = ConnectionPool.getConnection())
{
if (!ownsCharacter(con, account, characterId)) return Optional.empty();
try (PreparedStatement ps = con.prepareStatement(SELECT_SKILLS))
{
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 = ConnectionPool.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 raceId = Math.max(0, Math.min(RACES.length - 1, rs.getInt("race")));
return new GameCharacter(
rs.getInt("obj_Id"),
rs.getString("char_name"),
Math.max(1, Math.min(99, rs.getInt("level"))),
rs.getInt("classid"),
RACES[raceId],
rs.getInt("sex") == 0 ? GameSex.MALE : GameSex.FEMALE,
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 obj_Id = ? 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 (Item item : ItemData.getInstance().getTemplates())
{
if (item == null || item.getName() == null || item.getName().isEmpty()) continue;
result.add(new GameItem(
item.getItemId(), item.getName(), itemType(item), itemGrade(item), item.isStackable()));
}
return result.build();
}
private static Stream<GameSkill> skills()
{
final Stream.Builder<GameSkill> result = Stream.builder();
for (L2Skill skill : SkillTable.getInstance().getSkills())
{
if (skill == null || skill.getLevel() >= 99) continue;
final int maxLevel = SkillTable.getInstance().getMaxLevel(skill.getId());
if (maxLevel > 0) result.add(new GameSkill(skill.getId(), skill.getName(), maxLevel));
}
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 (!player.getAccountName().equalsIgnoreCase(target.account())) return Delivery.invalidTarget();
if (ItemData.getInstance().getTemplate(grant.itemId()) == null) return Delivery.invalidItem();
return Delivery.apply(() -> grantItems(player, grant));
}
private static Delivery prepareSkillDelivery(DeliveryTarget target, SkillGrant grant)
{
final Player player = World.getInstance().getPlayer(target.requiredCharacterId());
if (player == null) return Delivery.offline();
if (!player.getAccountName().equalsIgnoreCase(target.account())) return Delivery.invalidTarget();
final L2Skill skill = SkillTable.getInstance().getInfo(grant.skillId(), grant.level());
if (skill == null) return Delivery.invalidSkill();
final L2Skill known = player.getSkill(grant.skillId());
if (known != null && known.getLevel() >= grant.level())
return refuse("SKILL_ALREADY_KNOWN");
return Delivery.apply(() ->
{
player.addSkill(skill, true);
player.sendPacket(new SkillList(player));
player.sendPacket(SystemMessage.getSystemMessage(SystemMessageId.LEARNED_SKILL_S1).addSkillName(skill));
});
}
private static Delivery prepareRename(DeliveryTarget target, RenameGrant grant)
{
final Player player = World.getInstance().getPlayer(target.requiredCharacterId());
if (player == null) return Delivery.offline();
if (!player.getAccountName().equalsIgnoreCase(target.account())) return Delivery.invalidTarget();
if (NpcData.getInstance().getTemplateByName(grant.newName()) != null)
return refuse("RESERVED_CHARACTER_NAME");
final int existingId = PlayerInfoTable.getInstance().getPlayerObjectId(grant.newName());
if (existingId > 0 && existingId != player.getObjectId())
return refuse("CHARACTER_NAME_TAKEN");
return Delivery.apply(() ->
{
player.setName(grant.newName());
PlayerInfoTable.getInstance().updatePlayerData(player, false);
player.store();
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 (!player.getAccountName().equalsIgnoreCase(target.account())) return Delivery.invalidTarget();
final net.sf.l2j.gameserver.enums.actors.Sex wanted = grant.newSex() == GameSex.MALE
? net.sf.l2j.gameserver.enums.actors.Sex.MALE
: net.sf.l2j.gameserver.enums.actors.Sex.FEMALE;
if (player.getAppearance().getSex() == wanted)
return refuse("GENDER_UNCHANGED");
return Delivery.apply(() ->
{
player.getAppearance().setSex(wanted);
player.store();
player.broadcastUserInfo();
});
}
private static UnstuckResult unstuck(String account, int characterId) throws Exception
{
try (Connection con = ConnectionPool.getConnection())
{
try (PreparedStatement ps = con.prepareStatement("SELECT online FROM characters WHERE account_name = ? AND obj_Id = ?"))
{
ps.setString(1, account);
ps.setInt(2, characterId);
try (ResultSet rs = ps.executeQuery())
{
if (!rs.next()) return UnstuckResult.NOT_FOUND;
if (rs.getInt("online") == 1) return UnstuckResult.CHARACTER_ONLINE;
}
}
try (PreparedStatement ps = con.prepareStatement("UPDATE characters SET x = ?, y = ?, z = ? WHERE obj_Id = ?"))
{
ps.setInt(1, PortalConfig.UNSTUCK_X);
ps.setInt(2, PortalConfig.UNSTUCK_Y);
ps.setInt(3, PortalConfig.UNSTUCK_Z);
ps.setInt(4, characterId);
return ps.executeUpdate() == 1 ? UnstuckResult.UNSTUCK : UnstuckResult.FAILED;
}
}
}
private static void grantItems(Player player, ItemGrant grant)
{
final Item template = ItemData.getInstance().getTemplate(grant.itemId());
if (grant.enchantLevel() <= 0 || template.isStackable())
{
player.addItem(grant.itemId(), grant.count(), true);
return;
}
for (int i = 0; i < grant.count(); i++)
{
final ItemInstance created = player.addItem(grant.itemId(), 1, false);
if (created == null) continue;
created.setEnchantLevel(grant.enchantLevel(), player);
player.sendPacket(SystemMessage.getSystemMessage(SystemMessageId.OBTAINED_S1_S2)
.addString(grant.enchantLevel() + " ").addItemName(created));
}
}
private static Delivery refuse(String code)
{
return Delivery.refused(code);
}
private static String itemType(Item item)
{
if (item instanceof Weapon) return "Weapon";
if (item instanceof Armor) return "Armor";
if (item instanceof EtcItem) return "EtcItem";
return "Item";
}
private static String itemGrade(Item item)
{
try { return item.getCrystalType().name(); }
catch (Exception ignored) { return null; }
}
private ForgeportIntegration()
{
}
}The integration never receives JSON, capability names, command ids, order ids
or journal states. Delivery.apply(...) captures the aCis mutation; the core
writes APPLYING before invoking it and writes APPLIED only after it returns.
Hooks
Start after world and data initialization:
ForgeportIntegration.start();After player.spawnMe() in EnterWorld:
ForgeportIntegration.playerOnline(player);During shutdown:
ForgeportIntegration.shutdown();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=-3473The game database user needs CREATE, ALTER, SELECT, INSERT and UPDATE
rights on the core-owned portal_command table. Keep the 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.