跳到主要内容

脚本的可共用部分

脚本的出处

该脚本为Apache Commons JEXL ™

语法和js类似,又能基本使用java的所有方法。

建议先看jexl教程:

1.JEXL – Apache Commons JEXL Syntax

2.JEXL – Apache Commons JEXL Examples

翻译版:

1.JEXL 操作手册

第三方文档

1.古剑传技术枫溪著 - 古剑传别册

脚本的优势

  • 可以随意调用java内的任意API,代码风格和JS基本可以平替降低学习成本。
  • 甚至可以直接调用客户端Forge的API,例如获得玩家所在世界的天气,所在地区的群系,周围怪物的数量来实现想要达到的效果。
  • 经过二次开发拥有更详细的错误提示和语法检测
  • 支持代码补全,开发方便,前后端可完全分离。

脚本的API

数据类型转换

操作代码
转为字符串str(obj)
转为intint(obj)
转为longlong(obj)
转为floatfloat(obj)
转为booleanbool(obj)
转为doubledouble(obj)

可直接静态调用的API

方法介绍方法参数方法案例版本
设置客户端标题Utils.setClientTitle(str)Utils.setClientTitle("haha"+new Date().toString())
获得客户端标题Utils.getClientTitle()var title = Utils.getClientTitle()
获得客户端当前tick次数Utils.getTick()
判断客户端是否打开了萌芽界面Utils.hasOpenGermScreen()
判断客户端是否打开了界面Utils.hasOpenScreen()
获取客户端的HudProxy(和GuiProxy类似)Utils.getHudProxy()Utils.getHudProxy().runDos("message<->555)版本>4.4.2
设置客户端按键激活状态Utils.setKeyBindActive(KeyType,bool)可以模型玩家按下W向前移动等,需要在startTick时调用才有效
在客户端发给玩家一句话Log.chat(str, obj...)Log.chat('今天&#123;&#125;也&#123;&#125;了', '你', '迟到');
获取当前时间戳Utils.now();
设置物品显示冷却效果Utils.setItemCooldown(item,tick);
获取物品冷却var tick = Utils.getItemCooldown(item);
获取玩家当前所在世界时间Utils.getWorldTime()Log.chat('范围&#123;&#125;类型&#123;&#125;', '[0-24000]', 'double');
获取容器内悬浮的组件Utils.getHoverPart(Container)Utils.getHoverPart(GuiScreen) 可能返回null
打印某个实例或者类中的方法Utils.printMethods(obj)Utils.printMethods(GuiProxy)
判断按键是否按下Utils.isKeyDown(KeyType);Utils.isKeyDown(Utils.getKeyType("1"));
判断物品是否是空气Utils.isAir(itemStack);Utils.isAir(slot.getItemStack())
获得鼠标悬浮的物品Utils.getHoverItem() var item = Utils.getHoverItem();
获得物品的loreUtils.getItemLore(itemstack)var list=Utils.getItemLore(Utils.getHoverItem())
获得物品的名字Utils.getItemName(itemstack)var name=Utils.getItemName(Utils.getHoverItem())
设置物品的LoreUtils.setItemLore(list)Utils.setItemLore(Arrays.asList("哈哈哈"))
设置物品的名字Utils.setItemName(str)Utils.setItemName("哈哈哈)
获得物品的NBTUtils.getItemNbt(itemStack,str)var str = Utils.getItemNbt(Utils.getHoverItem(),'PxRpg.data.sell.money');
设置物品的NBTUtils.setItemNbt(itemstack,str,str)Utils.setItemNbt(Utils.getHoverItem(),'PxRpg.display',"你好")
获得一个类,方便调用静态方法Utils.forClass('java.lang.Double')var dclazz = Utils.forClass('java.lang.Double'); dclazz.parseDouble('99.99');
新建一个物品Utils.newItem('type','displayName','lore')Utils.newItem('DIAMOND_SWROD','卍开 天锁斩月',Arrays.asList('第一行','第二行'))
根据Identity获取物品Utils.getIdentityItem(str)首先在服务端调用GermPacketAPI.sendSlotItemStack('chajianming_test',ItemStack)然后脚本内调用Utils.getIdentityItem('chajianming_test')
获得游戏窗口像素宽度Utils.getPixelWidth()游戏窗口的像素宽度,和变量w结果一样
获得游戏窗口像素高度Utils.getPixelHeight()游戏窗口的像素高度,和变量h结果一样
获得游戏窗口高宽Utils.getWindowWidth() Utils.getWindowHeight()游戏窗口真实宽高,此数值一般比wh大
获得鼠标位置XUtils.getMouseX()鼠标位置X
获得鼠标位置YUtils.getMouseY()鼠标位置Y
计算公式Utils.calc(str)Utils.calc('w*20');
获得KeyTypeUtils.getKeyType(str)从按键名获取Utils.getKeyType("A")
获得KeyTypeUtils.getKeyType(int)从ID获取Utils.getKeyType(0)
发送left hud msgUtils.sendLeftMsg(str)Utils.sendLeftMsg('哈哈哈哈');
发送center hud msgUtils.sendCenterMsg(str)Utils.sendCenterMsg('哈哈哈哈');
发送anchor hud msgUtils.sendAnchorMsg(str,str)需要在服务端Config.yml内配置好anchorUtils.sendAnchorMsg('anchor1','哈哈哈哈');
去除颜色Utils.stripColor(str)Utils.stripColor('§a哈哈');
将字符串转为mapUtils.parseMap(str)转换格式为Utils.parseMap('path=textures/gui.png;width=20;height=20')
将json字符串转为mapUtils.jsonToMap(str)var map = Utils.jsonToMap('&#123;"formatVersion":3,"atlasSize":25&#125;')
延迟tick执行方法Utils.runTimeoutTick(JexlContext,jexlMethod,tick,args...)例子 var test = (txt,txt2)=>&#123;Log.chat("text "+txt+" "+txt2);&#125;;Utils.runTimeoutTick(JexlContext,test,10,"我是tickTimeout","哈哈哈");
延迟毫秒执行方法Utils.runTimeout(JexlContext,jexlMethod,tick,args...)点击查看更详细例子 客户端事件监听
异步延迟tick执行方法Utils.runTimeoutTickAsync(JexlContext,jexlMethod,tick,args...)点击查看更详细例子 客户端事件监听
异步延迟毫秒执行方法Utils.runTimeoutAsync(JexlContext,jexlMethod,tick,args...)点击查看更详细例子 客户端事件监听
脚本调度器var scheduler = Utils.getScheduler()点击查看详情 客户端事件监听
删除所有字符替换图标的字符Utils.stripFontReplacer(str)
获取所有导航Utils.getNavigates()返回list
排序listUtils.sort(list,(o1,o2)=>&#123;return o1-02;&#125;)Utils.sort(JexlContext,list,(o1,o2)=>&#123;return o1-02;&#125;)var list=[3,4,2,1];Utils.sort(JexlContext,list,(o1,o2)=>{return o1-02;});
防抖节流工具点击查看 防抖节流工具版本>=4.4.2-8

变量相关的API

方法介绍方法参数方法案例版本
解析字符串中的变量值,如有不存在值的变量就请求服务端PAPI.request(str);var str = PAPI.request('界面是:%germ_gui_open%');
解析字符串中的变量值PAPI.parse(str);var str = PAPI.request('界面是:%germ_gui_open%');
字符串中是否存在变量PAPI.has(str);var bool = PAPI.has('有变量%germ_gui_open%');
放入变量值,一段时间未使用会过期PAPI.put(str, str);PAPI.put('%germ_gui_open%', 'true');
放入变量值,不会过期,除非手动移除变量PAPI.put2(str, str);PAPI.put2('%germ_gui_open%', 'true');
放入变量值到客户端缓存(客户端重启后,也可使用%cache_变量%直接读取到值),不会过期,除非手动移除变量PAPI.put3(str, str);PAPI.put3('%变量%', 'true');
删除变量PAPI.remove(str);PAPI.remove('%germ_gui_open%');
向服务端请求变量的值PAPI.refresh(str)PAPI.refresh("player_name") or PAPI.refresh("%player_name%") 可以用来快速刷新变量的值
异步请求服务端变量并执行回调(单个变量)Utils.reqPapi(JexlContext, str)Utils.reqPapi(JexlContext, "%variable%").then((response)=>&#123;Log.chat("变量值:"+response.value);&#125;);版本>=4.4.2-9
异步请求服务端变量并执行回调(多个变量)Utils.reqPapi(JexlContext, Map)Utils.reqPapi(JexlContext, &#123;"var1":"%variable%","var2":"variable2"&#125;).then((response)=>&#123;Log.chat("响应:"+response);&#125;);版本>=4.4.2-9
异步请求服务端变量并执行回调(指定目标)Utils.reqPapi(JexlContext, Map, target)Utils.reqPapi(JexlContext, &#123;"var1":"%player_name%"&#125;, "target_player_name").then((response)=>&#123;Log.chat("响应:"+response);&#125;);版本>=4.4.2-9

共享数据相关的API,可以用来插件A调用插件B的数据,或者多个界面之间传递数据,或者暂储存一些数据,当玩家退出服务器会清空globalMap(在代理内切换服务器不会清理)

方法介绍方法参数方法案例
客户端公用MapUtils.putGlobal(str,obj)Utils.putGlobal('germpass',map)
Utils.getGlobalMap()
Utils.getGlobal(str)var obj = Utils.getGlobal('germpass')
Utils.removeGlobal(str)
Utils.clearGlobal()
缓存入客户端GermCacheUtils.saveCache(str,obj)Utils.saveCache('str',map)
取出缓存入客户端的内容Utils.getCache(str,class)Utils.getCache('str',Utils.forClass('java.util.Map'))

一些示例

//创建一个Map
var mapsUtils = Utils.forClass('com.google.common.collect.Maps');
var map = mapsUtils.newHashMap();
//创建一个List
var listUtils = Utils.forClass('com.google.common.collect.Lists');
var list = listUtils.newArrayList();
var list2 = new("java.util.ArrayList");

//mcp搜索网站 https://mcp.thiakil.com/#/search
//获得Forge的Minecraft实例
var minecraft = Utils.forClass('net.minecraft.client.Minecraft').func_71410_x();
//获得Forge的客户端玩家实例
var player = minecraft.field_71439_g;

//遍历map
for (let entry of map.entrySet()) {
let key = entry.getKey();
let value = entry.getValue();
}
//遍历list
for(let entry of list){
}

// 参考 GuiScreen.post 的使用方式
GuiScreen.post(JexlContext, 'g_germ/get_money', {
"debug": true,
"money_type": "铜币"
}).then((responseContent) => {
Log.chat(`获取成功 ${responseContent["money"]}`);
});
// 单个变量请求
Utils.reqPapi(JexlContext, "%player_name%").then((response) => {
Log.chat("玩家名称: " + response.value);
}).catch((error) => {
Log.chat("请求失败: " + error.getMessage());
});

// 多个变量请求
Utils.reqPapi(JexlContext, {
"playerName": "%player_name%",
"playerLevel": "%player_level%",
"playerMoney": "%player_money%"
}).then((response) => {
Log.chat("响应数据: " + response);
// response 包含所有请求的变量数据
});

// 指定目标请求
Utils.reqPapi(JexlContext, {
"targetData": "%some_variable%"
}, "target_player_name").then((response) => {
Log.chat("特定目标响应: " + response);
});

注意事项

1.在方法内部不要写return; 用return 0;来替代

var b= tickCount % 4 ;
if(b != 0){
return 0;
}

2.注意保持方法有序,只有在上面声明的方法,在下面才可以调用。 3.不要使用jexl的关键字、操作符、静态变量作为变量名(var size = 666;),如size,str,double,new等,更多请查看上面的jexl文档。 4.如果是直接在yml的脚本,建议使用/*我是注释*/ 双斜杠 //我是注释 可能无效 5.槽位组件只能在服务端创建,在客户端脚本里创建无法点击和显示物品(除非自己完成逻辑,例如使用Utils里面创建物品并设置上)。 6.在vscode中开发时for(let switchLine : switches) vscode会报错,可以写成for(let switchLine of switches)

在GermPlugin/jexl中添加脚本

在GUI中导入jexl脚本

建议

  • 文件以.js结尾可以使用vscode的一些补全比较方便

导入到GUI中使用: 例如有个GUI的组件名为button_click_next,那么可以创建一个ButtonUtils.js文件里面添加一个 方法名为 button_click_next_clickScript的方法 那么点击这个按钮就可以触发这个脚本 。

// ButtonUtils.js文件 放置 GermPlugin/jexl中

//这个方法相当于gui配置中的 options.script.methods
function options_methods() {
Log.chat("6666");
}
//相当于 options.script.openScript
function options_openScript(){
Log.chat("7777");
}

//点击界面内的 button_click_next 的按钮就会执行下面的内容
function button_click_next_clickScript() {
Log.chat("click");
}

function options_keyScript$keydown_D() {
Log.chat("按下D");
}

function options_keyScript$keydown_A() {
Log.chat("按下A");
}
# gui.yml 文件 放置 GermPlugin/gui中
测试脚本:
options:
#注意后面的;
scriptjs: |
@import ButtonUtils.js;
button_click_next:
type: button
defaultPath: textures/misc/logo.png
hoverColor: 0xAAAAAAAA
width: 50
height: 50

在脚本内导入包

图片来自https://pxpmc.com/作者老何 导入包.png

在脚本中创建对象

其中obj为map类型,图片来自https://pxpmc.com/作者老何

创建对象.png

更多使用方法查看下面的开发示例

在GUI内使用脚本

脚本的配置案例

ScriptGui:
options:
#一般用来做脚本处理时的数据,填写json
data: |
{
num:10,
}
#variables字段,例如下方的示例,则可以在该ui内的字段里直接使用 {{a}} {{c.d}} 来获取variable内的值
variables: |
{
a:1,
c:{
d:3
}
}
#比较推荐使用此方案使用脚本 使用js后缀 可利用js文件的代码补全
#更多使用详情请看下方的开发示例
#脚本资源放入服务端GermPlugin/jexl目录下
scriptjs: |
@import main1.js; #省略 GermPlugun/jexl路径
#可在界面中嵌入文本 但是无法使用补全
script:
#嵌入该界面的脚本内容
methods: |
var abc = (x,y)->{
return x+y+10;
}
#循环执行
#每次更新界面都会重新执行一次脚本 默认最多一秒执行60次(最小次数由玩家客户端帧率决定)
updateScript: |
Log.chat('cycle '+updateCount);
#tick执行 一秒20次
tickScript: |
Log.chat('tick '+tickCount);
#脚本初始化的时候执行
openScript: |
Log.chat('open');
#收到服务端下发的数据会触发一次
dataScript: |
Log.chat('data');
#脚本卸载的时候执行
closeScript: |
Log.chat('close');
#滚轮的时候执行
scrollScript: |
Log.chat('滚动了 '+scrollChange);
#滚动之前进行 支持return true;return false; 会阻止滚动框滚动
scrollPreScript: |
Log.chat('滚动了 '+scrollChange);
return true;
#服务端响应脚本 使用流程为 从客户端主动请求服务端 `GermScreen.post('germplugin_test1',{});`
#然后在服务端重写GermGuiScreen方法`void onReceivePost(String path, Map&lt;String, Object&gt; contentMap, Map&lt;String, Object&gt; responseMap)`
#在responseMap写入内容客户端就会响应germplugin_test1
responseScript:
germplugin_test1: |
Log.chat('回应消息 '+responseContent.toString());
Log.chat('客户端data '+GuiOptions.getData().toString());
#按键脚本
keyScript:
keyDown@U: |
Log.chat('按下按键 '+str(keyType));
keyUp@K: |
Log.chat('松开按键 '+str(keyType));
#按键脚本写法2
keyDownScript: |
Log.chat('按下按键 '+str(keyType));
keyUpScript: |
Log.chat('松开按键 '+str(keyType));

脚本触发器

界面内支持的脚本触发

脚本名称脚本触发时机脚本内独有变量
tickScript客户端每次tick时触发一次tickCount:代表当前的tick次数
updateScript客户端每帧渲染时触发一次updateCount:代表update执行的次数
openScript界面打开时触发
dataScript收到服务端下发的数据触发
closeScript界面关闭时触发
resizeScript窗口大小变化时触发(4.4.1+)
scrollScript滚动滚轮时触发scrollChange:代表当次滚动的距离
scrollPreScript滚动之前进行 支持return true;会阻止滚动框滚动scrollChange:代表当次滚动的距离
responseScript收到服务端的数据时触发
keyScript按下指定按键时触发keyType:代表按下的按键

openScript,dataScript都会在打开界面的时候响应一次。 打开界面之后,服务端再调用options.setData(Map) 就只会响应 dataScript了,注意这里setData传入的只能是map

除了脚本内独有变量,调用以上script(界面内的script)还可以使用预设的变量 GuiOptions,GuiScreen,GuiProxy,JexlContext

JexlContext 为一个map,map的key为变量名,val为变量值,可以打印map获得执行改脚本时的所有入参变量

组件内支持的通用脚本触发

脚本名称脚本触发时机脚本独有变量
tickScript客户端每次tick时触发tickCount:代表当前的tick次数
initScript首次加载组件时触发
closeSciprt组件被卸载时触发(被remove界面关闭)

除了脚本内独有变量,调用以上script(组件内的Script)还可以使用预设的变量GuiOptions,GuiScreen,GuiProxy,GuiPart

按钮组件支持的脚本触发

脚本名称脚本触发时机脚本独有变量
clickScript鼠标左键点击触发
clickReleaseScript
rightScript
rightReleaseScript
hoverScript
leaveScript
shiftClickScript
shiftClickReleaseScript
shiftRightScript
shiftRightReleaseScript
middleScript鼠标中间点击触发
shiftMiddleScript按下shift并鼠标点击触发

输入框组件支持的脚本触发

脚本名称脚本触发时机脚本独有变量
enterScript按下回车触发input
inputScript输入字符触发input
focusScript触发聚焦触发focus: bool 聚焦时为true 失去聚焦时为false

复选框组件支持的脚本触发

脚本名称脚本触发时机脚本独有变量
clickScript鼠标左键点击触发
rightScript
hoverScript
leaveScript
checkedScript
cancelledScript

slot组件支持的脚本触发

脚本名称脚本触发时机脚本独有变量
shiftClickScript
clickScript
shiftRightScript
rightScript
shiftMiddleScript
middleScript
hoverScript
leaveScript

调用脚本都可以使用变量ScriptName来获得当前脚本的名字

界面内的API

界面内预设的变量和预设变量内的方法

变量名称变量方法方法介绍
GuiOptionsgetData()获得服务端下发的数据,会以map的形式存在
get...有和服务端Options类中相同的get方法
set...有和服务端Options类中相同set方法
setVariable(key,val)修改或添加变量的值例如修改{{c.d}}的值 GuiOptions.setVariable('c.d','10')
clearVariable()清空所有变量
removeVariable(key)删除一个变量的值 GuiOptions.removeVariable('c.d');
getVariable(key)获得一个变量的值
GuiPartgetCacheMap()获得组件内的缓存Map
get...有和服务端GermGuiPart类中相同的get方法
set...有和服务端GermGuiPart类中相同set方法
getPreset()GuiPart.getPreset("locationX"),获得配置中预设的值(如果直接getLocationX()获得是数值)
坐标操作var number = getLocationX/Y/Z()获得组件的绝对坐标位置
setLocationX/Y/Z(number丨str)设置组件的相对坐标
var number = getRelativeX/Y/Z()获得组件的相对坐标位置
setRelativeX/Y/Z(number丨str)设置组件的相对坐标
var number = getAbsoluteX/Y/Z()获得组件的绝对坐标
setAbsoluteX/Y/Z(number丨str)设置组件的绝对坐标
GuiScreengetCacheMap()获得界面内的缓存Map
...有和服务端GermGuiScreen内相同的方法
getWidth()获得options内设置的width
getHeight()获得options内设置的height
runResponseScript(String scriptPath, Map responseContent)执行指定response脚本
getScreenLevel()返回字符串 PARENT,CHILD
getScreenType()返回字符串 GUI,HUD,EFFECT_GUI
post(path, json)GuiScreen.post('tower/used',{'used':used,'towerId':towerMap.get('towerId')});
close()关闭界面
GuiProxyrunDos(str)立刻执行dos会阻塞程序至dos执行结束
runDos(strlist)
analyzeDos(str)在下一帧执行dos
analyzeDos(strlist)
var guiscreens = getGuiScreens()
var guisceen = getParentGuiScreen()
getGuiScreen("guiname")
init()重新初始化
runResponseScript(String guiName, String scriptPath, Map responseContent)执行指定gui中的指定response脚本

界面内组件的拓展方法(除了字段默认的get和set以外)

组件类型方法方法介绍
labelgetVisibleTexts()获得界面正式渲染的文本(被spilitWidth,应用变量后等后处理过的文本) 返回一个List<String>
labelsetText(str)设置字符串支持\n换行

可调用的静态方法

方法介绍方法参数方法案例
生成组件-根据模版GuiHelper.newPart(configSrc,indexName,guiContainer)GuiHelper.newPart(towerTemplate.getSrc(),'tower'+tower.get('towerId'),towersContanier);
生成组件重载方法-根据类型新建一个组件GuiHelper.newPart(type,indexName,guiContainer)GuiHelper.newPart('canvas','tower'+tower.get('towerId'),towersContanier);
生成组件重载方法2-根据模板和变量GuiHelper.newPart(type,indexName,guiContainer,map<string,string>)GuiHelper.newPart('canvas','tower'+tower.get('towerId'),towersContanier,{'{towerName}':'无敌','{towerLevel}':'100');

配置示例1

以下为利用脚本实现的功能性界面 >>>自定义玩家状态HUD

开发示例1

以下为可开源的部分界面代码,以嵌入的方式实现了,根据options中的数据生成界面,在点击按钮时操作界面等操作。

bandicam_2023-09-15_11-22-30-052.gif

界面实现代码

以直接在界面配置中嵌入脚本的方式实现了,根据options中的数据生成界面,在点击按钮时操作界面等操作。

开发示例2

bandicam_2023-09-13_17-21-50-764.gif

界面实现代码

以.js文件的形式使用客户端脚本的形式,实现了上面的界面逻辑

简述GUI开发流程

简述GUI开发流程

简述一下开发GUI时,如何无感使用脚本和服务端API实现前后端分离。