完善客户端功能并同步差距文档

This commit is contained in:
shenlei
2026-09-03 18:40:01 +09:00
parent f93a172296
commit 13ccb8d02d
135 changed files with 21881 additions and 1259 deletions
+678 -34
View File
@@ -17,19 +17,37 @@
# quest_confirm_askGC_QUEST_CONFIRM)也在这里弹 是/否 -> quest_confirm(yes, pid)。
extends Node
const UiAssets = preload("res://ui/ui_assets.gd")
signal opened()
signal closed()
# EventManager EVENT_TYPE_SELECT_ITEM —— 对应 interfacemodule.BINARY_OpenSelectItemWindow
signal select_item_requested()
# EventManager side effects that are owned by the interface layer in ClientVS22.
# Keeping them as signals makes the dialog usable with both the real client and
# headless fixtures without smuggling minimap/camera state into the parser.
signal quest_button_received(index: int, title: String, icon_type: String, icon_name: String)
signal quest_button_cleared(index: int)
signal map_signal_added(x: float, y: float)
signal map_signals_cleared()
signal atlas_center_requested(x: int, y: int)
signal camera_event_requested(kind: String, values: Array)
signal fade_event_requested(kind: String, speed: float)
var client: Node
var proto: Node
var _assets_root := ""
var _root: Control
var _text: RichTextLabel
var _btnrow: VBoxContainer
var _image_layer: Control
var _confirm_pid := 0
var _confirm_wait := false
func setup(m2client: Node, parent: Node) -> void:
func setup(m2client: Node, parent: Node, proto_node: Node = null, assets_root := "") -> void:
client = m2client
proto = proto_node
_assets_root = assets_root if assets_root != "" else AssetRoot.path()
set_process_unhandled_input(true)
_build(parent)
if client.has_signal("script_dialog"):
@@ -45,14 +63,45 @@ func is_open() -> bool:
func _on_script(_skin: int, text: String) -> void:
var parsed := parse_script(text)
if parsed.body.strip_edges() == "" and parsed.choices.is_empty():
_emit_side_effects(parsed)
close()
return
_text.text = parsed.body
_fill_buttons(parsed.choices, parsed.has_next, parsed.has_input)
_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER if parsed.text_centered else HORIZONTAL_ALIGNMENT_LEFT
if parsed.window_size.size() >= 2:
var width := maxi(320, int(parsed.window_size[0]))
var height := maxi(220, int(parsed.window_size[1]))
_root.size = Vector2(width, height)
_text.size = Vector2(width - 32, height - 100)
_btnrow.position = Vector2(16, height - 78)
_btnrow.custom_minimum_size = Vector2(width - 32, 0)
_confirm_wait = parsed.confirm_wait
_render_images(parsed.images, parsed.title_image)
_fill_buttons(parsed.choices, parsed.has_next, parsed.has_input, _confirm_wait)
_root.visible = true
opened.emit()
_emit_side_effects(parsed)
func _emit_side_effects(parsed: Dictionary) -> void:
if parsed.has_select_item:
select_item_requested.emit()
for quest in parsed.quest_buttons:
quest_button_received.emit(int(quest.get("index", -1)), String(quest.get("title", "")),
String(quest.get("icon_type", "")), String(quest.get("icon_name", "")))
for index in parsed.quest_button_closes:
quest_button_cleared.emit(int(index))
for point in parsed.map_signals:
map_signal_added.emit(float(point.x), float(point.y))
if parsed.clear_map_signals:
map_signals_cleared.emit()
if parsed.atlas_center.size() >= 2:
atlas_center_requested.emit(int(parsed.atlas_center[0]), int(parsed.atlas_center[1]))
for camera in parsed.camera_events:
camera_event_requested.emit(String(camera.get("kind", "")), camera.get("values", []))
for fade in parsed.fade_events:
fade_event_requested.emit(String(fade.get("kind", "")), float(fade.get("speed", 0.0)))
if String(parsed.get("cinema_file", "")) != "":
cinema_requested.emit(String(parsed.cinema_file))
func _on_confirm(msg: String, _timeout: int, request_pid: int) -> void:
_confirm_pid = request_pid
@@ -64,7 +113,8 @@ func _on_confirm(msg: String, _timeout: int, request_pid: int) -> void:
func close() -> void:
if _root:
_root.visible = false
closed.emit()
_confirm_wait = false
closed.emit()
func _unhandled_input(event: InputEvent) -> void:
if not is_open() or not (event is InputEventKey):
@@ -78,15 +128,224 @@ func _unhandled_input(event: InputEvent) -> void:
close()
get_viewport().set_input_as_handled()
# --- 脚本解析 --------------------------------------------------------
# --- 脚本解析CPythonEventManager 1:1----------------------------------
#
# 参照 REF/UserInterface/PythonEventManager.{h,cpp}
# · EVENT_TYPE_MAP —— EventTypeMapPythonEventManager.cpp:1148 起 42 条)
# · _get_script_event_index —— GetScriptEventIndex:1093,前导 '/' = EVENT_POSITION_END
# · EventSet —— SEventSetPythonEventManager.h:98,字段名保持一致)
# · _process_event_set —— ProcessEventSet:377 的 switch 逐 case 对拷)
# · EventSet.insert_line —— __InsertLine:976
# · EventSet.clear_line —— ClearLine:953
# · _make_question —— MakeQuestion:884
# · _make_next_button 效果 —— MakeNextButton:878,本 POC 只置 has_next / iAdjustLine
#
# 网络 GC_SCRIPT 里的裸文本被折成合成 LETTERis_raw=true,跳过 iRestrictedCharacterCount
# 的 30 字断行——reference 脚本只有显式 [LETTER],裸文本是本客户端的扩展,逐字符断句会
# 破坏服务器写好的句子)。
#
# seam(无逐帧 Update 循环):isLock/isWaitFlag/lLastDelayTime/lWaitingTime 作为 EventSet
# 状态保留并透出,但一次性把剩余 token 跑完(保证按钮 / 选项 / 副作用仍然落地);Skip /
# EndEventProcess / iConfirmEndTime 的 timeGetTime 走 seam。
# -> { body: String, choices: [String], has_next: bool }
func parse_script(src: String) -> Dictionary:
var body := ""
var choices := []
signal cinema_requested(filename: String)
enum {
EVT_LETTER, EVT_COLOR, EVT_DELAY, EVT_ENTER, EVT_WAIT, EVT_CLEAR, EVT_QUESTION,
EVT_NEXT, EVT_DONE,
EVT_LEFT_IMAGE, EVT_TOP_IMAGE, EVT_BACKGROUND_IMAGE, EVT_IMAGE,
EVT_ADD_MAP_SIGNAL, EVT_CLEAR_MAP_SIGNAL, EVT_SET_MESSAGE_POSITION,
EVT_ADJUST_MESSAGE_POSITION, EVT_SET_CENTER_MAP_POSITION,
EVT_QUEST_BUTTON, EVT_QUEST_BUTTON_CLOSE,
EVT_SLEEP, EVT_SET_CAMERA, EVT_BLEND_CAMERA, EVT_RESTORE_CAMERA,
EVT_FADE_OUT, EVT_FADE_IN, EVT_WHITE_OUT, EVT_WHITE_IN,
EVT_CLEAR_TEXT, EVT_TEXT_HORIZONTAL_ALIGN_CENTER, EVT_TITLE_IMAGE,
EVT_RUN_CINEMA, EVT_DUNGEON_RESULT,
EVT_ITEM_NAME, EVT_MONSTER_NAME,
EVT_COLOR256, EVT_WINDOW_SIZE,
EVT_INPUT, EVT_CONFIRM_WAIT, EVT_END_CONFIRM_WAIT,
EVT_INSERT_IMAGE, EVT_SELECT_ITEM,
}
# EventTypeMapPythonEventManager.cpp:1148-1207)。键即脚本 tag,前导 '/' 由
# _get_script_event_index 剥离后再查表。
const EVENT_TYPE_MAP := {
"LETTER": EVT_LETTER, "COLOR": EVT_COLOR, "DELAY": EVT_DELAY, "ENTER": EVT_ENTER,
"WAIT": EVT_WAIT, "CLEAR": EVT_CLEAR, "QUESTION": EVT_QUESTION, "NEXT": EVT_NEXT, "DONE": EVT_DONE,
"LEFTIMAGE": EVT_LEFT_IMAGE, "TOPIMAGE": EVT_TOP_IMAGE, "BGIMAGE": EVT_BACKGROUND_IMAGE, "IMAGE": EVT_IMAGE,
"ADDMAPSIGNAL": EVT_ADD_MAP_SIGNAL, "CLEARMAPSIGNAL": EVT_CLEAR_MAP_SIGNAL,
"SETMSGPOS": EVT_SET_MESSAGE_POSITION, "ADJMSGPOS": EVT_ADJUST_MESSAGE_POSITION,
"SETCMAPPOS": EVT_SET_CENTER_MAP_POSITION,
"QUESTBUTTON": EVT_QUEST_BUTTON, "QUESTBUTTON_CLOSE": EVT_QUEST_BUTTON_CLOSE,
"SLEEP": EVT_SLEEP, "SET_CAMERA": EVT_SET_CAMERA, "BLEND_CAMERA": EVT_BLEND_CAMERA,
"RESTORE_CAMERA": EVT_RESTORE_CAMERA, "FADE_OUT": EVT_FADE_OUT, "FADE_IN": EVT_FADE_IN,
"WHITE_OUT": EVT_WHITE_OUT, "WHITE_IN": EVT_WHITE_IN, "CLEAR_TEXT": EVT_CLEAR_TEXT,
"TEXT_HORIZONTAL_ALIGN_CENTER": EVT_TEXT_HORIZONTAL_ALIGN_CENTER, "TITLE_IMAGE": EVT_TITLE_IMAGE,
"RUN_CINEMA": EVT_RUN_CINEMA, "DUNGEON_RESULT": EVT_DUNGEON_RESULT,
"ITEM": EVT_ITEM_NAME, "MOB": EVT_MONSTER_NAME,
"COLOR256": EVT_COLOR256, "WINDOW_SIZE": EVT_WINDOW_SIZE,
"INPUT": EVT_INPUT, "CONFIRM_WAIT": EVT_CONFIRM_WAIT, "END_CONFIRM_WAIT": EVT_END_CONFIRM_WAIT,
"INSERT_IMAGE": EVT_INSERT_IMAGE, "SELECT_ITEM": EVT_SELECT_ITEM,
}
const EVENT_POSITION_START := 0
const EVENT_POSITION_END := 1
const C_NORMAL_WAITING_TIME := 10 # c_lNormal_Waiting_Time
const C_LINE_TEMP := 16 # c_fLine_Temp
const BOX_VISIBLE_LINE_COUNT := 5
# SEventSetPythonEventManager.h:98)—— 字段名与 C++ 保持一致,另加本 POC 的
# 行累加 / 副作用收集容器(顶替 CGraphicTextInstance 链表与 Python handler 回调)。
class EventSet:
extends RefCounted
var ix := 0
var iy := 0
var iWidth := 0
var iyLocal := 0
var isLock := false
var lLastDelayTime := 0
var iCurrentLetter := 0
var CurrentColor := Color(1, 1, 1, 1)
var strCurrentLine := ""
var isConfirmWait := false
var iConfirmEndTime := 0
var DiffuseColor := Color(1, 1, 1, 1)
var lWaitingTime := C_NORMAL_WAITING_TIME
var iRestrictedCharacterCount := 30
var iVisibleStartLine := 0
var iVisibleLineCount := BOX_VISIBLE_LINE_COUNT
var iAdjustLine := 0
var isTextCenterMode := false
var isWaitFlag := false
var nAnswer := 0
# --- POC 累加 ---
var lines: Array[String] = []
var color_open := false
var end_confirm := false
var choices: Array[String] = []
var has_next := false
var has_input := false
var has_select_item := false
var confirm_timeout := 0
var window_size: Array[int] = []
var images: Array[Dictionary] = []
var insert_images: Array[Dictionary] = []
var title_image := ""
var quest_buttons: Array[Dictionary] = []
var quest_button_closes: Array[int] = []
var map_signals: Array[Vector2] = []
var clear_map_signals := false
var atlas_center: Array[int] = []
var camera_events: Array[Dictionary] = []
var fade_events: Array[Dictionary] = []
var dungeon_result: Dictionary = {}
var cinema_file := ""
func init_event_set() -> void: # __InitEventSetPythonEventManager.cpp:72
ix = 0; iy = 0; iWidth = 0; iyLocal = 0
isLock = false
lLastDelayTime = 0
iCurrentLetter = 0
CurrentColor = Color(1, 1, 1, 1)
strCurrentLine = ""
isConfirmWait = false
iConfirmEndTime = 0
DiffuseColor = Color(1, 1, 1, 1)
lWaitingTime = C_NORMAL_WAITING_TIME
iRestrictedCharacterCount = 30
iVisibleStartLine = 0
iVisibleLineCount = BOX_VISIBLE_LINE_COUNT
iAdjustLine = 0
isTextCenterMode = false
isWaitFlag = false
# __InsertLineinit 时 pCurrentTextLine==NULL,不推行,只备一条空当前行。
func insert_line(_center := false) -> void: # __InsertLine:976
lines.append(strCurrentLine)
iyLocal += C_LINE_TEMP # __AddSpace(c_fLine_Temp)
strCurrentLine = ""
iCurrentLetter = 0
func clear_line() -> void: # ClearLine:953
lines.clear()
strCurrentLine = ""
iCurrentLetter = 0
color_open = false
func body_text() -> String:
var parts := lines.duplicate()
parts.append(strCurrentLine)
var s := "\n".join(parts)
if color_open:
s += "[/color]"
return s.strip_edges()
# GetScriptEventIndexPythonEventManager.cpp:1093)—— 前导 '/' = EVENT_POSITION_END。
# -> { ok:bool, position:int, type:int }
func _get_script_event_index(raw_name: String) -> Dictionary:
var position := EVENT_POSITION_START
var name := raw_name
if name.begins_with("/"):
position = EVENT_POSITION_END
name = name.substr(1)
name = name.to_upper()
if not EVENT_TYPE_MAP.has(name):
return {"ok": false, "position": position, "type": -1}
return {"ok": true, "position": position, "type": int(EVENT_TYPE_MAP[name])}
# -> { body, choices, has_next, has_input, has_select_item, confirm_wait,
# confirm_timeout, text_centered, window_size, images, title_image,
# quest_buttons, quest_button_closes, map_signals, clear_map_signals,
# atlas_center, camera_events, fade_events,
# insert_images, dungeon_result, adjust_line, waiting_time,
# is_locked, wait_flag, answer_count, cinema_file, end_confirm }
func parse_script(src: String) -> Dictionary:
var es := EventSet.new()
es.init_event_set()
var cmds := _tokenize(src)
# RUN_CINEMA 首命令特例(RegisterEventSetFromString:142 —— ScriptGroup.ReadCmd 读
# 第一条命令)。本 POC 无法加载 .msc 事件文件,记录文件名走 seam;其余 token 照常
# 处理。仅当 RUN_CINEMA 是脚本的第一个 token(无前导文本)时生效。
if not cmds.is_empty() and String(cmds[0].get("literal", "")) == "" \
and String(cmds[0].get("name", "")).to_upper().trim_prefix("/") == "RUN_CINEMA":
es.cinema_file = _arg_s(String(cmds[0].get("tok", "")), "value")
for cmd in cmds:
_process_event_set(es, cmd)
return {
"body": es.body_text(),
"choices": es.choices,
"has_next": es.has_next,
"has_input": es.has_input,
"has_select_item": es.has_select_item,
"confirm_wait": es.isConfirmWait,
"confirm_timeout": es.confirm_timeout,
"text_centered": es.isTextCenterMode,
"window_size": es.window_size,
"images": es.images,
"title_image": es.title_image,
"quest_buttons": es.quest_buttons,
"quest_button_closes": es.quest_button_closes,
"map_signals": es.map_signals,
"clear_map_signals": es.clear_map_signals,
"atlas_center": es.atlas_center,
"camera_events": es.camera_events,
"fade_events": es.fade_events,
# --- 增量 95 新增:EventSet 状态透出 ---
"insert_images": es.insert_images,
"dungeon_result": es.dungeon_result,
"adjust_line": es.iAdjustLine,
"waiting_time": es.lWaitingTime,
"is_locked": es.isLock,
"wait_flag": es.isWaitFlag,
"answer_count": es.nAnswer,
"cinema_file": es.cinema_file,
"end_confirm": es.end_confirm,
}
# 把混排文本 / [tag ...] 拆成命令流。裸文本 -> { literal:String }
# tag -> { name:String(大写,带/前缀), pos:int, tok:String(方括号内原文) }。
func _tokenize(src: String) -> Array:
var out := []
var i := 0
var n := src.length()
while i < n:
@@ -94,36 +353,370 @@ func parse_script(src: String) -> Dictionary:
if c == "[":
var close_i := src.find("]", i)
if close_i == -1:
body += src.substr(i)
out.append({"literal": src.substr(i)})
break
var tok := src.substr(i + 1, close_i - i - 1).strip_edges()
i = close_i + 1
var name := tok.split(" ", false)[0].to_upper() if tok != "" else ""
match name:
"ENTER":
body += "\n"
"CLEAR", "CLEAR_TEXT":
body = ""
"NEXT", "DONE":
has_next = true
"QUESTION":
for m in _rx_args(tok):
choices.append(m)
"INPUT":
has_input = true
"SELECT_ITEM":
has_select_item = true
"LETTER":
var vv := _rx_args(tok)
if vv.size() > 0:
body += vv[0]
_:
pass # COLOR/DELAY/WAIT/IMAGE/... 忽略
if tok == "":
continue
var first := tok.split(" ", false)[0]
var pos := EVENT_POSITION_START
if first.begins_with("/"):
pos = EVENT_POSITION_END
out.append({"name": first.to_upper(), "pos": pos, "tok": tok})
else:
body += c
var next_tag := src.find("[", i)
if next_tag == -1:
next_tag = n
var lit := src.substr(i, next_tag - i)
if lit != "":
out.append({"literal": lit})
i = next_tag
return out
# ProcessEventSetPythonEventManager.cpp:377)的 switch 逐 case 对拷。
func _process_event_set(es: EventSet, cmd: Dictionary) -> void:
var literal := String(cmd.get("literal", ""))
if literal != "":
_evt_letter(es, literal, true)
return
var idx := _get_script_event_index(String(cmd.get("name", "")))
if not idx.ok:
# GetScriptEventIndex: 未知命令 -> PARSING ERROR,跳过(不进 switch)。
return
var etype: int = idx.type
var pos: int = idx.position
var tok := String(cmd.get("tok", ""))
match etype:
EVT_LETTER:
var vv := _tag_strings(tok)
if vv.size() > 0:
_evt_letter(es, vv[0], false)
EVT_DELAY:
# START: lWaitingTime = atoi(value)END(/DELAY): 复位 c_lNormal_Waiting_Time。
es.lWaitingTime = _arg_i(tok, "value", 0) if pos == EVENT_POSITION_START else C_NORMAL_WAITING_TIME
EVT_COLOR:
_evt_color(es, tok, pos, 1.0)
EVT_COLOR256:
_evt_color(es, tok, pos, 255.0)
EVT_ENTER:
es.insert_line()
EVT_WAIT:
es.iyLocal = 0
es.isLock = true
EVT_CLEAR, EVT_CLEAR_TEXT:
es.clear_line()
EVT_QUESTION:
_make_question(es, tok)
EVT_NEXT:
# MakeNextButton(BUTTON_TYPE_NEXT); iAdjustLine += 2。
es.has_next = true
es.iAdjustLine += 2
EVT_DONE:
# MakeNextButton(BUTTON_TYPE_DONE); DoneEvent; iAdjustLine += 2。
es.has_next = true
es.iAdjustLine += 2
EVT_LEFT_IMAGE:
es.images.append({"panel": "LEFTIMAGE", "path": _arg_s(tok, "src")})
EVT_TOP_IMAGE:
es.images.append({"panel": "TOPIMAGE", "path": _arg_s(tok, "src")})
EVT_BACKGROUND_IMAGE:
es.images.append({"panel": "BGIMAGE", "path": _arg_s(tok, "src")})
EVT_IMAGE:
var ipath := _arg_s(tok, "src", 0)
if ipath != "":
es.images.append({"x": _arg_i(tok, "x", 0), "y": _arg_i(tok, "y", 1), "path": ipath})
EVT_INSERT_IMAGE:
var img := _arg_s(tok, "image_name")
es.insert_images.append({
"image_name": img,
"image_type": _arg_s(tok, "image_type"),
"idx": _arg_i(tok, "idx"),
"title": _arg_s(tok, "title"),
"desc": _arg_s(tok, "desc"),
"index": _arg_i(tok, "index"),
"total": _arg_i(tok, "total"),
"is_item_icon": img == "",
})
es.iAdjustLine += 2
EVT_ADD_MAP_SIGNAL:
es.map_signals.append(Vector2(_arg_f(tok, "x", 0), _arg_f(tok, "y", 1)))
EVT_CLEAR_MAP_SIGNAL:
es.clear_map_signals = true
EVT_QUEST_BUTTON_CLOSE:
es.quest_button_closes.append(_arg_i(tok, "idx", 0))
EVT_QUEST_BUTTON:
_make_quest_button(es, tok)
EVT_SET_MESSAGE_POSITION, EVT_ADJUST_MESSAGE_POSITION:
# PythonEventManager.cpp:562 / :566 —— 客户端故意留空(无副作用)。
pass
EVT_SET_CENTER_MAP_POSITION:
es.atlas_center = [_arg_i(tok, "x", 0), _arg_i(tok, "y", 1)]
EVT_SLEEP:
es.lLastDelayTime = _arg_i(tok, "value", 0)
EVT_SET_CAMERA:
es.camera_events.append({"kind": "SET_CAMERA", "values": _tag_numbers(tok),
"setting": _camera_setting(tok)})
EVT_BLEND_CAMERA:
es.camera_events.append({"kind": "BLEND_CAMERA", "values": _tag_numbers(tok),
"setting": _camera_setting(tok), "blendtime": _arg_f(tok, "blendtime")})
EVT_RESTORE_CAMERA:
es.camera_events.append({"kind": "RESTORE_CAMERA", "values": []})
EVT_FADE_OUT, EVT_FADE_IN, EVT_WHITE_OUT, EVT_WHITE_IN:
var kinds := {EVT_FADE_OUT: "FADE_OUT", EVT_FADE_IN: "FADE_IN",
EVT_WHITE_OUT: "WHITE_OUT", EVT_WHITE_IN: "WHITE_IN"}
es.fade_events.append({"kind": kinds[etype], "speed": _arg_f(tok, "speed", 0)})
es.isWaitFlag = true # 等 EndEventProcessseam:无逐帧循环,不真正阻塞)。
EVT_TEXT_HORIZONTAL_ALIGN_CENTER:
es.isTextCenterMode = true
EVT_TITLE_IMAGE:
es.title_image = _arg_s(tok, "src", 0)
EVT_RUN_CINEMA:
# ProcessEventSet 无此 case(仅 RegisterEventSetFromString 首命令阶段处理)。
pass
EVT_DUNGEON_RESULT:
es.dungeon_result = {
"killstone_count": _arg_i(tok, "killstone_count"),
"killmob_count": _arg_i(tok, "killmob_count"),
"find_hidden": _arg_i(tok, "find_hidden"),
"hidden_total": _arg_i(tok, "hidden_total"),
"use_potion": _arg_i(tok, "use_potion"),
"is_revived": _arg_i(tok, "is_revived"),
"killallmob": _arg_i(tok, "killallmob"),
"total_time": _arg_i(tok, "total_time"),
"bonus_exp": _arg_i(tok, "bonus_exp"),
}
EVT_ITEM_NAME:
_evt_item_name(es, _arg_i(tok, "value", 0))
EVT_MONSTER_NAME:
_evt_mob_name(es, _arg_i(tok, "value", 0))
EVT_WINDOW_SIZE:
var w := _arg_i(tok, "width", 0)
var h := _arg_i(tok, "height", 1)
if w > 0 and h > 0:
es.window_size = [w, h]
EVT_INPUT:
es.insert_line()
es.has_input = true
EVT_CONFIRM_WAIT:
es.confirm_timeout = _arg_i(tok, "timeout", 0)
es.isConfirmWait = true
es.iConfirmEndTime = es.confirm_timeout # timeGetTime()/1000 + timeoutseam
es.insert_line(true)
# MakeNextButton(BUTTON_TYPE_CANCEL) —— 取消按钮由 _fill_buttons(confirm_wait) 出。
EVT_END_CONFIRM_WAIT:
es.isConfirmWait = false
es.iConfirmEndTime = 0
es.end_confirm = true
EVT_SELECT_ITEM:
es.has_select_item = true
_:
pass
# EVENT_TYPE_LETTER / 裸文本(is_raw)。
func _evt_letter(es: EventSet, value: String, is_raw: bool) -> void:
es.strCurrentLine += value
if not is_raw:
es.iCurrentLetter += value.length()
if es.iCurrentLetter >= es.iRestrictedCharacterCount:
es.insert_line()
es.lLastDelayTime = es.lWaitingTime
# EVENT_TYPE_COLOR / EVENT_TYPE_COLOR256。BBCode span:新 span 前先关旧的。
func _evt_color(es: EventSet, tok: String, pos: int, divisor: float) -> void:
if es.color_open:
es.strCurrentLine += "[/color]"
es.color_open = false
if pos == EVENT_POSITION_START:
var kv := _tag_kv(tok)
var r: float
var g: float
var b: float
if kv.has("r") or kv.has("g") or kv.has("b"):
r = float(kv.get("r", "0"))
g = float(kv.get("g", "0"))
b = float(kv.get("b", "0"))
else:
var nums := _tag_numbers(tok)
if nums.size() < 3:
return
r = nums[0]; g = nums[1]; b = nums[2]
es.CurrentColor = Color(clampf(r / divisor, 0.0, 1.0),
clampf(g / divisor, 0.0, 1.0), clampf(b / divisor, 0.0, 1.0), 1.0)
es.strCurrentLine += "[color=#%s]" % es.CurrentColor.to_html(false)
es.color_open = true
else:
es.CurrentColor = Color(1, 1, 1, 1)
# MakeQuestionPythonEventManager.cpp:884)。
func _make_question(es: EventSet, tok: String) -> void:
var args := _tag_strings(tok)
if args.is_empty():
return
for a in args:
es.choices.append(String(a))
es.nAnswer = args.size()
# EVENT_TYPE_QUEST_BUTTON:558)—— idx/name/icon_type/icon_name,带位置回退。
func _make_quest_button(es: EventSet, tok: String) -> void:
var kv := _tag_kv(tok)
var nums := _tag_numbers(tok)
var strs := _tag_strings(tok)
var qidx := -1
if kv.has("idx") and String(kv["idx"]).is_valid_int():
qidx = int(kv["idx"])
elif nums.size() > 0:
qidx = int(nums[0])
if qidx < 0:
return
var nm := String(kv.get("name", strs[0] if strs.size() > 0 else ""))
var itype := String(kv.get("icon_type", strs[1] if strs.size() > 1 else ""))
var iname := String(kv.get("icon_name", strs[2] if strs.size() > 2 else ""))
es.quest_buttons.append({"index": qidx, "title": nm, "icon_type": itype, "icon_name": iname})
func _evt_item_name(es: EventSet, vnum: int) -> void:
var nm := _quest_item_name([float(vnum)])
es.strCurrentLine += nm
es.iCurrentLetter += nm.length()
if es.iCurrentLetter >= es.iRestrictedCharacterCount:
es.insert_line()
es.lLastDelayTime = es.lWaitingTime
func _evt_mob_name(es: EventSet, vnum: int) -> void:
var nm := _quest_mob_name([float(vnum)])
es.strCurrentLine += nm
es.iCurrentLetter += nm.length()
if es.iCurrentLetter >= es.iRestrictedCharacterCount:
es.insert_line()
es.lLastDelayTime = es.lWaitingTime
# GetCameraSettingFromArgListPythonEventManager.cpp:48)。
func _camera_setting(tok: String) -> Dictionary:
return {
"x": _arg_i(tok, "x"), "y": _arg_i(tok, "y"), "z": _arg_i(tok, "z"),
"up": _arg_i(tok, "up"), "view": _arg_i(tok, "view"), "cross": _arg_i(tok, "cross"),
"distance": _arg_i(tok, "distance"), "rot": _arg_i(tok, "rot"), "pitch": _arg_i(tok, "pitch"),
}
# --- arg 提取(keyword 优先,位置回退)--------------------------------
# key(val) / key("val") / key;val 的键值对。重复键取最后一个。
func _tag_kv(tok: String) -> Dictionary:
var out := {}
var raw := tok.substr(tok.find(" ") + 1) if tok.find(" ") >= 0 else ""
var i := 0
var n := raw.length()
while i < n:
while i < n and raw[i] in [" ", "\t", ","]:
i += 1
return {"body": body.strip_edges(), "choices": choices, "has_next": has_next,
"has_input": has_input, "has_select_item": has_select_item}
var start := i
while i < n and not (raw[i] in [" ", "\t", ",", "(", ";"]):
i += 1
if i >= n:
break
var key := raw.substr(start, i - start)
var val := ""
if raw[i] == "(":
var depth := 1
i += 1
var vs := i
while i < n and depth > 0:
if raw[i] == "(":
depth += 1
elif raw[i] == ")":
depth -= 1
if depth == 0:
break
i += 1
val = raw.substr(vs, i - vs)
i += 1
elif raw[i] == ";":
i += 1
var vs2 := i
while i < n and not (raw[i] in [" ", "\t", ","]):
i += 1
val = raw.substr(vs2, i - vs2)
val = val.strip_edges()
if val.length() >= 2 and val[0] == '"' and val[val.length() - 1] == '"':
val = val.substr(1, val.length() - 2)
if key != "":
out[key] = val
return out
func _arg_s(tok: String, key: String, fallback_idx: int = -1) -> String:
var kv := _tag_kv(tok)
if kv.has(key):
return String(kv[key])
if fallback_idx >= 0:
var strs := _tag_strings(tok)
if fallback_idx < strs.size():
return String(strs[fallback_idx])
return ""
func _arg_f(tok: String, key: String, fallback_idx: int = -1) -> float:
var kv := _tag_kv(tok)
if kv.has(key) and String(kv[key]).is_valid_float():
return float(kv[key])
if fallback_idx >= 0:
var nums := _tag_numbers(tok)
if fallback_idx < nums.size():
return nums[fallback_idx]
return 0.0
func _arg_i(tok: String, key: String, fallback_idx: int = -1) -> int:
return int(_arg_f(tok, key, fallback_idx))
func _tag_strings(tok: String) -> Array[String]:
var quoted: Array[String] = []
for raw in _rx_args(tok):
quoted.append(String(raw))
if not quoted.is_empty():
return quoted
var out: Array[String] = []
var raw := tok.substr(tok.find(" ") + 1).strip_edges() if tok.find(" ") >= 0 else ""
for part in raw.replace(",", ";").split(";", false):
var value := part.strip_edges()
if value in ["value", "arg"] or value.is_empty():
continue
if value.begins_with("value("):
value = value.trim_prefix("value(").trim_suffix(")")
if value.begins_with("arg("):
value = value.trim_prefix("arg(").trim_suffix(")")
if not value.is_empty():
out.append(value)
return out
func _tag_numbers(tok: String) -> Array[float]:
var out: Array[float] = []
var raw := tok.substr(tok.find(" ") + 1).strip_edges() if tok.find(" ") >= 0 else ""
raw = raw.replace(",", " ").replace(";", " ").replace("(", " ").replace(")", " ")
for part in raw.split(" ", false):
var value := part.strip_edges()
if value.is_valid_float():
out.append(float(value))
return out
func _quest_item_name(values: Array[float]) -> String:
if values.is_empty():
return ""
var vnum := int(values[0])
if proto and proto.has_method("item"):
var data: Dictionary = proto.item(vnum)
var name := String(data.get("locale_name", data.get("name", "")))
if name != "":
return name
return "物品 #%d" % vnum
func _quest_mob_name(values: Array[float]) -> String:
if values.is_empty():
return ""
var vid := int(values[0])
if client and client.has_method("get_entity"):
var entity: Dictionary = client.get_entity(vid)
var name := String(entity.get("name", ""))
if name != "":
return name
return "怪物 #%d" % vid
# 抓 tok 里所有 arg("...") / value("...") 的字符串
func _rx_args(tok: String) -> Array:
@@ -159,6 +752,10 @@ func _build(parent: Node) -> void:
sb.set_corner_radius_all(4)
panel.add_theme_stylebox_override("panel", sb)
_root.add_child(panel)
_image_layer = Control.new()
_image_layer.set_anchors_preset(Control.PRESET_FULL_RECT)
_image_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
_root.add_child(_image_layer)
_text = RichTextLabel.new()
_text.bbcode_enabled = true
_text.position = Vector2(16, 14)
@@ -171,11 +768,51 @@ func _build(parent: Node) -> void:
_btnrow.add_theme_constant_override("separation", 4)
_root.add_child(_btnrow)
func _render_images(images: Array, title_image: String) -> void:
if _image_layer == null:
return
for child in _image_layer.get_children():
child.queue_free()
for spec in images:
var path := String(spec.get("path", ""))
var tex: Texture2D = UiAssets.load_tex(_assets_root, path)
if tex == null:
continue
var image := TextureRect.new()
image.texture = tex
image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
image.mouse_filter = Control.MOUSE_FILTER_IGNORE
var panel_name := String(spec.get("panel", ""))
if panel_name == "BGIMAGE":
image.set_anchors_preset(Control.PRESET_FULL_RECT)
elif panel_name == "TOPIMAGE":
image.position = Vector2(0, 0)
image.size = Vector2(_root.size.x, minf(84.0, tex.get_size().y))
elif panel_name == "LEFTIMAGE":
image.position = Vector2(0, 0)
image.size = Vector2(minf(150.0, tex.get_size().x), _root.size.y)
else:
image.position = Vector2(float(spec.get("x", 0)), float(spec.get("y", 0)))
image.size = tex.get_size()
_image_layer.add_child(image)
if title_image != "":
var title_tex: Texture2D = UiAssets.load_tex(_assets_root, title_image)
if title_tex:
var title := TextureRect.new()
title.texture = title_tex
title.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
title.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
title.position = Vector2((_root.size.x - title_tex.get_size().x) * 0.5, 4)
title.size = title_tex.get_size()
_image_layer.add_child(title)
func _clear_buttons() -> void:
for c in _btnrow.get_children():
c.queue_free()
func _fill_buttons(choices: Array, has_next: bool, has_input := false) -> void:
func _fill_buttons(choices: Array, has_next: bool, has_input := false, confirm_wait := false) -> void:
_clear_buttons()
if has_input:
var input := LineEdit.new()
@@ -186,6 +823,13 @@ func _fill_buttons(choices: Array, has_next: bool, has_input := false) -> void:
submit.pressed.connect(func(): _submit_input(input))
input.text_submitted.connect(func(_text): _submit_input(input))
return
if confirm_wait:
var cancel := _mkbtn("取消")
cancel.pressed.connect(func():
if client and client.has_method("quest_cancel"):
client.quest_cancel()
close())
return
if choices.is_empty():
var b := _mkbtn("继续" if has_next else "关闭")
b.pressed.connect(func():