@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
432 lines (431 loc) • 16 kB
JavaScript
"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;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const ws_1 = __importStar(require("ws"));
const uuid_1 = require("uuid");
const http = __importStar(require("http"));
const ste_events_1 = require("ste-events");
const Log_1 = __importDefault(require("../core/Log"));
const ICreatorToolsData_1 = require("../app/ICreatorToolsData");
const NodeStorage_1 = __importDefault(require("./NodeStorage"));
const fs = __importStar(require("fs"));
const Utilities_1 = __importDefault(require("../core/Utilities"));
const SecurityUtilities_1 = __importDefault(require("../core/SecurityUtilities"));
class MinecraftWebSocketServer {
_port = 19136;
server;
_wss;
_ws;
_env;
_pendingCommands = [];
_pendingCommandIds = [];
_pendingRequestIds = [];
_pendingData = [];
_curEventIndex = 0;
_eventSeed;
_onClientConnected = new ste_events_1.EventDispatcher();
_onClientDisconnected = new ste_events_1.EventDispatcher();
_onCommandCompleted = new ste_events_1.EventDispatcher();
_onEventReceived = new ste_events_1.EventDispatcher();
_currentCommandId = 0;
constructor(env) {
this._env = env;
this._eventSeed = Utilities_1.default.createRandomId(4);
this._handleMessage = this._handleMessage.bind(this);
this._handleConnection = this._handleConnection.bind(this);
this.openServer = this.openServer.bind(this);
this.executeNextCommand = this.executeNextCommand.bind(this);
}
get onCommandCompleted() {
return this._onCommandCompleted.asEvent();
}
get onEventReceived() {
return this._onEventReceived.asEvent();
}
get onClientConnected() {
return this._onClientConnected.asEvent();
}
get onClientDisconnected() {
return this._onClientDisconnected.asEvent();
}
runCommand(command, requestId, data) {
// Security: Sanitize command
const sanitizedCommand = SecurityUtilities_1.default.sanitizeCommand(command);
if (!SecurityUtilities_1.default.isCommandSafe(sanitizedCommand)) {
Log_1.default.message("WebSocket command rejected as unsafe: " + command);
return;
}
const newCommand = this._pendingCommands.length;
this._pendingCommands[newCommand] = sanitizedCommand;
this._pendingCommandIds[newCommand] = (0, uuid_1.v4)();
this._pendingRequestIds[newCommand] = requestId;
this._pendingData[newCommand] = data;
if (newCommand === this._currentCommandId) {
this.executeNextCommand();
}
}
executeNextCommand() {
if (this._currentCommandId < this._pendingCommands.length) {
this._currentCommandId++;
const nextCommand = this._currentCommandId - 1;
const commandLine = this._pendingCommands[nextCommand];
const commandId = this._pendingCommandIds[nextCommand];
const nextCommandStr = JSON.stringify({
header: {
version: 1,
requestId: commandId,
messageType: "commandRequest",
messagePurpose: "commandRequest",
},
body: {
// version: 22, NOTE: if version is not included in request msg,
// MC *should* use latest command version
commandLine: commandLine,
origin: {
type: "server",
},
},
});
Log_1.default.message("Command " + this._currentCommandId + " sent:" + nextCommandStr);
if (this._ws) {
this._ws.send(nextCommandStr);
}
}
}
getWebSocketWorldPath(state) {
if (state === ICreatorToolsData_1.MinecraftGameConnectionMode.remoteMinecraft) {
return "";
}
let sourcePath = "";
if (state === ICreatorToolsData_1.MinecraftGameConnectionMode.localMinecraftPreview) {
sourcePath = this._env.utilities.minecraftPreviewUwpPath;
}
else {
sourcePath = this._env.utilities.minecraftUwpPath;
}
const worldPath = NodeStorage_1.default.ensureEndsWithDelimiter(sourcePath) + "minecraftWorlds" + NodeStorage_1.default.platformFolderDelimiter;
const subfolders = fs.readdirSync(worldPath);
let maxTime = 0;
let maxPath = "";
if (subfolders) {
for (let i = 0; i < subfolders.length; i++) {
const subfolder = subfolders[i];
const stat = fs.statSync(worldPath + subfolder);
if (stat.isDirectory()) {
if (stat.mtimeMs > maxTime) {
maxTime = stat.mtimeMs;
maxPath = worldPath + subfolder + NodeStorage_1.default.platformFolderDelimiter;
}
}
}
}
console.log("Reading path " + worldPath + "|" + maxPath);
return maxPath;
}
closeServer() {
if (!this.server) {
return;
}
}
openServer() {
if (this.server) {
return;
}
const listenHost = "127.0.0.1";
console.log("Starting Minecraft websocket server on " + listenHost + ":" + this._port);
this.server = http.createServer();
this._wss = new ws_1.WebSocketServer({
server: this.server,
verifyClient: (info) => {
const remoteAddress = info.req.socket.remoteAddress;
return remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
},
});
this.server.on("error", (e) => {
Log_1.default.message("Error on server." + e);
});
this._wss.on("connection", this._handleConnection);
this._wss.on("close", this._handleDisconnection);
this._wss.on("error", (e) => {
Log_1.default.message("Error on web socket server." + e);
});
try {
this.server.listen(this._port, listenHost, () => {
Log_1.default.message("Minecraft websocket server started on " + listenHost + ":" + this._port + ".");
});
}
catch (e) {
Log_1.default.message("Error opening port " + this._port + ".");
}
}
_handleMessage(message) {
if (!message) {
return;
}
try {
if (message.indexOf("{") >= 0) {
const struct = JSON.parse(message);
if (struct.body !== undefined && struct.header !== undefined && struct.header.messagePurpose !== undefined) {
const purpose = struct.header.messagePurpose;
if (purpose === "commandResponse") {
Log_1.default.message("Received command response: " + struct.header.requestId + "(" + message + ")");
const commandId = struct.header.requestId.toLowerCase();
let found = false;
for (let i = this._pendingCommandIds.length - 1; i >= 0; i--) {
const _reqId = this._pendingCommandIds[i];
if (_reqId.toLowerCase() === commandId) {
console.log("Found request id " + _reqId);
this._onCommandCompleted.dispatch(this, {
requestId: this._pendingRequestIds[i],
result: struct.body,
data: this._pendingData[i],
});
found = true;
break;
}
}
if (!found) {
Log_1.default.debug("Could not find a matching request for id " + commandId);
}
this.executeNextCommand();
}
else if (purpose === "event") {
Log_1.default.message("Received event: " + struct.header.eventName + "(" + message + ")");
struct.eventId = this._eventSeed + ++this._curEventIndex;
this._onEventReceived.dispatch(this, struct);
}
}
}
}
catch (e) {
console.log("Error processing command: " + e);
}
}
_handleConnection(ws) {
// Close existing connection if present
if (this._ws && this._ws.readyState === ws_1.default.OPEN) {
Log_1.default.message("Closing existing WebSocket connection to accept new client");
this._ws.close(1000, "New client connected");
}
this._ws = ws;
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "PlayerMessage",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "PlayerTravelled",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "PlayerTeleported",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "WorldGenerated",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "WorldLoaded",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "PlayerJoin",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "PlayerLeave",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "SlashCommandExecuted",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "ItemAcquired",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "ItemCrafted",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "ItemEquipped",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "PlayerTransform",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "ItemUsed",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "ItemInteracted",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "BlockPlaced",
},
}));
ws.send(JSON.stringify({
header: {
version: 1,
requestId: (0, uuid_1.v4)(),
messageType: "commandRequest",
messagePurpose: "subscribe",
},
body: {
eventName: "BlockBroken",
},
}));
ws.on("message", this._handleMessage);
console.log("Inbound web socket connection.");
this._onClientConnected.dispatch(this, ws.url);
}
_handleDisconnection(ws) {
this._ws = undefined;
this._onClientDisconnected.dispatch(this, ws.url);
}
}
exports.default = MinecraftWebSocketServer;