UNPKG

@rtsdk/lance-topia

Version:

A Node.js based real-time multiplayer game server

942 lines (693 loc) 26.3 kB
import SocketIO from 'socket.io'; import CANNON from 'cannon'; import p2 from "p2"; // Type definitions for lance-gg 4.0 // Project: https://github.com/lance-gg/lance#readme // Definitions by: Tomasz Sterna <https://github.com/smokku> export class AFrameRenderer<GE extends GameEngine<PhysicsEngine>, CE extends ClientEngine<GE>> extends Renderer<GE, CE> { draw(): void; init(): Promise<any>; reportSlowFrameRate(): void; tick(t?: number, dt?: number): void; } declare type CannonPhysicsEngineOptions = PhysicsEngineOptions & { dt?: number; } export class CannonPhysicsEngine extends PhysicsEngine { options: CannonPhysicsEngineOptions; constructor(options: CannonPhysicsEngineOptions); addBox(x: number, y: number, z: number, mass: number, friction: any): typeof CANNON.Body; addCylinder(radiusTop: number, radiusBottom: number, height: number, numSegments: number, mass: number): typeof CANNON.Body; addSphere(radius: number, mass: number): typeof CANNON.Body; removeObject(obj: typeof CANNON.Body): void; step(dt: number, objectFilter?: any): void; } declare type ClientEngineInputOptions = { verbose?: boolean; autoConnect?: boolean; standaloneMode?: boolean; delayInputCount?: number; healthCheckInterval?: number; healthCheckRTTSample?: number; scheduler?: "render-schedule" | "fixed"; syncOptions?: { sync: "interpolate" | "extrapolate" | "frameSync"; localObjBending?: number; remoteObjBending?: number; }; serverURL?: String; } export class ClientEngine<GE extends GameEngine<PhysicsEngine>> { constructor(gameEngine: GE, inputOptions: ClientEngineInputOptions, rendererClass: { new(...args: any[]): Renderer<GE, ClientEngine<GE>>; }); applyDelayedInputs(): void; checkDrift(checkType: string): void; configureSynchronization(): void; connect(options: object): Promise<any>; disconnect(): void; doInputLocal(message: { data: any }): void; handleInboundMessage(syncData: object): void; handleOutboundInput(): void; sendInput(input: string, inputOptions: object): any; start(): Promise<any>; step(t: number, dt: number, physicsOnly?: boolean): void; } export class DynamicObject<GE extends GameEngine<PE>, PE extends PhysicsEngine> extends GameObject<GE, PE> { constructor(gameEngine: any, options: any, props: any); bendingIncrements: number; position: TwoVector; velocity: TwoVector; width: number; height: number; isStatic: number; friction: TwoVector; angle: number; isAccelerating: boolean; rotationSpeed: number; accelerate(acceleration: any): any; applyIncrementalBending(stepDesc: any): void; bendToCurrent(original: any, percent: any, worldSettings: any, isLocal: any, increments: any): void; bendingToString(): string; collidesWith(other: any): any; getAABB(): any; syncTo(other: any): void; toString(): string; turnLeft(deltaAngle: any): any; turnRight(deltaAngle: any): any; static netScheme: { angle: { type: string; }; height: { type: string; }; id: { type: string; }; isStatic: { type: string; }; playerId: { type: string; }; position: { type: string; }; velocity: { type: string; }; width: { type: string; }; } & GameObjectNetScheme; } declare type GameEngineOptions = { traceLevel?: number; } /** * The GameEngine contains the game logic. Extend this class * to implement game mechanics. The GameEngine derived * instance runs once on the server, where the final decisions * are always taken, and one instance will run on each client as well, * where the client emulates what it expects to be happening * on the server. * * The game engine's logic must listen to user inputs and * act on these inputs to change the game state. For example, * the game engine listens to controller/keyboard inputs to infer * movement for the player/ship/first-person. The game engine listens * to clicks, button-presses to infer firing, etc.. * * Note that the game engine runs on both the server and on the * clients - but the server decisions always have the final say, * and therefore clients must resolve server updates which conflict * with client-side predictions. */ export class GameEngine<PE extends PhysicsEngine> { options: GameEngineOptions; /** * client's player ID, as a string. If running on the client, this is set at runtime by the clientEngine */ playerId: string; world: GameWorld; /** * The worldSettings defines the game world constants, such * as width, height, depth, etc. such that all other classes * can reference these values. */ worldSettings: object; physicsEngine: PE; ignorePhysics: boolean; constructor(options: GameEngineOptions); /** * Add object to the game world. * On the client side, the object may not be created, if the server copy * of this object is already in the game world. This could happen when the client * is using delayed-input, and the RTT is very low. * * @param object - the object. * @return the final object. */ addObjectToWorld(object: GameObject<GameEngine<PE>, PE>): GameObject<GameEngine<PE>, PE>; findLocalShadow(serverObj: GameObject<GameEngine<PE>, PE>): GameObject<GameEngine<PE>, PE> | null; /** * Decide whether the player game is over by returning an Object, need to be implemented * * @return truthful if the game is over for the player and the object is returned as GameOver data */ getPlayerGameOverResult(): object | void; initWorld(worldSettings: object): void; /** * Check if a given object is owned by the player on this client * * @param object the game object to check * @return true if the game object is owned by the player on this client */ isOwnedByPlayer(object: GameObject<GameEngine<PE>, PE>): boolean; /** * Override this function to implement input handling. * This method will be called on the specific client where the * input was received, and will also be called on the server * when the input reaches the server. The client does not call this * method directly, rather the client calls {@link ClientEngine#sendInput} * so that the input is sent to both server and client, and so that * the input is delayed artificially if so configured. * * The input is described by a short string, and is given an index. * The index is used internally to keep track of inputs which have already been applied * on the client during synchronization. The input is also associated with * the ID of a player. * * @param inputDesc - input descriptor object * @param playerId - the player ID * @param isServer - indicate if this function is being called on the server side */ processInput(inputDesc: InputData, playerId: number, isServer?: boolean): void; /** * Register Game Object Classes * * @example * registerClasses(serializer) { * serializer.registerClass(Paddle); * serializer.registerClass(Ball); * } * * @param serializer - the serializer */ registerClasses(serializer: Serializer): void; /** * Remove an object from the game world. * * @param objectId - the object or object ID */ removeObjectFromWorld(objectId: GameObject<GameEngine<PE>, PE> | string): void; /** * Start the game. This method runs on both server * and client. Extending the start method is useful * for setting up the game's worldSettings attribute, * and registering methods on the event handler. */ start(): void; /** * Single game step. * * @param isReenact - is this step a re-enactment of the past. * @param t - the current time (optional) * @param dt - elapsed time since last step was called. (optional) * @param physicsOnly - do a physics step only, no game logic */ step(isReenact: boolean, t: number, dt: number, physicsOnly: boolean): void; /** * Register a handler for an event * * @param eventName - name of the event * @param eventHandler - handler function */ on(eventName: string, eventHandler: Function): void; /** * Register a handler for an event, called just once (if at all) * * @param eventName - name of the event * @param eventHandler - handler function */ once(eventName: string, eventHandler: Function): void; /** * Remove a handler * * @param eventName - name of the event * @param eventHandler - handler function */ off(eventName: string, eventHandler: Function): void; /** * @alias off */ removeListener(eventName: string, eventHandler: Function): void; emit(eventName: string, data?: any): void; trace: Lib.Trace; } declare type SerializedObject = { dataBuffer: ArrayBuffer; bufferOffset: number; } declare type SerializableNetScheme = NetSchemeProps; export class Serializable { classId: string; static netScheme: NetSchemeProps; serialize(serializer: object, options: object): { dataBuffer: object; bufferOffset: number }; prunedStringsClone(serializer: object, prevObject: object): Serializable; syncTo(other: object): void; } declare type GameObjectOptions = { id?: number; } declare type GameObjectProps = { playerId?: number; } declare type GameObjectNetScheme = { id: { type: string; }; playerId: { type: string; }; } & SerializableNetScheme; export class GameObject<GE extends GameEngine<PE>, PE extends PhysicsEngine> extends Serializable { gameEngine: GE; id: number; playerId: number; components: object; constructor(gameEngine: GE, options: GameObjectOptions, props: GameObjectProps); addComponent(componentInstance: any): void; applyIncrementalBending(stepDesc: any): void; bendToCurrent(original: any, bending: any, worldSettings: any, isLocal: any, bendingIncrements: any): void; bendToCurrentState(bending: any, worldSettings: any, isLocal: any, bendingIncrements: any): void; bendingToString(): string; destroy(): void; getComponent(componentClass: any): any; hasComponent(componentClass: any): any; onAddToWorld(gameEngine: any): void; onRemoveFromWorld(gameEngine: any): void; refreshFromPhysics(): void; refreshToPhysics(): void; removeComponent(componentName: any): void; saveState(other: any): void; syncTo(other: any): void; toString(): string; static netScheme: GameObjectNetScheme; } export class GameWorld { constructor(); addObject(object: any): void; forEachObject(callback: any): void; getNewId(): any; queryObject(query: any): any; queryObjects(query: any): any; removeObject(id: any): void; } /** * @param input - describe the input (e.g. "up", "down", "fire") * @param messageIndex - input identifier * @param step - the step on which this input occurred */ declare type InputData = { input: string; messageIndex: number; step: number; } export class KeyboardControls { constructor(clientEngine: any); bindKey(keys: string, actionName: string, options?: object): void; onKeyChange(e: any, isDown: any): void; setupListeners(): void; } declare type PhysicsEngineOptions = { gameEngine: GameEngine<PhysicsEngine>; } export class PhysicsEngine { options: PhysicsEngineOptions; gameEngine: GameEngine<PhysicsEngine>; constructor(options: PhysicsEngineOptions); step(dt: number, objectFilter: Function): void; } declare type P2PhysicsEngineOptions = PhysicsEngineOptions & { dt?: number; } export class P2PhysicsEngine extends PhysicsEngine { p2: p2; world: { [key: string]: any }; constructor(options: P2PhysicsEngineOptions); addBox(width: number, height: number, mass: number): typeof p2.Body; addCircle(radius: number, mass: number): typeof p2.Body; removeObject(obj: typeof p2.Body): void; step(dt: number, objectFilter: Function): void; } /** * @param position - position vector * @param velocity - velocity vector * @param angle - orientation angle * @param mass - the mass * @param angularVelocity - angular velocity */ declare type PhysicalObject2DProps = GameObjectProps & { position?: TwoVector; velocity?: TwoVector; angle?: number; mass?: number; angularVelocity?: number; } declare type PhysicalObject2DSyncToOptions = { keepVelocity?: boolean; } export class PhysicalObject2D<GE extends GameEngine<PE>, PE extends PhysicsEngine> extends GameObject<GE, PE> { position: TwoVector; velocity: TwoVector; angle: number; angularVelocity: number; mass: number; physicsObj: typeof p2.Body; /** * Creates an instance of a physical object. * Override to provide starting values for position, velocity, angle and angular velocity. * NOTE: all subclasses of this class must comply with this constructor signature. * This is required because the engine will create temporary instances when * syncs arrive on the clients. * @param gameEngine - the gameEngine this object will be used in * @param options - options for the new object. See {@link GameObject} * @param props - properties to be set in the new object */ constructor(gameEngine: GameEngine<PE>, options?: GameObjectOptions, props?: PhysicalObject2DProps); /// apply one increment of bending applyIncrementalBending(stepDesc?: { dt?: number }): void; /// derive and save the bending increment parameters: /// - bendingPositionDelta /// - bendingVelocityDelta /// - bendingAVDelta /// - bendingAngleDelta /// these can later be used to "bend" incrementally from the state described /// by "original" to the state described by "self" bendToCurrent(original: any, percent: any, worldSettings: any, isLocal: any, increments: any): void; /** * Each object class can define its own bending overrides. * return an object which can include attributes: position, velocity, * angle, and angularVelocity. In each case, you can specify a min value, max * value, and a percent value. * * example: * position: { percent: 0.8, min: 0.0, max: 4.0 }, * velocity: { percent: 0.4, min: 0.0 }, * angularVelocity: { percent: 0.0 }, * angleLocal: { percent: 0.0 } * * @return bending - an object with bending parameters */ bending: { [key: string]: { percent?: number, min?: number, max?: number }; } /// display object's physical attributes as a string /// for debugging purposes mostly bendingToString(): string; /// generic vector copy. We need this because different /// physics engines have different implementations. copyVector(source: TwoVector, target: Function | Float32Array | TwoVector): void; /// interpolate implementation interpolate(nextObj: PhysicalObject2D<GE, PE>, percent: number): void; /** * Called after the object is added to to the game world. * This is the right place to add renderer sub-objects, physics sub-objects * and any other resources that should be created */ onAddToWorld(): void; /// update position, angle, angular velocity, and velocity from new physical state. refreshFromPhysics(): void; /// update position, angle, angular velocity, and velocity from new game state. refreshToPhysics(): void; syncTo(other: PhysicalObject2D<GE, PE>, options?: PhysicalObject2DSyncToOptions): void; /** * Formatted textual description of the dynamic object. * The output of this method is used to describe each instance in the traces, * which significantly helps in debugging. * * @return description - a string describing the PhysicalObject2D */ toString(): string; /** * The netScheme is a dictionary of attributes in this game * object. The attributes listed in the netScheme are those exact * attributes which will be serialized and sent from the server * to each client on every server update. * The netScheme member is implemented as a getter. * * You may choose not to implement this method, in which * case your object only transmits the default attributes * which are already part of {@link PhysicalObject2D}. * But if you choose to add more attributes, make sure * the return value includes the netScheme of the super class. * * @example * static get netScheme() { * return Object.assign({ * mojo: { type: BaseTypes.TYPES.UINT8 }, * }, super.netScheme); * } */ static netScheme: { angle: { type: string; }; angularVelocity: { type: string; }; id: { type: string; }; mass: { type: string; }; playerId: { type: string; }; position: { type: string; }; velocity: { type: string; }; } & GameObjectNetScheme; } declare type PhysicalObject3DProps = GameObjectProps & { } declare type PhysicalObject3DSyncToOptions = { } export class PhysicalObject3D<GE extends GameEngine<PE>, PE extends PhysicsEngine> extends GameObject<GE, PE> { physicsObj: typeof CANNON.Body; constructor(gameEngine: any, options: any, props: any); applyIncrementalBending(stepDesc: any): void; bendToCurrent(original: any, percent: any, worldSettings: any, isLocal: any, increments: any): void; bendingToString(): string; interpolate(nextObj: any, percent: any): void; refreshFromPhysics(): void; refreshToPhysics(): void; syncTo(other: PhysicalObject3D<GE, PE>, options?: PhysicalObject3DSyncToOptions): void; toString(): string; static netScheme: { angularVelocity: { type: string; }; id: { type: string; }; playerId: { type: string; }; position: { type: string; }; quaternion: { type: string; }; velocity: { type: string; }; } & GameObjectNetScheme; } export class Quaternion extends Serializable { constructor(w: any, x: any, y: any, z: any); conjugate(): any; copy(sourceObj: any): any; multiply(other: any): any; normalize(): any; set(w: any, x: any, y: any, z: any): any; setFromAxisAngle(axis: any, angle: any): any; slerp(target: any, bending: any): any; toAxisAngle(): any; toString(): string; static netScheme: { w: { type: string; }; x: { type: string; }; y: { type: string; }; z: { type: string; }; } & SerializableNetScheme; } export class Renderer<GE extends GameEngine<PhysicsEngine>, CE extends ClientEngine<GE>> { static getInstance<R extends Renderer<GameEngine<PhysicsEngine>, ClientEngine<GameEngine<PhysicsEngine>>>>(): R; gameEngine: GE; clientEngine: CE; constructor(gameEngine: GE, clientEngine: CE); addObject(obj: GameObject<GE, PhysicsEngine>): void; draw(t: number, dt?: number): void; init(): Promise<any>; removeObject(obj: GameObject<GE, PhysicsEngine>): void; reportSlowFrameRate(): void; runClientStep(t: number): void; stop(): void; } declare type NetSchemeProp = { type: string; } declare type NetSchemeProps = { [key: string]: NetSchemeProp; } /** * The Serializer is responsible for serializing the game world and its * objects on the server, before they are sent to each client. On the client side the * Serializer deserializes these objects. * */ export class Serializer { constructor(); /** * Checks if type can be assigned by value. * @param type Type to Checks * @return True if type can be assigned */ static typeCanAssign(type: string): boolean; /** * Registers a new class with the serializer, so it may be deserialized later * @param classObj reference to the class (not an instance!) * @param classId Unit specifying a class ID */ registerClass(classObj: { new(...args: any[]): Serializable; }, classId?: string): void; deserialize(dataBuffer: ArrayBuffer, byteOffset: number): { obj: Serializable, byteOffset: number }; writeDataView(dataView: DataView, value: any, bufferOffset: number, netSchemProp: NetSchemeProp): void; readDataView(dataView: DataView, bufferOffset: number, netSchemProp: NetSchemeProp): { data: any, bufferSize: number }; getTypeByteSize(type: string): void; } declare type ServerEngineOptions = { stepRate?: number; updateRate?: number; fullSyncRate?: number; tracesPath?: string; countConnections?: boolean; updateOnObjectCreation?: boolean; timeoutInterval?: number; debug?: { serverSendLag?: boolean; }; } /** * ServerEngine is the main server-side singleton code. * Extend this class with your own server-side logic, and * start a single instance. * * This class should not be used to contain the actual * game logic. That belongs in the GameEngine class, where the mechanics * of the gameplay are actually implemented. * The ServerEngine singleton is typically a lightweight * implementation, logging gameplay statistics and registering * user activity and user data. * * The base class implementation is responsible for starting * the server, initiating each game step, accepting new * connections and dis-connections, emitting periodic game-state * updates, and capturing remote user inputs. */ export class ServerEngine<PE extends PhysicsEngine = PhysicsEngine> { gameEngine: GameEngine<PE>; DEFAULT_ROOM_NAME: string; rooms: {}; connectedPlayers: {}; playerInputQueues: {}; objMemory: {}; constructor(io: SocketIO, gameEngine: GameEngine<PE>, options?: ServerEngineOptions); /** * Assign an object to a room * * @param obj - the object to move * @param roomName - the target room */ assignObjectToRoom(obj: GameObject<GameEngine<PE>, PE>, roomName: string): void; /** * Assign a player to a room * * @param playerId - the playerId * @param roomName - the target room */ assignPlayerToRoom(playerId: number, roomName: string): void; /** * Create a room * * There is a default room called "/lobby". All newly created players * and objects are assigned to the default room. When the server sends * periodic syncs to the players, each player is only sent those objects * which are present in his room. * * @param roomName - the new room name */ createRoom(roomName: string): void; /** * Report game status * This method is only relevant if the game uses MatchMaker functionality. * This method must return the game status. * * @return Stringified game status object. */ gameStatus(): string; getPlayerId(socket: SocketIO.Socket): void; onObjectAdded(obj: GameObject<GameEngine<PE>, PE>): void; onObjectDestroyed(obj: GameObject<GameEngine<PE>, PE>): void; onPlayerConnected(socket: SocketIO.Socket): void; onPlayerDisconnected(socketId: string, playerId: string): void; onPlayerTimeout(socket: SocketIO.Socket): void; onReceivedInput(data: InputData, socket: SocketIO.Socket): void; queueInputForPlayer(data: InputData, playerId: string): void; resetIdleTimeout(socket: SocketIO.Socket): void; serializeUpdate(roomName: string, options: { diffUpdate: boolean }): SerializedObject; start(): void; step(): void; syncStateToClients(roomName: string): void; } declare type SimplePhysicsEngineOptions = PhysicsEngineOptions & { collisions: { type: "HSHG"; } | { type: "bruteForce"; collisionDistance?: number; autoResolve?: boolean; }, gravity: TwoVector; } export class SimplePhysicsEngine extends PhysicsEngine { options: SimplePhysicsEngineOptions; constructor(options: SimplePhysicsEngineOptions); objectStep(o: any, dt: any): void; step(dt: any, objectFilter: any): void; } export class ThreeVector extends Serializable { constructor(x: any, y: any, z: any); add(other: any): any; clone(): any; copy(sourceObj: any): any; getBendingDelta(target: any, options: any): any; length(): any; lerp(target: any, p: any): any; multiplyScalar(s: any): any; normalize(): any; set(x: any, y: any, z: any): any; subtract(other: any): any; toString(): string; static netScheme: { x: { type: string; }; y: { type: string; }; z: { type: string; }; } & SerializableNetScheme; } export class TwoVector extends Serializable { x: number; y: number; constructor(x: number, y: number); add(other: TwoVector): TwoVector; clone(): TwoVector; copy(source: TwoVector): TwoVector; getBendingDelta(target: TwoVector, options: object): TwoVector; length(): number; lerp(target: TwoVector, p: number): TwoVector; multiply(other: TwoVector): TwoVector; multiplyScalar(s: number): TwoVector; normalize(): TwoVector; set(x: number, y: number): TwoVector; subtract(other: TwoVector): TwoVector; toString(): string; static netScheme: { x: { type: string; }; y: { type: string; }; } & SerializableNetScheme; } export namespace BaseTypes { const TYPES: { CLASSINSTANCE: string; FLOAT32: string; INT16: string; INT32: string; INT8: string; LIST: string; STRING: string; UINT8: string; }; } export namespace Lib { class Trace { constructor(options: any); rotate(): any; setStep(s: any): void; trace(level: any, dataCB: any): void; static TRACE_ALL: number; static TRACE_DEBUG: number; static TRACE_ERROR: number; static TRACE_INFO: number; static TRACE_NONE: number; static TRACE_WARN: number; } }