Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update #35

Merged
merged 17 commits into from
Dec 3, 2024
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pixelwalker.js",
"version": "1.3.2",
"version": "1.3.3",
"description": "PixelWalker.JS API Bindings",
"main": "dist/index.js",
"type": "module",
Expand Down
53 changes: 33 additions & 20 deletions src/game.connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ type WorldEventNames = Protocol.WorldPacket["packet"]["case"];
type WorldEventData<Name extends WorldEventNames> = Protocol.WorldPacket["packet"] & { name: Name };
export type Events = { [K in WorldEventNames & string]: [(WorldEventData<K> & { case: K })["value"]] };

export type JoinData = Partial<{
world_title: string;
world_width: 636 | 400 | 375 | 350 | 325 | 300 | 275 | 250 | 225 | 200 | 175 | 150 | 125 | 100 | 75 | 50;
world_height: 400 | 375 | 350 | 325 | 300 | 275 | 250 | 225 | 200 | 175 | 150 | 125 | 100 | 75 | 50;
spawnId: number;
}>;

/**
* The GameConnection is a connection to the game server at the
* {@link https://game.pixelwalker.net/ PixelWalker Game Server}.
Expand All @@ -29,7 +36,7 @@ export type Events = { [K in WorldEventNames & string]: [(WorldEventData<K> & {
* game.listen('playerInitPacket', () => {
* game.send('playerInitReceived');
* });
*
*
* this.connection.listen("ping", () => {
* this.connection.send("ping");
* });
Expand All @@ -42,7 +49,7 @@ export default class GameConnection {
* The protocol is a collection of all the possible messages that can be
* sent and received from the game server. It is used to serialize and
* deserialize messages to and from the game server.
*
*
* @kind file
*/
public static Protocol = Protocol;
Expand All @@ -60,32 +67,32 @@ export default class GameConnection {
#receiver: EventEmitter<Events> = new EventEmitter();

/**
*
*
* @param joinkey The joinkey retrieved from the API server.
*
*
* ```ts
* import { LobbyClient } from "pixelwalker.js/localhost"
*
*
* const client = LobbyClient.withToken(process.env.token)
* const joinkey = await client.getJoinKey(process.env.world_id);
* ```
*
*
* @returns {GameConnection} A new instance of the GameConnection.
*
*
* ```ts
* const connection = GameConnection.withJoinKey(joinkey);
* ```
*/
public static withJoinKey(joinkey: string) {
return new this(joinkey);
public static withJoinKey(joinkey: string, joinData?: JoinData) {
return new this(joinkey, joinData);
}

/**
* **NOTE**: Creating a `GameConnection` is not enough to connect to the game.
* You need to manually call the `bind` method to establish a connection, after
* registering event handlersand managing the state of your program.
*/
protected constructor(private joinkey: string) {}
protected constructor(private joinkey: string, private joinData?: JoinData) {}

//
//
Expand All @@ -99,7 +106,7 @@ export default class GameConnection {
* already been added. Multiple calls passing the same combination of
* `eventNameand` listener will result in the listener being added, and
* called, multiple times.
*
*
* | Event Name | Description |
* |--------------------|-------------|
* | `playerInitPacket` | The message event is received when the client opens the connection.
Expand All @@ -112,9 +119,9 @@ export default class GameConnection {
/**
* Sends a message to the game server without any body. Only two events, `ping` and
* `playerInitReceived` can be sent without any body.
*
*
* ### Events
*
*
* | Event Name | Description |
* |----------------------|-------------|
* | `ping` | The message has to be sent for every `ping` received from the server.
Expand All @@ -125,9 +132,9 @@ export default class GameConnection {
/**
* Sends a message to the game server, evaluating the header bytes and argument
* format based on `eventName`.
*
*
* ### Events
*
*
* | Event Name | Description |
* |----------------------|-------------|
* | `playerInitReceived` | The message has to be sent when the client receives `playerInitPacket`.
Expand All @@ -138,14 +145,14 @@ export default class GameConnection {
* Sends a message to the game server, evaluating the header bytes and argument
* format based on `eventName`. *You can optionally omit the `$typeName` and `playerId`
* fields from the message.*
*
*
* ### Events
*
*
* | Event Name | Description |
* |----------------------|-------------|
* | `playerInitReceived` | The message has to be sent when the client receives `playerInitPacket`.
*/
public send<Event extends keyof Events>(eventName: Event, value: Omit<Events[Event][0], '$typeName' | 'playerId'>): void;
public send<Event extends keyof Events>(eventName: Event, value: Omit<Events[Event][0], "$typeName" | "playerId">): void;

public send<Event extends keyof Events>(eventName: Event, value: Events[Event][0] = <any>{}) {
const message = create(Protocol.WorldPacketSchema, { packet: { case: eventName, value } } as any);
Expand All @@ -164,10 +171,16 @@ export default class GameConnection {
* creates a socket in the connection class and appends core event listeners.
*/
public bind(): this {
let url = `${Config.GameServerSocketLink}/room/${this.joinkey}`;

if (this.joinData) {
url += `?joinData=${btoa(JSON.stringify(this.joinData))}`;
}

if (process.env.LOCALHOST) {
this.socket = new WebSocket(`${Config.GameServerSocketLink}/room/${this.joinkey}`, { port: 5148 }) as any;
this.socket = new WebSocket(url, { port: 5148 });
} else {
this.socket = new WebSocket(`${Config.GameServerSocketLink}/room/${this.joinkey}`) as any;
this.socket = new WebSocket(url);
}

this.socket.binaryType = "arraybuffer";
Expand Down
113 changes: 108 additions & 5 deletions src/game.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import GameConnection from "./game.connection.js";
import EventEmitter from "events";
import GameConnection, { JoinData } from "./game.connection.js";

import LobbyClient from "./lobby.js";
import PlayerMap from "./players/map.js";
import World from "./world/world.js";
import Player from "./types/player.js";
import BlockScheduler from "./scheduler/block.js";

/**
* The GameClient is a connection interface with the game server. It is used to
Expand Down Expand Up @@ -52,6 +55,56 @@ export default class GameClient extends GameConnection {
*/
// #receiver: EventEmitter<Events> = new EventEmitter();

/**
* The command prefix is a list of prefixes that are used to identify
* bot commands in the chat.
*/
public commandPrefix: string[] = ["!", "."];

/**
* The command event is called to handle chat commands from players.
* You can listen for commands like in the example below.
*
* ```ts
* game.commands.on('giveedit', ([player, username]) => {
* // `player` is the command caller, `username` is the argument.
* // You can do permission checking here.
*
* game.send('playerChatPacket', {
* message: `/giveedit ${username}`,
* })
* })
*/
private commands = new EventEmitter<{ [commandName: string]: [Player, ...string[]] }>();

/**
* This static variable is used for command argument parsing. You
* can test it at a website like [Regex101](https://regex101.com/)
*
* The regular expression consists of three components: a double
* quoted string, a single quoted string and a word. The string
* components consists of a bracket structure to match for beginning
* and end of a string. The center part `(\\"|\\.|.)*?` matches for
* string escapes non-greedy. The word component `\S+` matches for
* a word (any combination of non-whitespace characters.)
*
* @example
*
* Here is an example of a command and the resulting matches.
*
* ```
* !test "Hello, \"World!\"" 256 wonderful-evening! --help
* ^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^ ^^^^^^^^^^^^^^^^^^ ^^^^^^
* ```
*/
private static CommandLineParser = /"(\\"|\\.|.)*?"|'(\\'|\\.|.)*?'|\S+/mg;

/**
* The block scheduler is a utility to manage block updates in the game and
* efficiently place blocks in accordance to the rate limit.
*/
private blockScheduler = new BlockScheduler(this);

/**
*
* @param joinkey The joinkey retrieved from the API server.
Expand Down Expand Up @@ -88,13 +141,12 @@ export default class GameClient extends GameConnection {
* You need to manually call the `bind` method to establish a connection, after
* registering event handlersand managing the state of your program.
*/
constructor(joinkey: string) {
super(joinkey);
constructor(joinkey: string, joinData?: JoinData) {
super(joinkey, joinData);

// this.connection = GameConnection.withJoinKey(joinkey);
// this.chat = new Chat(this.connection);
this.players = new PlayerMap(this);
this.world = new World(this);
this.world = new World(this, this.blockScheduler);
}

//
Expand All @@ -109,6 +161,7 @@ export default class GameClient extends GameConnection {
*/
public override bind(): this {
super.bind();
this.blockScheduler.start();

/**
* @event Ping
Expand All @@ -132,6 +185,56 @@ export default class GameClient extends GameConnection {
super.send("playerInitReceived");
});

/**
* @event PlayerChat
*
* The `PlayerChat` event is emitted when a player sends a chat message.
* This event handler will only listen for chat commands and emit the
* command manager.
*/
super.listen("playerChatPacket", ({ playerId, message }) => {
let idx = this.commandPrefix.findIndex((prefix) => message.startsWith(prefix));
if (idx === -1) return;

const [command, ...args] = message.substring(this.commandPrefix[idx].length).match(GameClient.CommandLineParser) ?? [];
if (!command) return;

const player = this.players[playerId];
if (!player) return;

this.commands.emit(command, player, ...args);
});

return this;
}

/**
* Closes the connection to the game server. This method is used to
* close the connection to the game server and stop the schedulers and
* other running entities.
*/
public override close(): void {
super.close();
this.blockScheduler.stop();
}

/**
* Register a command handler. The command handler is called when a player
* sends a chat message that starts with the command prefix. The command
* arguments are then parsed with a regular expression and passes the results
* to the callback function.
*
* @param commandName The name of the command to listen for. This is the first
* argument of the command message.
*
* @param player The player who sent the command. This is the first argument
* of the callback function, you do permission checking on this instance to
* determine if the player is allowed to execute the command.
*/
public listenCommand(commandName: string, callback: (player: Player, ...args: string[]) => void): this;

public listenCommand(commandName: string, callback: (player: Player, ...args: string[]) => void): this {
this.commands.on(commandName, callback);
return this;
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export { default as GameClient } from "./game.js";

export { default as PlayerMap } from "./players/map.js";

export { default as Block } from "./world/block.js";
export { default as Block, BlockId, BlockName } from "./world/block.js";
export { default as Layer } from "./world/layer.js";
export { default as Structure } from "./world/structure.js";
// export { default as World } from "./world/world.js";
Expand Down
Loading
Loading