C#からマイクラ統合版にコマンドを送信して実行したい
Java 版はTCP、統合版はUDPプロトコルで違いがあるみたいだ。C#で実装するならば HttpListener
を使うのが一般的なのだろうか?
とりあえずサーバを立てられそうな参考サイトを模写して作ってみる。
とりあえず作ってみた。コマンドは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#側では接続できてる判定だけど、マイクラでは接続できてない判定になる。多分マイクラに接続できたレスポンスを返さないとダメなんだと思う...。ちょっと面倒かも。
ここまでの参考文献
これは一旦保留
マイクラ統合版用のJson構造体
中国語しかないけど WebSocket での仕様が書かれている
WebSocketSharp を使うといいみたい
いつか自力で実装してみたいなぁ。コードの例はこちらを参考に。
using WebSocketSharp;
public static void Main(string[] args)
{
var ws = new WebSocket("ws://localhost:8000");
ws.OnOpen += (s, e) => Console.WriteLine("Open.");
ws.Connect();
}
いや、草。もしかして教育版にもループバック許可を与えないといけないんか。。。
どうやら間違っていたみたい。上記のやり方はクライアント側で、サーバ側は書き方がこうなる。
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)
Echoできた
あとはマイクラのイベントを subscribe できるような処理を書けば良さそう
マイクラ統合版からのメッセージを受け取れるようになった。クライアントへは文字列そのまま送っても大丈夫みたいなので、バイナリにする必要はなかった。
あと、マイクラへのイベント登録はサーバが接続された時点で登録する必要があるみたいなので、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);
}
}
接続したあとに、適当な文字列を送ると、コンソール側に表示される。あとは条件分岐を使って文字列比較を行えば、特定の文字列が送られてきたらコマンドを実行するような処理も作れそう。
結果のJson構造体。これをデシリアライズすれば属性ごとで判定ができる。例えば、プレイヤーの発言だったり、メッセージの内容だったりで。
{
"body": {
"message": "hello",
"receiver": "",
"sender": "takunology",
"type": "chat"
},
"header": {
"eventName": "PlayerMessage",
"messagePurpose": "event",
"version": 16973824
}
}
適当に石レンガを置いてみるところまで。
とりあえず、ここまでのソースコードを全てメモ。
メインは WebSocket サーバを立てて、クライアントからの応答を待機。接続が完了したらマイクラへイベント登録コマンドを送信して、プレイヤーからメッセージが送信されるのを待つ。メッセージにて "build" という発言があれば石レンガを置くコマンドを実行する。
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プロパティ
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プロパティ
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#からマイクラ統合版へのコマンド送信は完了!
ちなみに、教育版マイクラ上でもコマンド送信は可能。
ただし、「暗号化された WebSocket の要求」をオフにする必要はある。