Minecraft websocket 服务器

这两天学习了nodejs,于是想着做点什么来实践一下,正好对Minecraft 基岩版的websocket功能比较感兴趣,于是做了一个websocket服务器的demo,可以用来获得游戏内的信息以及执行命令。

准备

在服务器上使用Websocket

不少人已经在bds服务器上尝试过使用wsserver/connect命令了吧?但是这条命令不知道为什么是不能直接使用的,要想使用这条命令我们需要做以下准备。

  • 获得op权限
    只有op可以执行这条命令
  • 修改权限等级
    修改服务器配置文件server.proterties,加入一行op-permission-level = 4,使op拥有与控制台同级的命令权限

做完上述操作之后就可以使用/connect命令了,当然如果只是输入/connect,会提示Command version mismatch,这里我们暂时不管它。

上代码

我使用的使nodejs+typescript的方式创建了这个demo,只有3个文件。注释已经写的很清楚了,有问题就留下评论吧。

app.ts server.ts packet.ts

//app.ts 用于启动程序,端口可以在这里设置
import {WSServer} from './server';
let server = new WSServer();
server.listen(6800);</code></pre>

//server.ts 包含ws服务器类,用于创建server对象
import { Packet,UnSubscribe,Subscribe,CommandPacket } from './packet/packet';
import { Server } from 'ws';
import {EventEmitter} from 'events';
import { Socket } from 'net';
import {createInterface} from 'readline';
//import {v4} from 'uuid';
let uuid4 = require('uuid/v4');
//创建一个负责输入输出的对象
const rl = createInterface({
    input: process.stdin,
    output: process.stdout
});

export class WSServer extends EventEmitter{
    //strictPropertyInitialization:false
    _socket:Server;

    public listen(port:number):void{
        let server:WSServer = this;

        this._socket = new Server({port:port});
        //当有客户端连接时回调
        console.log("WS开始监听" + port);
        this._socket.on("connection",socket => {
            console.log("客户端连接");

            //发送subscribe包建立监听
            registerSubscribe(socket,"PlayerMessage");
            registerSubscribe(socket,"BlockPlaced");

            //当socket收到信息时回调
            socket.on("message", message => {
                console.log("接收到客户端的信息");

                let data = JSON.parse(message as string);
                let msgPurpose = data.header.messagePurpose;
                if(msgPurpose == "error"){
                    console.log("出现错误:", data);
                }
                else if(msgPurpose == "event"){
                    console.log(data.body.eventName);
                    //发送unsubscribe包,取消对该事件的监听,具体表现为服务端只能获得一次客户端的方块放置事件
                    if (data.body.eventName == "BlockPlaced"){
                        sendCommand(socket,<code>say 发送unsubscribe包解除对方块放置事件的监听</code>);
                        unRegisterSubscribe(socket,"BlockPlaced");
                    }

                }
                else if(msgPurpose == "commandResponse"){
                    console.log("命令返回:" + data.body.statusCode);
                }

            });
            //接收到控制台的发送信息事件
            server.on("sendMsg",msg=>{
                console.log("[sendMsg]:" + msg);
                sendCommand(socket,<code>say ${msg}</code>);
            });

            socket.on("error",err=>{
                console.log("建立的socket出现错误" + err.message);
            });

            socket.on("close", () => {console.log("客户端断开连接")});

        });

        this._socket.on("error",error=>{
            console.log(<code>出现错误${error}</code>);
        });

        //持续获得用户输入
        rl.on('line', (input) => {
            console.log(<code>[consoleInput]:${input}</code>);
            let [cmd,content] = input.split(":");
            if(cmd == "send"){
                //触发sendMsg事件
                server.emit("sendMsg",content);
            }
            else if(cmd == "exit"){
                process.exit();
            }
        });

    }
}

function registerSubscribe(socket:any,eventName:string):void{
    let packet:Subscribe = new Subscribe(eventName);
    socket.send(JSON.stringify(packet));
}

function unRegisterSubscribe(socket:any,eventName:string):void{
    let packet:UnSubscribe = new UnSubscribe(eventName);
    socket.send(JSON.stringify(packet));
}

function sendCommand(socket:any,command:string):void{
    let packet:CommandPacket = new CommandPacket(command);
    socket.send(JSON.stringify(packet));
}</code></pre>
<pre><code class="language-javascript">//packet 这里是三种数据包的类
let uuid4 = require('uuid/v4');
//数据包
export abstract class Packet{
    body:{};
    header:{};
}

export class Subscribe extends Packet{
    body:{
        eventName: string;
    };
    header:{
        requestId: string;
        messagePurpose: string;
        version: number;
        messageType: string;
    };

    constructor(eventName:string){
        super();
        this.body = {
            eventName:eventName
        };

        this.header = {
            requestId: uuid4(),
            messagePurpose: "subscribe",
            version: 1,
            messageType: "commandRequest"
        };
    }
}

export class UnSubscribe extends Packet{
    body:{
        eventName: string;
    };
    header:{
        requestId: string;
        messagePurpose: string;
        version: number;
        messageType: string;
    }

    constructor(eventName:string,uuid = uuid4()){
        super();

        this.body = {
            eventName: eventName
        };

        this.header = {
            requestId: uuid,
            messagePurpose: "unsubscribe",
            version: 1,
            messageType: "commandRequest"
        }

    }
}

export class CommandPacket extends Packet{
    body:{};
    header:{};

    constructor(cmd:string){
        super();
        this.body = {
            origin: 
            {
                type: "player"
            },
            commandLine: cmd,
            version: 1
        };

        this.header = {
            requestId: uuid4(),
            messagePurpose: "commandRequest",
            version: 1,
            messageType: "commandRequest"
        };
    }
}

运行程序

如果想部署一个ws服务器试试看

git clone https://github.com/aoyouer/BedrockWsServer.git
cd BedrockWsServer
npm install
node build/app.js

#默认使用6800端口

之后我们在游戏里即可使用 /connect ip:6800 连接ws服务器了,但是不知道为什么,我在自己的电脑(win10)上运行的ws服务器可以通过浏览器访问,但是无法在游戏中连接,在我将程序放到远程的服务器上才正常连接。

在这个demo中,我尝试发送/接收了mc中文wiki ws教程中提到的几种数据包

  • subscribe
    用于监听一个事件,当用户成功连接以后,我们依旧是无法获得用户相关的信息的,我们需要发送subscribe包表明我们对哪些数据"感兴趣",之后客户端才会通过ws发送给我们。在demo中我发送了两个时间的subscribe包,分别是 "PlayerMessage" 表示用户发送信息 与 "BlockPlaced" 表示用户放置方块这两个事件,我没有取出更多信息,感兴趣的朋友可以查看返回的json,并取出详细信息。
  • unsubscribe
    当我们对一个事件不再感兴趣之后,可以发送unsubscribe包解除对此事件的接收,在demo中,我在接收到了一个“BlockPlaced”事件之后即发送该事件的unsubscribe包,因此我们只会接收到一次BlockPlaced事件。
  • commandrequest
    该数据包用于执行命令,在demo中,你只需要在程序运行的后台中输入 send:你想发送的信息 按下回车后,程序即会构建一个执行 say 你想发送的信息的命令的数据包并发送,还会获得该命令执行的结果。

在后台输入exit退出程序。

总结

写这个小程序算是加深了一些我对nodejs的了解,Minecraft基岩版的websocket虽然看上去可以获得很多信息,但是在我看来,它的功能还是相当有限的,因为有几个巨大的限制

  • 只有op在客户端上才能执行,而不是bds服务端直接连接另一个websocket服务端
  • 要想保持连接需要op持续在线

因为这些限制,我暂时想不出什么有趣的应用方向(也许之后我会慢慢扩展这个项目),不过github上依旧有几个比较成熟的基于Minecraft websocket的项目可以了解一下

参考链接
https://minecraft-zh.gamepedia.com/%E6%95%99%E7%A8%8B/WebSocket
https://minecraft-zh.gamepedia.com/%E6%95%99%E7%A8%8B/WebSocket
https://github.com/eDroiid/BedrockWS
文章目录