Dev tools
Turn on Developer mode and the Developer section works on whatever game you're playing, with no plugin or dev server needed:
- Inspector. Pick anything in the game and see what it is, where it sits, its current values, and code that reaches it.
- Console. Run TypeScript against the game with
phantom,flash,aj,packetsand$0bound, with completions from the SDK's types. - Events. SDK events you trace from the console, and the game's packets, live.
phantom-plugin dev also gives a plugin in development hot reload for its logic as well as Vue, and its own Dev tools panel that runs the same inspector and console with the plugin's permissions.
The Developer section
Opening it
- Open Plugins in the sidebar and press Developer mode at the top right.
- Switch Developer mode on.
- A Developer row appears in the sidebar under Extend, and a Developer button on the game's toolbar. Either opens it in the pane beside the game, so the game stays on screen.
It has three tabs: Inspector, Console and Events. It runs in the same sandbox as Scripts, with full access to the game and no permissions to ask for, since it's your tool rather than a plugin. Like a script, it has no Node and can't reach outside its game.
Inspector
The inspector works like the Elements panel in a browser's dev tools, for the game's display list. The display tree is on the left, and the details of the selection are on the right in tabs, with the Watch strip under them. Drag the divider to resize either side.
Picking
Press Pick, or Ctrl+Shift+C (in the game or in Phantom's window). While picking, a clear shield covers the stage so the game gets no mouse events, and pointing at a button doesn't press it. What's under the pointer is outlined, the rest of the stage is dimmed, and a label shows its class, name, size on the stage and position in the stack there: ChatButton "chatBtn" 60×24 2/4.
Flash reports every object whose bounding box contains the point, so picking checks what's actually drawn there, topmost first, and takes the first match:
- hidden objects (
visiblefalse on it or any parent) and faded ones (combined alpha under 0.05) are skipped - a bitmap (the client's
LayerBitmap, which every avatar and room is painted into) only counts where its pixel is at least slightly opaque, so a transparent part of a room-sized picture lets the animal underneath through - a shape only counts inside its outline (
hitTestPointwith the shape flag), and a text field anywhere in its box
From that object and its parents, picking offers the most useful one (an avatar, a button, one of the client's own classes, or something the game named) rather than the bitmap or label it's drawn with. It never offers a container: a room layer, a manager, or anything covering most of the stage (60% of it, or 90% of its width or height), such as GamePlay or the HUD bar. Over bare ground it offers the room's layer. An avatar is labelled with its player: AvatarWorldView "localhost". Then:
- Click selects the one offered; the tree opens down to it from the stage and shows it.
- The wheel, or [ and ], moves out to its parents and back in (a button's label too).
- Alt+click selects the parent of the one offered.
- Right-click freezes and lists Everything here: every distinct thing drawn at that point, topmost first, as
Class "name" W×Hwith what part of it is drawn there. Hover a row to outline it in the game, click it to select it. Use this to reach something underneath something else (an avatar under a HUD button). Right-click again to follow the pointer. - Escape or Ctrl+Shift+C stops picking.
While you pick, the stack under the pointer shows under the inspector's toolbar. After a pick, the Everything here button (the layers icon) lists what's drawn at that point, and its rows select like tree rows.
Copy pick report (the clipboard icon) copies as JSON what the game had at the last pick's point: every leaf getObjectsUnderPoint returned (up to 30), its parents with their sizes, visibility and alpha, how each was tested and why it was or wasn't taken, and what was offered. While Developer mode is on, every pick writes the same report to the games log (games.log in Phantom's logs folder, event devtools-pick), so a wrong pick can be explained afterwards.
The display tree
The stage is at the top, then the client, its layers and their objects, loaded a level at a time as you open them. Each row is Class "name", with the class's package dimmed. Hover a row to outline its object in the game; click to select it. The eye beside a row toggles the object's visible.
The keys work as in a browser: ↑ and ↓ move a row, → opens a node or goes into it, ← closes it or goes up to its parent, Home and End go to the ends and Enter selects the row. The filter box narrows the tree to nodes whose class or name contains what you type, keeping the path to each open. The breadcrumb under the tree shows the selection's path from the stage; click any part to go there.
Drag a row onto another to move its object in the game, like moving a node in Elements: onto the middle of a row puts it inside that object, on top of its children; onto the top or bottom edge puts it before or after it in the same parent. Every move can be undone (see Changes).
Layout
The Layout tab beside the Inspector shows a whole piece of the screen at once. Pick a root (the open Buddy card, the HUD, the Windows layer, the Room or the Stage), or type a Class (its first open object) or a Path (stage:child.name, or a class and its members: buddy.BuddyManager:_buddyCard._buddyCardMC). Every object under it is listed: its name and class, timeline if its parent's timeline placed it (Flash won't rename those), hidden, its frame and label, its text, and its size and position on the stage.
Hover a row to outline its object in the game. Click it to see every field below the tree; double-click it, or press its crosshair, to open it in the Inspector. Live refreshes every second, so you can watch a button's frames change on hover or a panel slide out. The copy button copies the tree as text, one object per line. In code it's phantom.aj.ui.inspect.
Properties
Every property of the selected object, refreshed every second, in three groups: Display (x, y, width, height, scaleX, scaleY, rotation, alpha, visible), the class's own members, and inherited ones. Values are coloured by type, and changed values flash.
Edit values in place. Drag a number left and right to scrub it, or step it with ↑ and ↓ (Shift for ten steps, Alt for a tenth); ratios like alpha step by a hundredth. Booleans are toggles, colours (textColor, nameColor, a filter's color) have a swatch that opens the colour picker, and members that take one of a few strings (blendMode, a text field's autoSize) are a list. Anything else takes what you type, read as JSON (3, true, "text", null, [1, 2]) or as plain text otherwise; $0 to $4 and $_ on their own write the console's objects. The filter box narrows the list by name or value, and the pin on a row adds it to the Watch strip.
A property holding an object expands with the arrow beside it, like in a browser's dev tools: its members, an array's items or a plain object's keys, all live and editable. Flash returns copies of filters, transform.colorTransform, scrollRect and a colour matrix's matrix, so changing a field of one does nothing by itself; the inspector writes the whole copy back for you. Arrays and plain objects end with Add item or Add property, and their rows can be removed (the key is set to undefined and made non-enumerable, since ActionScript's delete can't be reached from outside the game). Add property at the end of Own members adds a property the class doesn't list, on a dynamic class such as MovieClip.
Beside each row: the crosshair selects the object a property holds, the pulse logs every change to it in the change log, and the copy button copies its path as code ($0.filters[0].blurX).
Find on screen (the lens beside a string or number) lists the text fields on screen whose text contains that value, with their class, name, position and surrounding text. Hover a row to outline it; click it to select it. Use it when an edit seems to do nothing: writing a data field such as gamePlayFlow.GamePlay._loginUsername changes the data, but the name over your avatar is a text field filled in once, so edit that field's text instead. After a write to a data field (a member of an object that isn't drawn, or a private _field), the inspector points this out beside it, with a button to find what shows it.
Display
How the selected object is drawn and positioned, all editable:
- Timeline, for a movie clip: its current frame and frame count, play and stop, step a frame either way, a scrubber to any frame (it stops there; Play from here plays on), and its labels, each of which you can go to and stop or play from.
- Drawing:
alpha,blendMode, and the switchesvisible,mouseEnabled,mouseChildren,buttonModeandcacheAsBitmap. - Colour: a tint (a colour and how much of it), or the colour transform's eight numbers, and Reset for Flash's own colours.
- Filters: every filter on it, in order, each expandable to edit, reorder or remove; Add filter adds a glow, drop shadow, blur, colour matrix or bevel.
- Clip: a
scrollRectthe size of the object, narrowed from there, or none. - Place: its depth among its parent's children (to the back, a step back, a step forward, to the front), Move on stage, and Remove from parent (with Put back while it's removed). Move on stage covers the game with a shield, like picking: drag anywhere and the object follows; let go to drop it, or press Escape to put it back.
- Make: a coloured box or a line of text, created inside the selected object (or beside it, in its parent) and selected. Objects the inspector makes are named
phantom_made.
Methods
The class's methods with their signatures (addMessage(text:String, color?:uint):void), its own first, with static ones marked.
- Call… opens a form with a field for each parameter, named and typed as the client declares it. Type each as JSON, or
$0to$4and$_for the console's objects; empty optional parameters at the end are left off. A static method runs on its class. The return value expands like any value, an object in the game opens in the inspector, and it's$_in the console afterwards. - Hook… sets what a hook does on every call of the method: log it (the object, its arguments by name and the return value), replace any argument, replace the return value, or skip the client's method (returning a value you choose), on every object of the class or only the selected one. The loader does this inside the call. A hooked method shows how many calls it has matched. The call log is under the list, with pause and clear, and Copy as hook code for the same hook in a plugin.
Events
Pick an event (click, mouseDown, added, removed, enterFrame…) and Listen to log it on the selected object, with its phase, target and pointer position. Only plain values are read from each event, so logging holds no references in the game. Stop a listener from its chip.
Geometry
The selected object's box model: its position and size in its parent, its scale and rotation, and the area it covers on the stage, in stage pixels. Highlight makes it glow briefly, Hide hides it until you press it again, and Snapshot shows a picture of it, drawn by the game, to copy.
Code
Code to reach the selected object, ready to paste: Access (typed access by its stable path, or a search), Find, UI rule, Attach, Replace, Hook and Watch. Each is highlighted and editable before you copy it, and Run in console runs it there.
The stable path is the shortest route to the object from something the client keeps permanently (a class's static, the client or gMainFrame), such as phantom.flash.use('gui.GuiManager').mainHud.chatBar. A display list path (instance40) is no use in code, since Flash renames those each time the screen is built. If nothing holds the object, the code searches for its class and name.
A UI rule matches the object now and whenever the game draws it again. One of the game's own classes is enough; a Flash class (MovieClip, TextField) would match half the screen, so the rule is narrowed to the nearest reachable parent and the object's name:
const stop = await flash.ui.rule(
{ ofClass: 'flash.display.MovieClip', where: flash.use('gui.GuiManager').mainHud },
async (el) => {
await el.hide();
},
{ unless: async (el) => (await el.name.$get()) !== 'closeBtn' }
);Open in console binds the selection as $0, like a browser's dev tools.
Classes
The library button in the toolbar opens the class browser: every class the game has loaded (from its application domain, or the hook table where that isn't available), searchable by any part of the name (gui manager), with the game's own first and gMainFrame at the top. A class opens to its current static side: static properties, as a tree you can expand and edit like Properties, static methods to call and hook, and Inspect .instance where the class keeps a single instance.
Hooks
The radio button in the toolbar lists every running hook, whoever started it (Methods, the class browser or the console), with what each does and how many calls it has matched. The switch pauses a hook and keeps its rules, the pencil edits them, and the copy button copies it as code. Hooks only act in this game. They're all stopped when the game's page reloads (a server switch or a reconnect), along with every watch and listener, since the objects they were on are gone.
Changes and undo
Everything you change from the inspector goes in its edit history (the history button in the toolbar): values at any depth, filters, colours, clips, depths, moves, reparenting, and objects removed or made. Each entry shows the old and new value, and Undo reverts it on its own; Ctrl+Z undoes the latest. Reset in the selected object's header (the counter-clockwise arrow) reverts every change to that object, latest first, as does an object's chip in the history. Rapid writes to one value, such as a drag, count as one change.
The change log
The pulse beside a property watches it for changes: the game reads it every frame and reports only when it differs. The change log (in the same panel as the history) lists each change with its time, newest first. It's the closest thing to a breakpoint on a change.
Watch
Members pinned from any object, with live values, under the tabs. Each stays until you remove it, whatever you select. You can find a string or number on screen from here too.
Search
Ctrl+F in the inspector searches the game: objects on screen by class (gui.ChatHistory) or by name (chatBar), and the client's methods and accessors by name, from the hook table. A member and a value search by value instead: alpha<1, visible=false, text~coins (~ means contains, ignoring case; =, !=, <, >, <= and >= compare). Click an object to select it, or a member's class to open it in the class browser. Find text on screen, the second box, lists the text fields showing whatever you type: a name, a number, a line of chat.
Make plugin
Make plugin (in the inspector's toolbar) turns what you did in the session into a plugin: the picked object reached by its stable path (or a UI rule for its class if there's none), each value you wrote kept ($keep, el.keep) or, for a data field, set once ($set), each method you called, your running hooks as observers (flash.hook) and your pinned members as flash.watch, all undone by the cleanup setup returns. Its manifest asks for "game": "read", or "act" if something was written or called. Copy src/index.ts and package.json from the preview into a plugin made with npm create @phantomclient/plugin. Saving the folder from the app isn't supported yet.
Console
await aj.room.current();
const me = await aj.player.current();
me?.userName;
await $0.alpha.$set(0.5);phantomis{ flash, aj, packets }, and each is also bound on its own.$0is the inspector's selection,$_the last value, andsleep(ms),consoleandtrace(name)are available too.- It's TypeScript, with top-level
await. An expression, or the last of several statements, returns its value; so doesreturn. - Names declared with
const,letorvarcarry over to the next line, as in a browser console. - Values expand a level at a time. An object in the game shows its class and expands to its current members.
- An error shows its code, the code in words, its message and its stack.
- Enter runs it, Shift+Enter adds a new line, and ↑ and ↓ go through your history, which is kept between sessions.
- Completions and types come from the same TypeScript service as the Scripts editor, so
flash.use('gui.completes the client's classes. After$0.(and$1to$4,$_, and their members such as$0.avatarData.) the object's actual current members are suggested first. inspect(value)shows a value in the inspector (selecting an object in the game),copy(value)copies it (text as is, anything else as JSON), andhook(name, rules)hooks a method like the inspector does, listed in its Hooks.hook('gui.ChatHistory.addMessage', { skip: true })returns the hook's number, andunhook(number)(or a name, or nothing for all of them) stops it.hooks()lists them, andundo()undoes the inspector's latest change.
Events
trace(name) makes a listener that logs each call in the Events tab, with expandable values:
aj.chat.onMessage(trace('chat'));
aj.room.onJoin(trace('room'));The tab also shows the game's packets, using the same packet console as the Packets tool, filtered to the game the pane is beside.
Hot reload
Component changes use Vue's hot reload: the component is swapped in place and keeps its state.
A change to src/index.ts, or to anything it imports that no component imports, reloads the plugin's logic in place:
- The running
setup's cleanup runs and is waited for (up to two seconds). - The SDK releases everything the old code held: every UI rule, hook, override, overlay node, highlight, watch, key, drive and traffic subscription, whether the cleanup undid it or not. Listeners passed to
ctx(onConfigChange,onGames) are released too. setupruns again with the new code, waiting forrequiresas on the first start.
So a rule that hides something is undone and reapplied with your change, without restarting the app, the game or the frame. The terminal shows when it happens:
12:04:31 logic g1 Reloaded: the old setup was cleaned up, and setup runs again.
12:04:31 The plugin's logic reloaded.If the new setup throws, the plugin crashes with the error shown, as on a first start. Fix it and save again.
The plugin's dev tools panel
While a plugin is served by the dev server, it gets a Dev tools panel (installed plugins never do). It has the same inspector, code generation, values and history as the Developer section, but runs in the plugin's frame with the plugin's own permissions, so you can check your code works with what your manifest asks for. For anything else, the Developer section is quicker.
Its inspector
Open it from the game's panel rail like your other panels; it works on the game it's beside. It needs "game": "read".
Press Pick, then click anything in the game. The click is caught on the stage on its way down, so it finds the object even if the game's own handler stops the click. You see:
- Its class, as the SDK names it (
avatar.AvatarWorldView), and the class it extends. - Its parents, up to the stage, with each one's instance name.
- Its members, live: every property with its current value, and its methods. Press Refresh to read them again.
- A picture of it, drawn by the game.
- Copy as code and Copy as UI rule, from its stable path, as in the Developer section, once that has been worked out; and the plain rule and search:
const stop = await flash.ui.rule('avatar.AvatarWorldView', async (el) => {
await el.hide();
});const [found] = await flash.find({
ofClass: 'avatar.AvatarWorldView',
named: 'av_12',
limit: 1,
});
if (found) console.log(await flash.describe(found));Picking only reads. The picked object glows briefly so you can see which one it was.
Its console
The Console tab runs code in the panel's frame, against its game:
await aj.room.current();
await flash.use('avatar.AvatarManager').playerAvatarWorldView.x.$get();
const me = await aj.player.current();
me?.userName;phantomisctx.phantom():{ flash, aj, packets, keybinds }. Each is also bound on its own, andctxis the plugin's context.- It's TypeScript. An expression returns its value. Several statements run as a function body, where
returngives the value.awaitworks anywhere. - Enter runs it, Shift+Enter adds a new line, and the arrow keys go through your history. Values expand a level at a time, as in the Developer section.
- It has your plugin's permissions and no more: code that acts needs
"game": "act", and a binding your plugin wasn't granted says so when used.
The same console is in the terminal running phantom-plugin dev, at the phantom> prompt. A line there runs in every logic frame of the plugin, each on its own game, and each replies with its game's id:
phantom> (await aj.room.current())?.name
g1 "jamaa_township.room_main"How it runs under the frame's rules
A plugin's frame can only run scripts from its own origin, and has no eval. So what you type is sent to the dev server, which compiles it into a module like the rest of your TypeScript and returns its address. The frame imports that module and runs it with the bindings. The frame's rules aren't loosened in development.
Errors
A crash shows the message, the error code in words and the stack, on the panel or page that broke and in the plugin's log. In development the stack is expanded. The console and the inspector show errors the same way:
TIMEOUT: Joining the room did not finish within 10000ms (The game did not answer in time.)The SDK's error codes (PhantomError):
| Code | Means |
|---|---|
NO_GAME | No game is open, or its client hasn't loaded yet. |
NOT_READY | The game isn't far enough along yet; it's waited for and retried. |
TIMEOUT | The game didn't answer in time. |
REFUSED | The game or the server said no. |
ABORTED | It was cancelled before it finished. |
PERMISSION_DENIED | The plugin wasn't allowed to; the message names what to ask for. |
A refusal names the scope and the package.json line that asks for it:
My Plugin was not allowed to act in the game as you (game:act), which calling
gMainFrame.server.sendXtMessage needs. Ask for it in package.json: "permissions": { "game": "act" }.