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.
{ "normalizedAccountName": "robert", "characterId": 268485666 }{ "outcome": "OK", "skills": [{ "skillId": 3, "level": 9 }, { "skillId": 1040, "level": 3 }] }| Outcome | Meaning |
|---|---|
OK | The list is complete, empty included |
NOT_FOUND | That account does not own that character |
FAILED | The 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) ENDColumn 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.