在 Kotlin 使用 SpaceEngineers Remote API

众所周知, SpaceEngineers 的服务端(vanilla, 非 torch)上那个查看服务器内游戏信息的玩意是通过网络来获取信息的(localhost). 实际上这个东西有独立版本应用程序, 也在服务端根目录下, 叫做 VRageRemoteClient . VRage 是 SpaceEngineers 使用的游戏引擎, 这个游戏引擎本身也是 Keen 开发的.

于是在谷歌上搜索 SpaceEngineers API, 只能找到这个页面 https://www.spaceengineersgame.com/dedicated-servers.html 并且介绍 API 有哪几个的, 只有页面最下方的一张图 https://www.spaceengineersgame.com/uploads/2/1/9/6/21961362/736604853_orig.png. 页面下方有一段 c# 代码用于示例, 但是有些东西是 .Net 平台特定的, 在其他语言可能有麻烦.

示例代码是这样的

private readonly string m_remoteUrl = "/vrageremote/{0}";

public RestRequest CreateRequest(string resourceLink, Method method, 
params Tuple<string, string>[] queryParams)
{
    string methodUrl = string.Format(m_remoteUrl, resourceLink);
    RestRequest request = new RestRequest(methodUrl, method); 
    string date = DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture);
    request.AddHeader("Date", date); 
    m_nonce = random.Next(0, int.MaxValue);
    string nonce = m_nonce.ToString();
    StringBuilder message = new StringBuilder();
    message.Append(methodUrl); 
    if (queryParams.Length > 0)
    {
        message.Append("?");
    }

    for (int i = 0; i < queryParams.Length; i++)
    {
        var param = queryParams[i];
        request.AddQueryParameter(param.Item1, param.Item2);
        message.AppendFormat("{0}={1}", param.Item1, param.Item2);
        if (i != queryParams.Length - 1)
        {
            message.Append("&");
        }
    }

    message.AppendLine();
    message.AppendLine(nonce);
    message.AppendLine(date);
    byte[] messageBuffer = Encoding.UTF8.GetBytes(message.ToString());

    byte[] key = Convert.FromBase64String(m_securityKey);
    byte[] computedHash;
    using (HMACSHA1 hmac = new HMACSHA1(key))
    {
        computedHash = hmac.ComputeHash(messageBuffer);
    }

    string hash = Convert.ToBase64String(computedHash);
    request.AddHeader("Authorization", string.Format("{0}:{1}", nonce, hash));
    return request;
}

可以看出, 最终的请求必须有至少以下两个 header

Date: httpDateString()
Authorization: "$nonce:$hash"

在示例代码中, 我们可以看到 c# 中是这样得到 Date 的: DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture) , c# 在输出时间字符串的时候会根据不同的 culture(fr, de, cn...) 来得到不同的结果, 而 InvariantCulture 用于得到统一的结果. 它的输出是这样的

Tue, 15 May 2012 16:34:16 GMT

这其实就是平时俗称的 HttpDateString , 在不同的语言中根据这个格式来格式化时间即可, 例如在 Java 中

DateTimeFormatter
    .ofPattern("EEE, dd MMM yyyy HH:mm:ss z")
    .withLocale(Locale.US)
    .withZone("GMT")

Authorization 由两部分组成, nonce 就是一个大于 0 的 Int 类型随机数. 而这个 hash 就很有来头了, hash 的值是把 url(包含 query params), nonce, date 用 StringBuilder.AppendLine 拼接在一起然后做一次 HmacSHA1 , 用到的 key 就是 VRageRemoteClient 上需要填入的 Security Key. .Net 的 AppendLine 是平台相关的, 所以使用的是

val nonce = Random.nextInt(0..Int.MAX_VALUE)
val date = Instant.now().toHttpDateString()
val message = "$url\r\n$nonce\r\n$date\r\n"
val hash = Mac.getInstance("HmacSHA1").apply {
    init(secretKey)
}.doFinal(message.toByteArray()).let {
    Base64.getEncoder().encodeToString(it)
}

如果想要百分百还原, 还可以加上 UA

userAgent("RestSharp/106.6.10")

好了, 我们现在知道如何发送一个合法的请求了. 不过有些东西网页上没有讲到, 以下记录一些遇到的坑.

send message(POST /v1/session/chat) 是唯一一个 BODY 有内容的请求, 需要特别注意的是, BODY 应该是这样的

"This is a message"

它 自 身 包 含 引 号!

get message(GET /v1/session/chat) 有一个可选参数(Query Param)名称为 Date , 值是一个 DateTime.Ticks (最后转换为 Long 类型), 用于控制服务器返回的消息(复数)从哪个时间之后开始(闭区间).

如果没有这个参数, 服务器将返回所有聊天消息. 聊天消息中包含 timestamp 字段, 最后一条消息的这个字段的值加一, 就可以作为下一次请求聊天消息时所用的 Date 的值.

c# 的 DateTime.Ticks 是一个从 0001-01-01T00:00:00.00Z 开始以 100毫秒 为时间间隔递增的数字. 如果要对这个时间进行处理, 就需要转换为对应语言中的时间类型. 以 Kotlin 为例

//c# ticks
internal typealias Ticks = Long

private val offset = Duration.between(
    Instant.parse("0001-01-01T00:00:00.00Z"),
    Instant.ofEpochSecond(0)
).seconds
private val zoneOffset = ZoneOffset.ofTotalSeconds(TimeZone.getDefault().rawOffset / 1000)

fun Ticks.toLocalDateTime() =
    LocalDateTime.ofEpochSecond(
        this / 10_000_000 - offset,
        0,
        zoneOffset
    )!!

banned players 和 kicked players 这两个 API 返回的列表总是空的, 不太明确原因.

之前把 SpaceEngineers Remote API 完整的用 Kotlin 实现了一遍 https://github.com/czp3009/space-engineers-remote-api

然后又试着写了一个可以在手机上管理服务器的 Android App https://github.com/czp3009/SpaceEngineersRemoteClient

不过这个 APP 有很多问题, 重构又懒得弄, 你猜我能咕咕咕到什么时候