From 0e20f7d4f93ddb2f268dafa21c6ea9dd820a1708 Mon Sep 17 00:00:00 2001
From: Nulled <63370961+Nul-led@users.noreply.github.com>
Date: Mon, 21 Nov 2022 23:15:13 +0100
Subject: [PATCH] Implement custom commands (client console) loading (#63)
* Update dma.js
* add config for custom commands
* implement custom commands & add docs
* Update config.js
* this should work
* test (#77)
* guardiaaaaaaan
* tiny fixes?
* fix the summoner again probably
* fix bug in bosses and dominator (#64)
* Update FallenOverlord.ts
* Update Dominator.ts
* fix bugs in bosses and dominator
* mothership gamemode (#61)
* Add files via upload
* Add files via upload
* Update Arena.ts
* Update Game.ts
* Update index.ts
* Update Mothership.ts
* Update Mothership.ts
* Update Mothership.ts
* Update Mothership.ts
* Update Mothership.ts
* Update Mothership.ts
* Update Mothership.ts
* Update index.ts
* Update Mothership.ts
* Update Mothership.ts
* circle on rect more accurate
* docs: fix license link (#65)
* patch(#59): disable flags and movement on disconnection
* patch some mothership stuff (#66)
* patch some mothership stuff
- removed a random bug
- changed colors to accurate and also added color map
- made mothership stop being possessed after 10 minutes
- changed "hasFinished" prop to a new state in ArenaState in preparation for Domination and Tagmode
* sort scoreboard by health
* rework spawns
Co-authored-by: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com>
* chore
* annoying
* undo deletion
* drone ai doesn't care if inputs deleted
* cleanup
* cleanup (remove useless imports and code)
* cleanup
* fix: allow utf-8 in changelog (and all other content served)
* fix domination once again
* cleanup and bugs
* remove useless stuff probably
* Update Mothership.ts
* fix: domination "peaceful" bases do not kill bases anymore (#67)
* checks for pushFactor before deleting
* fix: mothership implemented
* patch some mothership stuff
- removed a random bug
- changed colors to accurate and also added color map
- made mothership stop being possessed after 10 minutes
- changed "hasFinished" prop to a new state in ArenaState in preparation for Domination and Tagmode
* sort scoreboard by health
* rework spawns
* merge conflict + fix up mothership
* fix barrels shooting from opposite team on first creation
* Update TeamEntity.ts
Co-authored-by: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com>
* fix: let non teamed players get broadcasted on death of mot
Co-authored-by: ABC <79597906+ABCxFF@users.noreply.github.com>
* feat: barrel addons and "swarm" (#69)
* fix: double ai view range of swarm drones
* ALLOW PUSH TO MAIN
* add back license spaces
Co-authored-by: ABC <79597906+ABCxFF@users.noreply.github.com>
Co-authored-by: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com>
Co-authored-by: ABCxFF <79597906+ABCxFF@users.noreply.github.com>
Co-authored-by: Somka000 <51973243+Somka000@users.noreply.github.com>
* Create Commands.ts
* modularize possession of ais
* add command parser
* implement custom commands on the client
* redo fix (#1 probably)
:')
* redo fix (2 :x)
* implement other util clientside commands
* add getTankByName func
* send accessLevel to client
* fix typo
* fix Clientinputs for ai possessing
* update endpoints
- made /servers only serve running servers
- added /commands
* import commanddefs
* fix next weird bug :d
* Update loader.js
* add semicolon
* add config option for custom commands
* implement execution prevention option for remote commands
* endpoint /commands results in empty array if command execution is disabled
* change to hasOwnProperty
* use map instead of object, also verbose client logging
Co-authored-by: c86ec23b-fef1-4979-b2fa-b9adc351b8cc <87239823+c86ec23b-fef1-4979-b2fa-b9adc351b8cc@users.noreply.github.com>
Co-authored-by: ABCxFF <79597906+ABCxFF@users.noreply.github.com>
Co-authored-by: Somka000 <51973243+Somka000@users.noreply.github.com>
---
client/config.js | 91 +++++++++-
client/dma.js | 48 ++++-
client/loader.js | 337 +++++++++++++++++++++++++++++------
src/Client.ts | 110 +++++++-----
src/Const/Commands.ts | 238 +++++++++++++++++++++++++
src/Const/TankDefinitions.ts | 4 +
src/config.ts | 5 +-
src/index.ts | 7 +-
8 files changed, 728 insertions(+), 112 deletions(-)
create mode 100644 src/Const/Commands.ts
diff --git a/client/config.js b/client/config.js
index 1c246aa2..1f5bd74b 100644
--- a/client/config.js
+++ b/client/config.js
@@ -240,14 +240,16 @@ const MOD_CONFIG = {
"loadVectorDone": 22,
"loadChangelog": 447,
"loadTankDefinitions": 277,
- "getTankDefinition": 101
+ "getTankDefinition": 101,
+ "findCommand": 496
},
"memory": {
"gamemodeButtons": 113480,
"changelog": 167328,
"changelogLoaded": 168632,
"tankDefinitions": 166572,
- "tankDefinitionsCount": 166576
+ "tankDefinitionsCount": 166576,
+ "commandList": 53064
},
"wasmFunctionHookOffset": {
"gamemodeButtons": 33,
@@ -274,9 +276,92 @@ const ADDON_MAP = {
}
};
+const CUSTOM_COMMANDS = [
+ {
+ "id": "test",
+ "description": "Test command to check if custom commands are working, prints 'Hello World' to the console",
+ "callback": args => { // array of strings, you need to parse them yourself
+ console.log("Hello World");
+ }
+ }, {
+ "id": "util_reload_servers",
+ "usage": "[?interval]",
+ "description": "Sets the interval in which gamemodes are reloaded automatically (milliseconds, 'never' or 'connect') or reloads once if no interval is given",
+ "callback": args => {
+ if(args[0]) {
+ const num = parseInt(args[0]);
+ if(isNaN(num)) {
+ switch(args[0]) {
+ case "never":
+ return Module.reloadServersInterval = -1;
+ case "connect":
+ return Module.reloadServersInterval = -2;
+ }
+ }
+ return Module.reloadServersInterval = num;
+ }
+ Game.reloadServers();
+ }
+ }, {
+ "id": "util_reload_tanks",
+ "usage": "[?interval]",
+ "description": "Sets the interval in which tanks are reloaded automatically (milliseconds, 'never' or 'connect') or reloads once if no interval is given",
+ "callback": args => {
+ if(args[0]) {
+ const num = parseInt(args[0]);
+ if(isNaN(num)) {
+ switch(args[0]) {
+ case "never":
+ return Module.reloadTanksInterval = -1;
+ case "connect":
+ return Module.reloadTanksInterval = -2;
+ }
+ }
+ return Module.reloadTanksInterval = num;
+ }
+ Game.reloadTanks();
+ }
+ }, {
+ "id": "util_reload_commands",
+ "usage": "[?interval]",
+ "description": "Sets the interval in which commands are reloaded automatically (milliseconds, 'never' or 'connect') or reloads once if no interval is given",
+ "callback": args => {
+ if(args[0]) {
+ const num = parseInt(args[0]);
+ if(isNaN(num)) {
+ switch(args[0]) {
+ case "never":
+ return Module.reloadCommandsInterval = -1;
+ case "connect":
+ return Module.reloadCommandsInterval = -2;
+ }
+ }
+ return Module.reloadCommandsInterval = num;
+ }
+ Game.reloadCommands();
+ }
+ }, {
+ "id": "util_set_changelog",
+ "usage": "[line 1\\n] [line 2] ...",
+ "description": "Sets the changelog to the given text, remember to use \\n before and after each line",
+ "callback": args => {
+ Game.changeChangelog(args.join(' ').split("\\n"));
+ }
+ }
+];
+
+const COMMANDS_LOOKUP = {
+ "con_toggle": 52952,
+ "game_spawn": 52992,
+ "help": 49956,
+ "lb_reconnect": 50056,
+ "net_replace_color": 50152,
+ "net_replace_colors": 50192,
+ "ui_replace_colors": 49916
+};
+
const WASM_TABLE = {
"initial": 687,
- "maximum": 687,
"element": "anyfunc"
};
diff --git a/client/dma.js b/client/dma.js
index c3dba837..4cf64852 100644
--- a/client/dma.js
+++ b/client/dma.js
@@ -30,6 +30,48 @@ const setupDMAHelpers = () => {
$.write(ptr + offset, type, value);
}
+ $.List = class LinkedList {
+ constructor(ptr, type, typeSize) {
+ this.ptr = ptr;
+ this.type = type;
+ this.typeSize = typeSize;
+ }
+
+ push(...entries) {
+ for(const entry of entries) this._append(this._end, entry);
+ this.length += entries.length;
+ }
+
+ forEach(cb) {
+ let current = $(this.ptr);
+ while(current.i32) {
+ cb(current.i32);
+ current = current.$;
+ }
+ }
+
+ get length() {
+ return $(this.ptr)[4].i32;
+ }
+
+ set length(val) {
+ $.write(this.ptr + 4, "u32", val);
+ }
+
+ get _end() {
+ let current = $(this.ptr);
+ while(current.i32) current = current.$;
+ return current.at;
+ }
+
+ _append(last, entry) {
+ const ptr = Module.exports.malloc(this.typeSize);
+ $.write(ptr, this.type, entry);
+ $.write(last, "$", ptr);
+ }
+ }
+
+
$.Vector = class Vector {
constructor(ptr, type, typeSize) {
this.ptr = ptr;
@@ -56,11 +98,7 @@ const setupDMAHelpers = () => {
Module.HEAPU8.subarray(this.start, this.maxCapacity).fill(0);
}
- delete() {
- this._free();
- }
-
- _free() {
+ destroy() {
Module.exports.free(this.start);
this.start = 0;
this.end = 0;
diff --git a/client/loader.js b/client/loader.js
index 7d755775..e8e51746 100644
--- a/client/loader.js
+++ b/client/loader.js
@@ -16,29 +16,65 @@
along with this program. If not, see
*/
+// make module globally accessable
window.Module = {};
+// todolist
Module.todo = [];
+// todo status
Module.status = null;
+
+// is the todo list done?
Module.isRunning = false;
+
+// has the module been aborted?
Module.isAborted = false;
+
+// exception name
Module.exception = null;
+
+// function index for dynamic calling of the main func
Module.mainFunc = null;
+
+// content contexts
Module.cp5 = null;
+
+// client input
window.input = null;
+
+// arenas
Module.servers = null;
+
+// tanks
Module.tankDefinitions = null;
Module.tankDefinitionsTable = null;
+
+// commands
+Module.executeCommandFunctionIndex = null;
+Module.executionCallbackMap = {};
+Module.commandDefinitions = null;
+
+// name input
Module.textInput = document.getElementById("textInput");
Module.textInputContainer = document.getElementById("textInputContainer");
+// permission level is sent to client in the accept packet
+Module.permissionLevel = -1;
+
+// (polling) intervals, can be a number (ms), -1 aka never or -2 aka whenever a new connection is initiated
+Module.reloadServersInterval = 60000;
+Module.reloadTanksInterval = -1;
+Module.reloadCommandsInterval = -2;
+
+// abort client
Module.abort = cause => {
Module.isAborted = true;
Module.isRunning = false;
throw new WebAssembly.RuntimeError(`abort(${cause})`);
};
+// run ASMConst method, basically replaces a lot of "real wasm imports"
Module.runASMConst = (code, sigPtr, argbuf) => {
const args = [];
let char;
@@ -52,12 +88,14 @@ Module.runASMConst = (code, sigPtr, argbuf) => {
return ASMConsts[ASM_CONSTS[code]].apply(null, args);
};
+// initializing the looper
Module.setLoop = func => {
if(!Module.isRunning || Module.isAborted || Module.exception === "quit") return;
Module.mainFunc = func;
window.requestAnimationFrame(Module.loop);
};
+// process todo
Module.run = async () => {
let args = [];
while(Module.todo.length) {
@@ -68,6 +106,7 @@ Module.run = async () => {
}
};
+// looper, 1 animation frame = 1 main call, except for stack unwinds
Module.loop = () => {
if(!Module.isRunning || Module.isAborted || Module.exception === "quit") return;
switch(Module.exception) {
@@ -82,55 +121,78 @@ Module.loop = () => {
}
};
+// exit runtime (no unwind, originally unwind would be catched here)
Module.exit = status => {
Module.exception = "quit";
Module.isRunning = false;
throw `Stopped runtime with status ${status}`;
};
+// read utf8 from memory
Module.UTF8ToString = ptr => ptr ? new TextDecoder().decode(Module.HEAPU8.subarray(ptr, Module.HEAPU8.indexOf(0, ptr))) : "";
+// i/o write used for console, not fully understood
Module.fdWrite = (stream, ptr, count, res) => {
let out = 0;
for(let i = 0; i < count; i++) out += Module.HEAP32[(ptr + (i * 8 + 4)) >> 2];
Module.HEAP32[res >> 2] = out;
};
+// write utf8 to memory
Module.allocateUTF8 = str => {
if(!str) return 0;
const encoded = new TextEncoder().encode(str);
- const ptr = Module.exports.malloc(encoded.byteLength + 1); // stringNT
+ const ptr = Module.exports.malloc(encoded.byteLength + 1); // stringNT aka *char[]
if(!ptr) return;
Module.HEAPU8.set(encoded, ptr);
Module.HEAPU8[ptr + encoded.byteLength] = 0;
return ptr;
};
+// Refreshes UI Components
Module.loadGamemodeButtons = () => {
- const vec = new $.Vector(MOD_CONFIG.memory.gamemodeButtons, 'struct', 28);
- if(vec.start) vec.delete();
- vec.push(...Module.servers.map(server => ([{ offset: 0, type: 'cstr', value: server.gamemode }, { offset: 12, type: 'cstr', value: server.name }, { offset: 24, type: 'i32', value: 0 }])));
- Module.rawExports.loadVectorDone(MOD_CONFIG.memory.gamemodeButtons + 12);
+ const vec = new $.Vector(MOD_CONFIG.memory.gamemodeButtons, "struct", 28);
+ if(vec.start) vec.destroy(); // remove old arenas
+ // map server response to memory struct
+ vec.push(...Module.servers.map(server => ([
+ { offset: 0, type: "cstr", value: server.gamemode },
+ { offset: 12, type: "cstr", value: server.name },
+ { offset: 24, type: "i32", value: 0 }
+ ])));
+ // placeholders to prevent single/no gamemode bugs
+ const placeholderId = Module.servers.find(e => e.gamemode === 'ffa') ? 'survival' : 'ffa'
+ for(let i = 0; i < 2 - Module.servers.length; ++i) {
+ vec.push(...[[
+ { offset: 0, type: "cstr", value: placeholderId },
+ { offset: 12, type: "cstr", value: "Placeholder" },
+ { offset: 24, type: "i32", value: 1 }
+ ]]);
+ }
+ Module.rawExports.loadVectorDone(MOD_CONFIG.memory.gamemodeButtons + 12); // not understood
};
-Module.loadChangelog = (changelog=CHANGELOG) => {
- const vec = new $.Vector(MOD_CONFIG.memory.changelog, 'cstr', 12);
- if(vec.start) vec.delete();
- vec.push(...changelog);
- $(MOD_CONFIG.memory.changelogLoaded).i8 = 1;
+// Refreshes UI Components
+Module.loadChangelog = (changelog) => {
+ const vec = new $.Vector(MOD_CONFIG.memory.changelog, "cstr", 12);
+ if(vec.start) vec.destroy(); // remove old changelog
+ vec.push(...(changelog || CHANGELOG)); // either load custom or default
+ $(MOD_CONFIG.memory.changelogLoaded).i8 = 1; // not understood
};
+// Ignore Hashtable, instead read from custom table
Module.getTankDefinition = tankId => {
if(!Module.tankDefinitions) return 0;
- if(!Module.tankDefinitionsTable) Module.loadTankDefinitions();
+ if(!Module.tankDefinitionsTable) Module.loadTankDefinitions(); // load tankdefs dynmically when requested
if(!Module.tankDefinitionsTable[tankId]) return 0;
- return Module.tankDefinitionsTable[tankId] + 12;
+ return Module.tankDefinitionsTable[tankId] + 12; // 12 bytes for tankIds
};
+Module.getCommand = cmdIdPtr => COMMANDS_LOOKUP[$(cmdIdPtr).cstr] || 0;
+
Module.loadTankDefinitions = () => {
const writeTankDef = (ptr, tank) => {
// Please note that this is not the full tank/barrel struct but just the portion needed for the client to function properly
- const barrels = tank.barrels ? tank.barrels.map(barrel => {
+ const barrels = tank.barrels ? tank.barrels.map(barrel => { // barrel fields
return [
{ offset: 0, type: "f32", value: barrel.angle },
{ offset: 4, type: "f32", value: barrel.delay },
@@ -145,7 +207,7 @@ Module.loadTankDefinitions = () => {
];
}) : [];
- const fields = [
+ const fields = [ // tankdef fields
{ offset: 4, type: "u32", value: tank.id },
{ offset: 8, type: "u32", value: tank.id },
{ offset: 12, type: "u32", value: tank.id },
@@ -163,11 +225,12 @@ Module.loadTankDefinitions = () => {
$.writeStruct(ptr, fields);
};
- Module.tankDefinitionsTable = new Array(Module.tankDefinitions.length).fill(0);
+ // TODO Rewrite with new $.List datastructure
+ Module.tankDefinitionsTable = new Array(Module.tankDefinitions.length).fill(0); // clear memory
let lastPtr = MOD_CONFIG.memory.tankDefinitions;
for(const tank of Module.tankDefinitions) {
if(!tank) continue;
- const ptr = Module.exports.malloc(244);
+ const ptr = Module.exports.malloc(244); // length of a tankdef
Module.HEAPU8.subarray(ptr, ptr + 244).fill(0);
$(lastPtr).i32 = ptr;
writeTankDef(ptr, tank);
@@ -175,17 +238,71 @@ Module.loadTankDefinitions = () => {
lastPtr = ptr;
}
- $(MOD_CONFIG.memory.tankDefinitionsCount).i32 = Module.tankDefinitions.filter(e => Boolean(e)).length;
+ $(MOD_CONFIG.memory.tankDefinitionsCount).i32 = Module.tankDefinitions.filter(e => Boolean(e)).length; // tankId xor based off this
+};
+
+// Executes a command callback from a command context
+Module.executeCommand = execCtx => {
+ const cmd = $(execCtx)[0].cstr;
+ const tokens = $(execCtx)[12].vector("cstr", 12);
+
+ if(!cmd || !tokens.length) throw `Invalid execution context (ptr: ${execCtx}) received`;
+ if(typeof Module.executionCallbackMap[tokens[0]] !== "function") {
+ if(!Module.commandDefinitions.find(({ id }) => id === tokens[0])) {
+ throw `${Module.executionCallbackMap[tokens]} for command ${cmd} is an invalid callback`;
+ }
+ const encoder = new TextEncoder();
+ return Game.socket.send(new Uint8Array([
+ 6,
+ ...encoder.encode(tokens[0]), 0,
+ tokens.slice(1).length,
+ ...tokens.slice(1).flatMap(token => [...encoder.encode(token), 0])
+ ]));
+ }
+
+ // [id, ...args], we only need args
+ Module.executionCallbackMap[tokens[0]](tokens.slice(1));
+};
+
+/*
+ Command object: { id, usage, description, callback }
+ The execute command function will not check for validity of arguments, you need to do that on your own
+*/
+Module.loadCommands = (commands = CUSTOM_COMMANDS) => {
+ const cmdList = new $.List(MOD_CONFIG.memory.commandList, "struct", 24);
+ for(let { id, usage, description, callback, permissionLevel } of commands) {
+ if(COMMANDS_LOOKUP[id] || permissionLevel > Module.permissionLevel) continue; // ignore duplicates
+
+ // allocate Command
+ const cmdPtr = Module.exports.malloc(40);
+ $.writeStruct(cmdPtr, [
+ { offset: 0, type: "cstr", value: id },
+ { offset: 12, type: "cstr", value: usage || "" },
+ { offset: 24, type: "cstr", value: description || "" },
+ { offset: 36, type: "u32", value: Module.executeCommandFunctionIndex } // we handle every custom command with the same function
+ ]);
+
+ COMMANDS_LOOKUP[id] = cmdPtr;
+ if(callback) Module.executionCallbackMap[id] = callback;
+
+ // allocate HashNode
+ cmdList.push([
+ { offset: 0, type: "u32", value: 0 }, // next node
+ { offset: 4, type: "u32", value: 0 }, // hash
+ { offset: 8, type: "cstr", value: id }, // command id
+ { offset: 20, type: "$", value: cmdPtr } // command def ptr
+ ]);
+ }
};
const wasmImports = {
assertFail: (condition, filename, line, func) => Module.abort("Assertion failed: " + UTF8ToString(condition) + ", at: " + [filename ? UTF8ToString(filename) : "unknown filename", line, func ? UTF8ToString(func) : "unknown function"]),
mapFile: () => -1, // unused
- sysMunmap: (addr, len) => addr === -1 || !len ? -28 : 0,
+ sysMunmap: (addr, len) => addr === -1 || !len ? -28 : 0, // not really used
abort: Module.abort,
asmConstsDII: Module.runASMConst,
asmConstsIII: Module.runASMConst,
- exitLive: () => Module.exception = "unwind", // unwind
+ exitLive: () => Module.exception = "unwind", // unwind stack
exitForce: () => Module.exit(1), // exit / quit
getNow: () => performance.now(),
memCopyBig: (dest, src, num) => { Module.HEAPU8.copyWithin(dest, src, src + num) }, // for large packets
@@ -195,7 +312,7 @@ const wasmImports = {
envGet: () => 0, // unused
envSize: () => 0, // unused
fdWrite: Module.fdWrite, // used for diep client console
- roundF: d => d >= 0 ? Math.floor(d + 0.5) : Math.ceil(d - 0.5),
+ roundF: d => d >= 0 ? Math.floor(d + 0.5) : Math.ceil(d - 0.5), // no, default Math.round doesn't work :D
timeString: () => 0, // unused
wasmMemory: new WebAssembly.Memory(WASM_MEMORY),
wasmTable: new WebAssembly.Table(WASM_TABLE)
@@ -203,12 +320,14 @@ const wasmImports = {
Module.todo.push([() => {
Module.status = "PREPARE";
+ // map imports to config
Module.imports = { a: Object.fromEntries(Object.entries(WASM_IMPORTS).map(([key, name]) => [key, wasmImports[name]])) };
return [];
}, false]);
Module.todo.push([() => {
Module.status = "FETCH";
+ // fetch necessary info and build
return [fetch(`${CDN}build_${BUILD}.wasm.wasm`).then(res => res.arrayBuffer()), fetch(`${API_URL}servers`).then(res => res.json()), fetch(`${API_URL}tanks`).then(res => res.json())];
}, true]);
@@ -219,96 +338,150 @@ Module.todo.push([(dependency, servers, tanks) => {
const parser = new WailParser(new Uint8Array(dependency));
+ // original function, we want to modify these
const originalVectorDone = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.loadVectorDone);
const originalLoadChangelog = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.loadChangelog);
const originalLoadGamemodeButtons = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.loadGamemodeButtons);
const originalLoadTankDefs = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.loadTankDefinitions);
const originalGetTankDef = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.getTankDefinition);
+ const originalFindCommand = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.findCommand);
- const loadGamemodeButtons = parser.addImportEntry({
- moduleStr: "mods",
- fieldStr: "loadGamemodeButtons",
- kind: "func",
- type: parser.addTypeEntry({
+ // function types
+ const types = {
+ // void []
+ vn: parser.addTypeEntry({
form: "func",
params: [],
returnType: null
- })
- });
-
- const loadChangelog = parser.addImportEntry({
- moduleStr: "mods",
- fieldStr: "loadChangelog",
- kind: "func",
- type: parser.addTypeEntry({
+ }),
+ // void [int]
+ vi: parser.addTypeEntry({
form: "func",
- params: [],
+ params: ["i32"],
returnType: null
- })
- });
-
- const getTankDefinition = parser.addImportEntry({
- moduleStr: "mods",
- fieldStr: "getTankDefinition",
- kind: "func",
- type: parser.addTypeEntry({
+ }),
+ // int [int]
+ ii: parser.addTypeEntry({
form: "func",
params: ["i32"],
returnType: "i32"
})
- });
+ }
+
+ // custom imports
+ const imports = {
+ loadGamemodeButtons: parser.addImportEntry({
+ moduleStr: "mods",
+ fieldStr: "loadGamemodeButtons",
+ kind: "func",
+ type: types.vn
+ }),
+ loadChangelog: parser.addImportEntry({
+ moduleStr: "mods",
+ fieldStr: "loadChangelog",
+ kind: "func",
+ type: types.vn
+ }),
+ getTankDefinition: parser.addImportEntry({
+ moduleStr: "mods",
+ fieldStr: "getTankDefinition",
+ kind: "func",
+ type: types.ii
+ }),
+ findCommand: parser.addImportEntry({
+ moduleStr: "mods",
+ fieldStr: "findCommand",
+ kind: "func",
+ type: types.ii
+ }),
+ executeCommand: parser.addImportEntry({
+ moduleStr: "mods",
+ fieldStr: "executeCommand",
+ kind: "func",
+ type: types.vi
+ })
+ }
+
+ // Modded imports, see above
Module.imports.mods = {
loadGamemodeButtons: Module.loadGamemodeButtons,
loadChangelog: Module.loadChangelog,
- getTankDefinition: Module.getTankDefinition
+ getTankDefinition: Module.getTankDefinition,
+ findCommand: Module.getCommand,
+ executeCommand: Module.executeCommand
};
+ // export to be able to add as a function table element
+ parser.addExportEntry(imports.executeCommand, {
+ fieldStr: "executeCommand",
+ kind: "func"
+ });
+
+ // not understood entirely
parser.addExportEntry(originalVectorDone, {
fieldStr: "loadVectorDone",
kind: "func"
});
+ // parses & modifies code function by function
parser.addCodeElementParser(null, function({ index, bytes }) {
switch(index) {
+ // modify load changelog function
case originalLoadChangelog.i32(): // we only need the part where it checks if the changelog is already loaded to avoid too many import calls
return new Uint8Array([
...bytes.subarray(0, MOD_CONFIG.wasmFunctionHookOffset.changelog),
- OP_CALL, ...VarUint32ToArray(loadChangelog.i32()),
+ OP_CALL, ...VarUint32ToArray(imports.loadChangelog.i32()),
OP_RETURN,
...bytes.subarray(MOD_CONFIG.wasmFunctionHookOffset.changelog)
- ]);
+ ]);
+ // modify load gamemode buttons function
case originalLoadGamemodeButtons.i32(): // we only need the part where it checks if the buttons are already loaded to avoid too many import calls
return new Uint8Array([
...bytes.subarray(0, MOD_CONFIG.wasmFunctionHookOffset.gamemodeButtons),
- OP_CALL, ...VarUint32ToArray(loadGamemodeButtons.i32()),
+ OP_CALL, ...VarUint32ToArray(imports.loadGamemodeButtons.i32()),
OP_RETURN,
...bytes.subarray(MOD_CONFIG.wasmFunctionHookOffset.gamemodeButtons)
]);
+ // overwrite get tankdef function
case originalGetTankDef.i32(): // we modify this to call a js function which then returns the tank def ptr from a table
return new Uint8Array([
OP_GET_LOCAL, 0,
- OP_CALL, ...VarUint32ToArray(getTankDefinition.i32()),
+ OP_CALL, ...VarUint32ToArray(imports.getTankDefinition.i32()),
OP_RETURN,
OP_END
]);
+ // overwrite find command function
+ case originalFindCommand.i32():
+ return new Uint8Array([
+ OP_GET_LOCAL, 0,
+ OP_CALL, ...VarUint32ToArray(imports.findCommand.i32()),
+ OP_RETURN,
+ OP_END
+ ]);
+ // delete tankdefs loading function
case originalLoadTankDefs.i32(): // we dont want this to run anymore because it will call the original tank wrapper function
return new Uint8Array([
OP_END
]);
+ // no interesting index
default:
return false;
}
});
+ // parse modded wasm
parser.parse();
+ // instantiate
return [new Promise(resolve => WebAssembly.instantiate(parser.write(), Module.imports).then(res => resolve(res.instance), reason => Module.abort(reason)))];
}, true]);
Module.todo.push([instance => {
Module.status = "INITIALIZE";
+ // Exports
Module.exports = Object.fromEntries(Object.entries(instance.exports).map(([key, func]) => [WASM_EXPORTS[key], func]));
Module.rawExports = instance.exports;
+ // Memory
Module.memBuf = wasmImports.wasmMemory.buffer,
Module.HEAPU8 = new Uint8Array(Module.memBuf);
Module.HEAP8 = new Int8Array(Module.memBuf);
@@ -320,41 +493,86 @@ Module.todo.push([instance => {
Module.HEAPF64 = new Float64Array(Module.memBuf);
Module.HEAPU64 = new BigUint64Array(Module.memBuf);
Module.HEAP64 = new BigInt64Array(Module.memBuf);
+ // Cp5 Contexts
Module.cp5 = {
contexts: [],
images: [],
sockets: [],
patterns: []
};
+ // window.input & misc, see input.js
window.setupInput();
+ // Diep Memory Analyzer, see dma.js
window.setupDMA();
return [];
}, false]);
Module.todo.push([() => {
window.Game = {
+ // refetches servers & resets gamemode buttons
reloadServers: async () => {
Module.servers = await fetch(`${API_URL}servers`).then(res => res.json());
Module.loadGamemodeButtons();
},
+ // refetches tankdefs & resets them
reloadTanks: async () => {
Module.tankDefinitions = await fetch(`${API_URL}tanks`).then(res => res.json());
for(const tankDef of Module.tankDefinitionsTable) Module.exports.free(tankDef);
Module.loadTankDefinitions();
},
+ reloadCommands: async () => {
+ Module.commandDefinitions = await fetch(`${API_URL}commands`).then(res => res.json());
+ Module.loadCommands(Module.commandDefinitions); // remote
+ Module.loadCommands(); // local
+ },
+ // sets changelog (input: [...""])
changeChangelog: (lines) => Module.loadChangelog(lines),
- socket: null,
+ // main socket, see also Module.cp5.sockets[0]
+ get socket() {
+ return Module.cp5.sockets[0];
+ },
+ // executes spawn command
spawn: name => window.input.execute(`game_spawn ${name}`),
+ // executes reconnect command
reconnect: () => window.input.execute(`lb_reconnect`)
};
+
+ // custom commands
+ Module.executeCommandFunctionIndex = Module.imports.a.table.grow(1);
+ Module.imports.a.table.set(Module.executeCommandFunctionIndex, Module.rawExports.executeCommand);
Module.status = "START";
+ // emscripten requirements
Module.HEAP32[DYNAMIC_TOP_PTR >> 2] = DYNAMIC_BASE;
Module.isRunning = true;
Module.exports.wasmCallCtors();
Module.exports.main();
+
+
+ const reloadServersInterval = () => setTimeout(() => {
+ reloadServersInterval();
+ if(Module.reloadServersInterval < 0) return;
+ Game.reloadServers();
+ }, Module.reloadServersInterval);
+ reloadServersInterval();
+
+ const reloadTanksInterval = () => setTimeout(() => {
+ reloadTanksInterval();
+ if(Module.reloadCommandsInterval < 0) return;
+ Game.reloadTanks();
+ }, Module.reloadTanksInterval);
+ reloadTanksInterval();
+
+ const reloadCommandsInterval = () => setTimeout(() => {
+ reloadCommandsInterval();
+ if(Module.reloadCommandsInterval < 0) return;
+ Game.reloadCommands();
+ }, Module.reloadCommandsInterval);
+ reloadCommandsInterval();
}, false]);
+
+// Part of the original emscripten bootstrap
class ASMConsts {
static createCanvasCtxWithAlpha(canvasId, alpha) {
const canvas = document.getElementById(Module.UTF8ToString(canvasId));
@@ -697,7 +915,8 @@ class ASMConsts {
}
static setLocation(newLocation) {
- window.localStorage = Module.UTF8ToString(newLocation);
+ // open in new tab instead
+ window.open(Module.UTF8ToString(newLocation));
}
static contextDrawImage(ctxId, imgId) {
@@ -905,7 +1124,7 @@ class ASMConsts {
static createWebSocket(url) {
url = Module.UTF8ToString(url);
- if (url.split('.').length === 4) url = `ws${location.protocol.slice(4)}//${location.host}/game/${url.slice(url.indexOf("//") + 2, url.indexOf('.'))}`;
+ if (url.split(".").length === 4) url = `ws${location.protocol.slice(4)}//${location.host}/game/${url.slice(url.indexOf("//") + 2, url.indexOf("."))}`;
else if (url.endsWith(":443")) url = `ws${location.protocol.slice(4)}//${location.host}/game/${url.slice(url.indexOf("//") + 2, url.length - 4)}`
else return prompt("Error loading into game. Take a picture of this then send to our support server (github.com/ABCxFF/diepcustom)", url);
@@ -926,6 +1145,16 @@ class ASMConsts {
};
ws.onmessage = function(e) {
const view = new Uint8Array(e.data);
+ if(view[0] === 7) {
+ let out = 0, i = 0, at = 1;
+ while(view[at] & 0x80) {
+ out |= (view[at++] & 0x7f) << i;
+ i += 7;
+ }
+ out |= (view[at++] & 0x7f) << i;
+ Module.permissionLevel = (0 - (out & 1)) ^ (out >>> 1);
+ window.Game.reloadCommands();
+ }
const ptr = Module.exports.malloc(view.length);
Module.HEAP8.set(view, ptr);
ws.events.push([1, ptr, view.length]);
@@ -937,8 +1166,12 @@ class ASMConsts {
Module.cp5.sockets[i] = ws;
return i;
}
+
+ if(Module.reloadServersInterval === -2) Game.reloadServers();
+ if(Module.reloadTanksInterval === -2) Game.reloadTanks();
+ if(Module.reloadCommandsInterval === -2) Game.reloadCommands();
+
Module.cp5.sockets.push(ws);
- window.Game.socket = ws;
return Module.cp5.sockets.length - 1;
}
diff --git a/src/Client.ts b/src/Client.ts
index e02f34d2..8fb87b46 100644
--- a/src/Client.ts
+++ b/src/Client.ts
@@ -39,6 +39,7 @@ import { Entity, EntityStateFlags } from "./Native/Entity";
import { CameraFlags, ClientBound, InputFlags, NametagFlags, ServerBound, Stat, StatCount, Tank } from "./Const/Enums";
import { AI, AIState, Inputs } from "./Entity/AI";
import AbstractBoss from "./Entity/Boss/AbstractBoss";
+import { executeCommand } from "./Const/Commands";
/** XORed onto the tank id in the Tank Upgrade packet. */
const TANK_XOR = config.magicNum % TankCount;
@@ -246,7 +247,7 @@ export default class Client {
}
// Finish handshake
- this.write().u8(ClientBound.Accept).send();
+ this.write().u8(ClientBound.Accept).vi(this.accessLevel).send();
this.write().u8(ClientBound.ServerInfo).stringNT(this.game.gamemode).stringNT(config.host).send();
this.write().u8(ClientBound.PlayerCount).vu(GameServer.globalPlayerCount).send();
this.camera = new Camera(this.game, this);
@@ -460,54 +461,8 @@ export default class Client {
});
for (let i = 0; i < AIs.length; ++i) {
- if (AIs[i].state !== AIState.possessed && ((!AIs[i].isTaken && AIs[i].owner.relations.values.team === camera.relations.values.team)|| this.accessLevel === config.AccessLevel.FullAccess)) {
- // if (AIs[i].state !== AIState.possessed && (AIs[i].owner.relations.values.team === camera.relations.values.team|| this.accessLevel === config.AccessLevel.FullAccess)) {
- const ai = AIs[i];
-
- this.inputs.deleted = true;
- ai.inputs = this.inputs = new ClientInputs(this);
- this.inputs.isPossessing = true;
- ai.isTaken = true;
- ai.state = AIState.possessed;
-
- // Silly workaround to change color of player when needed
- if (camera.camera.values.player instanceof ObjectEntity) camera.camera.values.player.state |= camera.camera.values.player.style.state.color = 1;
-
- camera.camera.tankOverride = ai.owner.name?.values.name || "";
-
- camera.camera.tank = 53;
-
- // AI stats, confirmed by Mounted Turret videos
- for (let i = 0; i < StatCount; ++i) camera.camera.statLevels[i as Stat] = 0;
- for (let i = 0; i < StatCount; ++i) camera.camera.statLimits[i as Stat] = 7;
- for (let i = 0; i < StatCount; ++i) camera.camera.statNames[i as Stat] = "";
-
-
- camera.camera.killedBy = "";
- camera.camera.player = ai.owner;
- camera.camera.movementSpeed = ai.movementSpeed;
-
- if (ai.owner instanceof TankBody) {
- // If its a TankBody, set the stats, level, and tank to that of the TankBody
- camera.camera.tank = ai.owner.cameraEntity.camera.values.tank;
- camera.setLevel(ai.owner.cameraEntity.camera.values.level);
-
- for (let i = 0; i < StatCount; ++i) camera.camera.statLevels[i as Stat] = ai.owner.cameraEntity.camera.statLevels.values[i];
- for (let i = 0; i < StatCount; ++i) camera.camera.statLimits[i as Stat] = ai.owner.cameraEntity.camera.statLimits.values[i];
- for (let i = 0; i < StatCount; ++i) camera.camera.statNames[i as Stat] = ai.owner.cameraEntity.camera.statNames.values[i];
-
- camera.camera.FOV = 0.35;
- } else if (ai.owner instanceof AbstractBoss) {
- camera.setLevel(75);
- camera.camera.FOV = 0.25;
- } else {
- camera.setLevel(30);
- // this.camera.movementSpeed = 0;
- }
-
- camera.camera.statsAvailable = 0;
- camera.camera.scorebar = 0;
-
+ if (((!AIs[i].isTaken && AIs[i].owner.relations.values.team === camera.relations.values.team) || this.accessLevel === config.AccessLevel.FullAccess)) {
+ if(!this.possess(AIs[i])) continue;
this.notify("Press H to surrender control of your tank", 0x000000, 5000);
return;
}
@@ -520,12 +475,69 @@ export default class Client {
return;
}
case ServerBound.TCPInit:
+ if(!config.enableCommands) return;
+ const cmd = r.stringNT();
+ const argsLength = r.u8();
+ const args: string[] = [];
+ for(let i = 0; i < argsLength; ++i) {
+ args.push(r.stringNT());
+ }
+ executeCommand(this, cmd, args);
return;
default:
util.log("Suspicious activies have been evaded")
return this.ban();
}
}
+
+ /** Attempts possession of an AI */
+ public possess(ai: AI) {
+ if (!this.camera?.camera || ai.state === AIState.possessed) return false;
+
+ this.inputs.deleted = true;
+ ai.inputs = this.inputs = new ClientInputs(this);
+ this.inputs.isPossessing = true;
+ ai.isTaken = true;
+ ai.state = AIState.possessed;
+
+ // Silly workaround to change color of player when needed
+ if (this.camera?.camera.values.player instanceof ObjectEntity) this.camera.camera.values.player.state |= this.camera.camera.values.player.style.state.color = 1;
+
+ this.camera.camera.tankOverride = ai.owner.name?.values.name || "";
+
+ this.camera.camera.tank = 53;
+
+ // AI stats, confirmed by Mounted Turret videos
+ for (let i = 0; i < StatCount; ++i) this.camera.camera.statLevels[i as Stat] = 0;
+ for (let i = 0; i < StatCount; ++i) this.camera.camera.statLimits[i as Stat] = 7;
+ for (let i = 0; i < StatCount; ++i) this.camera.camera.statNames[i as Stat] = "";
+
+
+ this.camera.camera.killedBy = "";
+ this.camera.camera.player = ai.owner;
+ this.camera.camera.movementSpeed = ai.movementSpeed;
+
+ if (ai.owner instanceof TankBody) {
+ // If its a TankBody, set the stats, level, and tank to that of the TankBody
+ this.camera.camera.tank = ai.owner.cameraEntity.camera.values.tank;
+ this.camera.setLevel(ai.owner.cameraEntity.camera.values.level);
+
+ for (let i = 0; i < StatCount; ++i) this.camera.camera.statLevels[i as Stat] = ai.owner.cameraEntity.camera.statLevels.values[i];
+ for (let i = 0; i < StatCount; ++i) this.camera.camera.statLimits[i as Stat] = ai.owner.cameraEntity.camera.statLimits.values[i];
+ for (let i = 0; i < StatCount; ++i) this.camera.camera.statNames[i as Stat] = ai.owner.cameraEntity.camera.statNames.values[i];
+
+ this.camera.camera.FOV = 0.35;
+ } else if (ai.owner instanceof AbstractBoss) {
+ this.camera.setLevel(75);
+ this.camera.camera.FOV = 0.25;
+ } else {
+ this.camera.setLevel(30);
+ }
+
+ this.camera.camera.statsAvailable = 0;
+ this.camera.camera.scorebar = 0;
+ return true;
+ }
/** Sends a notification packet to the client. */
public notify(text: string, color = 0x000000, time = 4000, id = "") {
diff --git a/src/Const/Commands.ts b/src/Const/Commands.ts
new file mode 100644
index 00000000..c6dbc9e1
--- /dev/null
+++ b/src/Const/Commands.ts
@@ -0,0 +1,238 @@
+import Client from "../Client"
+import { AccessLevel } from "../config";
+import AbstractBoss from "../Entity/Boss/AbstractBoss";
+import Defender from "../Entity/Boss/Defender";
+import FallenBooster from "../Entity/Boss/FallenBooster";
+import FallenOverlord from "../Entity/Boss/FallenOverlord";
+import Guardian from "../Entity/Boss/Guardian";
+import Summoner from "../Entity/Boss/Summoner";
+import LivingEntity from "../Entity/Live";
+import ArenaCloser from "../Entity/Misc/ArenaCloser";
+import FallenAC from "../Entity/Misc/Boss/FallenAC";
+import FallenSpike from "../Entity/Misc/Boss/FallenSpike";
+import Dominator from "../Entity/Misc/Dominator";
+import AbstractShape from "../Entity/Shape/AbstractShape";
+import Crasher from "../Entity/Shape/Crasher";
+import Pentagon from "../Entity/Shape/Pentagon";
+import Square from "../Entity/Shape/Square";
+import Triangle from "../Entity/Shape/Triangle";
+import AutoTurret from "../Entity/Tank/AutoTurret";
+import Bullet from "../Entity/Tank/Projectile/Bullet";
+import TankBody from "../Entity/Tank/TankBody";
+import { Entity, EntityStateFlags } from "../Native/Entity";
+import { saveToVLog } from "../util";
+import { StyleFlags } from "./Enums";
+import { getTankByName } from "./TankDefinitions"
+
+export enum CommandID {
+ gameSetTank = "game_set_tank",
+ gameSetLevel = "game_set_level",
+ gameSetScore = "game_set_score",
+ gameTeleport = "game_teleport",
+ gameClaim = "game_claim",
+ adminGodmode = "admin_godmode",
+ adminSummon= "admin_summon",
+ adminKillAll = "admin_kill_all",
+ adminKillEntity = "admin_kill_entity",
+ adminCloseArena = "admin_close_arena"
+}
+
+export interface CommandDefinition {
+ id: CommandID,
+ usage?: string,
+ description?: string,
+ permissionLevel: AccessLevel,
+}
+
+export interface CommandCallback {
+ (client: Client, ...args: string[]): void
+}
+
+export const commandDefinitions = {
+ game_set_tank: {
+ id: CommandID.gameSetTank,
+ usage: "[tank]",
+ description: "Changes your tank to the given class",
+ permissionLevel: AccessLevel.BetaAccess
+ },
+ game_set_level: {
+ id: CommandID.gameSetLevel,
+ usage: "[level]",
+ description: "Changes your level to the given whole number",
+ permissionLevel: AccessLevel.BetaAccess
+ },
+ game_set_score: {
+ id: CommandID.gameSetScore,
+ usage: "[score]",
+ description: "Changes your score to the given whole number",
+ permissionLevel: AccessLevel.BetaAccess
+ },
+ game_teleport: {
+ id: CommandID.gameTeleport,
+ usage: "[x] [y]",
+ description: "Teleports you to the given position",
+ permissionLevel: AccessLevel.BetaAccess
+ },
+ game_claim: {
+ id: CommandID.gameClaim,
+ usage: "[entityName]",
+ description: "Attempts claiming an entity of the given type",
+ permissionLevel: AccessLevel.BetaAccess
+ },
+ admin_godmode: {
+ id: CommandID.adminGodmode,
+ description: "Toggles godmode",
+ permissionLevel: AccessLevel.FullAccess
+ },
+ admin_summon: {
+ id: CommandID.adminSummon,
+ usage: "[entityName] [?count] [?x] [?y]",
+ description: "Spawns entities at a certain location",
+ permissionLevel: AccessLevel.FullAccess
+ },
+ admin_kill_all: {
+ id: CommandID.adminKillAll,
+ description: "Kills all entities in the arena",
+ permissionLevel: AccessLevel.FullAccess
+ },
+ admin_kill_entity: {
+ id: CommandID.adminKillEntity,
+ usage: "[entityName]",
+ description: "Kills all entities of the given type (might include self)",
+ permissionLevel: AccessLevel.FullAccess
+ },
+ admin_close_arena: {
+ id: CommandID.adminCloseArena,
+ description: "Closes the current arena",
+ permissionLevel: AccessLevel.FullAccess
+ }
+} as Record
+
+export const commandCallbacks = {
+ game_set_tank: (client: Client, tankNameArg: string) => {
+ const tankDef = getTankByName(tankNameArg);
+ const player = client.camera?.camera.player;
+ if (!tankDef || !Entity.exists(player) || !(player instanceof TankBody)) return;
+ player.setTank(tankDef.id);
+ },
+ game_set_level: (client: Client, levelArg: string) => {
+ const level = parseInt(levelArg);
+ const player = client.camera?.camera.player;
+ if (isNaN(level) || !Entity.exists(player) || !(player instanceof TankBody)) return;
+ client.camera?.setLevel(level);
+ },
+ game_set_score: (client: Client, scoreArg: string) => {
+ const score = parseInt(scoreArg);
+ const camera = client.camera?.camera;
+ const player = client.camera?.camera.player;
+ if (isNaN(score) || score > Number.MAX_SAFE_INTEGER || score < Number.MIN_SAFE_INTEGER || !Entity.exists(player) || !(player instanceof TankBody) || !camera) return;
+ camera.scorebar = score;
+ },
+ game_teleport: (client: Client, xArg: string, yArg: string) => {
+ const x = parseInt(xArg);
+ const y = parseInt(yArg);
+ const player = client.camera?.camera.player;
+ if (isNaN(x) || isNaN(y) || !Entity.exists(player) || !(player instanceof TankBody)) return;
+ player.position.x = x;
+ player.position.y = y;
+ player.setVelocity(0, 0);
+ player.state |= EntityStateFlags.needsCreate | EntityStateFlags.needsDelete;
+ },
+ game_claim: (client: Client, entityArg: string) => {
+ const TEntity = new Map([
+ ["ArenaCloser", ArenaCloser],
+ ["Dominator", Dominator],
+ ["Shape", AbstractShape],
+ ["Boss", AbstractBoss],
+ ["AutoTurret", AutoTurret]
+ ]).get(entityArg);
+
+ if (!TEntity || !client.camera?.game.entities.AIs.length) return;
+
+ const AIs = Array.from(client.camera.game.entities.AIs);
+ for (let i = 0; i < AIs.length; ++i) {
+ if (!(AIs[i].owner instanceof TEntity)) continue;
+ client.possess(AIs[i]);
+ return;
+ }
+ },
+ admin_godmode: (client: Client) => {
+ if(client.camera?.camera.player?.style?.styleFlags) {
+ if(client.camera.camera.player.style.styleFlags & StyleFlags.invincibility) {
+ client.camera.camera.player.style.styleFlags ^= StyleFlags.invincibility;
+ } else {
+ client.camera.camera.player.style.styleFlags |= StyleFlags.invincibility;
+ }
+ }
+ },
+ admin_summon: (client: Client, entityArg: string, countArg?: string, xArg?: string, yArg?: string) => {
+ const count = countArg ? parseInt(countArg) : 1;
+ const x = parseInt(xArg || "");
+ const y = parseInt(yArg || "");
+ const game = client.camera?.game;
+ const TEntity = new Map([
+ ["Defender", Defender],
+ ["Summoner", Summoner],
+ ["Guardian", Guardian],
+ ["FallenOverlord", FallenOverlord],
+ ["FallenBooster", FallenBooster],
+ ["FallenAC", FallenAC],
+ ["FallenSpike", FallenSpike],
+ ["ArenaCloser", ArenaCloser],
+ ["Crasher", Crasher],
+ ["Pentagon", Pentagon],
+ ["Square", Square],
+ ["Triangle", Triangle]
+ ])[entityArg];
+
+ if (isNaN(count) || count < 0 || !game || !TEntity) return;
+
+ for (let i = 0; i < count; ++i) {
+ const boss = new TEntity(game);
+ if (!isNaN(x) && !isNaN(y)) {
+ boss.position.x = x;
+ boss.position.y = y;
+ }
+ }
+ },
+ admin_kill_all: (client: Client) => {
+ const game = client.camera?.game;
+ if(!game) return;
+ for (let id = 0; id <= game.entities.lastId; ++id) {
+ const entity = game.entities.inner[id];
+ if (Entity.exists(entity) && entity instanceof LivingEntity && entity !== client.camera?.camera.player) entity.health.health = 0;
+ }
+ },
+ admin_close_arena: (client: Client) => {
+ client?.camera?.game.arena.close();
+ },
+ admin_kill_entity: (client: Client, entityArg: string) => {
+ const TEntity = new Map([
+ ["ArenaCloser", ArenaCloser],
+ ["Dominator", Dominator],
+ ["Bullet", Bullet],
+ ["Tank", TankBody],
+ ["Shape", AbstractShape],
+ ["Boss", AbstractBoss]
+ ])[entityArg];
+ const game = client.camera?.game;
+ if (!TEntity || !game) return;
+
+ for (let id = 0; id <= game.entities.lastId; ++id) {
+ const entity = game.entities.inner[id];
+ if (Entity.exists(entity) && entity instanceof TEntity) entity.health.health = 0;
+ }
+ }
+} as Record
+
+export const executeCommand = (client: Client, cmd: string, args: string[]) => {
+ if (!commandDefinitions.hasOwnProperty(cmd) || !commandCallbacks.hasOwnProperty(cmd)) {
+ return saveToVLog(`${client.toString()} tried to run the invalid command ${cmd}`);
+ }
+
+ if (client.accessLevel < commandDefinitions[cmd as CommandID].permissionLevel) {
+ return saveToVLog(`${client.toString()} tried to run the command ${cmd} with a permission that was too low`);
+ }
+
+ commandCallbacks[cmd as CommandID](client, ...args);
+}
diff --git a/src/Const/TankDefinitions.ts b/src/Const/TankDefinitions.ts
index 7b867a42..5885c1b2 100644
--- a/src/Const/TankDefinitions.ts
+++ b/src/Const/TankDefinitions.ts
@@ -6760,3 +6760,7 @@ export const TankCount = TankDefinitions.reduce((a, b) => b ? a + 1 : a, 0);
export const getTankById = function (id: number): TankDefinition | null {
return (id < 0 ? DevTankDefinitions[~id] : TankDefinitions[id]) || null;
}
+
+export const getTankByName = function (tankName: string): TankDefinition | null {
+ return TankDefinitions.find(tank => tank && tank.name === tankName) || DevTankDefinitions.find(tank => tank && tank.name === tankName) || null;
+}
diff --git a/src/config.ts b/src/config.ts
index 719c38f8..03703df4 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -47,7 +47,10 @@ export const mode: string = process.env.NODE_ENV || "development";
export const enableApi: boolean = true;
/** Rest API location (root of all other endpoints), ignored if enableApi is false */
-export const apiLocation: string = "api"
+export const apiLocation: string = "api";
+
+/** Allows execution of custom commands */
+export const enableCommands: boolean = true;
/** Is hosting a client */
export const enableClient: boolean = true;
diff --git a/src/index.ts b/src/index.ts
index 936700fe..bebb2832 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -24,6 +24,7 @@ import * as util from "./util";
import GameServer from "./Game";
import auth from "./Auth";
import TankDefinitions from "./Const/TankDefinitions";
+import { commandDefinitions } from "./Const/Commands";
const PORT = config.serverPort;
const ENABLE_API = config.enableApi && config.apiLocation;
@@ -53,11 +54,13 @@ const server = http.createServer((req, res) => {
return res.end(JSON.stringify(TankDefinitions));
case "/servers":
res.writeHead(200);
- return res.end(JSON.stringify(games.map(({ gamemode, name }) => ({ gamemode, name }))));
+ return res.end(JSON.stringify(games.filter(({ running }) => running).map(({ gamemode, name }) => ({ gamemode, name }))));
+ case "/commands":
+ res.writeHead(200);
+ return res.end(JSON.stringify(config.enableCommands ? Object.values(commandDefinitions) : []));
}
}
-
if (ENABLE_CLIENT) {
let file: string | null = null;
let contentType = "text/html"