Complete remaining client gap features
This commit is contained in:
+260
-19
@@ -33,6 +33,9 @@ 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)
|
||||
signal camera_event_requested_full(kind: String, setting: Dictionary, blendtime: float)
|
||||
signal fade_event_requested_full(kind: String, speed: float)
|
||||
signal done_event()
|
||||
|
||||
var client: Node
|
||||
var proto: Node
|
||||
@@ -43,27 +46,204 @@ var _btnrow: VBoxContainer
|
||||
var _image_layer: Control
|
||||
var _confirm_pid := 0
|
||||
var _confirm_wait := false
|
||||
var _active_set
|
||||
var _active_commands: Array = []
|
||||
var _active_cursor := 0
|
||||
var _active_script := false
|
||||
|
||||
signal dungeon_result_requested(result: Dictionary)
|
||||
|
||||
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)
|
||||
set_process(true)
|
||||
_build(parent)
|
||||
if client.has_signal("script_dialog"):
|
||||
client.script_dialog.connect(_on_script)
|
||||
if client.has_signal("quest_confirm_ask"):
|
||||
client.quest_confirm_ask.connect(_on_confirm)
|
||||
|
||||
func set_proto(proto_node: Node) -> void:
|
||||
# Proto is created after the dialog in GameScene because item/mob loading is
|
||||
# expensive. Keep the dialog usable before that point, then resolve future
|
||||
# [ITEM]/[MOB] tags against the real tables.
|
||||
proto = proto_node
|
||||
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
# --- 收 GC_SCRIPT ------------------------------------------------------
|
||||
func _process(delta: float) -> void:
|
||||
if not _active_script or _active_set == null:
|
||||
return
|
||||
# The first headless frame may report a zero delta. ClientVS22 still runs
|
||||
# one UpdateEventSet tick, so use one nominal 60 FPS frame as the floor.
|
||||
_process_active(maxf(delta * 1000.0, 16.0))
|
||||
|
||||
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)
|
||||
# Deterministic entry point used by the frame loop and headless tests. The
|
||||
# argument is milliseconds because EventManager's lLastDelayTime/lWaitingTime
|
||||
# are millisecond values in ClientVS22.
|
||||
func process_events(elapsed_ms: float) -> void:
|
||||
if _active_script and _active_set != null:
|
||||
_process_active(maxf(0.0, elapsed_ms))
|
||||
|
||||
func begin_script(src: String) -> void:
|
||||
_active_set = EventSet.new()
|
||||
_active_set.init_event_set()
|
||||
_active_commands = _tokenize(src)
|
||||
_active_cursor = 0
|
||||
_active_script = true
|
||||
# RegisterEventSetFromString replaces a leading RUN_CINEMA event set with
|
||||
# the referenced .msc file. Keep the signal for the presentation layer and
|
||||
# load the event text when the loose asset is available.
|
||||
if not _active_commands.is_empty() and String(_active_commands[0].get("literal", "")) == "" \
|
||||
and String(_active_commands[0].get("name", "")).to_upper().trim_prefix("/") == "RUN_CINEMA":
|
||||
var cinema := _arg_s(String(_active_commands[0].get("tok", "")), "value")
|
||||
if cinema != "":
|
||||
cinema_requested.emit(cinema)
|
||||
var cinema_text := _read_script_file(cinema)
|
||||
if cinema_text != "":
|
||||
_active_commands = _tokenize(cinema_text)
|
||||
else:
|
||||
# No local .msc: keep the empty event set alive so the caller can
|
||||
# show its own cinema player, but do not re-process RUN_CINEMA.
|
||||
_active_commands = []
|
||||
_present_event_set(_active_set)
|
||||
_root.visible = true
|
||||
opened.emit()
|
||||
# Start the same nominal first tick immediately. This keeps the dialog
|
||||
# responsive even when a SceneTree caller resumes before child _process()
|
||||
# callbacks (notably in headless fixtures).
|
||||
_process_active(16.0)
|
||||
|
||||
func _read_script_file(vpath: String) -> String:
|
||||
var rel := vpath.replace("\\", "/")
|
||||
if rel.length() >= 2 and rel[1] == ":":
|
||||
rel = rel.substr(2)
|
||||
rel = rel.lstrip("/")
|
||||
var candidates: Array[String] = []
|
||||
if _assets_root != "":
|
||||
candidates.append(_assets_root.path_join(rel))
|
||||
var resolved := UiAssets._resolve(_assets_root, rel)
|
||||
if resolved != "":
|
||||
candidates.append(resolved)
|
||||
for path in candidates:
|
||||
if FileAccess.file_exists(path):
|
||||
return FileAccess.get_file_as_string(path)
|
||||
return ""
|
||||
|
||||
func skip() -> void:
|
||||
if not _active_script or _active_set == null:
|
||||
return
|
||||
# CPythonEventManager::Skip: an input lock is released; otherwise the
|
||||
# current delay is forced to expire and the next command is consumed.
|
||||
if _active_set.isLock:
|
||||
_active_set.lLastDelayTime = 0
|
||||
_active_set.isLock = false
|
||||
else:
|
||||
_active_set.lLastDelayTime = -1
|
||||
_process_active(0.0)
|
||||
|
||||
func end_event_process() -> void:
|
||||
if _active_set != null:
|
||||
_active_set.isWaitFlag = false
|
||||
|
||||
func _process_active(elapsed_ms: float) -> void:
|
||||
var remaining := elapsed_ms
|
||||
var forced_step: bool = _active_set.lLastDelayTime < 0
|
||||
if forced_step:
|
||||
_active_set.lLastDelayTime = 0
|
||||
while _active_script and _active_set != null:
|
||||
if _active_set.isConfirmWait:
|
||||
if int(Time.get_ticks_msec() / 1000) >= _active_set.iConfirmEndTime:
|
||||
_active_set.isConfirmWait = false
|
||||
_active_set.iConfirmEndTime = 0
|
||||
_confirm_wait = false
|
||||
if client and client.has_method("quest_cancel"):
|
||||
client.quest_cancel()
|
||||
_active_script = false
|
||||
close()
|
||||
else:
|
||||
_present_event_set(_active_set)
|
||||
return
|
||||
if _active_set.isWaitFlag or _active_set.isLock:
|
||||
_present_event_set(_active_set)
|
||||
return
|
||||
if _active_set.lLastDelayTime > 0:
|
||||
if remaining <= 0.0:
|
||||
return
|
||||
var wait := float(_active_set.lLastDelayTime)
|
||||
if remaining < wait:
|
||||
_active_set.lLastDelayTime -= int(remaining)
|
||||
return
|
||||
_active_set.lLastDelayTime = 0
|
||||
remaining -= wait
|
||||
if _active_cursor >= _active_commands.size():
|
||||
_active_script = false
|
||||
_active_set.isLock = true
|
||||
_present_event_set(_active_set)
|
||||
return
|
||||
var command: Dictionary = _active_commands[_active_cursor]
|
||||
_active_cursor += 1
|
||||
_process_event_set(_active_set, command)
|
||||
_emit_runtime_side_effect(command, _active_set)
|
||||
_present_event_set(_active_set)
|
||||
forced_step = false
|
||||
if _active_set.isWaitFlag or _active_set.isLock or _active_set.isConfirmWait:
|
||||
return
|
||||
if remaining <= 0.0:
|
||||
return
|
||||
|
||||
func _finish_runtime() -> void:
|
||||
_active_script = false
|
||||
_active_commands.clear()
|
||||
_active_cursor = 0
|
||||
|
||||
func _emit_runtime_side_effect(command: Dictionary, es: EventSet) -> void:
|
||||
var idx := _get_script_event_index(String(command.get("name", "")))
|
||||
if not idx.ok:
|
||||
return
|
||||
var tok := String(command.get("tok", ""))
|
||||
match int(idx.type):
|
||||
EVT_SELECT_ITEM:
|
||||
select_item_requested.emit()
|
||||
EVT_QUEST_BUTTON:
|
||||
if not es.quest_buttons.is_empty():
|
||||
var q: Dictionary = es.quest_buttons.back()
|
||||
quest_button_received.emit(int(q.get("index", -1)), String(q.get("title", "")),
|
||||
String(q.get("icon_type", "")), String(q.get("icon_name", "")))
|
||||
EVT_QUEST_BUTTON_CLOSE:
|
||||
quest_button_cleared.emit(_arg_i(tok, "idx", 0))
|
||||
EVT_ADD_MAP_SIGNAL:
|
||||
map_signal_added.emit(_arg_f(tok, "x", 0), _arg_f(tok, "y", 1))
|
||||
EVT_CLEAR_MAP_SIGNAL:
|
||||
map_signals_cleared.emit()
|
||||
EVT_SET_CENTER_MAP_POSITION:
|
||||
atlas_center_requested.emit(_arg_i(tok, "x", 0), _arg_i(tok, "y", 1))
|
||||
EVT_SET_CAMERA, EVT_BLEND_CAMERA, EVT_RESTORE_CAMERA:
|
||||
var camera: Dictionary = es.camera_events.back() if not es.camera_events.is_empty() else {}
|
||||
var kind := String(camera.get("kind", ""))
|
||||
camera_event_requested.emit(kind, camera.get("values", []))
|
||||
camera_event_requested_full.emit(kind, camera.get("setting", {}).duplicate(true),
|
||||
float(camera.get("blendtime", 0.0)))
|
||||
EVT_FADE_OUT, EVT_FADE_IN, EVT_WHITE_OUT, EVT_WHITE_IN:
|
||||
var fade: Dictionary = es.fade_events.back() if not es.fade_events.is_empty() else {}
|
||||
var fade_kind := String(fade.get("kind", ""))
|
||||
var fade_speed := float(fade.get("speed", 0.0))
|
||||
fade_event_requested.emit(fade_kind, fade_speed)
|
||||
fade_event_requested_full.emit(fade_kind, fade_speed)
|
||||
EVT_DUNGEON_RESULT:
|
||||
dungeon_result_requested.emit(es.dungeon_result.duplicate())
|
||||
EVT_END_CONFIRM_WAIT:
|
||||
_confirm_wait = false
|
||||
_:
|
||||
pass
|
||||
|
||||
func _present_event_set(es: EventSet) -> void:
|
||||
var parsed := _event_set_result(es)
|
||||
if parsed.body.strip_edges() == "" and parsed.choices.is_empty() and not parsed.has_input \
|
||||
and not parsed.confirm_wait and not _active_script:
|
||||
close()
|
||||
return
|
||||
_text.text = parsed.body
|
||||
@@ -77,10 +257,13 @@ func _on_script(_skin: int, text: String) -> void:
|
||||
_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)
|
||||
_fill_buttons(parsed.choices, parsed.has_next, parsed.has_input, parsed.confirm_wait,
|
||||
String(parsed.get("next_button_type", "")))
|
||||
|
||||
# --- 收 GC_SCRIPT ------------------------------------------------------
|
||||
|
||||
func _on_script(_skin: int, text: String) -> void:
|
||||
begin_script(text)
|
||||
|
||||
func _emit_side_effects(parsed: Dictionary) -> void:
|
||||
if parsed.has_select_item:
|
||||
@@ -97,13 +280,20 @@ func _emit_side_effects(parsed: Dictionary) -> void:
|
||||
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", []))
|
||||
var kind := String(camera.get("kind", ""))
|
||||
camera_event_requested.emit(kind, camera.get("values", []))
|
||||
camera_event_requested_full.emit(kind, camera.get("setting", {}).duplicate(true),
|
||||
float(camera.get("blendtime", 0.0)))
|
||||
for fade in parsed.fade_events:
|
||||
fade_event_requested.emit(String(fade.get("kind", "")), float(fade.get("speed", 0.0)))
|
||||
var fade_kind := String(fade.get("kind", ""))
|
||||
var fade_speed := float(fade.get("speed", 0.0))
|
||||
fade_event_requested.emit(fade_kind, fade_speed)
|
||||
fade_event_requested_full.emit(fade_kind, fade_speed)
|
||||
if String(parsed.get("cinema_file", "")) != "":
|
||||
cinema_requested.emit(String(parsed.cinema_file))
|
||||
|
||||
func _on_confirm(msg: String, _timeout: int, request_pid: int) -> void:
|
||||
_finish_runtime()
|
||||
_confirm_pid = request_pid
|
||||
_text.text = msg
|
||||
_fill_confirm()
|
||||
@@ -111,6 +301,7 @@ func _on_confirm(msg: String, _timeout: int, request_pid: int) -> void:
|
||||
opened.emit()
|
||||
|
||||
func close() -> void:
|
||||
_finish_runtime()
|
||||
if _root:
|
||||
_root.visible = false
|
||||
_confirm_wait = false
|
||||
@@ -123,6 +314,10 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
if key.pressed and not key.echo and key.keycode == KEY_ESCAPE:
|
||||
# The legacy client sends QUEST_CANCEL when the active script is dismissed
|
||||
# with Escape; ordinary answer buttons continue to use SCRIPT_ANSWER.
|
||||
if _active_script:
|
||||
skip()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if client and client.has_method("quest_cancel"):
|
||||
client.quest_cancel()
|
||||
close()
|
||||
@@ -144,9 +339,8 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
# 的 30 字断行——reference 脚本只有显式 [LETTER],裸文本是本客户端的扩展,逐字符断句会
|
||||
# 破坏服务器写好的句子)。
|
||||
#
|
||||
# seam(无逐帧 Update 循环):isLock/isWaitFlag/lLastDelayTime/lWaitingTime 作为 EventSet
|
||||
# 状态保留并透出,但一次性把剩余 token 跑完(保证按钮 / 选项 / 副作用仍然落地);Skip /
|
||||
# EndEventProcess / iConfirmEndTime 的 timeGetTime 走 seam。
|
||||
# EventSet 现在由 _process_active 按 ClientVS22::UpdateEventSet 的毫秒延迟逐步消费;
|
||||
# parse_script() 仍保留为离线兼容接口,用于一次性检查整个脚本的最终状态。
|
||||
|
||||
signal cinema_requested(filename: String)
|
||||
|
||||
@@ -224,6 +418,7 @@ class EventSet:
|
||||
var end_confirm := false
|
||||
var choices: Array[String] = []
|
||||
var has_next := false
|
||||
var next_button_type := ""
|
||||
var has_input := false
|
||||
var has_select_item := false
|
||||
var confirm_timeout := 0
|
||||
@@ -315,6 +510,7 @@ func parse_script(src: String) -> Dictionary:
|
||||
"body": es.body_text(),
|
||||
"choices": es.choices,
|
||||
"has_next": es.has_next,
|
||||
"next_button_type": es.next_button_type,
|
||||
"has_input": es.has_input,
|
||||
"has_select_item": es.has_select_item,
|
||||
"confirm_wait": es.isConfirmWait,
|
||||
@@ -342,6 +538,38 @@ func parse_script(src: String) -> Dictionary:
|
||||
"end_confirm": es.end_confirm,
|
||||
}
|
||||
|
||||
func _event_set_result(es: EventSet) -> Dictionary:
|
||||
return {
|
||||
"body": es.body_text(),
|
||||
"choices": es.choices,
|
||||
"has_next": es.has_next,
|
||||
"next_button_type": es.next_button_type,
|
||||
"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,
|
||||
"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:
|
||||
@@ -411,10 +639,13 @@ func _process_event_set(es: EventSet, cmd: Dictionary) -> void:
|
||||
EVT_NEXT:
|
||||
# MakeNextButton(BUTTON_TYPE_NEXT); iAdjustLine += 2。
|
||||
es.has_next = true
|
||||
es.next_button_type = "NEXT"
|
||||
es.iAdjustLine += 2
|
||||
EVT_DONE:
|
||||
# MakeNextButton(BUTTON_TYPE_DONE); DoneEvent; iAdjustLine += 2。
|
||||
# MakeNextButton(BUTTON_TYPE_DONE); the reference invokes DoneEvent
|
||||
# from the button callback, not while the token is being parsed.
|
||||
es.has_next = true
|
||||
es.next_button_type = "DONE"
|
||||
es.iAdjustLine += 2
|
||||
EVT_LEFT_IMAGE:
|
||||
es.images.append({"panel": "LEFTIMAGE", "path": _arg_s(tok, "src")})
|
||||
@@ -501,7 +732,9 @@ func _process_event_set(es: EventSet, cmd: Dictionary) -> void:
|
||||
EVT_CONFIRM_WAIT:
|
||||
es.confirm_timeout = _arg_i(tok, "timeout", 0)
|
||||
es.isConfirmWait = true
|
||||
es.iConfirmEndTime = es.confirm_timeout # timeGetTime()/1000 + timeout(seam)
|
||||
# ClientVS22 stores the deadline in seconds (timeGetTime()/1000),
|
||||
# not the timeout duration itself.
|
||||
es.iConfirmEndTime = int(Time.get_ticks_msec() / 1000) + es.confirm_timeout
|
||||
es.insert_line(true)
|
||||
# MakeNextButton(BUTTON_TYPE_CANCEL) —— 取消按钮由 _fill_buttons(confirm_wait) 出。
|
||||
EVT_END_CONFIRM_WAIT:
|
||||
@@ -812,7 +1045,8 @@ func _clear_buttons() -> void:
|
||||
for c in _btnrow.get_children():
|
||||
c.queue_free()
|
||||
|
||||
func _fill_buttons(choices: Array, has_next: bool, has_input := false, confirm_wait := false) -> void:
|
||||
func _fill_buttons(choices: Array, has_next: bool, has_input := false, confirm_wait := false,
|
||||
next_button_type := "") -> void:
|
||||
_clear_buttons()
|
||||
if has_input:
|
||||
var input := LineEdit.new()
|
||||
@@ -831,9 +1065,16 @@ func _fill_buttons(choices: Array, has_next: bool, has_input := false, confirm_w
|
||||
close())
|
||||
return
|
||||
if choices.is_empty():
|
||||
var b := _mkbtn("继续" if has_next else "关闭")
|
||||
var is_done := next_button_type == "DONE"
|
||||
var b := _mkbtn("关闭" if is_done else ("继续" if has_next else "关闭"))
|
||||
b.pressed.connect(func():
|
||||
client.script_answer(255)
|
||||
if is_done:
|
||||
# MakeNextButton(BUTTON_TYPE_DONE) wires DoneEvent to the
|
||||
# actual click. Keep parse_script() side-effect free and only
|
||||
# notify consumers when the player confirms the DONE button.
|
||||
done_event.emit()
|
||||
if client and client.has_method("script_answer"):
|
||||
client.script_answer(255)
|
||||
close())
|
||||
return
|
||||
for i in choices.size():
|
||||
|
||||
Reference in New Issue
Block a user