Closed12

C#からマイクラ統合版にコマンドを送信して実行したい

たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

とりあえず作ってみた。コマンドはJava版のように一行まるごと書けば良いものでもなく、JSON形式にしてあげないとダメらしい。ヘッダーも必要。

public class Program
{
    private static HttpListener listener;
    private static string subscribe = "{\"header\":{\"version\":1,\"requestId\":\"8344a99b-d958-5bb8-68e3-061da3f83dc3\",\"messageType\":\"commandRequest\",\"messagePurpose\":\"subscribe\"},\"body\":{\"eventName\":\"PlayerTravelled\"}}";
    private static string command = "{\"header\":{\"version\":1,\"requestId\":\"8344a99b-d958-5bb8-68e3-062da3f83dc3\",\"messageType\":\"commandRequest\",\"messagePurpose\":\"commandRequest\"},\"body\":{\"origin\":{\"type\":\"player\"},\"version\":1,\"commandLine\":\"summon chicken ~ ~ ~\"}}";

    public static void Main(string[] args)
    {
        listener = new HttpListener();
        listener.Prefixes.Clear();
        listener.Prefixes.Add("http://127.0.0.1:19132/");
        listener.Start();
        Receive();
        Console.ReadKey();
    }

    public static void Stop()
    {
        listener.Stop();
    }

    private static void Receive()
    {
        listener.BeginGetContext(new AsyncCallback(ListenerCallback), listener);
    }

    private static void ListenerCallback(IAsyncResult result)
    {
        if (listener.IsListening)
        {
            var context = listener.EndGetContext(result);
            var request = context.Request;
            Console.WriteLine($"{request.Url}");

            var response = context.Response;
            response.StatusCode = (int)HttpStatusCode.OK;
            response.ContentType = "text/plain";
            byte[] text = Encoding.UTF8.GetBytes(subscribe);
            response.OutputStream.Write(text, 0, text.Length);
            response.OutputStream.Close();

            Receive();
        }
    }
}

動かしてみた。

うーん?C#側では接続できてる判定だけど、マイクラでは接続できてない判定になる。多分マイクラに接続できたレスポンスを返さないとダメなんだと思う...。ちょっと面倒かも。

たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

WebSocketSharp を使うといいみたい
https://www.nuget.org/packages/WebSocketSharp#versions-body-tab
いつか自力で実装してみたいなぁ。

コードの例はこちらを参考に。
https://edom18.hateblo.jp/entry/2021/01/25/094602

using WebSocketSharp;

public static void Main(string[] args)
{
    var ws = new WebSocket("ws://localhost:8000");
    ws.OnOpen += (s, e) => Console.WriteLine("Open.");
    ws.Connect();
}

いや、草。もしかして教育版にもループバック許可を与えないといけないんか。。。

たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

どうやら間違っていたみたい。上記のやり方はクライアント側で、サーバ側は書き方がこうなる。
https://blog.csdn.net/sinolover/article/details/111086456

using WebSocketSharp;
using WebSocketSharp.Server;

public static void Main(string[] args)
{
    server = new WebSocketServer(8000);
    server.AddWebSocketService<Echo>("/");
    server.Start();
    Console.ReadKey();
    OnDestroy();
}

static void OnDestroy()
{
    server.Stop();
    server = null;
}

public class Echo : WebSocketBehavior
{
    protected override void OnMessage(MessageEventArgs e)
    {
        Console.WriteLine("Connected");
    }
}

そして接続完了!

ただし、一定時間経過すると接続が解除される。

2022/09/25 3:57:44|Fatal|<>c__DisplayClass9.<ReadBytesAsync>b__8|System.ObjectDisposedException: Cannot access a disposed object.
                         Object name: 'System.Net.Sockets.NetworkStream'.
                            at System.Net.Sockets.NetworkStream.<ThrowIfDisposed>g__ThrowObjectDisposedException|63_0()
                            at System.Net.Sockets.NetworkStream.EndRead(IAsyncResult asyncResult)
                            at WebSocketSharp.Ext.<>c__DisplayClass9.<ReadBytesAsync>b__8(IAsyncResult ar)
たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

マイクラ統合版からのメッセージを受け取れるようになった。クライアントへは文字列そのまま送っても大丈夫みたいなので、バイナリにする必要はなかった。

あと、マイクラへのイベント登録はサーバが接続された時点で登録する必要があるみたいなので、OnOpen メソッドをオーバーライドしておく必要がある。

public class Echo : WebSocketBehavior
{
    private string MakeSubscribeJson()
    {
        var json = new SubscribeJson()
        {
            header = new SubscribeJson.Header
            {
                requestId = Guid.NewGuid().ToString(),
                messagePurpose = "subscribe",
                messageType = "commandRequest"
            },
            body = new SubscribeJson.Body
            {
                eventName = "PlayerMessage"
            }
        };
        Debug.WriteLine(json);
        return JsonSerializer.Serialize(json);
    }

    protected override void OnOpen()
    {
        Sessions.Broadcast(MakeSubscribeJson());
    }

    protected override void OnMessage(MessageEventArgs e)
    {
        Console.WriteLine(e.Data);
    }
}

接続したあとに、適当な文字列を送ると、コンソール側に表示される。あとは条件分岐を使って文字列比較を行えば、特定の文字列が送られてきたらコマンドを実行するような処理も作れそう。

たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

結果のJson構造体。これをデシリアライズすれば属性ごとで判定ができる。例えば、プレイヤーの発言だったり、メッセージの内容だったりで。

{
	"body": {
		"message": "hello",
		"receiver": "",
		"sender": "takunology",
		"type": "chat"
	},
	"header": {
		"eventName": "PlayerMessage",
		"messagePurpose": "event",
		"version": 16973824
	}
}
たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

適当に石レンガを置いてみるところまで。
とりあえず、ここまでのソースコードを全てメモ。

メインは WebSocket サーバを立てて、クライアントからの応答を待機。接続が完了したらマイクラへイベント登録コマンドを送信して、プレイヤーからメッセージが送信されるのを待つ。メッセージにて "build" という発言があれば石レンガを置くコマンドを実行する。

Program.cs
using System.Diagnostics;
using System.Text.Json;
using WebSocketSharp;
using WebSocketSharp.Server;

public class Program
{
    private static WebSocketServer server;

    public static void Main(string[] args)
    {
        server = new WebSocketServer(8000);
        server.AddWebSocketService<Echo>("/");
        server.Start();
        Console.WriteLine($"WebSocket start.\n running at localhost:{server.Port}");
        Console.ReadKey(true);
        OnDestroy();
    }

    private static void OnDestroy()
    {
        server.Stop();
        server = null;
    }

    public class Echo : WebSocketBehavior
    {
        private string MakeSubscribeJson()
        {
            var json = new SubscribeJson()
            {
                header = new SubscribeJson.Header
                {
                    requestId = Guid.NewGuid().ToString(),
                    messagePurpose = "subscribe",
                    messageType = "commandRequest"
                },
                body = new SubscribeJson.Body
                {
                    eventName = "PlayerMessage"
                }
            };
            Debug.WriteLine(json);
            return JsonSerializer.Serialize(json);
        }

        private string MakeCommandJson()
        {
            var json = new SendCommandJson()
            {
                header = new SendCommandJson.Header
                {
                    requestId = Guid.NewGuid().ToString(),
                    messagePurpose = "commandRequest",
                    messageType = "commandRequest"
                },
                body = new SendCommandJson.Body
                {
                    origin = new SendCommandJson.Body.Origin
                    {
                        type = "player"
                    },
                    commandLine = "/setblock ~0 ~-1 ~0 stonebrick"
                }
            };
            return JsonSerializer.Serialize(json);
        }

        protected override void OnOpen()
        {
            Sessions.Broadcast(MakeSubscribeJson());
            Console.WriteLine("Connected.");
        }

        protected override void OnError(WebSocketSharp.ErrorEventArgs e)
        {
            Console.WriteLine(e);
        }

        protected override void OnMessage(MessageEventArgs e)
        {
            Console.WriteLine(e.Data);
            if (e.Data.Contains("build"))
            {
                string command = MakeCommandJson();
                Console.WriteLine(command);
                Sessions.Broadcast(command);
            }
        }
    }
}

ここはコマンド送信用のJSONプロパティ

SendCommandJson.cs
public class SendCommandJson
{
    public Header header { get; set; }
    public Body body { get; set; }

    public class Header
    {
        public string requestId { get; set; }
        public string messagePurpose { get; set; }
        public int version { get; set; } = 1;
        public string messageType { get; set; }
    }

    public class Body
    {
        public Origin origin { get; set; }
        public string commandLine { get; set; }
        public int version { get; set; } = 1;

        public class Origin
        {
            public string type { get; set; }
        }
    }
}

ここはイベント登録用のJSONプロパティ

SubscribeJson.cs
public class SubscribeJson
{
    public Header header { get; set; }
    public Body body { get; set; }

    public class Header
    {
        public string requestId { get; set; }
        public string messagePurpose { get; set; }
        public int version { get; set; } = 1;
        public string messageType { get; set; }
    }

    public class Body
    {
        public string eventName { get; set; }
    }
}

マイクラから接続して "build" と発言すると足元に石レンガが設置される。

コマンド実行後のレスポンスも帰ってくる。(マイクラ統合版の Json って body が先頭なのかな)

{
	"body": {
		"position": {
			"x": 1234,
			"y": 66,
			"z": 27
		},
		"statusCode": 0,
		"statusMessage": "ブロックを設置しました"
	},
	"header": {
		"messagePurpose": "commandResponse",
		"requestId": "4f795e8e-4770-42c1-8b79-67fc25ba8861",
		"version": 16973824
	}
}

これにてC#からマイクラ統合版へのコマンド送信は完了!

たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

ちなみに、教育版マイクラ上でもコマンド送信は可能。
ただし、「暗号化された WebSocket の要求」をオフにする必要はある。

このスクラップは2023/01/28にクローズされました