Capabilities

LIST_CHARACTER_SKILLS

What a character already knows, so a level it holds is never sold

Answers one question about one character: which skills does it already have, and at what level.

Forgeport uses it in the purchase dialog. A skill the character holds at the bought level or higher is refused there, before any money moves, instead of being sold and then declined at delivery.

The query
{ "normalizedAccountName": "robert", "characterId": 268485666 }
The answer
{ "outcome": "OK", "skills": [{ "skillId": 3, "level": 9 }, { "skillId": 1040, "level": 3 }] }
OutcomeMeaning
OKThe list is complete, empty included
NOT_FOUNDThat account does not own that character
FAILEDThe question could not be answered

Levels only. The names a player reads come from SEARCH_SKILL_CATALOG, which the portal has already loaded — repeating them here would be a second source for the same string.

An empty list and a failed query are different answers

An empty OK means the character has learned nothing, and the portal sells accordingly. FAILED means nobody knows, and the portal sells exactly as it did before this capability existed, leaving the refusal to delivery time.

Never report an empty list for a query that broke. That is the one way to turn this capability into the problem it exists to prevent: the sale goes through, the delivery is refused, and the player has paid for a level they already had.

The active class only

character_skills keeps a row per class index, and a subclass has its own skills. Answer for the class the character is currently on. A list covering every class refuses a purchase the player could legitimately make on the class they are playing.

Implementing it

JsonArray listCharacterSkills(String accountName, int characterId) throws Exception;

Return null when that account does not own that character; the core reports NOT_FOUND. Null and an empty array are different answers, and a fresh character legitimately knows nothing.

Read from the live character when there is one, and from the database otherwise. A skill learned this session is not written to character_skills until the character is saved, and selling it in the meantime is the exact purchase this query exists to prevent:

final Player online = World.getInstance().getPlayer(characterId);
if ((online != null) && accountName.equalsIgnoreCase(online.getAccountName()))
{
    // straight from memory, which is the only copy that is current
}

The database branch filters on the class index so it answers about the class the character is on. Neither reference schema stores that index on characters — Mobius CT 2.6 and aCis both keep only classid and base_class there — so it is derived: 0 while the character is on its base class, otherwise the class_index of the character_subclasses row whose class_id matches the current classid:

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

Column names follow the pack — aCis spells the identifier obj_Id on characters and char_obj_id on character_skills and character_subclasses. Verify both against your revision; a column this query cannot find fails every offline lookup, and the FAILED it turns into is easy to misread as the server being busy.

Verification

Connector check does not exercise this one. Confirm it in the purchase dialog: pick a character that holds a skill and watch the portal decline that level before payment. A wrong answer is visible there and nowhere else.

On this page