beryl-sdk
v1.1.2
Published
TypeScript SDK for building Dark Ages tools
Readme
beryl-sdk
TypeScript SDK for Dark Ages tools that run in the browser. Connects to a local Beryl relay over WebSocket, discovers running game clients, and exposes an API for reading/writing packets, walking the character, reading files, and reading/writing process memory.
Install
npm install beryl-sdkBeryl requires the Beryl relay to be running locally. The relay attaches to Dark Ages clients and exposes them over WebSocket; this SDK is the interface you use to interact with them.
Quick start
import { Beryl, Packet } from 'beryl-sdk';
const beryl = new Beryl();
beryl.on('ready', () => {
console.log('Connected to relay');
});
beryl.on('clientConnected', ({ client, name }) => {
console.log(`Client attached: ${name}`);
client.on('sent', (packet) => {
console.log('client -> server', packet.toString());
});
client.on('received', (packet) => {
console.log('server -> client', packet.toString());
});
});
beryl.on('clientDisconnected', ({ name }) => {
console.log(`Client detached: ${name}`);
});
beryl.connect();API
class Beryl
Extends an event emitter with the events listed below.
Methods
| Method | Description |
| --- | --- |
| connect(): void | Open the connection to the relay. If the connection drops, Beryl automatically reconnects every 2 seconds until close() is called. |
| close(): void | Close the connection to the relay and cancel any pending reconnect. |
| readFile(path: string): Promise<ArrayBuffer> | Read a file from the Dark Ages install directory. Paths are resolved relative to that directory and cannot escape it. Rejects with File not found, File read error, File request timed out, or WebSocket disconnected. |
| getAllowedOrigins(): Promise<string[]> | Fetch the relay's allowed-origins list. Rejects with Settings request timed out or WebSocket disconnected. |
| setAllowedOrigins(origins: string[]): void | Replace the relay's allowed-origins list. Throws if the relay is not connected. |
All request/response methods time out after 10 seconds.
Properties
| Property | Description |
| --- | --- |
| ready: boolean | true while the relay connection is open. |
Events (BerylEvents)
| Event | Payload |
| --- | --- |
| ready | () — relay connection opened. |
| error | Error — relay connection error. |
| clientConnected | { client: Client, name: string } |
| clientDisconnected | { name: string } |
| version | { relayVersion: string \| null, minVersion: string, updateRequired: boolean } — emitted on the first relay connect and again whenever the relay reports a different version. updateRequired is true if the relay is below the SDK's minimum (or doesn't report a version). |
class Client
Represents one attached game client. Obtained via the clientConnected event.
Methods
| Method | Description |
| --- | --- |
| send(packet: Packet): void | Inject a client→server packet. Throws if the client is not connected. |
| receive(packet: Packet): void | Inject a server→client packet. Throws if the client is not connected. |
| walk(direction: number): void | Move the character one tile (0=N, 1=E, 2=S, 3=W). Throws if the client is not connected. |
| readMemory(offsets: number[], size: number): Promise<Uint8Array> | Follow a pointer chain and read size bytes. Rejects with Memory read timed out or WebSocket disconnected. |
| writeMemory(offsets: number[], data: Uint8Array): Promise<boolean> | Follow a pointer chain and write data. Resolves true on success. Rejects with Memory write timed out or WebSocket disconnected. |
| readFile(path: string): Promise<ArrayBuffer> | Read a file from the Dark Ages install directory. See Beryl.readFile for details. |
| close(): void | Disconnect from this client. |
Memory operations time out after 10 seconds.
Walking has a dedicated method because the game maintains an internal walk sequence counter. Walking via packet injection would require manually updating that counter in game memory after every step to keep it in sync. walk() avoids that by invoking the game's own walk function directly, so the counter stays correct.
Properties
| Property | Description |
| --- | --- |
| name: string | Client identifier. |
| port: number | WebSocket port the client listens on. |
Events (ClientEvents)
| Event | Payload |
| --- | --- |
| sent | Packet observed going client→server. |
| received | Packet observed going server→client. |
| focus | boolean — true when the game window gains OS focus, false when it loses focus. Emitted on connect with the current state, then on each change. |
| close | () — connection closed. |
The focus signal comes from the Win32 foreground window and is reliable on Windows. Under Wine (e.g. running the game on macOS) the game never receives the Win32 focus change, so focus reports true on connect and never fires again.
class Packet
Cursor-based reader/writer for Dark Ages packets.
const p = new Packet(0x0e); // new packet with opcode 0x0e
p.writeByte(0);
p.writeString8('hello');
client.send(p);
const incoming = new Packet(bytes); // parse from Uint8Array
const opcode = incoming.opcode;
const type = packet.readByte();
const id = packet.readUint32();
const message = packet.readString8();Constructors
| Signature | Description |
| --- | --- |
| new Packet(opcode: number) | Build an empty packet with the given opcode. |
| new Packet(data: Uint8Array) | Parse an existing packet. opcode is data[0] and the cursor starts at the first body byte. |
Properties
| Property | Description |
| --- | --- |
| opcode: number | Packet opcode. |
| position: number | Current cursor position within the body. |
Methods
| Method | Description |
| --- | --- |
| read(length: number): number[] | Read length bytes. |
| readByte(): number | Read an unsigned byte. |
| readSbyte(): number | Read a signed byte. |
| readBoolean(): boolean | Read a byte as a boolean. |
| readInt16(): number | Read a signed 16-bit integer. |
| readUint16(): number | Read an unsigned 16-bit integer. |
| readInt32(): number | Read a signed 32-bit integer. |
| readUint32(): number | Read an unsigned 32-bit integer. |
| readString8(): string | Read a string with a 1-byte length prefix. |
| readString16(): string | Read a string with a 2-byte length prefix. |
| write(bytes: number[]): void | Append raw bytes. |
| writeByte(value: number): void | Write an unsigned byte. |
| writeSbyte(value: number): void | Write a signed byte. |
| writeBoolean(value: boolean): void | Write a byte as a boolean. |
| writeInt16(value: number): void | Write a signed 16-bit integer. |
| writeUint16(value: number): void | Write an unsigned 16-bit integer. |
| writeInt32(value: number): void | Write a signed 32-bit integer. |
| writeUint32(value: number): void | Write an unsigned 32-bit integer. |
| writeString(value: string): void | Write a string with no length prefix. |
| writeString8(value: string): void | Write a string with a 1-byte length prefix. |
| writeString16(value: string): void | Write a string with a 2-byte length prefix. |
| buffer(): Uint8Array | The packet's bytes. |
| toString(): string | Space-separated uppercase hex. |
Read methods advance position. Reads past the end return a zero value of the matching type: 0 for numbers, false for booleans, '' for strings, [] for read(length).
License
MIT
