rustplus-ts
Version:
Rust+ API Wrapper written in TypeScript for the game Rust.
425 lines (419 loc) • 17 kB
JavaScript
/*
Copyright (C) 2025 Alexander Emanuelsson (alexemanuelol)
This program is free software = you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https =//www.gnu.org/licenses/>.
https://github.com/alexemanuelol/rustplus-ts
*/
'use strict';
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Camera = exports.CameraType = exports.ControlFlags = exports.Buttons = void 0;
exports.getCameraType = getCameraType;
const events_1 = require("events");
const jimp = __importStar(require("jimp"));
const rp = __importStar(require("./rustplus"));
const validation = __importStar(require("./validation"));
/**
* The buttons that can be sent to the server.
*/
var Buttons;
(function (Buttons) {
Buttons[Buttons["NONE"] = 0] = "NONE";
Buttons[Buttons["FORWARD"] = 2] = "FORWARD";
Buttons[Buttons["BACKWARD"] = 4] = "BACKWARD";
Buttons[Buttons["LEFT"] = 8] = "LEFT";
Buttons[Buttons["RIGHT"] = 16] = "RIGHT";
Buttons[Buttons["JUMP"] = 32] = "JUMP";
Buttons[Buttons["DUCK"] = 64] = "DUCK";
Buttons[Buttons["SPRINT"] = 128] = "SPRINT";
Buttons[Buttons["USE"] = 256] = "USE";
Buttons[Buttons["FIRE_PRIMARY"] = 1024] = "FIRE_PRIMARY";
Buttons[Buttons["FIRE_SECONDARY"] = 2048] = "FIRE_SECONDARY";
Buttons[Buttons["RELOAD"] = 8192] = "RELOAD";
Buttons[Buttons["FIRE_THIRD"] = 134217728] = "FIRE_THIRD";
})(Buttons || (exports.Buttons = Buttons = {}));
/**
* The control flags that can be sent to the server.
* - CCTV Camera: 0, NONE
* - PTZ CCTV Camera: 10, NONE, MOUSE, FIRE
* - Drone: 7, NONE, MOVEMENT, MOUSE, SPRINT_AND_DUCK
* - Auto Turret: 58, NONE, MOUSE, FIRE, RELOAD, CROSSHAIR
*/
var ControlFlags;
(function (ControlFlags) {
ControlFlags[ControlFlags["NONE"] = 0] = "NONE";
ControlFlags[ControlFlags["MOVEMENT"] = 1] = "MOVEMENT";
ControlFlags[ControlFlags["MOUSE"] = 2] = "MOUSE";
ControlFlags[ControlFlags["SPRINT_AND_DUCK"] = 4] = "SPRINT_AND_DUCK";
ControlFlags[ControlFlags["FIRE"] = 8] = "FIRE";
ControlFlags[ControlFlags["RELOAD"] = 16] = "RELOAD";
ControlFlags[ControlFlags["CROSSHAIR"] = 32] = "CROSSHAIR";
})(ControlFlags || (exports.ControlFlags = ControlFlags = {}));
var CameraType;
(function (CameraType) {
CameraType[CameraType["UNKNOWN"] = 0] = "UNKNOWN";
CameraType[CameraType["CCTV_CAMERA"] = 1] = "CCTV_CAMERA";
CameraType[CameraType["PTZ_CCTV_CAMERA"] = 2] = "PTZ_CCTV_CAMERA";
CameraType[CameraType["DRONE"] = 3] = "DRONE";
CameraType[CameraType["AUTO_TURRET"] = 4] = "AUTO_TURRET";
})(CameraType || (exports.CameraType = CameraType = {}));
function getCameraType(controlFlags) {
switch (controlFlags) {
case 0: {
return CameraType.CCTV_CAMERA;
}
case 10: {
return CameraType.PTZ_CCTV_CAMERA;
}
case 7: {
return CameraType.DRONE;
}
case 58: {
return CameraType.AUTO_TURRET;
}
default: {
return CameraType.UNKNOWN;
}
}
}
class Camera extends events_1.EventEmitter {
rustplus;
identifier;
cameraType;
playerId;
playerToken;
isSubscribed;
cameraRays;
cameraSubscribeInfo;
/**
* @param {rp.RustPlus} rustplus An existing RustPlus instance.
*
* Events emitted by the Camera class instance
* - subscribing: When we are subscribing to a Camera.
* - subscribed: When we are subscribed to a Camera.
* - unsubscribing: When we are unsubscribing from the Camera.
* - unsubscribed: When we are unsubscribed from the Camera.
* - render: When a camera frame has been rendered. A png image buffer will be provided.
*/
constructor(rustplus) {
super();
this.rustplus = rustplus;
this.identifier = '';
this.playerId = '';
this.playerToken = 0;
this.isSubscribed = false;
this.cameraRays = [];
this.cameraSubscribeInfo = null;
this.cameraType = CameraType.UNKNOWN;
this.rustplus.on('message', async (appMessage, handled) => {
if (this.isSubscribed && appMessage.broadcast && appMessage.broadcast.cameraRays) {
/* Add new camera rays to cache. */
this.cameraRays.push(appMessage.broadcast.cameraRays);
if (this.cameraRays.length > 10) {
/* Remove oldest camera rays. */
this.cameraRays.shift();
/* Render to png. */
const image = await this.renderCameraFrame();
this.emit('render', image);
}
}
});
this.rustplus.on('disconnected', async () => {
if (this.isSubscribed) {
await this.unsubscribe();
}
});
}
/**
* Render a camera frame to a PNG image buffer.
* @returns {Promise<Buffer>} Returns a promise that resolves to Buffer.
*/
async renderCameraFrame() {
const frames = this.cameraRays;
const width = this.cameraSubscribeInfo.width;
const height = this.cameraSubscribeInfo.height;
/* First we populate the samplePositionBuffer with the positions of each sample. */
const samplePositionBuffer = new Int16Array(width * height * 2);
for (let y = 0, index = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
samplePositionBuffer[index++] = x;
samplePositionBuffer[index++] = y;
}
}
const indexGenerator = new IndexGenerator(1337);
for (let R = width * height - 1; R >= 1; R--) {
const C = 2 * R;
const I = 2 * indexGenerator.nextInt(R + 1);
const P = samplePositionBuffer[C];
const k = samplePositionBuffer[C + 1];
const A = samplePositionBuffer[I];
const F = samplePositionBuffer[I + 1];
samplePositionBuffer[I] = P;
samplePositionBuffer[I + 1] = k;
samplePositionBuffer[C] = A;
samplePositionBuffer[C + 1] = F;
}
/* Create the output buffer. */
const output = new Array(width * height);
/* Loop through each frame */
for (const frame of frames) {
/* Reset some look back and pointer variables. */
let sampleOffset = 2 * frame.sampleOffset;
let dataPointer = 0;
const rayLookback = new Array(64);
for (let r = 0; r < 64; r++) {
rayLookback[r] = [0, 0, 0];
}
const rayData = frame.rayData;
/* Loop through the ray data. */
while (true) {
if (dataPointer >= rayData.length - 1) {
break;
}
/* Get the first byte and set some variables. */
let t, r, i;
const n = rayData[dataPointer++];
/* Ray Decoding Logic. */
if (255 === n) {
const l = rayData[dataPointer++];
const o = rayData[dataPointer++];
const s = rayData[dataPointer++];
const u = (3 * (((t = (l << 2) | (o >> 6)) / 128) | 0) + 5 *
(((r = 63 & o) / 16) | 0) + 7 * (i = s)) & 63;
let f = rayLookback[u];
f[0] = t;
f[1] = r;
f[2] = i;
}
else {
const c = 192 & n;
if (0 === c) {
const h = 63 & n;
const y = rayLookback[h];
t = y[0];
r = y[1];
i = y[2];
}
else if (64 === c) {
const p = 63 & n;
const v = rayLookback[p];
const b = v[0];
const w = v[1];
const h = v[2];
const g = rayData[dataPointer++];
t = b + ((g >> 3) - 15);
r = w + ((7 & g) - 3);
i = h;
}
else if (128 === c) {
const R = 63 & n;
const C = rayLookback[R];
const I = C[0];
const P = C[1];
const k = C[2];
t = I + (rayData[dataPointer++] - 127);
r = P;
i = k;
}
else {
const A = rayData[dataPointer++];
const F = rayData[dataPointer++];
const D = (3 * (((t = (A << 2) | (F >> 6)) / 128) | 0) + 5 *
(((r = 63 & F) / 16) | 0) + 7 * (i = 63 & n)) & 63;
let E = rayLookback[D];
E[0] = t;
E[1] = r;
E[2] = i;
}
}
sampleOffset %= 2 * width * height;
const index = samplePositionBuffer[sampleOffset++] + samplePositionBuffer[sampleOffset++] * width;
output[index] = [t / 1023, r / 63, i];
}
}
const colours = [
[0.5, 0.5, 0.5], [0.8, 0.7, 0.7], [0.3, 0.7, 1], [0.6, 0.6, 0.6],
[0.7, 0.7, 0.7], [0.8, 0.6, 0.4], [1, 0.4, 0.4], [1, 0.1, 0.1],
];
const image = new jimp.Jimp({ width: width, height: height });
for (let i = 0; i < output.length; i++) {
const ray = output[i];
if (!ray) {
continue;
}
const distance = ray[0];
const alignment = ray[1];
const material = ray[2];
let target_colour;
if (distance === 1 && alignment === 0 && material === 0) {
target_colour = [208, 230, 252];
}
else {
const colour = colours[material];
target_colour = [
(alignment * colour[0] * 255),
(alignment * colour[1] * 255),
(alignment * colour[2] * 255)
];
}
const x = i % width;
const y = height - 1 - Math.floor(i / width);
image.setPixelColor(jimp.rgbaToInt(target_colour[0], target_colour[1], target_colour[2], 255), x, y);
}
/* Return png buffer. */
return await image.getBuffer(jimp.JimpMime.png);
}
/**
* Subscribe to a camera.
* @param {string} playerId The steamId of the player making the request.
* @param {number} playerToken The authentication token of the player making the request.
* @param {string} identifier Camera identifier.
* @returns {Promise<boolean>} Returns a promise that resolves true if successful, else false.
*/
async subscribe(playerId, playerToken, identifier) {
this.emit('subscribing');
const response = await this.rustplus.cameraSubscribeAsync(playerId, playerToken, identifier);
if (validation.isValidAppResponse(response) &&
this.rustplus.getAppResponseError(response) === rp.AppResponseError.NoError) {
this.identifier = identifier;
this.playerId = playerId;
this.playerToken = playerToken;
this.isSubscribed = true;
this.cameraRays = [];
this.cameraSubscribeInfo = response.cameraSubscribeInfo;
this.cameraType = getCameraType(this.cameraSubscribeInfo.controlFlags);
this.emit('subscribed');
return true;
}
this.identifier = '';
this.playerId = '';
this.playerToken = 0;
this.isSubscribed = false;
this.cameraRays = [];
this.cameraSubscribeInfo = null;
this.cameraType = CameraType.UNKNOWN;
return false;
}
/**
* Unsubscribe from a camera.
* @returns {Promise<boolean>} Returns a promise that resolves true if successful, else false.
*/
async unsubscribe() {
this.emit('unsubscribing');
if (this.rustplus.isConnected()) {
await this.rustplus.cameraUnsubscribeAsync(this.playerId, this.playerToken);
}
this.identifier = '';
this.playerId = '';
this.playerToken = 0;
this.isSubscribed = false;
this.cameraRays = [];
this.cameraSubscribeInfo = null;
this.cameraType = CameraType.UNKNOWN;
this.emit('unsubscribed');
return true;
}
/**
* Zoom a PTZ CCTV camera.
* @returns {Promise<boolean>} Returns a promise that resolves true if successful, else false.
*/
async zoom() {
if (!this.isSubscribed || this.cameraType !== CameraType.PTZ_CCTV_CAMERA) {
return false;
}
/* Press left mouse button to zoom in. */
await this.rustplus.cameraInputAsync(this.playerId, this.playerToken, Buttons.FIRE_PRIMARY, 0, 0);
/* Release all mouse buttons. */
await this.rustplus.cameraInputAsync(this.playerId, this.playerToken, Buttons.NONE, 0, 0);
return true;
}
/**
* Shoot a PTZ CCTV controlled Auto Turret.
* @returns {Promise<boolean>} Returns a promise that resolves true if successful, else false.
*/
async shoot() {
if (!this.isSubscribed || this.cameraType !== CameraType.AUTO_TURRET) {
return false;
}
/* Press left mouse button to shoot. */
await this.rustplus.cameraInputAsync(this.playerId, this.playerToken, Buttons.FIRE_PRIMARY, 0, 0);
/* Release all mouse buttons. */
await this.rustplus.cameraInputAsync(this.playerId, this.playerToken, Buttons.NONE, 0, 0);
return true;
}
/**
* Reload a PTZ CCTV controlled Auto Turret.
* @returns {Promise<boolean>} Returns a promise that resolves true if successful, else false.
*/
async reload() {
if (!this.isSubscribed || this.cameraType !== CameraType.AUTO_TURRET) {
return false;
}
/* Press left mouse button to shoot. */
await this.rustplus.cameraInputAsync(this.playerId, this.playerToken, Buttons.RELOAD, 0, 0);
/* Release all mouse buttons. */
await this.rustplus.cameraInputAsync(this.playerId, this.playerToken, Buttons.NONE, 0, 0);
return true;
}
}
exports.Camera = Camera;
class IndexGenerator {
state;
constructor(e) {
this.state = 0 | e;
this.nextState();
}
nextInt(e) {
let t = ((this.nextState() * (e | 0)) / 4294967295) | 0;
if (t < 0)
t = e + t - 1;
return t | 0;
}
nextState() {
let e = this.state;
let t = e;
e = ((e = ((e = (e ^ ((e << 13) | 0)) | 0) ^ ((e >>> 17) | 0)) | 0) ^ ((e << 5) | 0)) | 0;
this.state = e;
return t >= 0 ? t : 4294967295 + t - 1;
}
}