Fix 40250 classic login and world loading
This commit is contained in:
@@ -55,7 +55,7 @@ func _init() -> void:
|
||||
_client.connect("login_failed", func(r): _set_status("login failed: " + r))
|
||||
_client.connect("entered_game", func(): _set_status("entered game"))
|
||||
var host := "192.168.21.203"
|
||||
_client.call("connect_to_server", host, 11000, host, 11011, "admin", "123456789")
|
||||
_client.call("connect_to_server", host, 11000, host, 13002, "admin", "123456789")
|
||||
_set_status("connecting to " + host + " ...")
|
||||
else:
|
||||
_set_status("M2Client 未注册 —— 用 --mock")
|
||||
|
||||
@@ -8,6 +8,10 @@ extends Node
|
||||
const AppFlow = preload("res://app_flow.gd")
|
||||
|
||||
func _ready() -> void:
|
||||
# 打包 app 从 Finder/open 启动时没有 shell 环境;40250 服务器使用 classic
|
||||
# DH2/CTR 协议,因此默认固定到 classic,同时保留 MT_PROTOCOL 覆盖能力。
|
||||
if OS.get_environment("MT_PROTOCOL").is_empty():
|
||||
OS.set_environment("MT_PROTOCOL", "classic")
|
||||
# 移动端:APK/IPA 里没有资源 —— 挂载外部的 assets.zip(adb push / ios-deploy 上传)。
|
||||
AssetPack.ensure()
|
||||
var root := AssetRoot.path()
|
||||
|
||||
@@ -21,7 +21,7 @@ script_export_mode=2
|
||||
[preset.0.options]
|
||||
|
||||
export/distribution_type=1
|
||||
binary_format/architecture="arm64"
|
||||
binary_format/architecture="universal"
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
|
||||
@@ -121,6 +121,11 @@ func setup(m2client: Node, assets_root: String,
|
||||
map_path: String = "OutdoorA1/metin2_map_a1") -> void:
|
||||
client = m2client
|
||||
_assets = assets_root
|
||||
# GC_MAIN_CHARACTER only carries the server-space coordinates, not the map
|
||||
# folder. The old fixed A1 fallback puts characters from A2/A3 outside the
|
||||
# rendered terrain, which looks like an empty map. Resolve the map from the
|
||||
# character position before constructing Metin2World.
|
||||
map_path = _resolve_map_path(map_path)
|
||||
_build_lighting()
|
||||
# 关键:setup 是协程。每个重活之间让出一帧,好让 M2Client._process 抽 socket
|
||||
# (否则整段 ~5s 同步阻塞会漏 PONG → 服务器 10s 后 "peer closed")。
|
||||
@@ -672,6 +677,87 @@ func _map_loaded() -> bool:
|
||||
var rep: Dictionary = world.call("get_load_report")
|
||||
return int(rep.get("map_size_x", 0)) > 0
|
||||
|
||||
func _resolve_map_path(requested: String) -> String:
|
||||
if client == null or _assets == "" or not client.has_method("get_main_vid"):
|
||||
return requested
|
||||
var vid := int(client.get_main_vid())
|
||||
if vid == 0 or not client.has_method("get_entity"):
|
||||
return requested
|
||||
var entity: Dictionary = client.get_entity(vid)
|
||||
var pos: Variant = entity.get("pos_cm", null)
|
||||
if not (pos is Vector3):
|
||||
return requested
|
||||
|
||||
var settings: Array[String] = []
|
||||
_collect_map_settings(_assets, settings)
|
||||
var best := requested
|
||||
var best_score := 1000000
|
||||
for setting_path: String in settings:
|
||||
var bounds := _map_bounds(setting_path)
|
||||
if bounds.is_empty():
|
||||
continue
|
||||
var base: Vector2 = bounds["base"]
|
||||
var size: Vector2i = bounds["size"]
|
||||
var max_x := base.x + float(size.x) * 25600.0
|
||||
var max_y := base.y + float(size.y) * 25600.0
|
||||
if pos.x < base.x or pos.x >= max_x or pos.y < base.y or pos.y >= max_y:
|
||||
continue
|
||||
var root := _assets.trim_suffix("/")
|
||||
var rel_setting := setting_path.substr(root.length() + 1)
|
||||
var candidate := rel_setting.get_base_dir()
|
||||
var lower := candidate.to_lower()
|
||||
# Prefer the normal outdoor map over duplicate patch/season copies when
|
||||
# several map packages advertise the same world-space rectangle.
|
||||
var score := candidate.split("/").size()
|
||||
if lower.begins_with("outdoor"):
|
||||
score -= 20
|
||||
if lower.begins_with("season") or lower.begins_with("metin2_patch"):
|
||||
score += 20
|
||||
if candidate == requested:
|
||||
score -= 1000
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
best = candidate
|
||||
|
||||
if best != requested:
|
||||
print("[GameScene] map resolved by server position ", pos, " -> ", best)
|
||||
return best
|
||||
|
||||
func _collect_map_settings(dir_path: String, out: Array[String]) -> void:
|
||||
var dir := DirAccess.open(dir_path)
|
||||
if dir == null:
|
||||
return
|
||||
for file_name: String in dir.get_files():
|
||||
if file_name.to_lower() == "setting.txt":
|
||||
out.append(dir_path.path_join(file_name))
|
||||
for child: String in dir.get_directories():
|
||||
if child in [".godot", ".git", "build", "export"]:
|
||||
continue
|
||||
_collect_map_settings(dir_path.path_join(child), out)
|
||||
|
||||
func _map_bounds(setting_path: String) -> Dictionary:
|
||||
var f := FileAccess.open(setting_path, FileAccess.READ)
|
||||
if f == null:
|
||||
return {}
|
||||
var base := Vector2.ZERO
|
||||
var size := Vector2i.ZERO
|
||||
var have_base := false
|
||||
var have_size := false
|
||||
while not f.eof_reached():
|
||||
var fields := f.get_line().replace("\t", " ").strip_edges().split(" ", false)
|
||||
if fields.size() < 3:
|
||||
continue
|
||||
match String(fields[0]).to_lower():
|
||||
"baseposition":
|
||||
base = Vector2(float(fields[1]), float(fields[2]))
|
||||
have_base = true
|
||||
"mapsize":
|
||||
size = Vector2i(int(fields[1]), int(fields[2]))
|
||||
have_size = size.x > 0 and size.y > 0
|
||||
if not have_base or not have_size:
|
||||
return {}
|
||||
return {"base": base, "size": size}
|
||||
|
||||
func _build_lighting() -> void:
|
||||
var sun := DirectionalLight3D.new()
|
||||
sun.name = "FallbackSun"
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ func _connect() -> void:
|
||||
_client.call("reconnect"))
|
||||
_life.bind(_client, _audio)
|
||||
var h := _host.text
|
||||
_client.call("connect_to_server", h, 11000, h, 11011, _id.text, _pw.text)
|
||||
_client.call("connect_to_server", h, 11000, h, 13002, _id.text, _pw.text)
|
||||
_msg("连接 %s ..." % h)
|
||||
|
||||
func _on_chars(list: Array) -> void:
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
#
|
||||
# 文件格式(TSV,# 注释):
|
||||
# name auth_host auth_port game_host game_port channels(csv) [port_step] [mark_port]
|
||||
# 测试服 192.168.21.203 11000 192.168.21.203 11011 1,2,3 1 0
|
||||
# 测试服 192.168.21.203 11000 192.168.21.203 13002 1,2,3,4 10 0
|
||||
#
|
||||
# 频道 N 的 game 端口 = game_port + (N-1)*port_step。
|
||||
# 联调服实测端口是 11011/11012/11013(step=1);不同服可在文件第 7 列覆盖。
|
||||
# 这套 40250 配置的 Game2 端口是 13002/13012/13022/13032(step=10)。
|
||||
# 13000/13010/... 是 first 入口;admin 测试角色在 Game2 地图,直接连 Game2 才能完成进入游戏。
|
||||
# 第 8 列 mark_port = 公会会徽服端口(0 = 不下载会徽)。
|
||||
class_name ServerInfo
|
||||
extends RefCounted
|
||||
@@ -20,12 +21,12 @@ const DEFAULT_PORT_STEP := 1
|
||||
var _servers: Array = []
|
||||
|
||||
func _init() -> void:
|
||||
# 内置默认:当前联调服(CServerStateChecker 实测频道口 11011/12/13)
|
||||
# 内置默认:40250 服务器(认证 11000,channel1/Game2 端口 13002)
|
||||
_servers = [{
|
||||
"name": "测试服",
|
||||
"auth_host": "192.168.21.203", "auth_port": 11000,
|
||||
"game_host": "192.168.21.203", "game_port": 11011,
|
||||
"channels": [1, 2, 3], "port_step": 1, "mark_port": 0,
|
||||
"game_host": "192.168.21.203", "game_port": 13002,
|
||||
"channels": [1, 2, 3, 4], "port_step": 10, "mark_port": 0,
|
||||
}]
|
||||
|
||||
func load_file(path: String) -> bool:
|
||||
|
||||
+2
-2
@@ -41,7 +41,7 @@ func _run() -> void:
|
||||
var si: RefCounted = ServerInfoRes.new()
|
||||
_ck(si.count() == 1 and si.server(0)["auth_host"] == "192.168.21.203", "serverinfo: built-in default")
|
||||
var addr: Dictionary = si.address(0, 3)
|
||||
_ck(addr["game_port"] == 11013, "serverinfo: 内置 CH3 -> 11013 (step 1)")
|
||||
_ck(addr["game_port"] == 13022, "serverinfo: 内置 CH3 -> 13022 (step 10)")
|
||||
var tmp := "user://p10_servers.txt"
|
||||
var f := FileAccess.open(tmp, FileAccess.WRITE)
|
||||
f.store_line("# test")
|
||||
@@ -103,7 +103,7 @@ func _run() -> void:
|
||||
var reachable: bool = await af._probe_tcp("127.0.0.1", 1, 0.3)
|
||||
_ck(reachable == false, "flow: _probe_tcp unreachable -> false")
|
||||
af._do_connect()
|
||||
_ck(fc2.connects.size() == 1 and fc2.connects[0][3] == 11011, "flow: connect uses serverinfo addr")
|
||||
_ck(fc2.connects.size() == 1 and fc2.connects[0][3] == 13002, "flow: connect uses serverinfo addr")
|
||||
if af._loading and af._loading.has_method("show_for"):
|
||||
af._loading.show_for("载入角色…") # 模拟 phase=select 时遮罩已亮
|
||||
fc2.char_list.emit([{"index": 0, "name": "Hero", "level": 42}])
|
||||
|
||||
@@ -6,8 +6,8 @@ config_version=5
|
||||
[application]
|
||||
|
||||
config/name="mtgodot-poc"
|
||||
# 完整客户端入口(登录→选人→进游戏)。旧的模型查看器 harness 仍可跑:
|
||||
# godot --path project res://main.tscn
|
||||
; 完整客户端入口(登录→选人→进游戏)。旧的模型查看器 harness 仍可跑:
|
||||
; godot --path project res://main.tscn
|
||||
run/main_scene="res://client_main.tscn"
|
||||
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||
|
||||
|
||||
@@ -243,7 +243,9 @@ func _ensure_spec() -> void:
|
||||
if race < 0 or assets_root == "":
|
||||
return
|
||||
var cls: String = CLASS_OF[race & 3]
|
||||
for rel in ["PC/ymir work/pc/%s/%s.msm" % [cls, cls],
|
||||
var suffix := "w" if race in [1, 3, 4, 6] else "m"
|
||||
for rel in ["root/msm/%s_%s.msm" % [cls, suffix],
|
||||
"PC/ymir work/pc/%s/%s.msm" % [cls, cls],
|
||||
"pc2/ymir work/pc2/%s/%s.msm" % [cls, cls],
|
||||
"season1/season1/pc/%s.msm" % cls]:
|
||||
var p := _resolve_asset(rel)
|
||||
|
||||
@@ -57,7 +57,7 @@ func shape(idx: int) -> Dictionary:
|
||||
if e.is_empty():
|
||||
return {}
|
||||
e = e.duplicate()
|
||||
e["path"] = shape_path
|
||||
e["path"] = String(e.get("special_path", shape_path))
|
||||
e["spec_dir"] = _dir
|
||||
return e
|
||||
|
||||
@@ -66,7 +66,7 @@ func hair(idx: int) -> Dictionary:
|
||||
if e.is_empty():
|
||||
return {}
|
||||
e = e.duplicate()
|
||||
e["path"] = hair_path
|
||||
e["path"] = String(e.get("special_path", hair_path))
|
||||
e["spec_dir"] = _dir
|
||||
return e
|
||||
|
||||
@@ -77,6 +77,7 @@ func _entry(g: Dictionary) -> Dictionary:
|
||||
"model": _s(g.get("Model", g.get("local_model", ""))).strip_edges(),
|
||||
"source_skin": _s(g.get("SourceSkin", "")).strip_edges(),
|
||||
"target_skin": _s(g.get("TargetSkin", "")).strip_edges(),
|
||||
"special_path": _s(g.get("SpecialPath", "")).strip_edges(),
|
||||
}
|
||||
|
||||
func _s(v) -> String:
|
||||
|
||||
Reference in New Issue
Block a user