Skip to content

Commit 299e2cc

Browse files
committed
feat: implement native support for NovaScript with bracket-balanced parser
- Add NovaScript (NovaLang) as new script engine alongside Kether/JS/Jexl - Three entry points: action prefix `nova:`, inline `{nova: expr}`, condition `nova: expr` - Replace FunctionParser regex with hand-written bracket-balanced scanner to support nested braces (lambdas, when blocks, string interpolation) - Add HookNovaScript with trmenu function library (menu ops, data, meta, kether bridge) - Add NovaScriptAgent with compiled script caching and ThreadLocal session bridging - Use JitPack dependency for NovaScript API
1 parent 92767a3 commit 299e2cc

7 files changed

Lines changed: 373 additions & 6 deletions

File tree

plugin/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ taboolib {
3434
name("MeowEco").with("bukkit").optional(true)
3535
name("CraftEngine").with("bukkit").optional(true)
3636
name("SX-Item").with("bukkit").optional(true)
37+
name("NovaScript").with("bukkit").optional(true)
3738
}
3839
}
3940
relocate("trplugins.menu", group.toString())
@@ -99,6 +100,7 @@ dependencies {
99100
compileOnly("com.willfp:EcoItems:5.49.1") { isTransitive = false }
100101
compileOnly("net.momirealms:craft-engine-core:0.0.22") { isTransitive = false }
101102
compileOnly("net.momirealms:craft-engine-bukkit:0.0.22") { isTransitive = false }
103+
compileOnly("com.github.CoderKuo:novascript:v1.0.2") { isTransitive = false }
102104
compileOnly(files("libs/MeowEco-26.7.0.jar"))
103105
compileOnly(fileTree("libs"))
104106
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package trplugins.menu.api.action.impl.script
2+
3+
import taboolib.common.platform.ProxyPlayer
4+
import trplugins.menu.api.action.ActionHandle
5+
import trplugins.menu.api.action.base.ActionBase
6+
import trplugins.menu.api.action.base.ActionContents
7+
import trplugins.menu.module.display.session
8+
import trplugins.menu.module.internal.script.nova.NovaScriptAgent
9+
10+
class NovaScript(handle: ActionHandle) : ActionBase(handle) {
11+
12+
override val regex = "nova(script)?".toRegex()
13+
14+
override fun readContents(contents: Any): ActionContents {
15+
NovaScriptAgent.preCompile(contents.toString())
16+
return super.readContents(contents)
17+
}
18+
19+
override fun onExecute(contents: ActionContents, player: ProxyPlayer, placeholderPlayer: ProxyPlayer) {
20+
NovaScriptAgent.eval(player.session(), contents.stringContent())
21+
}
22+
}

plugin/src/main/kotlin/trplugins/menu/module/internal/hook/HookPlugin.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,8 @@ object HookPlugin {
135135
return get(HookCraftEngine::class.java)
136136
}
137137

138+
fun getNovaScript(): HookNovaScript {
139+
return get(HookNovaScript::class.java)
140+
}
141+
138142
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package trplugins.menu.module.internal.hook.impl
2+
3+
import com.dakuo.novascript.NovaScriptAPI
4+
import com.dakuo.novascript.ScriptHandler0
5+
import com.dakuo.novascript.ScriptHandler1
6+
import com.dakuo.novascript.ScriptHandler2
7+
import com.dakuo.novascript.ScriptHandler3
8+
import org.bukkit.Bukkit
9+
import org.bukkit.entity.Player
10+
import trplugins.menu.api.TrMenuAPI
11+
import trplugins.menu.api.event.MenuOpenEvent
12+
import trplugins.menu.module.display.Menu
13+
import trplugins.menu.module.display.MenuSession
14+
import trplugins.menu.module.internal.data.Metadata
15+
import trplugins.menu.module.internal.hook.HookAbstract
16+
17+
class HookNovaScript : HookAbstract() {
18+
19+
override fun bindingScript() {
20+
super.bindingScript()
21+
val p = plugin ?: return
22+
if (!p.isEnabled) return
23+
24+
NovaScriptAPI.defineLibrary(p.name, "trmenu") { lib ->
25+
// 菜单操作
26+
lib.defineFunction("openMenu", ScriptHandler2 { player, menuId ->
27+
val p = asPlayer(player)
28+
val menu = TrMenuAPI.getMenuById(menuId.toString())
29+
if (p != null && menu != null) {
30+
menu.open(p, reason = MenuOpenEvent.Reason.CONSOLE)
31+
}
32+
null
33+
})
34+
35+
lib.defineFunction("closeMenu", ScriptHandler1 { player ->
36+
val p = asPlayer(player)
37+
if (p != null) {
38+
val session = MenuSession.getSession(p)
39+
session.close(closePacket = true, updateInventory = true)
40+
}
41+
null
42+
})
43+
44+
lib.defineFunction("getMenuById", ScriptHandler1 { menuId ->
45+
TrMenuAPI.getMenuById(menuId.toString())
46+
})
47+
48+
lib.defineFunction("getMenuList", ScriptHandler0 {
49+
Menu.menus.map { it.id }
50+
})
51+
52+
// 玩家持久数据
53+
lib.defineFunction("getData", ScriptHandler2 { player, key ->
54+
val p = asPlayer(player) ?: return@ScriptHandler2 null
55+
Metadata.getData(p)[key.toString()]
56+
})
57+
58+
lib.defineFunction("setData", ScriptHandler3 { player, key, value ->
59+
val p = asPlayer(player) ?: return@ScriptHandler3 null
60+
Metadata.setData(p, Metadata.DataType.DATA, key.toString(), value)
61+
null
62+
})
63+
64+
// 玩家元数据
65+
lib.defineFunction("getMeta", ScriptHandler2 { player, key ->
66+
val p = asPlayer(player) ?: return@ScriptHandler2 null
67+
Metadata.getMeta(p)[key.toString()]
68+
})
69+
70+
lib.defineFunction("setMeta", ScriptHandler3 { player, key, value ->
71+
val p = asPlayer(player) ?: return@ScriptHandler3 null
72+
Metadata.setData(p, Metadata.DataType.META, key.toString(), value)
73+
null
74+
})
75+
76+
// 全局数据
77+
lib.defineFunction("getGlobalData", ScriptHandler1 { key ->
78+
Metadata.getGlobalData(key.toString())
79+
})
80+
81+
lib.defineFunction("setGlobalData", ScriptHandler2 { key, value ->
82+
Metadata.setGlobalData(key.toString(), value)
83+
null
84+
})
85+
86+
// Kether 脚本执行
87+
lib.defineFunction("eval", ScriptHandler2 { player, script ->
88+
val p = asPlayer(player) ?: return@ScriptHandler2 null
89+
TrMenuAPI.instantKether(p, script.toString()).any
90+
})
91+
92+
// 会话信息
93+
lib.defineFunction("isViewingMenu", ScriptHandler1 { player ->
94+
val p = asPlayer(player) ?: return@ScriptHandler1 false
95+
MenuSession.getSession(p).isViewing()
96+
})
97+
98+
lib.defineFunction("getViewingMenuId", ScriptHandler1 { player ->
99+
val p = asPlayer(player) ?: return@ScriptHandler1 null
100+
MenuSession.getSession(p).menu?.id
101+
})
102+
}
103+
}
104+
105+
private fun asPlayer(obj: Any?): Player? {
106+
return when (obj) {
107+
is Player -> obj
108+
is String -> Bukkit.getPlayerExact(obj)
109+
else -> null
110+
}
111+
}
112+
}

plugin/src/main/kotlin/trplugins/menu/module/internal/script/FunctionParser.kt

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import trplugins.menu.module.internal.data.Metadata
88
import trplugins.menu.module.internal.hook.HookPlugin
99
import trplugins.menu.module.internal.script.jexl.JexlAgent
1010
import trplugins.menu.module.internal.script.js.JavaScriptAgent
11+
import trplugins.menu.module.internal.script.nova.NovaScriptAgent
1112
import trplugins.menu.util.Regexs
1213
import trplugins.menu.util.collections.Variables
1314
import trplugins.menu.util.print
@@ -18,7 +19,6 @@ import trplugins.menu.util.print
1819
*/
1920
object FunctionParser {
2021

21-
private val functionPattern = "\\$?\\{(\\w+)s?: ?(.+?[^\\\\}])}".toRegex()
2222
private val internalFunctionPattern = "\\$\\{([^0-9].+?[^\\\\}])}".toRegex()
2323

2424
fun parse(
@@ -30,7 +30,7 @@ object FunctionParser {
3030
if (!Regexs.containsPlaceholder(input)) return input
3131
val session = MenuSession.getSession(player)
3232

33-
val functionParsed = Variables(input, functionPattern) { "${it[1]}:${it[2]}" }.element.joinToString("") {
33+
val functionParsed = parseFunctionExpressions(input).joinToString("") {
3434
if (it.isVariable) {
3535
val split = it.value.split(":", limit = 2)
3636
if (split.size < 2) return@joinToString it.value
@@ -40,6 +40,7 @@ object FunctionParser {
4040
"kether", "ke" -> parseKetherFunction(player, value)
4141
"javascript", "js" -> parseJavaScript(session, value)
4242
"jexl" -> parseJexlScript(session, value)
43+
"nova", "novalang","novascript" -> parseNovaScript(session, value)
4344
"meta", "m" -> Metadata.getMeta(player)[value].toString()
4445
"data", "d" -> Metadata.getData(player)[value].toString()
4546
"globaldata", "gdata", "g" -> Metadata.getGlobalData(value)?.toString() ?: "null"
@@ -61,6 +62,121 @@ object FunctionParser {
6162
}.getOrElse { input }
6263
}
6364

65+
/**
66+
* 使用括号平衡扫描解析 {type: content} 或 ${type: content} 表达式,
67+
* 替代原先的正则匹配,以支持内容中包含嵌套 {} 的脚本语言(如 Lambda、when 块等)。
68+
*/
69+
private fun parseFunctionExpressions(input: String): List<Variables.Element> {
70+
val elements = mutableListOf<Variables.Element>()
71+
var lastEnd = 0
72+
var i = 0
73+
74+
while (i < input.length) {
75+
val matchStart = i
76+
77+
// 检测 ${ 或 {
78+
if (input[i] == '$' && i + 1 < input.length && input[i + 1] == '{') {
79+
i += 2
80+
} else if (input[i] == '{') {
81+
i++
82+
} else {
83+
i++
84+
continue
85+
}
86+
87+
// 匹配类型名: \w+ (字母、数字、下划线)
88+
val typeStart = i
89+
while (i < input.length && (input[i].isLetterOrDigit() || input[i] == '_')) {
90+
i++
91+
}
92+
if (i == typeStart) {
93+
i = matchStart + 1
94+
continue
95+
}
96+
val type = input.substring(typeStart, i)
97+
98+
// 要求 ': ' (冒号 + 可选空格)
99+
if (i >= input.length || input[i] != ':') {
100+
i = matchStart + 1
101+
continue
102+
}
103+
i++ // 跳过 ':'
104+
if (i < input.length && input[i] == ' ') {
105+
i++ // 跳过可选空格
106+
}
107+
108+
val contentStart = i
109+
110+
// 括号平衡扫描
111+
var depth = 1
112+
var found = false
113+
while (i < input.length) {
114+
val ch = input[i]
115+
116+
// 字符串字面量: 跳过直到匹配的引号
117+
if (ch == '"' || ch == '\'') {
118+
val quote = ch
119+
i++
120+
while (i < input.length) {
121+
if (input[i] == '\\' && i + 1 < input.length) {
122+
i += 2
123+
} else if (input[i] == quote) {
124+
i++
125+
break
126+
} else {
127+
i++
128+
}
129+
}
130+
continue
131+
}
132+
133+
// 转义字符: 跳过下一个字符 (兼容旧版 \} 转义)
134+
if (ch == '\\' && i + 1 < input.length) {
135+
i += 2
136+
continue
137+
}
138+
139+
when (ch) {
140+
'{' -> depth++
141+
'}' -> {
142+
depth--
143+
if (depth == 0) {
144+
found = true
145+
break
146+
}
147+
}
148+
}
149+
i++
150+
}
151+
152+
if (!found) {
153+
i = matchStart + 1
154+
continue
155+
}
156+
157+
// 提取内容
158+
val content = input.substring(contentStart, i)
159+
160+
// 添加前置的非变量文本
161+
if (matchStart > lastEnd) {
162+
elements.add(Variables.Element(input.substring(lastEnd, matchStart)))
163+
}
164+
165+
// 添加变量元素 (格式与原 Variables 回调一致: "type:content")
166+
elements.add(Variables.Element("$type:$content", true))
167+
168+
i++ // 跳过闭合 '}'
169+
lastEnd = i
170+
}
171+
172+
// 添加剩余文本
173+
if (lastEnd < input.length) {
174+
elements.add(Variables.Element(input.substring(lastEnd)))
175+
}
176+
177+
return elements
178+
}
179+
64180
private fun parseInternalFunction(session: MenuSession, input: String): String {
65181
val func = input.split("_")
66182

@@ -85,6 +201,10 @@ object FunctionParser {
85201
return JexlAgent.eval(session, input).asString()
86202
}
87203

204+
private fun parseNovaScript(session: MenuSession, input: String): String {
205+
return NovaScriptAgent.eval(session, input).asString()
206+
}
207+
88208
private fun parseLangText(player: Player, text: String): String {
89209
val split = text.split("=", limit = 2)
90210
return HookPlugin.getTriton()

plugin/src/main/kotlin/trplugins/menu/module/internal/script/Script.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import trplugins.menu.api.action.base.ActionEntry
1010
import trplugins.menu.module.display.MenuSession
1111
import trplugins.menu.module.internal.script.jexl.JexlAgent
1212
import trplugins.menu.module.internal.script.js.JavaScriptAgent
13+
import trplugins.menu.module.internal.script.nova.NovaScriptAgent
1314
import trplugins.menu.util.EvalResult
1415

1516
/**
@@ -45,11 +46,14 @@ fun Player.evalScript(script: String?): EvalResult {
4546
return JavaScriptAgent.eval(MenuSession.getSession(this), js!!)
4647
}
4748
val (isJexlScript,jexl) = JexlAgent.serialize(script)
48-
return if (isJexlScript) {
49-
JexlAgent.eval(MenuSession.getSession(this), jexl!!)
50-
} else {
51-
return TrMenuAPI.instantKether(this, script)
49+
if (isJexlScript) {
50+
return JexlAgent.eval(MenuSession.getSession(this), jexl!!)
5251
}
52+
val (isNovaScript, nova) = NovaScriptAgent.serialize(script)
53+
if (isNovaScript) {
54+
return NovaScriptAgent.eval(MenuSession.getSession(this), nova!!)
55+
}
56+
return TrMenuAPI.instantKether(this, script)
5357
}
5458

5559
/*

0 commit comments

Comments
 (0)