Reaching the game client
Phantom runs the real Animal Jam client, with two changes made as it's served. A plugin can reach any class, method or field in it, including ones marked private.
const audio = phantom.flash.use('com.sbi.corelib.audio.SBAudio');
if (!(await audio.areSoundsMuted.$get())) await audio.muteSounds();Most of what you'll need is already an area of the SDK, which is easier to read and needs the same permissions. Use phantom.flash when an area doesn't go far enough, or when you're working something out from the client's code.
If you don't know the name of the class or member yet, start with Reading the client: phantom.flash.search finds a member by part of its name, phantom.flash.describe says what a class holds, and one command fetches Animal Jam's own ActionScript, which is the only place that shows what the client does with a member.
Reading the client needs "game": "read" in the manifest, and calling or changing it needs "game": "act". See Permissions.
The basics
Under the SDK areas is plain reflection over the running client. phantom.flash.use(className) gives you a class. Each member you read moves one step along the path, and calling a method sends the call.
import { definePlugin } from '@phantomclient/plugin';
export default definePlugin({
async setup(ctx) {
const phantom = ctx.phantom();
const palette = phantom.flash.use('com.sbi.graphics.PaletteHelper');
const colors = await palette.gamePalette.$get();
console.log('palette entries:', Array.isArray(colors) ? colors.length : 0);
const point = await phantom.flash.use('flash.geom.Point').$new(10, 20);
console.log('length', await point.length.$get());
const main = phantom.flash.global('gMainFrame');
console.log(await main.userInfo.myUserName.$get());
console.log(await palette.$describe());
},
});| What | How |
|---|---|
| A class, and its statics | phantom.flash.use('gui.AvatarEditor') |
| A package-level global | phantom.flash.global('gMainFrame') |
| Make an object | phantom.flash.use('flash.geom.Point').$new(10, 20) |
| Read or write a member | place.member.$get(), place.member.$set(value) |
| A private member | the same: place.$at('_colorIndexes').$get() |
| Call a method | place.method(arg) |
| Call an undeclared one | place.$at('name').$call(arg) |
| Listen for an event | place.$listen('click', listener) |
| Say what an object is | place.$as('gui.ColorTable') |
| What it has | place.$describe() |
| Let an object go | place.$release() |
Getting the types back with $as
The declarations know each member's type, so a path through named members completes and type checks all the way. Some steps lose the type: $at might name something the declarations don't know, a hook might give you an argument the client typed as Object, or a method might return *. You still have the right object, but TypeScript doesn't know its class.
$as tells it. Nothing is sent to the game or checked there, so it costs no round trip.
import { definePlugin } from '@phantomclient/plugin';
export default definePlugin({
async setup(ctx) {
const phantom = ctx.phantom();
const grid = phantom.flash
.use('gui.AvatarEditor')
.$at('_colorTableColor1')
.$as('gui.ColorTable');
console.log(await grid._numCols.$get(), 'by', await grid._numRows.$get());
},
});Name the wrong class and nothing catches it while you type. $as only changes what the editor thinks the object is, so the read or call after it is what fails.
Private and protected members
Private, protected and package-internal members work like public ones. You read, write and call them by name, and your editor completes them alongside the public ones.
const table = phantom.flash.use('gui.ColorTable');
const indexes = await table.$at('_colorIndexes').$get();The same goes for a private member declared further up the class chain. A subclass object reaches it by its plain name, because Phantom looks for the name in each class the object inherits from.
If two classes in the same chain declare the same private name, the plain name reaches the lower one. To pick one exactly, qualify it, as in place.$at('phantom:private:gui:ColorTable::_colorIndexes'). The namespace is phantom:private:, phantom:protected: or phantom:internal:, then the declaring class with its package (gui:ColorTable), then :: and the member.
Cost
Each call is a message to the game and back. Reads in the same tick are batched, so Promise.all over five $get() calls is one round trip. To read several members of one object, use phantom.flash.collect, which does it in one message.
const { rows } = await phantom.flash.collect(phantom.flash.use('gui.ColorTable'), [
'_colorIndexes',
'_numCols',
'_numRows',
]);
console.log(rows[0]);Anything that isn't a plain value is held in the game once it crosses over. A read that returns an object holds it. Hook captures hold theirs unless you asked for plain values. An override's arguments are only held until the handler finishes. Call $release() on anything you keep, or read the values you need and drop the object.
Coverage
What each package of the client holds, and how much of it a plugin can hook or replace. Counts are from deploy 1829.
| Package | Classes | Interfaces | Methods | Getters | Setters | Constructors | Hooked |
|---|---|---|---|---|---|---|---|
gui | 132 | 0 | 2338 | 178 | 38 | 132 | 2686 |
game | 148 | 1 | 2713 | 13 | 5 | 148 | 2876 |
avatar | 23 | 1 | 497 | 190 | 63 | 23 | 767 |
com.sbi | 66 | 1 | 584 | 99 | 52 | 66 | 800 |
room | 20 | 0 | 321 | 50 | 12 | 20 | 403 |
quest | 8 | 0 | 400 | 16 | 3 | 8 | 427 |
buddy | 11 | 0 | 272 | 15 | 5 | 11 | 303 |
den | 6 | 0 | 90 | 129 | 67 | 6 | 292 |
it.gotoandplay | 13 | 2 | 198 | 6 | 3 | 13 | 217 |
shop | 9 | 0 | 173 | 21 | 7 | 9 | 210 |
pet | 9 | 0 | 119 | 72 | 24 | 9 | 224 |
playerWall | 5 | 0 | 193 | 28 | 20 | 5 | 246 |
item | 4 | 0 | 76 | 53 | 13 | 4 | 146 |
loader | 14 | 0 | 43 | 40 | 2 | 14 | 99 |
collection | 19 | 0 | 64 | 2 | 0 | 19 | 85 |
ecard | 4 | 0 | 42 | 51 | 9 | 4 | 106 |
achievement | 3 | 0 | 33 | 13 | 2 | 3 | 51 |
trade | 3 | 0 | 31 | 0 | 0 | 3 | 34 |
inventory | 3 | 1 | 15 | 38 | 11 | 3 | 21 |
org.osmf | 232 | 3 | 807 | 500 | 227 | 232 | 1756 |
com.hurlant | 34 | 8 | 295 | 2 | 1 | 34 | 305 |
Box2D | 74 | 0 | 421 | 6 | 8 | 74 | 0 |
com.greensock | 45 | 0 | 223 | 16 | 17 | 45 | 0 |
gskinner | 6 | 1 | 34 | 4 | 3 | 6 | 0 |
| the other 51 packages | 131 | 3 | 1017 | 259 | 55 | 131 | 1437 |
| total | 1022 | 21 | 10999 | 1801 | 647 | 1022 | 13491 |
Reading, writing and calling work on all of it, hooked or not, fields as well as methods, because it's reflection over the running client. Use phantom.flash.use(name) for a class, a path from there for a member, a call for a method, $new for a constructor and $listen for an event. That covers 1,022 classes, 21 interfaces, 10,999 methods, 1,801 getters, 647 setters and all the fields.
Hooking and replacing is the Hooked column: 13,491 members. That's everything except the interfaces and the three libraries that run every frame, including all the methods, getters and setters, and all 893 constructors outside those libraries.
What's left out is counted by reason in the patch's own skipped list for this client:
| Reason | How many | Why |
|---|---|---|
static initializer | 1,022 | It has already run by the time a plugin loads, so there would be nothing to catch. |
excluded package or class | 863 | Box2D, com.greensock and gskinner: physics and tweening, stepped for every body and every tween every frame. A hook that is off is still a read on entry. |
interface method | 115 | An interface declares a method and has no body to hook. Hook the class that implements it. |
constructor whose super call could be reached more than one way | 0 | None in this client. A constructor that can jump past its own super() to where the hook goes would run it with this unbuilt, so it would be left alone. |
native or without a body | 0 | None in this client. |
body shared by more than one trait | 0 | None in this client. |
code could not be decoded | 0 | None in this client. |
code whose stack heights could not be followed | 0 | None in this client. The patch checks every body it changes and fails rather than serving one it could not follow. |
The five zeroes mean nothing was lost to a method the tooling couldn't read or a constructor with no safe spot. Everything left out is left out on purpose.
Hooking a constructor
A constructor isn't a member of its class, and the generated declarations have no name for it. The patch calls it <constructor>, which no ActionScript member can be called.
import { definePlugin } from '@phantomclient/plugin';
export default definePlugin({
async setup(ctx) {
const phantom = ctx.phantom();
const stop = await phantom.flash.override(
'gui.ColorTable.<constructor>',
async (call) => {
await call.original();
console.log('the client built another colour grid');
}
);
return () => void stop();
},
});The hook runs after the constructor's super(), since the object isn't built before then. Code before super() can branch freely. That's how the compiler writes a default argument (param1 || new Thing()), and the tile classes in gui/itemWindows/ are written that way.
So anything the constructor did before super() has already happened. And if you skip the call (return from an override without call.original(), or use skip on a hook), the constructor returns right there, leaving an object whose base class ran but whose own fields were never set. You almost never want that. Use replaceArgs to change what it builds.
Hook a constructor when a client object decides something once, when it's built, and never checks again. Grid tiles are a good example. ItemWindowOriginal works out which item it stands for in its constructor, from initCustomParams.indexArray[slot] (gui/itemWindows/ItemWindowOriginal.as, lines 87 to 90), and update() only redraws that item. So gui.itemWindows.ItemWindowOriginal.<constructor> is the only place to change which item a tile shows. Nothing later reads indexArray again.
Member kinds
| Kind | What you can do with it | How |
|---|---|---|
| Public field | read, write | place.member.$get(), $set(value) |
| Private, protected or internal field | read, write | the same, by the name the source spells |
| Static field or constant | read, write | phantom.flash.use('gui.GuiManager').guiLayer.$get() |
| Package-level global | read, write | phantom.flash.global('gMainFrame') |
| Public method | call, hook, replace | place.method(arg), phantom.flash.hook('cls.method') |
| Private or protected method | call, hook, replace | the same; the hook table carries its namespace |
| Static method | call, hook, replace | phantom.flash.use(cls).method(arg) |
| Getter | read, hook | place.member.$get(), hook('cls.member@get') |
| Setter | write, hook | place.member.$set(v), hook('cls.member@set') |
| Constructor | construct, hook | phantom.flash.use(cls).$new(...), hook('cls.<constructor>') |
| Interface method | call; hook the class behind it | call it on the object, and hook the class that implements it |
| Event handler | call, hook, replace | it is an ordinary method |
| Event the client dispatches | listen | place.$listen('click', listener) |
| An event you want to send | send | phantom.aj.gui.click(target), or construct the event and call dispatchEvent |
| Static initialiser | nothing | it has already run before a plugin loads |
Access modifiers don't matter for any of these. The only thing you can't touch is a static initialiser, which has run long before a plugin loads.
Errors
A call that reaches the game and fails comes back as a BridgeError. The release Flash Player only gives an error number, not a message. The common ones:
| Error | What it means |
|---|---|
| 1009 | something the call reached was null: usually a screen or a list that is not up yet |
| 1069 | the object has no member of that name |
| 1063 | the call was given the wrong number of arguments |
| 1034 | a value was not of the type the member takes |
| 1502 | a script in the game ran for longer than the player allows |
A 1009 from one of the client's own methods usually means the game isn't ready, not that your code is wrong. The method wanted a room, a path or an open screen that isn't there. Check first, or if it has to run every frame, use repeat with while, which is tested in the game before the call.
A call that never reaches the game fails differently. phantom.flash has four sub-areas, so a name that belongs one level down (phantom.flash.definition, which is on the bridge, or phantom.flash.text, on the overlay) would be undefined and fail a line later with undefined is not a function. Instead it tells you where the call lives:
phantom.flash has no "definition". The bridge underneath has it: write
phantom.flash.bridge.definition. For a class with its types,
phantom.flash.use("avatar.AvatarManager") is the same object typed.A typo gets the nearest match, and a name nothing like anything gets the list of areas: drawing is phantom.flash.overlay, the room's players and props are phantom.flash.entities, and the camera is phantom.flash.view.
Other tools
Reading, writing, calling and hooking cover what a class declares. These tools cover the rest: your own data stored against an object, finding objects with no path, giving the game a function to call, holding a value the client keeps resetting, making the game repeat one of its calls, and reading a whole list at once.
import { definePlugin } from '@phantomclient/plugin';
export default definePlugin({
async setup(ctx) {
const phantom = ctx.phantom();
const avatars = phantom.flash.use('avatar.AvatarManager');
const greeted = phantom.flash.notes<number>();
const me = avatars.playerAvatar;
if (!(await greeted.has(me))) await greeted.set(me, Date.now());
const buttons = await phantom.flash.find({
ofClass: 'flash.display.SimpleButton',
visibleOnly: true,
});
console.log(buttons.length, 'buttons on screen');
const replies = phantom.flash.answering<boolean>(
(command) => console.log('heard', command),
true
);
await phantom.flash.use('XtReplyDemuxer').addModule(replies.argument, 'gl');
await replies.answers(false);
const big = await phantom.flash.hold(avatars.playerAvatarWorldView.scaleX, 2);
const { exists, place } = phantom.flash.rules;
const room = phantom.flash.use('room.RoomManagerWorld').instance;
const quicker = await phantom.flash.repeat(room.movePlayerOnPath, {
while: exists(place(room, '_followPath')),
times: 2,
});
const { rows, total } = await phantom.flash.collect(avatars.avatarList, {
who: 'userName',
x: 'avatarWorldView.x',
});
console.log(`${rows.length} of ${total} players`, rows[0]?.who);
},
});Notes. ActionScript classes are sealed, so you can't add your own property to a game object. Phantom keeps a table in the game keyed by the object, with set, get, has, forget, objects and clear. Two identical-looking chests are still told apart, because the key is the chest itself, not its name. Keys are weak, so a note won't keep an object alive. The table goes when your plugin stops.
Find. Some objects have no useful path, like a screen the game just built, a button inside a list or the clip an animal is drawn in. A search starts at the stage, or wherever you point it, and returns places typed as the class you asked for. Narrow it with ofClass, named, containing, visibleOnly, depth and limit. With through: 'properties' it walks what objects hold, not just what's drawn, which gets you the managers and lists behind a screen.
Answering. The game calls your function in the middle of its own code and can't wait for TypeScript. So the answer has to be in the game already, and the call reads it from there. Set the answer first and change it whenever you like. Your listener still hears each call a moment later.
Holding. The client writes some members every frame, so a write from a plugin is overwritten before your message even lands. A hold does the write inside the game, every frame, until you stop it, then puts back what was there. Speeds, sizes and flags the client recalculates are the usual cases. Use while to hold it only on frames where a condition is true, and everyFrames if once a frame is more than you need. The value can be a constant, another place or a rule value.
Repeating is the same for a call. A method the client runs once a frame can be made to run twice in that frame, with nothing sent over the bridge. times sets how many. while is checked in the game each frame, so a call that only makes sense mid-walk isn't made while you're standing still. Doing this from a script would cost a message each way, and you'd get whatever error the client throws when it isn't ready.
Tracking is a find that stays up to date. A track keeps the list current inside the game and only tells you what was added or removed, so following every avatar in a room costs nothing on frames where nothing changes. It includes subclasses, and anything that implements an interface you name. mode: 'holder' reads one of the game's own collections instead of the display list, for things that are never drawn.
Snapshots draw one of the game's display objects to a PNG and return it as a data: URL. Flash has no image encoder, so the loader has its own. You get the object at its own size, with its filters and children, upright however its parent is transformed.
Collecting. Reading four members from forty players one at a time is a hundred and sixty messages. With collect it's one. Point it at an array, a vector, a dictionary or a display object. A missing member comes back as null and the rest of the read still works.
Rules can read a place too, not just the call they're attached to. Pass a place where a hook's when wants a value, or add a path with rules.place(...), so a hook can compare an argument with something live in the game. A hold reads its value the same way.
The one limit is that your TypeScript can't run inside one of the game's calls. The value has to be there before the call, or it has to be a rule. when, replaceArgs, replaceReturn and skip: { returns } do run inside the call. They're built from constants, classes and places, and cover most decisions; anything more has to happen afterwards. You also can't extend one of the game's classes, since you're writing TypeScript, not ActionScript. Hook the constructor to change what it builds, use override with when to replace a method for the class, and keep your own per-object data in notes.
Work done in the game
Three jobs that would otherwise be a loop on your side of the bridge run inside the game in one message: reading a list to keep one item, waiting for something to appear, and a call whose answer the next call needs. An older loader refuses each one by name, and the raw form of each is on phantom.flash.bridge for anything the calls below don't cover.
Filtering a list in the game
collect reads the same members from every item of a list. Give it a where and the game decides which items to read, and sortBy sorts them before the answer is built. "The nearest thing I can click" is then one message and one row, not forty rows and a sort on your side.
const { item, exists } = phantom.flash.rules;
const actors = phantom.flash.use('quest.QuestManager').$at('_questActors');
const { rows, total, scanned } = await phantom.flash.collect(
actors,
{ id: '_actorId', x: '_mc.x' },
{
where: exists(item('_mc')),
sortBy: '_distanceToPlayer',
limit: 1,
}
);
console.log(
`the nearest of ${total} drawn actors, out of ${scanned} slots`,
rows[0]?.['id']
);rules.item is the item being tested, which only a where has. Everything else a rule can read is available as usual: a constant, a class's value, another place in the game. total is how many items passed the where, which is what to page against, and scanned is how long the list was. rules.changed makes no sense in a where, since each item is tested once, so it's refused.
Waiting in the game
phantom.flash.when waits for a condition inside the game and sends one message back, when it holds or when time runs out. Nothing is read across the bridge while it waits, and a condition that already holds is answered without waiting a frame.
const hud = phantom.flash.use('gui.GuiManager').mainHud;
const waited = await phantom.flash.when(phantom.flash.rules.exists(hud), {
read: [hud],
timeoutMs: 30_000,
});
console.log(
waited.met ? `up after ${waited.frames} frames` : 'no HUD in half a minute'
);read is read in the frame the condition held, so the values match the condition. It takes at most 32 places. Timing out isn't a throw: met is false and values is empty. The default timeout is ten seconds and the maximum is ten minutes, the longest the game will hold a wait.
For a place that just has to exist, phantom.flash.until is shorter, and uses this path itself: a wait with no test function goes to the game, and one with a TypeScript test is watched from your side, since the test has to run there on every reading. See Timing.
Cancelling a wait
until and when both take a signal. When it's aborted they return the same as a timeout: until with null, when with met: false. A supervised automation's task.signal works here, so a run told to stop doesn't sit out the rest of a thirty-second wait for a window the player has already closed.
const giving = new AbortController();
const hud = phantom.flash.use('gui.GuiManager').mainHud;
const waiting = phantom.flash.until(hud, { timeoutMs: 60_000, signal: giving.signal });
giving.abort();
console.log(await waiting);The cost depends on which path the wait took. A watch is ended in the game, so nothing is left reading there. A wait the game is holding can only be dropped on your side: the protocol has unwatch, untick, undrive and untrack but no unwhen, so the game keeps checking until the condition holds or its own timeoutMs runs out, and the answer is thrown away. That's one pending request and a condition checked every few frames, which is cheap but not free. Give a wait you might cancel a timeoutMs you're happy to pay for.
Several steps in one frame
A batch sends several requests at once, but each names its target up front. So a call followed by reads from the object it returned can't be batched: it's a call, a read and a release, with the object crossing as a handle in between. A chain does it without the object ever leaving Flash.
const avatars = phantom.flash.use('avatar.AvatarManager');
const chained = await phantom.flash.chain((step) => [
{ call: avatars.getAvatarByUserName, args: ['localhost'] },
{ collect: step(-1).at('avatarWorldView'), fields: { x: 'x', y: 'y' } },
]);
console.log(
'one message, not three:',
chained.collected(1).rows[0],
chained.stopped === null ? '' : `stopped at ${chained.stopped}`
);A step works on a place, like any call, or on step(n) for what an earlier step returned, where a negative n counts back from the current step. step(n).at('member') goes further from it, and step(n) can go anywhere a value goes, including a list item or an object field. Each step does one of get, set, call, make or collect.
Nothing crosses unless a step says hold: true, so a ten-step chain leaves nothing to release. And the game does nothing between two steps: a value one step writes is what the next step's call reads, and an object the client would have thrown away before a second message arrived is still there for the next step.
A step can have an if, checked before it runs, whose rules.item is the previous step's result. If it doesn't hold, the chain stops and stopped says which step, so "open the window, and press the button only if it's the right window" is one message. The first step has no previous step, so an item in its if is refused.
Rules that only fire on a new value
rules.changed holds when the value differs from the last time it was read. The first read counts as a change, like a watch's first read, so the rule holds once for the current value and then only for new ones. Values compare by value and objects by identity: the client rebuilding a window is a change, a field moving inside it isn't.
const avatars = phantom.flash.use('avatar.AvatarManager');
await phantom.flash.when(phantom.flash.rules.changed(avatars.playerAvatar), {
timeoutMs: 120_000,
});
console.log('a new animal');A wait reads its condition once when it starts and discards that read, so a wait on a change is about what happens from then on. A hook's when takes it too, which is the difference between hearing every call of a per-frame method and hearing only the calls that changed something. So does a hold's while. collect and chain read each thing once, so they refuse it.
Method list
One line each. For arguments and examples, see the SDK reference.
Getting objects
| Method | What it does |
|---|---|
use(className) | A class: its static members, and $new where it can be made |
global(name) | A package-level variable, such as gMainFrame |
stage() | Flash's stage, which everything is drawn on |
client() | The game client's own object, the one the loader loaded |
find(options, where?) | Objects by what they are, for when nothing says where they are |
whenReady() | Waits until the loader is running |
describe(what) | What a class holds: its members, their types and the names to hook them by |
search(query) | Finds a method, getter or setter anywhere in the client by part of its name |
bridge | The untyped bridge underneath, for whatever the rest does not cover |
One bridge call worth knowing: bridge.vector('shop.MyShopItem', items), for places the client declares a Vector.<T>. An array won't work there and throws inside the client. A Vector is not an array explains why.
Places
| Member | What it does |
|---|---|
$get(), $set(v) | Read it, write it |
place.method(a, b) | Call it |
$call(a, b) | Call it when the declarations describe nothing here |
$at('_name') | A member by name, private ones included |
$as(className) | Say what an object is, so its members type |
$describe() | What the object really has, asked of the running game |
$listen(event, fn) | An event it dispatches |
$new(...args) | Make one, on a class |
$release() | Let go of it, so Flash can collect it |
Methods and calls
| Method | What it does |
|---|---|
hook(name, options, listener?) | Hear a method's calls, change its arguments, its answer, or skip it |
override(name, handler, options) | Take the call yourself, and run the original when you want to |
hooks(url?) | Everything the running client can be hooked at |
rules | The pieces a rule is built from: values to read, and tests to combine |
answering(listener, answer?) | A function of yours the game calls, with an answer waiting for it |
Values, lists and time
| Method | What it does |
|---|---|
hold(place, value, options?) | Keeps a value where the client keeps writing its own |
holdAll(held, options?) | The same, for values that go together |
repeat(method, options?) | The game calls one of its own methods, frame after frame |
track(className, listener, ...) | Follows every instance of a class as they come and go |
snapshot(place, options?) | One of the game's display objects, drawn to a picture |
collect(place, fields, options?) | The same members out of every item of a list, in one answer |
chain(steps) | A few steps in one frame, each using what the last answered |
when(condition, options?) | Waits in the game for a condition, reading places as it holds |
notes<Value>() | Your own data, kept beside the game's objects |
watch(places, options, listener) | Told when a value changes, not every time it is read |
onTick(listener, options?) | A call every few frames, in step with the game |
until(places, test?, options?) | Waits for a value to be there, or to pass a test of yours |
frames(count?) and nextFrame() | Waits in the game's own frames rather than in milliseconds |
whenever(place, attach, options?) | Runs on an object each time the client builds it again |
Use until, frames and whenever instead of a setTimeout and a guess. Timing covers them, along with the area calls that do the same.
Looks and input
| Method | What it does |
|---|---|
highlight(place, options?) | A glow, a tint or a pulse on one of the game's own display objects |
hide(place) | Hides one until you put it back |
recolor(options, place?) | Brightness, contrast, hue and saturation, over the game or one object |
style(place, style) | Font, size, colour, background and outline on one of its text fields |
onKey(key, listener, options?) | A shortcut while the game has focus |
Related areas
| Area | What is in it |
|---|---|
phantom.flash.overlay | Drawing above the game: text, rich, rect, circle, line, image, marker, world, group, and the widgets input, button, panel and list. replace puts a node where one of the game's display objects was, attach follows one about |
phantom.flash.view | zoom, zoomLevel, stretch, hideHud, hideLayer, setQuality, setFrameRate |
phantom.flash.entities | players, npcs and objects in the room, each with highlight, tag, onClick and release |
Permissions
{
"name": "phantom-plugin-every-colour",
"version": "1.0.0",
"phantom": {
"id": "acme.every-colour",
"title": "Every colour",
"description": "Puts every colour the game knows in the avatar editor's own grids.",
"author": "Acme",
"permissions": { "game": "act" }
}
}The whole client has two permission levels rather than one per class. The client has thousands of classes and tens of thousands of members, so a line for each would be unreadable, and it wouldn't tell a player much: code that can call any method in the game can do anything the game can.
Phantom checks what each message does, not which part of the SDK sent it.
"game": "read"reads the client and draws over it. Reading a member, collecting, finding, watching, tracking, a hook that only observes, the overlay, highlights, hiding and styling all work withread."game": "act"also acts as the player. Calling a method, setting a member, constructing an object,$override,drive,hold, and a hook that skips a method or changes what it is given or gives back all needact. It includesread.
The same levels cover phantom.aj. aj.room.current() is a read and aj.chat.say is an act, like the flash calls they're made of. In this example, changing which colours the editor offers and sending the one you picked are both acts, so the manifest asks for act.
The install dialog marks act as high risk. Only ask for it if your plugin acts in the game. A plugin that only reads and draws should ask for read.
Scripts don't need permissions. A script is your own code, run by you in your own app, so it gets everything. Permissions are for code you install from someone else.