使用LongUI和LongUIS让你的插件在客户端显示网页吧~

LongUI 是由 hookan 开发的基于 WebCraft 的一个界面模组(基于ForgeAPI)。它最基本的功能是修改主界面,除此之外,它还能配合 LongUIS(一个基于SpigotAPI的服务器插件),让插件开发者能在客户端使用 HTML 来显示界面。

为什么要使用 LongUI ?

  • 可以通过网页的形式写 MC 的 GUI,而写网页前端已经有很多详细的教程以及可视化编辑器。
  • 通过 CSS 可以方便的修改页面的风格以及写一些动画。
  • 不用额外学习一个在别处都用不到的复杂的配置文件格式,HTML/CSS/JS 在大部分场合都可以应用。( LongUI 除网页以外的配置文件格式及其简单)
  • 开源,且使用相当自由的开源协议,WebCraft 使用 LGPL 协议开源,LongUI 使用 MIT 协议开源,LongUIS 使用 WTFPL 开源。

注意,本文并非 0 基础教程,请确定你在开始阅读前有一定 Java 基础且有一定插件开发基础。

关于 HTML/CSS/JS 的使用教程可以查看:w3school 或者 菜鸟教程

由于 WebCraft 所使用的网页引擎 Ultralight 并没有支持全部的 HTML5 特性,所以在编写时请注意这点。关于它不支持的特性,请查看这里 。另外可以通过 Ultralight 浏览器调试而不用每次重启 MC ,如何使用 Ultralight 浏览器调试可以看 Drenal的教程 。(如何安装本 mod 也请看 Drenal的教程 ,WebCraft 的安装过程较为特殊,请您务必查看

在本文,我将带领你们写一个简单的基于 AuthMeReload 和 LongUIS 的界面登陆插件,那么接下来让我们开始。

小贴士:用 MC 调试 GUI 修改的时候并不用关闭 MC ,在 MC 中重新打开 GUI 就可以让其重新加载。

LongUI GUI Json

在开始前,先介绍一下 LongUI 的配置文件格式。LongUI 使用一个 Json 来描述一个 GUI,其格式如下:

{
  "name": "MyGui",
  "url": "http://mydomain",
  "drawBackground": true,
  "shouldCloseOnEsc": true
}

name 为 GUI 的名称,它用于标记一个 GUI,是必填项。

url 为 GUI 的链接,是一个选填项,它在不填时默认为 file:///mods/longui/<name>/index.html (其中 <name> 为上方的 GUI 名称)(特别提示file:/// 为相对于运行路径的路径,运行路径不一定等同于加载 mod 的 mods 文件夹的父目录(基于 KMCCC 的启动器运行路径为 .minecraft),不过对于大部分启动器这二者是等同的(比如HMCL无论开启版本隔离还是不开启))

drawBackground 为是否绘制 MC 的 GUI 默认的背景,该背景在游戏外为泥土墙,在游戏内为黑色半透明背景,该项为选填项,不填时默认为 true

shouldCloseOnEsc 为是否在按下 Esc 键的时候关闭 GUI,为选填项,默认值为 true

本文中使用的 IDE 为 IDEA(且安装了插件 Minecraft Development ),系统为 Windows10 ,启动器为 HMCL3.2.149 的 jar 版本 ,Forge 版本为 1.15.2-31.1.0 ,Java 为 Oracle Java 8 ,服务端为 Paper-184 。

本文完整代码在文章末尾放出。

如何构建 LongUIS 子插件开发环境

首先我们新建一个 Spigot Plugin 项目:

我们这里使用 Gradle 做示范,如果你更习惯 Maven ,使用 Maven 也可以。

接下来,我们稍微整理一下 Minecraft Development 插件生成的 build.gradle ,然后在 repositories 里面加入:

maven {
    url = 'https://ci.qwq.cafe/maven/'
}

然后再在 dependencies 中添加:

implementation 'cafe.qwq:LongUIS:0.1.4'//写这个教程时的最新版本为0.1.4

查看 LongUIS 的最新版本可以到梦安构建站

完整的 build.gradle 文件示范:

apply plugin: 'java'

group = pluginGroup
version = pluginVersion

sourceCompatibility = '1.8'
targetCompatibility = '1.8'

repositories {
    mavenCentral()

    maven {
        name = 'spigotmc-repo'
        url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/'
    }

    maven {
        name = 'sonatype'
        url = 'https://oss.sonatype.org/content/groups/public/'
    }

    maven {
        url = 'https://ci.qwq.cafe/maven/'
    }
}

dependencies {
    compileOnly 'org.spigotmc:spigot-api:1.15.2-R0.1-SNAPSHOT'
    implementation 'cafe.qwq:LongUIS:0.1.3'
}

import org.apache.tools.ant.filters.ReplaceTokens

processResources {
    from(sourceSets.main.resources.srcDirs) {
        filter ReplaceTokens, tokens: [version: version]
    }
}

然后在侧边栏选择 Reimport Gradle Project 将项目重新导入。

如果你导入成功,你的 External Libraries 应该是这样的:

接下来在 plugin.yml 中加入 depend: [LongUIS]

name: LUIAuth
version: @version@
main: cafe.qwq.luiauth.LUIAuth
api-version: 1.13
depend: [LongUIS]

到此为止,你就构建好了 LongUIS 的子插件的开发环境。

如果你想在 IDEA 中调试你的插件,mcbbs已经有很多相关文章,故本文不再赘述。

不过,不要忘了,这篇教程中还有一个前置插件 --- AuthMeReload

在这里,我们可以根据 AuthMeReload 的 github 中写的配置方式配置我们的 build.gradle 。(由于不是重点,这块就不写了,会在文章末端放出全部代码)

如何在客户端添加一个 GUI 并在服务端发包打开

我们先来看如何在客户端添加一个 GUI 。

首先,在 mods/longui 文件夹(如果没有 longui 就新建)下创建一个文件夹 LUIAuth ,然后新建一个文件 index.html

写入:

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8">
        <style>
            body {
                font-family: Microsoft YaHei, Microsoft YaHei Light, Microsoft JhengHei, SimHei, SimSun;
            }
            
            .login {
                background-color: rgba(127,255,0,0.5);
                color: white;
                border: none;
                font-size: 18px;
                width: 300px;
                height: 35px;
                margin-top: 20px;
                transition: all 0.3s;
            }

            .login:hover {
                background-color: rgba(230,255,0,0.5);
            }

            .login:active {
                background-color: rgba(100, 100, 100, 0.5);
                background-image: none;
            }

            .main {
                text-align: center;
                display:-webkit-box;
                -webkit-box-orient: vertical;
                width: 300px;
                margin: 20% auto;
            }

            #password {
                width: 295px;
                height: 30px;
                font-size: 18px;
            }
        </style>
    </head>
    <body>
        <div class="main">
            <input id="password" type="password"></input>
            <button class="login">登录</button> 
        </div>
    </body>
</html>

然后我们监听 PlayerJoinEvent

@EventHandler
public void onJoin(PlayerJoinEvent event)
{
    JsonObject obj = new JsonObject();
    obj.addProperty("name", "LUIAuth");
    obj.addProperty("shouldCloseOnEsc", false);
    LongUIS.openGui(event.getPlayer(), obj);//这里的json格式为LongUI GUI Json
}

通过方法 LongUIS#openGui 就可以方便的在客户端打开一个GUI了。

我们可以在服务端利用 AuthMe 的 API 来判断玩家是否注册,然后给玩家发送不同的 GUI .

代码如下:

@EventHandler
public void onJoin(PlayerJoinEvent event)
{
    JsonObject obj = new JsonObject();
    obj.addProperty("name", "LUIAuth");
    if (!AuthMeApi.getInstance().isRegistered(event.getPlayer().getName()))
        obj.addProperty("url", "file:///mods/longui/LUIAuth/reg.html");
    obj.addProperty("shouldCloseOnEsc", false);
    LongUIS.openGui(event.getPlayer(), obj);
}

然后再在客户端的 LUIAuth 文件夹下添加文件 reg.html

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8">
        <style>
            body {
                font-family: Microsoft YaHei, Microsoft YaHei Light, Microsoft JhengHei, SimHei, SimSun;
            }

            #login {
                background-color: rgba(127,255,0,0.5);
                color: white;
                border: none;
                font-size: 18px;
                width: 300px;
                height: 35px;
                margin-top: 20px;
                transition: all 0.3s;
            }

            #login:hover {
                background-color: rgba(230,255,0,0.5);
            }

            #login:active {
                background-color: rgba(100, 100, 100, 0.5);
                background-image: none;
            }

            .main {
                text-align: center;
                display:-webkit-box;
                -webkit-box-orient: vertical;
                width: 300px;
                margin: 20% auto;
            }

            #password, #password2 {
                width: 295px;
                height: 30px;
                margin-top:20px;
                font-size: 18px;
            }
            
            #warn {
                color: red;
                font-size: 18px;
            }
        </style>
    </head>
    <body>
        <div class="main">
            <input id="password" type="password"></input>
            <input id="password2" type="password"></input>
            <button id="login" onclick="register();">注册</button> 
            <p id="warn"></p>
        </div>
        <script>
            function register()
            {
                var pwd1 = document.getElementById("password");
                var pwd2 = document.getElementById("password2");
                var warn = document.getElementById("warn");
                if(pwd1.value == "")
                    warn.innerText = "请输入密码!";
                else if(pwd1.value == pwd2.value)
                    sendChatMessage("/register " + pwd1.value + " " + pwd2.value);
                else
                    warn.innerText = "两次密码不一致!";
            }
        </script>
    </body>
</html>

然后我们便可以通过这个 GUI 来进行注册了。

值得注意的是 sendChatMessage 这个函数是 LongUI 添加的,用于代替玩家在游戏对话框中发布信息。关于 LongUI 都添加了哪些函数,可以参考这里

那么问题来了,我们虽然能成功注册了,但是还关闭不了这个 GUI 。那么该怎么办呢?

我们可以让我们的插件在玩家注册/登陆成功时(因为不一定注册/登陆成功),给客户端发一个包用于关闭 GUI 。

通过查询 AuthMeReload 的 API ,可以知道在玩家注册/登陆成功时,会触发事件 fr.xephi.authme.events.LoginEvent ,那么我们来监听它。

@EventHandler
public void onLogin(LoginEvent event)
{
    JsonObject obj = new JsonObject();
    obj.addProperty("type","close");
    LongUIS.sendPacket(event.getPlayer(), this, obj);//发包的json格式可以自己定
}

可以注意到我们使用了 LongUIS#sendPacket 来向客户端发包,那么客户端该怎么接收呢?

首先我们需要写一个函数 luiScreenInit ,这个函数会在 LongUI 添加完全部 js 函数后执行,也就是说,如果你想要调用一个 LongUI 添加的 js 函数,必须要在这个方法中或者这个方法执行完再执行。

然后我们在函数中执行 addPacketReceiver 方法:

function luiScreenInit()
{
    addPacketReceiver({plugin:"LUIAuth",callback:"myReceiver"});
}

在上面的代码中,plugin 表示插件的名称,callback 表示接收包的回调函数的名称。既然我们的回调函数叫 myReceiver ,那么接下来我们肯定是要写这个函数。

function myReceiver(packet)//packet的格式是刚刚发包的格式
{
    if(packet.type == "close") closeGui();
}

这样子,我们就能在玩家注册/登陆成功的时候关掉 GUI 了。

最后放出完整代码:

服务端:

build.gradle

apply plugin: 'java'

group = pluginGroup
version = pluginVersion

sourceCompatibility = '1.8'
targetCompatibility = '1.8'

repositories {
    mavenCentral()

    maven {
        name = 'spigotmc-repo'
        url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/'
    }

    maven {
        name = 'sonatype'
        url = 'https://oss.sonatype.org/content/groups/public/'
    }

    maven {
        url = 'https://ci.qwq.cafe/maven/'
    }

    maven {
        name = 'codemc-repo'
        url = 'https://repo.codemc.org/repository/maven-public/'
    }
}

dependencies {
    compileOnly 'org.spigotmc:spigot-api:1.15.2-R0.1-SNAPSHOT'
    implementation 'cafe.qwq:LongUIS:0.1.4'
    implementation 'fr.xephi:authme:5.6.0-SNAPSHOT'
}

import org.apache.tools.ant.filters.ReplaceTokens

processResources {
    from(sourceSets.main.resources.srcDirs) {
        filter ReplaceTokens, tokens: [version: version]
    }
}

plugin.yml

name: LUIAuth
version: @version@
main: cafe.qwq.luiauth.LUIAuth
api-version: 1.13
depend: [LongUIS,AuthMe]

cafe.qwq.luiauth.LUIAuth.java

package cafe.qwq.luiauth;

import cafe.qwq.longuis.LongUIS;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import fr.xephi.authme.api.v3.AuthMeApi;
import fr.xephi.authme.events.LoginEvent;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.java.JavaPlugin;

public final class LUIAuth extends JavaPlugin implements Listener
{
    public void onEnable()
    {
        Bukkit.getPluginManager().registerEvents(this, this);
    }
    
    public void onDisable()
    {
    
    }
    
    @EventHandler
    public void onJoin(PlayerJoinEvent event)
    {
        JsonObject obj = new JsonObject();
        obj.addProperty("name", "LUIAuth");
        if (!AuthMeApi.getInstance().isRegistered(event.getPlayer().getName()))
            obj.addProperty("url", "file:///mods/longui/LUIAuth/reg.html");
        obj.addProperty("shouldCloseOnEsc", false);
        LongUIS.openGui(event.getPlayer(), obj);
    }
    
    @EventHandler
    public void onLogin(LoginEvent event)
    {
        JsonObject obj = new JsonObject();
        obj.addProperty("type","close");
        LongUIS.sendPacket(event.getPlayer(), this, obj);
    }
}

客户端:

mods/longui/LUIAuth/index.html

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8">
        <style>
            body {
                font-family: Microsoft YaHei, Microsoft YaHei Light, Microsoft JhengHei, SimHei, SimSun;
            }

            .login {
                background-color: rgba(127,255,0,0.5);
                color: white;
                border: none;
                font-size: 18px;
                width: 300px;
                height: 35px;
                margin-top:20px;
                transition: all 0.3s;
            }

            .login:hover {
                background-color: rgba(230,255,0,0.5);
            }

            .login:active {
                background-color: rgba(100, 100, 100, 0.5);
                background-image: none;
            }

            .main {
                text-align: center;
                display:-webkit-box;
                -webkit-box-orient: vertical;
                width: 300px;
                margin: 20% auto;
            }

            #password {
                width: 295px;
                height: 30px;
                font-size: 18px;
            }
        </style>
    </head>
    <body>
        <div class="main">
            <input id="password" type="password"></input>
            <button class="login" onclick="login();">登录</button> 
        </div>
        <script>
            function login()
            {
                var pwd = document.getElementById("password");
                sendChatMessage("/login " + pwd.value);
            }

            function luiScreenInit()
            {
                addPacketReceiver({plugin:"LUIAuth",callback:"myReceiver"});
            }

            function myReceiver(packet)
            {
                if(packet.type == "close") closeGui();
            }
        </script>
    </body>
</html>

mods/longui/LUIAuth/reg.html

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8">
        <style>
            body {
                font-family: Microsoft YaHei, Microsoft YaHei Light, Microsoft JhengHei, SimHei, SimSun;
            }

            #login {
                background-color: rgba(127,255,0,0.5);
                color: white;
                border: none;
                font-size: 18px;
                width: 300px;
                height: 35px;
                margin-top: 20px;
                transition: all 0.3s;
            }

            #login:hover {
                background-color: rgba(230,255,0,0.5);
            }

            #login:active {
                background-color: rgba(100, 100, 100, 0.5);
                background-image: none;
            }

            .main {
                text-align: center;
                display:-webkit-box;
                -webkit-box-orient: vertical;
                width: 300px;
                margin: 20% auto;
            }

            #password, #password2 {
                width: 295px;
                height: 30px;
                margin-top:20px;
                font-size: 18px;
            }
            
            #warn {
                color: red;
                font-size: 18px;
            }
        </style>
    </head>
    <body>
        <div class="main">
            <input id="password" type="password"></input>
            <input id="password2" type="password"></input>
            <button id="login" onclick="register();">注册</button> 
            <p id="warn"></p>
        </div>
        <script>
            function register()
            {
                var pwd1 = document.getElementById("password");
                var pwd2 = document.getElementById("password2");
                var warn = document.getElementById("warn");
                if(pwd1.value == "")
                    warn.innerText = "请输入密码!";
                else if(pwd1.value == pwd2.value)
                    sendChatMessage("/register " + pwd1.value + " " + pwd2.value);
                else
                    warn.innerText = "两次密码不一致!";
            }
            
            function luiScreenInit()
            {
 addPacketReceiver({plugin:"LUIAuth",callback:"myReceiver"});
            }

            function myReceiver(packet)
            {
                if(packet.type == "close") closeGui();
            }
        </script>
    </body>
</html>

后记

其实除了客户端发指令到服务端来进行通信外,LongUI 还提供了一种直接从客户端往服务器发包的方式,如果感兴趣的话可以自行读文档研究。

LongUI 、 LongUIS 还有 WebCraft 其实都可以算是本人练手的项目,通过这些项目的开发,本人也学到了很多东西,当然,它们还是有很多缺陷的。所以如果你想进行改进,欢迎发 Pull Request ,如果发现 bug ,欢迎发 issue 亦或者是在帖子下方评论。