Reading the client
How to find the class and member you want, and what the client does with it. Use these in order:
| Question | Where the answer is |
|---|---|
| What is this member called? | phantom.flash.search('chatHist') |
| What does this class hold? | phantom.flash.describe('gui.ChatHistory') |
| What type is it, and what does it take? | the declarations, in your editor |
| What does the client do with it? | Animal Jam's own ActionScript, decompiled (the rest of this page) |
The first three take minutes. Skip the fourth and you can lose hours.
Finding a name
phantom.flash.search reads the hook table that every client Phantom serves includes. It lists every method, getter and setter in the client, with the parameter names from Animal Jam's source and the name to pass to hook and override.
for (const found of await phantom.flash.search('addMessage')) {
console.log(found.hook, found.parameters.map((each) => each.name).join(', '));
}A query with a dot is read as class and member, so you can paste a name straight from the client's source. Narrow it with inClass, kind, isStatic and limit.
await phantom.flash.search('ChatHistory.add');
await phantom.flash.search('color', { inClass: 'AvatarEditor', kind: 'setter' });What a class holds
phantom.flash.describe combines the hook table with the running game. The table has the methods, getters and setters, with real parameter names and hook names. The game, asked through describeType, has the fields, which have no hook because there's no call to catch.
const chat = await phantom.flash.describe('gui.ChatHistory');
for (const member of chat.members) {
console.log(member.kind, member.name, member.type, member.hooks.join(' '));
}| Field on a member | What it tells you |
|---|---|
kind | method, property or constructor |
hooks | the names to give hook and override; empty for a plain field |
parameters | named as Animal Jam's own source names them, where the table knows them |
access | read, write or readwrite for a property |
from | hooks, live, or both: a live-only member is a field or is inherited |
namespace | set when the member isn't public |
Pass a place instead of a name and the game tells you what the object there really is. Use this when the declarations type something as Object or *.
const hud = phantom.flash.use('gui.GuiManager').mainHud;
const described = await phantom.flash.describe(hud.$at('chatHistTxt'));
console.log(described.liveClassName);live: false answers from the table alone, with no round trip and no running game.
describe describes a class. place.$describe() describes one object, raw, exactly as Flash's describeType returns it.
How a piece of the screen is built
Before changing part of the screen you need to know how it's put together: which child is the button, which of its states is showing, which part is a timeline that will put its art back, and where everything sits. phantom.flash.inspect reads a whole display tree, one message per level, and returns it as plain data. phantom.aj.ui.inspect does the same from a root you name.
const card = await phantom.aj.ui.inspect('buddyCard', { depth: 4, bounds: true });
console.log(card.outline());
const ear = card.find((node) => node.name === 'gameTabBtn');Each node has:
name, dottedclassName,visible,alpha, position, scale and sizeframes(current, total, label and labels) for a movie cliptextfor a text fieldbounds: in the root's space withbounds: true, the stage's with'stage', or a place's- its child
path timeline: whether its parent holds it as a member under its name, which Flash does for every named object a timeline places. Don't rename those: Flash refuses with Error #2078. Tell them apart by handle instead, or put them in aSpriteof your own.
Roots aj.ui.inspect accepts: every spot ui.places() lists ('buddyCard', 'hud', 'gui', 'room', 'stage', …), { window: 'gui.GuiDenShop' } for the first open object of a class, or any place. A root that isn't open gives NOT_READY, saying which. handles: true keeps each node's handle so you can act on it; release() the inspection afterwards. The Developer section's Layout tab shows the same tree live.
Reading Animal Jam's own source
None of the above tells you what the client does with a member. Only the client's ActionScript does. One command fetches and decompiles the build Phantom serves:
bun run --cwd packages/client-types decompileIt writes ~/.cache/phantom-dev/client-<deploy>-src/scripts, one .as file per class, laid out the same as packages/client-types/src/client, so gui/ChatHistory.as matches gui/ChatHistory.ts.
| Flag | What it does |
|---|---|
| none | the deploy Phantom has served most recently, read off its patched clients |
--deploy 1829 | a build by number |
--force | download and decompile again |
It needs Java on the path and downloads JPEXS once into the same cache. PHANTOM_FFDEC_JAR points at a copy you already have, and PHANTOM_DEV_CACHE moves the cache. The decompile takes a few minutes, once per client build.
packages/client-types/scripts/generate.ts --source reads the same tree to check against the bytecode, and packages/sdk/scripts/client-snapshot.ts reads it for the coverage report.
Things only the source tells you
Some examples from work on the chat panel. Each was settled by one line of .as, and none shows in the declarations.
A field is built by concatenation, unescaped. gui/ChatHistory.as, addMessage:
_chatText.htmlText += "<a href=\"event:" + param2 + "\">"
+ (Utility.isSettingOn(MySettings.SETTINGS_USERNAME_BADGE) ? param3 : param1)
+ ": " + "</a>" + param4 + "\n";From this:
- The client writes the link around the account name itself, so never write that field yourself. The hundred-line trim counts what the client wrote, and the scrollbar follows the trim.
- The name and the message are joined raw, so markup in either is drawn as markup.
- The client picks between two of the four arguments based on a player setting, so changing only one of them changes whichever the player isn't seeing.
A stylesheet disables <font>. The same file gives that field a StyleSheet with a single a:hover rule. A Flash text field with a stylesheet ignores <font> completely, so colours drew nothing however they were written. Only that one assignment explains it.
Emotes are library symbols. gui/EmoticonUtility.as keeps its emotes as linkage names built with getDefinitionByName, so <img src="emo_blush"> draws one. The declarations only say emoteForId(emoteId: number): Sprite.
A list is drawn a hundred at a time. gui/PetInventory.as, updateActivePet, reads _itemWindows.bg.getChildAt(_idx) from a list that only holds the windows loaded so far. A player with more pets than that gets an error part way through closing the screen.
And three things the game deliberately doesn't do:
The chat box is unselectable on purpose. Flash text fields are selectable by default. gui/ChatHistory.as sets _chatText.selectable = false in resetChatPrivs and again in enableFreeChat, whatever the chat setting, which is why players can't copy a trade out of it. resetChatPrivs also sets htmlText = "" when the player has free chat, and setupInitialItems ends by calling it. That's why the scrollback is empty after every room change: GuiManager calls chatHist.reload(...) for each room.
Nothing in the chat path checks the block list. BuddyManager.isBlocked is checked in the buddy list rows (gui/itemWindows/ItemWindowBuddyList.as) and on name bars, but not in addMessage. A blocked player's messages still show in the chat box.
The tooltip takes label and value pairs. gui/ToolTipPopup.as, setToolTipText, splits the text on |. The first part is the bubble's own line, and the rest is read in pairs into att1/value1 and att2/value2 by a loop that runs exactly twice. So you get one line and at most two rows, and an odd number of parts leaves a row with no value.
Where the source misleads
It doesn't replace the declarations, and two things in it are wrong:
- Parameter names are made up. JPEXS writes
param1,param2and so on for 7,919 of the 7,924 parameter lists in the 1829 tree, because the real names are in the bytecode'smethod_infoand the decompiler doesn't restore them. The real names are in the declarations,phantom.flash.searchandcall.args. Read the source for the body and the declarations for the signature. - It's a decompile, not the source. Compiler-generated code shows up as if written by hand, default arguments appear as
param1 || new Thing()before thesuper()call, and the odd body doesn't decompile cleanly. The bytecode is the truth, which is why nothing is generated from the.astree.
How to read it
Start from the screen, not the class. Find the file in gui/, read its init to see what it builds, then find the Xt class in the same part of the game to see what it sends the server.
SRC=~/.cache/phantom-dev/client-1829-src/scripts
# Which class holds the chat history, and what it does with it.
grep -rln "chatHist" "$SRC/gui"
grep -n "htmlText\|styleSheet" "$SRC/gui/ChatHistory.as"
# Everything that calls a method you're about to override.
grep -rn "addMessage(" "$SRC"
# What a screen sends the server.
grep -rn "sendXtMessage" "$SRC/avatar"When you find the line that explains something, put it in a comment where your code depends on it, naming the class and method, so the next person doesn't have to decompile anything.
Putting it together
phantom.flash.search('partOfTheName')to find the member and its hook name.phantom.flash.describe('the.Class')to see what else is on it, fields included, andphantom.flash.inspect(place)to see how a piece of the screen is built.- The declarations, in your editor, for the types and real parameter names.
- The
.asfile for what the client does with it, before you write code against it. hookto watch it, thenoverrideto take it over.
import { definePlugin } from '@phantomclient/plugin';
export default definePlugin({
async setup(ctx) {
const phantom = ctx.phantom();
const [found] = await phantom.flash.search('ChatHistory.addMessage');
console.log(
found?.hook,
found?.parameters.map((each) => each.name)
);
const stop = await phantom.flash.hook(
'gui.ChatHistory.addMessage',
{ capture: { args: true } },
(call) => console.log(call.args['userName'], 'said', call.args['chatMessage'])
);
return () => void stop();
},
});