完善客户端功能并同步差距文档
This commit is contained in:
+678
-34
@@ -17,19 +17,37 @@
|
||||
# quest_confirm_ask(GC_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 —— EventTypeMap(PythonEventManager.cpp:1148 起 42 条)
|
||||
# · _get_script_event_index —— GetScriptEventIndex(:1093,前导 '/' = EVENT_POSITION_END)
|
||||
# · EventSet —— SEventSet(PythonEventManager.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 里的裸文本被折成合成 LETTER(is_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,
|
||||
}
|
||||
|
||||
# EventTypeMap(PythonEventManager.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
|
||||
|
||||
# SEventSet(PythonEventManager.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: # __InitEventSet(PythonEventManager.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
|
||||
# __InsertLine:init 时 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()
|
||||
|
||||
# GetScriptEventIndex(PythonEventManager.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
|
||||
|
||||
# ProcessEventSet(PythonEventManager.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 # 等 EndEventProcess(seam:无逐帧循环,不真正阻塞)。
|
||||
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 + timeout(seam)
|
||||
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)
|
||||
|
||||
# MakeQuestion(PythonEventManager.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
|
||||
|
||||
# GetCameraSettingFromArgList(PythonEventManager.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():
|
||||
|
||||
Reference in New Issue
Block a user