XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEServerManager

【Java】我的世界XFE服务器管理器

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEServerManager

完善变量渲染与接口文档

0f4f762
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

7 个文件 +141 -10
Modified common/core/src/main/java/com/xfestudio/xfeservermanager/core/trigger/TriggerEvaluator.java +15 -4
@@ -48,7 +48,7 @@ public final class TriggerEvaluator {
48 48 // a match; authors can explicitly use not_exists when absence is intended.
49 49 if (actual == null) return operator.equals("neq") || operator.equals("not_contains")
50 50 || operator.equals("not_in");
51 String text = Objects.toString(actual, "");
51 String text = conditionText(actual);
52 52 return switch (operator) {
53 53 case "eq" -> scalarEquals(actual, expected);
54 54 case "neq" -> !scalarEquals(actual, expected);
@@ -93,7 +93,14 @@ public final class TriggerEvaluator {
93 93 }
94 94 catch (NumberFormatException ignored) { return false; }
95 95 }
96 return Objects.toString(actual, "").equalsIgnoreCase(expected);
96 return conditionText(actual).equalsIgnoreCase(expected);
97 }
98
99 private static String conditionText(Object actual) {
100 if (actual instanceof Collection<?> || actual instanceof Map<?, ?>) {
101 return TriggerValueTypes.renderVariable(actual);
102 }
103 return Objects.toString(actual, "");
97 104 }
98 105
99 106 private static boolean compareNumber(String actual, String expected, String operator) {
@@ -330,10 +337,14 @@ public final class TriggerEvaluator {
330 337 value = formattedTime(path, format, safeContext);
331 338 }
332 339 boolean legacyVariable = !path.equals(variable) || variable.equals("date");
333 if ((value == null || value instanceof Map<?, ?>) && !legacyVariable) {
340 boolean stateVariable = path.startsWith("var.") || path.startsWith("global.");
341 if ((value == null || value instanceof Map<?, ?> && !stateVariable) && !legacyVariable) {
334 342 matcher.appendReplacement(rendered, Matcher.quoteReplacement(matcher.group()));
335 343 } else {
336 matcher.appendReplacement(rendered, Matcher.quoteReplacement(Objects.toString(value, "")));
344 String replacement = stateVariable
345 ? TriggerValueTypes.renderVariable(value)
346 : Objects.toString(value, "");
347 matcher.appendReplacement(rendered, Matcher.quoteReplacement(replacement));
337 348 }
338 349 }
339 350 matcher.appendTail(rendered);
Modified common/core/src/main/java/com/xfestudio/xfeservermanager/core/trigger/TriggerValueTypes.java +61 -0
@@ -140,6 +140,67 @@ public final class TriggerValueTypes {
140 140 };
141 141 }
142 142
143 /** Serializes container variables as compact JSON while leaving scalar values human-readable. */
144 public static String renderVariable(Object value) {
145 if (!(value instanceof List<?>) && !(value instanceof Map<?, ?>)) {
146 return String.valueOf(value);
147 }
148 StringBuilder result = new StringBuilder();
149 appendJson(result, value);
150 return result.toString();
151 }
152
153 private static void appendJson(StringBuilder result, Object value) {
154 if (value == null) {
155 result.append("null");
156 } else if (value instanceof String text) {
157 appendJsonString(result, text);
158 } else if (value instanceof Boolean || value instanceof Number) {
159 result.append(value);
160 } else if (value instanceof List<?> values) {
161 result.append('[');
162 for (int index = 0; index < values.size(); index++) {
163 if (index > 0) result.append(',');
164 appendJson(result, values.get(index));
165 }
166 result.append(']');
167 } else if (value instanceof Map<?, ?> values) {
168 result.append('{');
169 boolean first = true;
170 for (Map.Entry<?, ?> entry : values.entrySet()) {
171 if (!first) result.append(',');
172 first = false;
173 appendJsonString(result, String.valueOf(entry.getKey()));
174 result.append(':');
175 appendJson(result, entry.getValue());
176 }
177 result.append('}');
178 } else {
179 appendJsonString(result, String.valueOf(value));
180 }
181 }
182
183 private static void appendJsonString(StringBuilder result, String value) {
184 result.append('"');
185 for (int index = 0; index < value.length(); index++) {
186 char current = value.charAt(index);
187 switch (current) {
188 case '"' -> result.append("\\\"");
189 case '\\' -> result.append("\\\\");
190 case '\b' -> result.append("\\b");
191 case '\f' -> result.append("\\f");
192 case '\n' -> result.append("\\n");
193 case '\r' -> result.append("\\r");
194 case '\t' -> result.append("\\t");
195 default -> {
196 if (current < 0x20) result.append(String.format("\\u%04x", (int) current));
197 else result.append(current);
198 }
199 }
200 }
201 result.append('"');
202 }
203
143 204 private static Object coerce(VariableType type, Object value) {
144 205 if (value == null) throw new IllegalArgumentException("null is not valid for " + type.canonical());
145 206 if (type.name().equals("array")) {
Modified common/testkit/src/test/java/com/xfestudio/xfeservermanager/core/trigger/TriggerEngineTest.java +12 -0
@@ -59,6 +59,18 @@ class TriggerEngineTest {
59 59 TriggerEvaluator.render("unknown {custom.missing}", context));
60 60 }
61 61
62 @Test
63 void rendersAndComparesNestedStateVariablesAsCompactJson() {
64 Map<String, Object> context = Map.of(
65 "var.names", List.of("甲", "line\nbreak"),
66 "global.rewards", Map.of("coins", List.of(1L, 2L)));
67
68 assertEquals("[\"甲\",\"line\\nbreak\"] / {\"coins\":[1,2]}",
69 TriggerEvaluator.render("{var.names} / {global.rewards}", context));
70 assertTrue(conditionMatches("global.rewards", "contains", "\"coins\":[1,2]", context));
71 assertTrue(conditionMatches("var.names", "eq", "[\"甲\",\"line\\nbreak\"]", context));
72 }
73
62 74 @Test
63 75 void validatesSchedulesAndRejectsUnknownActions() {
64 76 assertEquals("08:30", TriggerScriptCompiler.compile("""
Modified common/testkit/src/test/java/com/xfestudio/xfeservermanager/core/trigger/TriggerValueTypesTest.java +6 -0
@@ -39,6 +39,12 @@ class TriggerValueTypesTest {
39 39 "dictionary<integer,string>", "{\"not-an-integer\":\"value\"}"));
40 40 assertThrows(IllegalArgumentException.class, () -> TriggerValueTypes.parseVariable(
41 41 "dictionary<string,string>", "{\"key\":\"one\",\"key\":\"two\"}"));
42
43 Map<String, Object> rendered = new java.util.LinkedHashMap<>();
44 rendered.put("scores", List.of(1L, 2L));
45 rendered.put("name", "a\nb");
46 assertEquals("{\"scores\":[1,2],\"name\":\"a\\nb\"}",
47 TriggerValueTypes.renderVariable(rendered));
42 48 }
43 49
44 50 @Test
Modified docs/implementation-status.md +2 -2
@@ -11,8 +11,8 @@ This document describes the current source boundary, not a release promise. `Imp
11 11 | Web authentication / Web 认证 | First-owner bootstrap, Argon2id, TOTP/recovery codes, session revocation, rate limits, Origin/CSRF, recent reauthentication, and owner-gated multi-account RBAC UI/API / 首位 owner、二次认证、会话与多账户 RBAC UI/API 已实现 | Deployment penetration testing and durable idempotency across process restarts / 部署渗透测试及跨进程重启的持久幂等 |
12 12 | Status and online players / 状态与在线玩家 | Live status DTO/SSE, player detail/actions, destructive preview token, inventory and ender-chest revision CAS, and Web workflows / 状态、玩家操作、预览确认及背包界面已实现 | Four-version gameplay/disconnect races; component-bearing slots are redacted and fail closed except deletion / 四版本实机与掉线竞态;含组件槽位除删除外故障关闭 |
13 13 | Player join experience / 玩家入服体验 | Legacy welcome, bulletin, rules and maintenance notices migrate idempotently into ordinary triggers; timezone and per-player/day delivery state remain durable / 旧欢迎、每日公告、规则及维护提醒会幂等迁移为普通触发器,时区与按玩家每日投递状态持久化 | Dedicated-server rendering, localization and reconnect-storm acceptance / 专服显示、本地化与重连风暴验收 |
14 | Trigger automation / 触发器自动化 | Durable bounded trigger groups, recoverable leased daily/interval schedules, broad Forge player/item/block/entity/world/chunk events, ALL/ANY comparisons, ordered actions, visual authoring, XFE Script, optimistic revisions, bounded execution, audit admission, rich-message validation, and idempotent migration of legacy messages / 已实现有上限的持久触发器组、可恢复租约定时任务、广泛 Forge 事件、条件与顺序动作、可视化和脚本双编辑、版本控制、有界执行、审计准入、富文本校验及旧消息迁移 | Dedicated-server event/action matrix on all four targets; third-party events beyond stable Forge hooks depend on custom adapters / 四版本专服事件动作矩阵;稳定 Forge 钩子之外的第三方事件依赖自定义适配器 |
15 | Multi-currency economy / 多货币经济 | Exact fixed-point balances, configurable currency icons/precision/bounds, one primary currency, atomic CAS adjustments/transfers, immutable ledger, Web UI, player commands, SSE, RBAC, audit admission, trigger events/actions/variables and menu-variable/image linkage / 已接通精确定点余额、图标/精度/边界、主货币、原子 CAS 调整与转账、不可变流水、Web、玩家指令、SSE、权限、审计、触发器与菜单联动 | Dedicated-server concurrency/load tests, UX acceptance and third-party economy compatibility adapters / 仍需专服并发压力、交互验收及第三方经济模组兼容适配 |
14 | Trigger automation / 触发器自动化 | Durable bounded trigger groups, recoverable leased daily/interval schedules, broad Forge player/item/block/entity/world/chunk events, ALL/ANY comparisons, ordered actions, visual authoring, XFE Script, scoped trigger/global variables with nested typed containers, module copy/paste, optimistic revisions, bounded execution, audit admission, rich-message validation, and idempotent migration of legacy messages / 已实现有上限的持久触发器组、可恢复租约定时任务、广泛 Forge 事件、条件与顺序动作、可视化和脚本双编辑、带嵌套强类型容器的触发器/全局作用域变量、模块复制粘贴、版本控制、有界执行、审计准入、富文本校验及旧消息迁移 | Dedicated-server event/action matrix on all four targets; third-party events beyond stable Forge hooks depend on custom adapters / 四版本专服事件动作矩阵;稳定 Forge 钩子之外的第三方事件依赖自定义适配器 |
15 | Multi-currency economy / 多货币经济 | Exact fixed-point balances, configurable currency icons/precision/bounds, one primary currency, atomic CAS adjustments/transfers, immutable ledger, Web UI, player commands, OP-only balance administration commands, SSE, RBAC, audit admission, trigger events/actions/variables and menu-variable/image linkage / 已接通精确定点余额、图标/精度/边界、主货币、原子 CAS 调整与转账、不可变流水、Web、玩家指令、仅 OP 可用的余额管理指令、SSE、权限、审计、触发器与菜单联动 | Dedicated-server concurrency/load tests, UX acceptance and third-party economy compatibility adapters / 仍需专服并发压力、交互验收及第三方经济模组兼容适配 |
16 16 | Audit and operations / 审计与操作 | Redaction, bounded SQLite queues, operation registry, SSE progress, checkpoint and gap accounting / 已实现 | Soak/load tests, crash-boundary validation, and explicit verification of the queue-admission durability boundary / 长稳压力、崩溃边界及队列准入持久性边界验证 |
17 17 | Metrics, webhook and soft probes / 指标、Webhook 与软检测 | Token-protected OpenMetrics formatter/endpoint, HMAC webhook sink, Spark/BlueMap presence probes / 部件已实现 | Outbound webhook configuration wiring and third-party API integrations / 出站 Webhook 配置接线及第三方 API 集成 |
18 18 | Graceful maintenance shutdown / 优雅维护关服 | Creation-time target notice, five-minute final-hour reminders, per-second final-minute countdown, `save-all flush`, SQLite checkpoint and vanilla stop are wired / 已接通创建提醒、末小时五分钟提醒、末分钟逐秒倒计时、保存、检查点与原版停服 | Dedicated-server shutdown ordering and failure-injection acceptance / 专服关停顺序与故障注入验收 |
Modified docs/openapi.yaml +40 -0
@@ -2808,6 +2808,46 @@ components:
2808 2808 maxProperties: 16
2809 2809 description: schedule.daily uses time and timezone. schedule.interval accepts legacy total seconds or hours/minutes/seconds components, normalizes them to total seconds (1..31536000), and aligns execution to server-local wall-clock boundaries. player.command_trigger uses command plus an optional whitespace-separated arguments declaration (up to 16 required word arguments).
2810 2810 additionalProperties: {type: string, maxLength: 4096}
2811 arguments:
2812 type: array
2813 maxItems: 16
2814 description: Recursive Brigadier alternatives for player.command_trigger. Siblings are alternatives and children form the next input level.
2815 items: {$ref: "#/components/schemas/TriggerCommandArgument"}
2816 variables:
2817 type: array
2818 maxItems: 32
2819 description: Strongly typed trigger-local or server-global state declarations. Missing visibility/storage fields migrate as trigger-local trigger storage.
2820 items: {$ref: "#/components/schemas/TriggerStateVariable"}
2821 TriggerCommandArgument:
2822 type: object
2823 required: [name, type, literal, optional, errorMessage, minimum, maximum, suggestions, children]
2824 properties:
2825 name: {type: string, pattern: '^[a-z][a-z0-9_-]{0,31}$'}
2826 type: {type: string, description: A supported vanilla Brigadier argument family or literal.}
2827 literal: {type: string, maxLength: 128}
2828 optional: {type: boolean}
2829 errorMessage: {type: string, maxLength: 256}
2830 minimum: {type: string}
2831 maximum: {type: string}
2832 suggestions: {type: array, maxItems: 64, items: {type: string, minLength: 1, maxLength: 128}}
2833 children: {type: array, maxItems: 16, items: {$ref: "#/components/schemas/TriggerCommandArgument"}}
2834 TriggerStateVariable:
2835 type: object
2836 required: [name, type, initialValue]
2837 properties:
2838 name: {type: string, pattern: '^[A-Za-z_][A-Za-z0-9_-]{0,47}$'}
2839 type:
2840 type: string
2841 description: Scalar type, legacy list, or recursively nested array<T>/dictionary<K,V>; dictionary keys must be scalar and generic depth is bounded.
2842 initialValue:
2843 type: string
2844 maxLength: 65536
2845 description: Scalars use their text form; arrays and dictionaries use strictly typed JSON.
2846 visibility: {type: string, enum: [trigger, global], default: trigger}
2847 storage:
2848 type: string
2849 enum: [server, player, dimension, trigger]
2850 description: Selects one shared value, a value per triggering player, per dimension, or per executing trigger.
2811 2851 TriggerCondition:
2812 2852 type: object
2813 2853 required: [field, operator, value]
Modified web-ui/src/pages/triggers.tsx +5 -4
@@ -1986,7 +1986,7 @@ function actionIsValid(action: TriggerAction, catalog: TriggerCatalog): boolean
1986 1986 return templateOr(action.parameters.level ?? 'info', (entry) =>
1987 1987 ['debug', 'info', 'warn', 'error'].includes(entry.trim().toLowerCase()));
1988 1988 case 'variable':
1989 return triggerVariableNameIsValid(action.parameters.name ?? '')
1989 return /^(?:global\.)?[A-Za-z_][A-Za-z0-9_-]{0,47}$/.test((action.parameters.name ?? '').trim())
1990 1990 && templateOr(action.parameters.operation ?? 'set', (entry) => variableOperations.includes(entry.trim().toLowerCase()));
1991 1991 case 'wait': {
1992 1992 const mode = (action.parameters.mode ?? 'duration').trim().toLowerCase();
@@ -2057,8 +2057,9 @@ function uuidIsValid(value: string): boolean {
2057 2057
2058 2058 function triggerVariableInitialValueIsValid(variable: TriggerStateVariableDefinition): boolean {
2059 2059 const value = variable.initialValue;
2060 if (value.length > 8_192) return false;
2061 if (variable.type.startsWith('array<') || variable.type.startsWith('dictionary<')) {
2060 const structured = variable.type.startsWith('array<') || variable.type.startsWith('dictionary<');
2061 if (value.length > (structured ? 65_536 : 8_192)) return false;
2062 if (structured) {
2062 2063 try { return structuredVariableValueIsValid(parseVariableTypeNode(variable.type), JSON.parse(value), 0); }
2063 2064 catch { return false; }
2064 2065 }
@@ -2289,7 +2290,7 @@ function toScript(value: TriggerDefinition): string {
2289 2290 const lines = [`# XFE Script v2`, `on ${value.event.type}`];
2290 2291 Object.entries(value.event.configuration).forEach(([key, entry]) => lines.push(`set ${key}=${quote(entry)}`));
2291 2292 (value.event.variables ?? []).forEach((variable) => lines.push(
2292 `var ${variable.name} ${variable.type} initial=${quote(variable.initialValue)} visibility=${quote(variable.visibility ?? 'trigger')} storage=${quote(variable.storage ?? 'trigger')}`));
2293 `var ${variable.name} ${variable.type} initial=${quote(variable.initialValue)} visibility=${quote(variable.visibility ?? 'trigger')} storage=${quote(variable.storage ?? ((variable.visibility ?? 'trigger') === 'global' ? 'server' : 'trigger'))}`));
2293 2294 const appendArguments = (argumentsList: TriggerCommandArgument[], parent = '') => argumentsList.forEach((argument) => {
2294 2295 const fields = [parent && `parent=${quote(parent)}`, argument.literal && `literal=${quote(argument.literal)}`,
2295 2296 argument.optional && 'optional=true', argument.errorMessage && `error=${quote(argument.errorMessage)}`,