Packets
phantom.packetsThe game client
The game's traffic, typed by @phantom/protocol.
server is the end that talks to the server: send goes to it, watch and on hear what it sends the game. client is the game's end: send reaches the game as though the server sent it, watch and on hear what the game sends the server. watch only listens; on may change or drop a packet on its way.
const stop = phantom.packets.server.watch(packets.RoomJoinResult, (result) =>
console.log(result.joinedRoom?.name),
);Scripts and plugins
A script has all of it, as phantom.packets, and so does a plugin, from ctx.phantom(): the SDK runs in the plugin's own frame. Hearing the traffic needs "packets": "read" in the manifest, and sending it, or changing what goes by, needs "packets": "send" (see Permissions).
Overview
| Member | What it does | Plugins |
|---|---|---|
channel | The connection underneath, as raw frames of text: send, intercept and observe, each by direction. | |
request() | Sends a packet to the server and resolves with the first reply that matches, failing with TIMEOUT when none comes in time. | packets:send |
packets.server: One end of the game's connection: phantom.packets.server or phantom.packets.client.
| Member | What it does | Plugins |
|---|---|---|
on() | Hears a packet, a command, or every message ('*') this end sends, before it is passed on. | packets:send |
send() | Sends a packet, a message or a frame's text to this end, past every hook. | packets:send |
watch() | Hears a packet, a command, or every message this end sends, after it was carried. | packets:read |
packets.client: One end of the game's connection: phantom.packets.server or phantom.packets.client.
| Member | What it does | Plugins |
|---|---|---|
on() | Hears a packet, a command, or every message ('*') this end sends, before it is passed on. | packets:send |
send() | Sends a packet, a message or a frame's text to this end, past every hook. | packets:send |
watch() | Hears a packet, a command, or every message this end sends, after it was carried. | packets:read |
Properties
channel
channel: NetworkChannelThe connection underneath, as raw frames of text: send, intercept and observe, each by direction. server and client are built on it. Reach for it only for what they do not cover, such as hearing both directions with one listener.
Types
NetworkChannel · Direction · MessageFilter · InterceptVerdict · Unsubscribe
Methods
request
request<Sent, Reply>(sent: PacketDefinition<Sent>, body: Packet<Sent>, reply: PacketDefinition<Reply>, options?: ReplyOptions & { matches?: ((reply: Packet<Reply>) => boolean) | undefined; }) => Promise<Packet<Reply>>Sends a packet to the server and resolves with the first reply that matches, failing with TIMEOUT when none comes in time. The watch starts before the send, so a fast reply is never missed.
Parameters
| Name | Type | |
|---|---|---|
sent | PacketDefinition<Sent> | |
body | Packet<Sent> | |
reply | PacketDefinition<Reply> | |
options optional | ReplyOptions & { matches?: ((reply: Packet<Reply>) => boolean) | undefined; } |
Returns
Promise<Packet<Reply>>
Types
PacketDefinition · Packet · ReplyOptions
Example
const body = {
room: (await phantom.aj.room.current())?.id ?? -1,
target: 'jamaa_township.room_main',
confirmLanguageChange: false,
invisible: false,
joinType: '',
};
const result = await phantom.packets.request(packets.RoomJoinRequest, body, packets.RoomJoinResult, {
matches: (reply) => reply.joinedRoom?.name.startsWith(body.target) ?? false,
});
console.log(result.joined);packets.server
One end of the game's connection: phantom.packets.server or phantom.packets.client.
send sends to that end. on hears what that end sends, before it arrives, and may change or drop it: phantom.packets.client.on(...) sees what the game is about to send to the server, and phantom.packets.server.on(...) what the server is about to send to the game. watch hears the same after the fact, and costs the traffic nothing.
on
on<Fields>(what: PacketDefinition<Fields>, hook: PacketHook<Fields>) => Unsubscribe
on(what: string, hook: MessageHook) => UnsubscribeHears a packet, a command, or every message ('*') this end sends, before it is passed on. A packet hook may return new fields to send in its place.
phantom.packets.server.on sees what the server sends before the game gets it, and phantom.packets.client.on what the game sends before the server gets it. The message waits for the hook, including an async one, so keep hooks quick: the proxy passes a message on without a hook that takes too long. A packet hook is called with the packet's fields and the Message: return changed fields to send those instead, or call message.drop() to stop it (a dropped message is not replaced). A command ('rj', 'pubMsg') or '*' hook is called with the Message alone, and may change or drop it. A message the packet definition cannot read is passed on as it came, and reported rather than given to the hook.
It throws a TypeError when the definition is not one this end sends (such as packets.RoomChat, which the server sends, given to phantom.packets.client.on).
Parameters
| Name | Type | |
|---|---|---|
what | PacketDefinition<Fields> | The packet definition to hear (such as packets.RoomChat), a command name, or '*' for every message this end sends. |
hook | PacketHook<Fields> | Called with each matching packet and its message (or, for a command or '*', the message alone) before it is passed on. It may return replacement fields, or drop the message. |
Returns
Unsubscribe. A function that removes the hook.
Types
PacketHook · MessageHook · Unsubscribe · PacketDefinition
Example
const stop = phantom.packets.client.on(packets.PublicChat, (packet) => ({
...packet,
text: packet.text.toUpperCase(),
}));
const quiet = phantom.packets.server.on(packets.RoomChat, (packet, message) => {
if (packet.text.includes('spam')) message.drop();
});send
send<Fields>(what: PacketDefinition<Fields>, packet: Packet<Fields>) => Promise<void>
send(what: string | Message) => Promise<void>Sends a packet, a message or a frame's text to this end, past every hook.
phantom.packets.server.send writes to the game server as though the client had sent it, and phantom.packets.client.send delivers to the game as though the server had sent it. What is sent here goes around every on hook, whoever made it, so a script's own hook does not see (or loop on) what the script sent; watch does hear it. Give a packet definition from packets and its fields, which are encoded as the client would; a Message from @phantom/protocol; or the raw text of one frame (%xt%...% or <msg ...>), which is read as a message first.
It throws a TypeError when the definition or message travels the other way (such as a toClient packet given to phantom.packets.server.send), and throws when the text is not a message. Those checks throw as the call is made, before any promise, so await it inside try to catch them. It rejects when the game is not connected. Nothing checks that the fields make sense to the server: a packet it does not expect can get the player disconnected.
Parameters
| Name | Type | |
|---|---|---|
what | PacketDefinition<Fields> | The packet definition (such as packets.RoomJoinRequest), a Message, or a frame's text. |
packet | Packet<Fields> | The packet's fields and its room, when what is a packet definition. |
Returns
Promise<void>. Resolves once the frame is handed to the connection.
Types
Example
await phantom.packets.server.send(packets.RoomJoinRequest, {
room: (await phantom.aj.room.current())?.id ?? -1,
target: 'jamaa_township.room_main',
confirmLanguageChange: false,
invisible: false,
joinType: '',
});
await phantom.packets.client.send(packets.AdminMessage, {
room: -1,
text: 'Hello from Phantom',
system: true,
extra: [],
});watch
watch<Fields>(what: PacketDefinition<Fields>, listener: (packet: Packet<Fields>) => void, options?: PacketWatchOptions | undefined) => Unsubscribe
watch(what: string, listener: (message: Message) => void, options?: PacketWatchOptions | undefined) => UnsubscribeHears a packet, a command, or every message this end sends, after it was carried.
Unlike on, a watcher cannot change or drop anything, and the traffic never waits for it: use it to follow the game, and on only to change it. phantom.packets.server.watch hears what the server sent the game, and phantom.packets.client.watch what the game sent the server. It also hears what send injected, and messages an on hook dropped or changed: a changed message is heard as the replacement that was sent and as the original that was stopped. A packet the definition cannot read is reported and skipped. It throws a TypeError when the definition is not one this end sends.
Parameters
| Name | Type | |
|---|---|---|
what | PacketDefinition<Fields> | The packet definition to hear (such as packets.RoomChat), a command name, or '*' for every message this end sends. |
listener | (packet: Packet<Fields>) => void | Called after each matching message was carried: with the packet's fields for a definition, or with the Message for a command or '*'. |
options optional | PacketWatchOptions | undefined | original: true hears only what this end itself sent, as it sent it (see PacketWatchOptions). |
Returns
Unsubscribe. A function that stops listening.
Types
PacketWatchOptions · Unsubscribe · PacketDefinition · Packet
Example
const stop = phantom.packets.server.watch(packets.RoomChat, (packet) =>
console.log(`${packet.userId} said ${packet.text}`));
const all = phantom.packets.client.watch('*', (message) => console.log(message.command));packets.client
One end of the game's connection: phantom.packets.server or phantom.packets.client.
send sends to that end. on hears what that end sends, before it arrives, and may change or drop it: phantom.packets.client.on(...) sees what the game is about to send to the server, and phantom.packets.server.on(...) what the server is about to send to the game. watch hears the same after the fact, and costs the traffic nothing.
on
on<Fields>(what: PacketDefinition<Fields>, hook: PacketHook<Fields>) => Unsubscribe
on(what: string, hook: MessageHook) => UnsubscribeHears a packet, a command, or every message ('*') this end sends, before it is passed on. A packet hook may return new fields to send in its place.
phantom.packets.server.on sees what the server sends before the game gets it, and phantom.packets.client.on what the game sends before the server gets it. The message waits for the hook, including an async one, so keep hooks quick: the proxy passes a message on without a hook that takes too long. A packet hook is called with the packet's fields and the Message: return changed fields to send those instead, or call message.drop() to stop it (a dropped message is not replaced). A command ('rj', 'pubMsg') or '*' hook is called with the Message alone, and may change or drop it. A message the packet definition cannot read is passed on as it came, and reported rather than given to the hook.
It throws a TypeError when the definition is not one this end sends (such as packets.RoomChat, which the server sends, given to phantom.packets.client.on).
Parameters
| Name | Type | |
|---|---|---|
what | PacketDefinition<Fields> | The packet definition to hear (such as packets.RoomChat), a command name, or '*' for every message this end sends. |
hook | PacketHook<Fields> | Called with each matching packet and its message (or, for a command or '*', the message alone) before it is passed on. It may return replacement fields, or drop the message. |
Returns
Unsubscribe. A function that removes the hook.
Types
Unsubscribe · PacketDefinition · PacketHook · MessageHook
Example
const stop = phantom.packets.client.on(packets.PublicChat, (packet) => ({
...packet,
text: packet.text.toUpperCase(),
}));
const quiet = phantom.packets.server.on(packets.RoomChat, (packet, message) => {
if (packet.text.includes('spam')) message.drop();
});send
send<Fields>(what: PacketDefinition<Fields>, packet: Packet<Fields>) => Promise<void>
send(what: string | Message) => Promise<void>Sends a packet, a message or a frame's text to this end, past every hook.
phantom.packets.server.send writes to the game server as though the client had sent it, and phantom.packets.client.send delivers to the game as though the server had sent it. What is sent here goes around every on hook, whoever made it, so a script's own hook does not see (or loop on) what the script sent; watch does hear it. Give a packet definition from packets and its fields, which are encoded as the client would; a Message from @phantom/protocol; or the raw text of one frame (%xt%...% or <msg ...>), which is read as a message first.
It throws a TypeError when the definition or message travels the other way (such as a toClient packet given to phantom.packets.server.send), and throws when the text is not a message. Those checks throw as the call is made, before any promise, so await it inside try to catch them. It rejects when the game is not connected. Nothing checks that the fields make sense to the server: a packet it does not expect can get the player disconnected.
Parameters
| Name | Type | |
|---|---|---|
what | PacketDefinition<Fields> | The packet definition (such as packets.RoomJoinRequest), a Message, or a frame's text. |
packet | Packet<Fields> | The packet's fields and its room, when what is a packet definition. |
Returns
Promise<void>. Resolves once the frame is handed to the connection.
Types
Example
await phantom.packets.server.send(packets.RoomJoinRequest, {
room: (await phantom.aj.room.current())?.id ?? -1,
target: 'jamaa_township.room_main',
confirmLanguageChange: false,
invisible: false,
joinType: '',
});
await phantom.packets.client.send(packets.AdminMessage, {
room: -1,
text: 'Hello from Phantom',
system: true,
extra: [],
});watch
watch<Fields>(what: PacketDefinition<Fields>, listener: (packet: Packet<Fields>) => void, options?: PacketWatchOptions | undefined) => Unsubscribe
watch(what: string, listener: (message: Message) => void, options?: PacketWatchOptions | undefined) => UnsubscribeHears a packet, a command, or every message this end sends, after it was carried.
Unlike on, a watcher cannot change or drop anything, and the traffic never waits for it: use it to follow the game, and on only to change it. phantom.packets.server.watch hears what the server sent the game, and phantom.packets.client.watch what the game sent the server. It also hears what send injected, and messages an on hook dropped or changed: a changed message is heard as the replacement that was sent and as the original that was stopped. A packet the definition cannot read is reported and skipped. It throws a TypeError when the definition is not one this end sends.
Parameters
| Name | Type | |
|---|---|---|
what | PacketDefinition<Fields> | The packet definition to hear (such as packets.RoomChat), a command name, or '*' for every message this end sends. |
listener | (packet: Packet<Fields>) => void | Called after each matching message was carried: with the packet's fields for a definition, or with the Message for a command or '*'. |
options optional | PacketWatchOptions | undefined | original: true hears only what this end itself sent, as it sent it (see PacketWatchOptions). |
Returns
Unsubscribe. A function that stops listening.
Types
Unsubscribe · PacketDefinition · Packet · PacketWatchOptions
Example
const stop = phantom.packets.server.watch(packets.RoomChat, (packet) =>
console.log(`${packet.userId} said ${packet.text}`));
const all = phantom.packets.client.watch('*', (message) => console.log(message.command));Types
The types the calls above take and give, with every field. Import any of them in a plugin from @phantomclient/plugin/sdk, or use them as they are in a script.
NetworkChannel
One game's traffic, as the SDK reaches it. Frames cross as text, so the same SDK runs in the desktop's main process, next to the proxy, and in a plugin's sandbox, a message away from it.
| Field | Type | |
|---|---|---|
send | (direction: Direction, text: string) => Promise<void> | Sends a frame past every hook: to the server, or to the game as though the server sent it. |
intercept | (filter: MessageFilter, handler: (text: string, direction: Direction) => InterceptVerdict | Promise<InterceptVerdict>) => Unsubscribe | Runs handler on each matching message before it is passed on. The message waits for it, so a handler should be quick; a slow one is skipped by the proxy after its timeout. |
observe | (filter: MessageFilter, listener: (text: string, direction: Direction, injected: boolean) => void) => Unsubscribe | Hears each matching message after it was carried, sent, or dropped. Never holds one up. |
unheard optional | string | null | undefined | Why observe hears nothing at all here, in words for a person, such as a plugin that was not granted packets:read; absent (or null) when it hears the traffic. What waits for the server's answer reads it before sending, so it fails at once with the reason instead of sending and timing out on an answer it could never hear. |
Direction
Which way a message travels, named for where it is going. Jam called messages from the server aj and sending to the server "remote", and plugins mixed the two up for years.
type Direction = 'toServer' | 'toClient'MessageFilter
Which messages a hook or a watcher wants.
| Field | Type | |
|---|---|---|
direction | Direction | |
commands optional | readonly string[] | undefined | Only these commands. Every command when left out. |
InterceptVerdict
What an intercepting hook decided about one message: whether it is dropped, and, if it was changed, its new text. text is null for a message passed on as it came.
| Field | Type | |
|---|---|---|
drop | boolean | |
text | string | null |
Unsubscribe
type Unsubscribe = () => voidPacketDefinition
A packet: one kind of message, with its fields named and typed.
Plugins and the SDK work with packets, not with argument positions: AvatarMove.decode(message) gives { room, points, animation, immediate }, and AvatarMove.encode({...}) gives the message the client would have sent. Each definition says where its layout comes from in the client, so the next person can check it against a new build.
| Field | Type | |
|---|---|---|
name | string | A stable, dotted name: avatar.move, chat.public. |
description | string | |
format | "xml" | "xt" | "json" | |
direction | Direction | |
command | string | |
matches | (message: Message) => boolean | Whether a message is this packet. |
decode | (message: Message) => Packet<Fields> | Reads a message as this packet. Throws a ProtocolError naming the field that is wrong. |
encode | (packet: Packet<Fields>) => Message | Writes the message for these fields, as the client would. |
layout optional | ((message: Message) => (string | undefined)[]) | undefined | For an extension message, the name each argument after the room is read as, by position: ['action', 'userName', 'clothing[0]', ...]. As far as the reader got when the frame does not follow the definition, and unnamed (undefined) past that. Absent for XML and JSON. |
Packet
A packet's fields, with the room every message is sent in.
type Packet<Fields> = Fields & {
/** The room the message is sent in, by its id; `-1` for none. */
room: number;
}ReplyOptions
Waiting for a reply: how long, and what may end it first.
| Field | Type | |
|---|---|---|
timeoutMs optional | number | undefined | 10 seconds by default. |
signal optional | AbortSignal | undefined |
PacketHook
What a hook for a packet is given: the packet's fields, and the message, to drop or inspect.
type PacketHook<Fields> = (
packet: Packet<Fields>,
message: Message
) => void | Packet<Fields> | Promise<void | Packet<Fields>>MessageHook
What a hook for a command, or for everything, is given: the message, to change or drop.
type MessageHook = (message: Message) => void | Promise<void>PacketWatchOptions
How watch listens.
| Field | Type | |
|---|---|---|
original optional | boolean | undefined | Hear only what this end itself sent, exactly as it sent it: not what Phantom, a plugin or a script sent in its name (send), and not the replacement an on hook sent in place of a message it changed. The message the hook stopped is still heard, as it came. For reading what the server really said, whatever anything else made of it on its way to the game. |