feat: complete native client rendering parity updates
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
extends SceneTree
|
||||
|
||||
const PythonUISurface = preload("res://python_ui_surface.gd")
|
||||
const Python3DSurface = preload("res://python_3d_surface.gd")
|
||||
|
||||
var ui: Control
|
||||
var world: Node3D
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func _py(source: String) -> bool:
|
||||
return Metin2PythonHost.run_line(source) == ""
|
||||
|
||||
func _pump_until(seconds: float, condition: Callable) -> bool:
|
||||
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
if condition.call():
|
||||
return true
|
||||
await process_frame
|
||||
return condition.call()
|
||||
|
||||
func run() -> void:
|
||||
root.size = Vector2i(1024, 768)
|
||||
world = Python3DSurface.new()
|
||||
world.name = "Python3DSurface"
|
||||
ui = PythonUISurface.new()
|
||||
ui.name = "PythonUISurface"
|
||||
root.add_child(ui)
|
||||
root.add_child(world)
|
||||
|
||||
var err: String = ui.run_app()
|
||||
if err != "":
|
||||
print("ERROR: run_app: ", err)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
await _pump_until(15, func(): return _py(
|
||||
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr\n"
|
||||
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
|
||||
))
|
||||
|
||||
await _pump_until(20, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
|
||||
))
|
||||
|
||||
var host = OS.get_environment("MT_LIVE_HOST")
|
||||
var login = OS.get_environment("MT_LIVE_LOGIN")
|
||||
var pwd = OS.get_environment("MT_LIVE_PASSWORD")
|
||||
var auth_port = int(OS.get_environment("MT_LIVE_AUTH_PORT"))
|
||||
var game_port = int(OS.get_environment("MT_LIVE_GAME_PORT"))
|
||||
|
||||
_py("_stream.SetConnectInfo('%s', %d, '%s', %d)\n" % [host, game_port, host, auth_port]
|
||||
+ "_stream.curPhaseWindow.Connect('%s', '%s')" % [login, pwd])
|
||||
|
||||
await _pump_until(30, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
|
||||
))
|
||||
|
||||
_py("_stream.curPhaseWindow.SelectSlot(0)\n_stream.curPhaseWindow.StartGame()")
|
||||
|
||||
await _pump_until(30, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"
|
||||
))
|
||||
|
||||
for i in 60:
|
||||
await process_frame
|
||||
|
||||
print("=== CAMERA WORLD & HEIGHT PROFILE ===")
|
||||
var draws: Array = Metin2PythonHost.render3d_draws()
|
||||
if not draws.is_empty():
|
||||
var vm: PackedFloat32Array = draws[0]["view"]
|
||||
var cam_inv: Transform3D = Python3DSurface.d3d_transform(vm).affine_inverse()
|
||||
print("Camera world origin: ", cam_inv.origin)
|
||||
print("Camera world basis: ", cam_inv.basis)
|
||||
print("Camera looking direction: ", -cam_inv.basis.z)
|
||||
|
||||
for y in range(68200, 66200, -100):
|
||||
var bg_h = Metin2PythonHost.evaluate("str(background.GetHeight(59700.0, %f))" % float(y))
|
||||
var terrain_h = float(world.terrain.call("sample_height", 597.0, float(y) / 100.0)) * 100.0 if world.terrain else 0.0
|
||||
print("Profile y=%d: bg_h=%s terrain_h=%.2f" % [y, bg_h, terrain_h])
|
||||
|
||||
print("Total draws: ", draws.size())
|
||||
|
||||
for d in draws:
|
||||
var tex: String = d.get("texture0", "")
|
||||
if tex.contains("warrior") or tex.contains("stray_dog") or tex.contains("goods"):
|
||||
var wm: PackedFloat32Array = d["world"]
|
||||
var vm: PackedFloat32Array = d["view"]
|
||||
var xform: Transform3D = Python3DSurface.d3d_transform(wm)
|
||||
var cam_xform: Transform3D = Python3DSurface.d3d_transform(Python3DSurface.multiply(wm, vm))
|
||||
|
||||
var min_z := 1e9
|
||||
var max_z := -1e9
|
||||
var min_local_z := 1e9
|
||||
var max_local_z := -1e9
|
||||
for pos in d["positions"]:
|
||||
min_local_z = minf(min_local_z, pos.z)
|
||||
max_local_z = maxf(max_local_z, pos.z)
|
||||
var wp: Vector3 = xform * pos
|
||||
min_z = minf(min_z, wp.z)
|
||||
max_z = maxf(max_z, wp.z)
|
||||
|
||||
# Map position: (xform.origin.x, -xform.origin.y)
|
||||
var map_x := xform.origin.x
|
||||
var map_y := -xform.origin.y
|
||||
var actor_z := xform.origin.z
|
||||
|
||||
var bg_h = Metin2PythonHost.evaluate("str(background.GetHeight(%f, %f))" % [map_x, map_y])
|
||||
var terrain_h = float(world.terrain.call("sample_height", map_x / 100.0, map_y / 100.0)) * 100.0 if world.terrain else 0.0
|
||||
|
||||
print("Actor [%s]:" % tex.get_file())
|
||||
print(" World origin: ", xform.origin, " (map_x=%.1f, map_y=%.1f, origin_z=%.1f)" % [map_x, map_y, actor_z])
|
||||
print(" Local vert Z range: [%.2f, %.2f]" % [min_local_z, max_local_z])
|
||||
print(" World vert Z range: [%.2f, %.2f]" % [min_z, max_z])
|
||||
print(" Background Height: %s, Terrain Height: %.2f" % [bg_h, terrain_h])
|
||||
print(" Vert_Z min vs Height: diff = %.2f cm" % (min_z - float(bg_h)))
|
||||
print(" Origin_Z vs Height: diff = %.2f cm" % (actor_z - float(bg_h)))
|
||||
print(" Camera-space origin: ", cam_xform.origin)
|
||||
|
||||
if tex.contains("stray_dog"):
|
||||
print("--- DOG TERRAIN DETAIL ---")
|
||||
var from_map := Transform3D(Basis(Vector3(100, 0, 0), Vector3(0, 0, 100), Vector3(0, -100, 0)), Vector3.ZERO)
|
||||
var dog_world := xform.origin # (59700, -66400, 19851.5)
|
||||
print("Dog 40250 world: ", dog_world)
|
||||
for child in world.terrain.find_children("*", "MeshInstance3D", true, false):
|
||||
var mi := child as MeshInstance3D
|
||||
if mi.mesh is ArrayMesh:
|
||||
var am := mi.mesh as ArrayMesh
|
||||
var arrays := am.surface_get_arrays(0)
|
||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
for v in verts:
|
||||
var world_v: Vector3 = from_map * (mi.transform * v)
|
||||
if abs(world_v.x - dog_world.x) < 300.0 and abs(world_v.y - dog_world.y) < 300.0:
|
||||
print("Terrain local vert: ", v, " -> mi.xform*v: ", mi.transform * v, " -> from_map: ", world_v)
|
||||
print("Diff Z (terrain world Z - dog world Z): ", world_v.z - dog_world.z)
|
||||
break
|
||||
|
||||
|
||||
quit(0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b5vx3h3lipsuf
|
||||
@@ -0,0 +1,116 @@
|
||||
extends SceneTree
|
||||
|
||||
const PythonUISurface = preload("res://python_ui_surface.gd")
|
||||
const Python3DSurface = preload("res://python_3d_surface.gd")
|
||||
|
||||
var ui: Control
|
||||
var world: Node3D
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func _py(source: String) -> bool:
|
||||
return Metin2PythonHost.run_line(source) == ""
|
||||
|
||||
func _pump_until(seconds: float, condition: Callable) -> bool:
|
||||
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
if condition.call():
|
||||
return true
|
||||
await process_frame
|
||||
return condition.call()
|
||||
|
||||
func run() -> void:
|
||||
root.size = Vector2i(1024, 768)
|
||||
world = Python3DSurface.new()
|
||||
ui = PythonUISurface.new()
|
||||
root.add_child(ui)
|
||||
root.add_child(world)
|
||||
|
||||
ui.run_app()
|
||||
|
||||
await _pump_until(15, func(): return _py(
|
||||
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr\n"
|
||||
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
|
||||
))
|
||||
|
||||
await _pump_until(20, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
|
||||
))
|
||||
|
||||
var host = OS.get_environment("MT_LIVE_HOST")
|
||||
var login = OS.get_environment("MT_LIVE_LOGIN")
|
||||
var pwd = OS.get_environment("MT_LIVE_PASSWORD")
|
||||
var auth_port = int(OS.get_environment("MT_LIVE_AUTH_PORT"))
|
||||
var game_port = int(OS.get_environment("MT_LIVE_GAME_PORT"))
|
||||
|
||||
_py("_stream.SetConnectInfo('%s', %d, '%s', %d)\n" % [host, game_port, host, auth_port]
|
||||
+ "_stream.curPhaseWindow.Connect('%s', '%s')" % [login, pwd])
|
||||
|
||||
await _pump_until(30, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
|
||||
))
|
||||
|
||||
_py("_stream.curPhaseWindow.SelectSlot(0)\n_stream.curPhaseWindow.StartGame()")
|
||||
|
||||
await _pump_until(30, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"
|
||||
))
|
||||
|
||||
for i in 60:
|
||||
await process_frame
|
||||
|
||||
print("=== INVESTIGATING ALL 518 ACTORS ===")
|
||||
var draws: Array = Metin2PythonHost.render3d_draws()
|
||||
var actor_draws := {}
|
||||
for d in draws:
|
||||
var wm: PackedFloat32Array = d["world"]
|
||||
var key := "%.1f,%.1f,%.1f" % [wm[12], wm[13], wm[14]]
|
||||
if not actor_draws.has(key):
|
||||
actor_draws[key] = []
|
||||
actor_draws[key].append(d)
|
||||
|
||||
print("Unique actor positions: ", actor_draws.size())
|
||||
|
||||
var anomalies := []
|
||||
for key in actor_draws:
|
||||
var ds: Array = actor_draws[key]
|
||||
var wm: PackedFloat32Array = ds[0]["world"]
|
||||
var map_x := wm[12]
|
||||
var map_y := -wm[13]
|
||||
var actor_z := wm[14]
|
||||
|
||||
var bg_h = float(Metin2PythonHost.evaluate("str(background.GetHeight(%f, %f))" % [map_x, map_y]))
|
||||
var terrain_h = float(world.terrain.call("sample_height", map_x / 100.0, map_y / 100.0)) * 100.0 if world.terrain else 0.0
|
||||
|
||||
var diff_bg = actor_z - bg_h
|
||||
var diff_terrain = actor_z - terrain_h
|
||||
|
||||
# Find textures
|
||||
var texs := []
|
||||
var min_vert_z := 1e9
|
||||
for d in ds:
|
||||
var t: String = d.get("texture0", "")
|
||||
if not t.is_empty():
|
||||
texs.append(t.get_file())
|
||||
for p in d["positions"]:
|
||||
min_vert_z = minf(min_vert_z, actor_z + p.z)
|
||||
|
||||
if absf(diff_bg) > 5.0 or absf(diff_terrain) > 5.0:
|
||||
anomalies.append({
|
||||
"pos": [map_x, map_y, actor_z],
|
||||
"bg_h": bg_h,
|
||||
"terrain_h": terrain_h,
|
||||
"diff_bg": diff_bg,
|
||||
"diff_terrain": diff_terrain,
|
||||
"vert_min_diff": min_vert_z - bg_h,
|
||||
"textures": texs
|
||||
})
|
||||
|
||||
print("Found %d anomalies with gap > 5cm!" % anomalies.size())
|
||||
for a in anomalies.slice(0, 30):
|
||||
print("Anomaly: pos=(%.1f, %.1f, %.1f), bg_h=%.1f, terrain_h=%.1f, diff_bg=%.1f, diff_terrain=%.1f, vert_min_diff=%.1f, tex=%s" % [
|
||||
a["pos"][0], a["pos"][1], a["pos"][2], a["bg_h"], a["terrain_h"], a["diff_bg"], a["diff_terrain"], a["vert_min_diff"], a["textures"]
|
||||
])
|
||||
|
||||
quit(0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://xn0yhjq7h8d
|
||||
@@ -0,0 +1,143 @@
|
||||
extends SceneTree
|
||||
|
||||
const PythonUISurface = preload("res://python_ui_surface.gd")
|
||||
const Python3DSurface = preload("res://python_3d_surface.gd")
|
||||
|
||||
var ui: Control
|
||||
var world: Node3D
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func _py(source: String) -> bool:
|
||||
return Metin2PythonHost.run_line(source) == ""
|
||||
|
||||
func _key(keycode: Key, unicode: int, pressed: bool) -> void:
|
||||
var event := InputEventKey.new()
|
||||
event.keycode = keycode
|
||||
event.physical_keycode = keycode
|
||||
event.unicode = unicode
|
||||
event.pressed = pressed
|
||||
root.push_input(event)
|
||||
|
||||
func _press(keycode: Key) -> void:
|
||||
_key(keycode, 0, true)
|
||||
_key(keycode, 0, false)
|
||||
|
||||
func _type(text: String) -> void:
|
||||
for i in text.length():
|
||||
var ch := text.unicode_at(i)
|
||||
var keycode := OS.find_keycode_from_string(text[i].to_upper())
|
||||
_key(keycode, ch, true)
|
||||
_key(keycode, ch, false)
|
||||
|
||||
func _pump_until(seconds: float, condition: Callable) -> bool:
|
||||
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
if condition.call():
|
||||
return true
|
||||
await process_frame
|
||||
return condition.call()
|
||||
|
||||
func run() -> void:
|
||||
root.size = Vector2i(1024, 768)
|
||||
|
||||
world = Python3DSurface.new()
|
||||
world.name = "Python3DSurface"
|
||||
ui = PythonUISurface.new()
|
||||
ui.name = "PythonUISurface"
|
||||
root.add_child(ui)
|
||||
root.add_child(world)
|
||||
|
||||
var err: String = ui.run_app()
|
||||
if err != "":
|
||||
print("ERROR: run_app failed: ", err)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
await _pump_until(15, func(): return _py(
|
||||
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr\n"
|
||||
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
|
||||
))
|
||||
|
||||
await _pump_until(20, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
|
||||
))
|
||||
|
||||
_py(
|
||||
"_env = os.environ\n"
|
||||
+ "_slot = int(_env.get('MT_LIVE_SLOT', '0'))\n"
|
||||
+ "_stream.SetConnectInfo(_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_GAME_PORT', '13000')), "
|
||||
+ "_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_AUTH_PORT', '11000')))\n"
|
||||
+ "_w = _stream.curPhaseWindow\n"
|
||||
+ "_w._LoginWindow__OpenLoginBoard()\n"
|
||||
+ "_w.idEditLine.SetText('')\n"
|
||||
+ "_w.pwdEditLine.SetText('')\n"
|
||||
+ "_w.idEditLine.SetFocus()"
|
||||
)
|
||||
_type(OS.get_environment("MT_LIVE_LOGIN"))
|
||||
_press(KEY_TAB)
|
||||
_type(OS.get_environment("MT_LIVE_PASSWORD"))
|
||||
_py("del _env, _w")
|
||||
_press(KEY_ENTER)
|
||||
|
||||
await _pump_until(30, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
|
||||
))
|
||||
|
||||
_py("_stream.curPhaseWindow.SelectSlot(_slot)\n_stream.curPhaseWindow.StartGame()")
|
||||
|
||||
await _pump_until(30, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"
|
||||
))
|
||||
|
||||
for i in 30:
|
||||
await process_frame
|
||||
|
||||
print("=== DEBUG HEIGHT REPORT ===")
|
||||
var test_points = [
|
||||
Vector2(59700, 68200),
|
||||
Vector2(59700, 66400),
|
||||
Vector2(60100, 66400),
|
||||
Vector2(50000, 50000),
|
||||
Vector2(30000, 30000),
|
||||
Vector2(70000, 70000),
|
||||
]
|
||||
for pt in test_points:
|
||||
var bg_h = Metin2PythonHost.evaluate("str(background.GetHeight(%f, %f))" % [pt.x, pt.y])
|
||||
var tm_h = float(world.terrain.call("sample_height", pt.x / 100.0, pt.y / 100.0)) * 100.0 if world.terrain else 0.0
|
||||
print("Point (%.0f, %.0f): 40250_bg_h=%s, Metin2World_h=%.1f, diff=%.1f" % [
|
||||
pt.x, pt.y, bg_h, tm_h, float(bg_h) - tm_h
|
||||
])
|
||||
|
||||
# Inspect terrain mesh triangles around dog
|
||||
print("=== TERRAIN MESH VERTICES AROUND DOG (59700, -66400) ===")
|
||||
# In camera space (since Camera3D is at origin):
|
||||
# Dog world pos in 40250: (59700, -66400, 19851.5)
|
||||
# What is dog in camera space?
|
||||
var draws = Metin2PythonHost.render3d_draws()
|
||||
for i in draws.size():
|
||||
var d = draws[i]
|
||||
var tex: String = d.get("texture0", "")
|
||||
if tex.contains("stray_dog"):
|
||||
var wm: PackedFloat32Array = d.get("world", PackedFloat32Array())
|
||||
var vm: PackedFloat32Array = d.get("view", PackedFloat32Array())
|
||||
var xform: Transform3D = Python3DSurface.d3d_transform(Python3DSurface.multiply(wm, vm))
|
||||
print("DOG camera-space transform origin: ", xform.origin)
|
||||
|
||||
# Now let's check terrain mesh global transform and vertices:
|
||||
for child in world.terrain.find_children("*", "MeshInstance3D", true, false):
|
||||
var mi := child as MeshInstance3D
|
||||
if mi.mesh is ArrayMesh:
|
||||
var am := mi.mesh as ArrayMesh
|
||||
var arrays := am.surface_get_arrays(0)
|
||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
for v in verts:
|
||||
var gv: Vector3 = mi.global_transform * v
|
||||
# Dog in camera space is around xform.origin. Let's find terrain vertices within 200cm:
|
||||
if abs(gv.x - xform.origin.x) < 200.0 and abs(gv.z - xform.origin.z) < 200.0:
|
||||
print("Terrain vertex near dog in camera-space: ", gv, " dog origin: ", xform.origin, " diff_y (height in cam space): ", gv.y - xform.origin.y)
|
||||
break
|
||||
break
|
||||
|
||||
quit(0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://mpcddwfiipkc
|
||||
@@ -0,0 +1,71 @@
|
||||
extends SceneTree
|
||||
|
||||
const PythonUISurface = preload("res://python_ui_surface.gd")
|
||||
const Python3DSurface = preload("res://python_3d_surface.gd")
|
||||
|
||||
var ui: Control
|
||||
var world: Node3D
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func _py(source: String) -> bool:
|
||||
return Metin2PythonHost.run_line(source) == ""
|
||||
|
||||
func _pump_until(seconds: float, condition: Callable) -> bool:
|
||||
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
if condition.call():
|
||||
return true
|
||||
await process_frame
|
||||
return condition.call()
|
||||
|
||||
func run() -> void:
|
||||
root.size = Vector2i(1024, 768)
|
||||
world = Python3DSurface.new()
|
||||
world.name = "Python3DSurface"
|
||||
ui = PythonUISurface.new()
|
||||
ui.name = "PythonUISurface"
|
||||
root.add_child(ui)
|
||||
root.add_child(world)
|
||||
|
||||
var err: String = ui.run_app()
|
||||
if err != "":
|
||||
print("ERROR: run_app: ", err)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
await _pump_until(15, func(): return _py(
|
||||
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr\n"
|
||||
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
|
||||
))
|
||||
|
||||
await _pump_until(20, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
|
||||
))
|
||||
|
||||
var host = OS.get_environment("MT_LIVE_HOST")
|
||||
var login = OS.get_environment("MT_LIVE_LOGIN")
|
||||
var pwd = OS.get_environment("MT_LIVE_PASSWORD")
|
||||
var auth_port = int(OS.get_environment("MT_LIVE_AUTH_PORT"))
|
||||
var game_port = int(OS.get_environment("MT_LIVE_GAME_PORT"))
|
||||
|
||||
_py("_stream.SetConnectInfo('%s', %d, '%s', %d)\n" % [host, game_port, host, auth_port]
|
||||
+ "_stream.curPhaseWindow.Connect('%s', '%s')" % [login, pwd])
|
||||
|
||||
await _pump_until(30, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
|
||||
))
|
||||
|
||||
print("=== IN INTRO SELECT PHASE ===")
|
||||
for i in 60:
|
||||
await process_frame
|
||||
|
||||
var cmds: Array = Metin2PythonHost.ui_render_commands()
|
||||
print("UI commands count: ", cmds.size())
|
||||
var file = FileAccess.open("/tmp/select_ui_cmds.json", FileAccess.WRITE)
|
||||
file.store_string(JSON.stringify(cmds, " "))
|
||||
file.close()
|
||||
print("Saved /tmp/select_ui_cmds.json")
|
||||
|
||||
quit(0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c0k854rmu55u7
|
||||
@@ -0,0 +1,70 @@
|
||||
extends SceneTree
|
||||
|
||||
const PythonUISurface = preload("res://python_ui_surface.gd")
|
||||
const Python3DSurface = preload("res://python_3d_surface.gd")
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func _py(source: String) -> bool:
|
||||
return Metin2PythonHost.run_line(source) == ""
|
||||
|
||||
func _pump_until(seconds: float, condition: Callable) -> bool:
|
||||
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
if condition.call():
|
||||
return true
|
||||
await process_frame
|
||||
return condition.call()
|
||||
|
||||
func run() -> void:
|
||||
root.size = Vector2i(1024, 768)
|
||||
var world = Python3DSurface.new()
|
||||
var ui = PythonUISurface.new()
|
||||
root.add_child(ui)
|
||||
root.add_child(world)
|
||||
|
||||
ui.run_app()
|
||||
|
||||
await _pump_until(15, func(): return _py(
|
||||
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr\n"
|
||||
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
|
||||
))
|
||||
await _pump_until(20, func(): return _py("assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"))
|
||||
|
||||
var host = OS.get_environment("MT_LIVE_HOST")
|
||||
var login = OS.get_environment("MT_LIVE_LOGIN")
|
||||
var pwd = OS.get_environment("MT_LIVE_PASSWORD")
|
||||
var auth_port = int(OS.get_environment("MT_LIVE_AUTH_PORT"))
|
||||
var game_port = int(OS.get_environment("MT_LIVE_GAME_PORT"))
|
||||
|
||||
_py("_stream.SetConnectInfo('%s', %d, '%s', %d)\n_stream.curPhaseWindow.Connect('%s', '%s')" % [host, game_port, host, auth_port, login, pwd])
|
||||
await _pump_until(30, func(): return _py("assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"))
|
||||
_py("_stream.curPhaseWindow.SelectSlot(0)\n_stream.curPhaseWindow.StartGame()")
|
||||
await _pump_until(30, func(): return _py("assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"))
|
||||
|
||||
for i in 60:
|
||||
await process_frame
|
||||
|
||||
print("=== INSPECTING STONE TILES AROUND PLAYER (59700, 68200) ===")
|
||||
var draws: Array = Metin2PythonHost.render3d_draws()
|
||||
var player_z := 19851.5
|
||||
|
||||
for d in draws:
|
||||
var tex: String = d.get("texture0", "")
|
||||
if tex.contains("stone") or tex.contains("floor") or tex.contains("tile") or tex.contains("sign"):
|
||||
var wm: PackedFloat32Array = d["world"]
|
||||
var xform: Transform3D = Python3DSurface.d3d_transform(wm)
|
||||
# check if this object is near player (within 2000cm):
|
||||
if absf(xform.origin.x - 59700.0) < 3000.0 and absf(-xform.origin.y - 68200.0) < 3000.0:
|
||||
var min_z := 1e9
|
||||
var max_z := -1e9
|
||||
for p in d["positions"]:
|
||||
var wp: Vector3 = xform * p
|
||||
min_z = minf(min_z, wp.z)
|
||||
max_z = maxf(max_z, wp.z)
|
||||
print("Object [%s] at (%.1f, %.1f, %.1f), Z range: [%.2f, %.2f], diff vs player_z(19851.5): min=%.2f, max=%.2f" % [
|
||||
tex.get_file(), xform.origin.x, -xform.origin.y, xform.origin.z, min_z, max_z, min_z - player_z, max_z - player_z
|
||||
])
|
||||
|
||||
quit(0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cy6rh17pn00hd
|
||||
@@ -0,0 +1,115 @@
|
||||
extends SceneTree
|
||||
|
||||
const PythonUISurface = preload("res://python_ui_surface.gd")
|
||||
const Python3DSurface = preload("res://python_3d_surface.gd")
|
||||
|
||||
var ui: Control
|
||||
var world: Node3D
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func _py(source: String) -> bool:
|
||||
return Metin2PythonHost.run_line(source) == ""
|
||||
|
||||
func _pump_until(seconds: float, condition: Callable) -> bool:
|
||||
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
if condition.call():
|
||||
return true
|
||||
await process_frame
|
||||
return condition.call()
|
||||
|
||||
func run() -> void:
|
||||
root.size = Vector2i(1024, 768)
|
||||
world = Python3DSurface.new()
|
||||
world.name = "Python3DSurface"
|
||||
ui = PythonUISurface.new()
|
||||
ui.name = "PythonUISurface"
|
||||
root.add_child(ui)
|
||||
root.add_child(world)
|
||||
|
||||
var err: String = ui.run_app()
|
||||
if err != "":
|
||||
print("ERROR: run_app: ", err)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
await _pump_until(15, func(): return _py(
|
||||
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr, app\n"
|
||||
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
|
||||
))
|
||||
|
||||
await _pump_until(20, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
|
||||
))
|
||||
|
||||
var host = OS.get_environment("MT_LIVE_HOST")
|
||||
var login = OS.get_environment("MT_LIVE_LOGIN")
|
||||
var pwd = OS.get_environment("MT_LIVE_PASSWORD")
|
||||
var auth_port = int(OS.get_environment("MT_LIVE_AUTH_PORT"))
|
||||
var game_port = int(OS.get_environment("MT_LIVE_GAME_PORT"))
|
||||
|
||||
_py("_stream.SetConnectInfo('%s', %d, '%s', %d)\n" % [host, game_port, host, auth_port]
|
||||
+ "_stream.curPhaseWindow.Connect('%s', '%s')" % [login, pwd])
|
||||
|
||||
await _pump_until(30, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
|
||||
))
|
||||
|
||||
_py("_stream.curPhaseWindow.SelectSlot(0)\n_stream.curPhaseWindow.StartGame()")
|
||||
|
||||
await _pump_until(30, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"
|
||||
))
|
||||
|
||||
print("=== IN-GAME LOADED ===")
|
||||
for i in 10:
|
||||
await process_frame
|
||||
|
||||
print("--- EQUIPPING WEAPON 10 ---")
|
||||
_py("chr.SelectInstance(player.GetMainCharacterIndex())\nchr.SetWeapon(10)")
|
||||
for i in 10:
|
||||
await process_frame
|
||||
|
||||
var check_draws = func(label: String):
|
||||
var draws: Array = Metin2PythonHost.render3d_draws()
|
||||
var body_draw = null
|
||||
var weapon_draws = []
|
||||
for d in draws:
|
||||
var tex: String = d.get("texture0", "").to_lower()
|
||||
if tex.contains("warrior") and not tex.contains("hair"):
|
||||
body_draw = d
|
||||
elif tex.contains("weapon") or tex.contains("sword") or tex.contains("00010") or tex.contains("00040"):
|
||||
weapon_draws.append(d)
|
||||
print("[%s] Total draws=%d, weapon_draws=%d" % [label, draws.size(), weapon_draws.size()])
|
||||
var b_xf: Transform3D = Transform3D()
|
||||
if body_draw != null:
|
||||
var wm: PackedFloat32Array = body_draw["world"]
|
||||
b_xf = Python3DSurface.d3d_transform(wm)
|
||||
print(" Body origin: ", b_xf.origin, " tex=", body_draw.get("texture0", ""))
|
||||
else:
|
||||
print(" Body draw not found!")
|
||||
for w in weapon_draws:
|
||||
var wm: PackedFloat32Array = w["world"]
|
||||
var xf: Transform3D = Python3DSurface.d3d_transform(wm)
|
||||
var dist := xf.origin.distance_to(b_xf.origin) if body_draw != null else 0.0
|
||||
print(" Weapon origin: ", xf.origin, " dist_to_body=%.2f cm tex=%s" % [dist, w.get("texture0", "")])
|
||||
|
||||
check_draws.call("IDLE_FRAME_0")
|
||||
|
||||
# Start moving!
|
||||
print("--- STARTING MOVEMENT (DIK_UP) ---")
|
||||
_py("player.SetSingleDIKKeyState(app.DIK_UP, True)")
|
||||
|
||||
for frame in range(1, 40):
|
||||
await process_frame
|
||||
if frame % 5 == 0:
|
||||
check_draws.call("MOVING_FRAME_%d" % frame)
|
||||
|
||||
_py("player.SetSingleDIKKeyState(app.DIK_UP, False)")
|
||||
for frame in range(1, 15):
|
||||
await process_frame
|
||||
check_draws.call("STOPPED")
|
||||
|
||||
quit(0)
|
||||
+164
-29
@@ -12,11 +12,13 @@
|
||||
# in the recorded D3D camera's coordinate frame, and its .msenv supplies the character light.
|
||||
extends Node3D
|
||||
|
||||
const AssetRoot = preload("res://asset_root.gd")
|
||||
const UiAssets = preload("res://ui/ui_assets.gd")
|
||||
|
||||
# D3DCULL / D3DBLEND / D3DCMPFUNC values used below (D3D8Types.h).
|
||||
const D3DCULL_NONE := 1
|
||||
const D3DCULL_CW := 2
|
||||
const D3DCULL_CCW := 3
|
||||
const D3DBLEND_ONE := 2
|
||||
const D3DBLEND_SRCALPHA := 5
|
||||
const D3DBLEND_INVSRCALPHA := 6
|
||||
@@ -31,6 +33,11 @@ var _atlas: Array = []
|
||||
var _terrain_attempted := false
|
||||
var _last_terrain_focus := Vector2(INF, INF)
|
||||
var _meshes: Array[MeshInstance3D] = []
|
||||
var _geometry_cache := {}
|
||||
var _geometry_frame := 0
|
||||
var _bg_mesh: MeshInstance3D
|
||||
var _bg_mat: StandardMaterial3D
|
||||
var _bg_quad: QuadMesh
|
||||
# Materials keyed by texture and state, so an unchanged draw keeps its material from frame to frame.
|
||||
var _materials := {}
|
||||
|
||||
@@ -39,6 +46,7 @@ var draw_count := 0
|
||||
var unlit_stand_in_count := 0 # Compatibility report field; real lighting keeps this at zero.
|
||||
|
||||
func _ready() -> void:
|
||||
get_viewport().transparent_bg = true
|
||||
camera = Camera3D.new()
|
||||
camera.name = "GameCamera"
|
||||
camera.keep_aspect = Camera3D.KEEP_HEIGHT
|
||||
@@ -53,8 +61,7 @@ func _ready() -> void:
|
||||
add_child(light)
|
||||
environment = WorldEnvironment.new()
|
||||
environment.environment = Environment.new()
|
||||
environment.environment.background_mode = Environment.BG_COLOR
|
||||
environment.environment.background_color = Color.BLACK
|
||||
environment.environment.background_mode = Environment.BG_CLEAR_COLOR
|
||||
environment.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
environment.environment.ambient_light_color = Color.BLACK
|
||||
environment.environment.tonemap_mode = Environment.TONE_MAPPER_LINEAR
|
||||
@@ -62,7 +69,13 @@ func _ready() -> void:
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if Metin2PythonHost.is_running():
|
||||
var profile_start := Time.get_ticks_usec() if _profile_frame else 0
|
||||
update_frame()
|
||||
if profile_start != 0:
|
||||
profile_update_ms = float(Time.get_ticks_usec() - profile_start) / 1000.0
|
||||
|
||||
var _profile_frame := OS.has_environment("MT_PROFILE_FRAME")
|
||||
var profile_update_ms := 0.0
|
||||
|
||||
# D3D row-vector matrix (v' = v * M) as a Godot Transform3D (v' = T * v): the rows of M are the basis
|
||||
# columns, row 3 is the origin.
|
||||
@@ -70,18 +83,8 @@ static func d3d_transform(m: PackedFloat32Array) -> Transform3D:
|
||||
return Transform3D(Basis(Vector3(m[0], m[1], m[2]), Vector3(m[4], m[5], m[6]), Vector3(m[8], m[9], m[10])),
|
||||
Vector3(m[12], m[13], m[14]))
|
||||
|
||||
static func multiply(a: PackedFloat32Array, b: PackedFloat32Array) -> PackedFloat32Array:
|
||||
var out := PackedFloat32Array()
|
||||
out.resize(16)
|
||||
for r in 4:
|
||||
for c in 4:
|
||||
var sum := 0.0
|
||||
for k in 4:
|
||||
sum += a[r * 4 + k] * b[k * 4 + c]
|
||||
out[r * 4 + c] = sum
|
||||
return out
|
||||
|
||||
func update_frame() -> void:
|
||||
_geometry_frame += 1
|
||||
var draws: Array = Metin2PythonHost.render3d_draws()
|
||||
var native_map: String = Metin2PythonHost.current_map_name()
|
||||
if terrain != null and not native_map.is_empty() and native_map != map_path:
|
||||
@@ -93,6 +96,7 @@ func update_frame() -> void:
|
||||
var shown := 0
|
||||
var camera_set := false
|
||||
var light_set := false
|
||||
var terrain_view := PackedFloat32Array()
|
||||
for draw in draws:
|
||||
# XYZRHW (screen-space) draws belong to the 2D pass; lines are debug geometry.
|
||||
if draw["pretransformed"] or draw["lines"] or draw["indices"].is_empty():
|
||||
@@ -102,21 +106,25 @@ func update_frame() -> void:
|
||||
if proj[11] != -1.0:
|
||||
continue
|
||||
if not camera_set:
|
||||
_apply_projection(proj)
|
||||
_apply_projection(proj, draw)
|
||||
camera_set = true
|
||||
var view: PackedFloat32Array = draw["view"]
|
||||
if terrain == null and not _terrain_attempted:
|
||||
_load_terrain(draw)
|
||||
if terrain != null:
|
||||
_place_terrain(view)
|
||||
terrain_view = view
|
||||
if not light_set and draw["light0"]:
|
||||
_apply_light(draw, view)
|
||||
light_set = true
|
||||
var instance := _mesh_instance(shown)
|
||||
instance.transform = d3d_transform(multiply(draw["world"], view))
|
||||
instance.mesh = _build_mesh(draw)
|
||||
instance.transform = d3d_transform(view) * d3d_transform(draw["world"])
|
||||
instance.mesh = _mesh_for_draw(draw)
|
||||
instance.material_override = _material(draw)
|
||||
instance.sorting_offset = float(shown) * 0.01
|
||||
instance.visible = true
|
||||
shown += 1
|
||||
if terrain != null and not terrain_view.is_empty():
|
||||
_place_terrain(terrain_view)
|
||||
for i in range(shown, _meshes.size()):
|
||||
_meshes[i].visible = false
|
||||
_meshes[i].mesh = null
|
||||
@@ -124,17 +132,75 @@ func update_frame() -> void:
|
||||
light.visible = false
|
||||
draw_count = shown
|
||||
unlit_stand_in_count = 0
|
||||
_update_background()
|
||||
if _geometry_frame % 120 == 0:
|
||||
for key in _geometry_cache.keys():
|
||||
if _geometry_frame - int(_geometry_cache[key][2]) > 120:
|
||||
_geometry_cache.erase(key)
|
||||
|
||||
func _update_background() -> void:
|
||||
var bg_cmd: Dictionary = {}
|
||||
for cmd in Metin2PythonHost.ui_render_commands_batched():
|
||||
if cmd.get("behind_3d", false) and cmd.get("kind", "") == "image":
|
||||
bg_cmd = cmd
|
||||
break
|
||||
if bg_cmd.is_empty():
|
||||
if _bg_mesh != null:
|
||||
_bg_mesh.visible = false
|
||||
return
|
||||
|
||||
if _bg_mesh == null:
|
||||
_bg_mesh = MeshInstance3D.new()
|
||||
_bg_mesh.name = "BackgroundQuad"
|
||||
_bg_mesh.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
_bg_quad = QuadMesh.new()
|
||||
_bg_mesh.mesh = _bg_quad
|
||||
_bg_mat = StandardMaterial3D.new()
|
||||
_bg_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
_bg_mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_ALWAYS
|
||||
_bg_mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
_bg_mesh.material_override = _bg_mat
|
||||
add_child(_bg_mesh)
|
||||
|
||||
var bg_tex_name: String = bg_cmd.get("text", "")
|
||||
var tex := UiAssets.load_tex(AssetRoot.path(), bg_tex_name)
|
||||
if tex is AtlasTexture:
|
||||
var at: AtlasTexture = tex
|
||||
_bg_mat.albedo_texture = at.atlas
|
||||
var asize: Vector2 = at.atlas.get_size()
|
||||
var r: Rect2 = at.region
|
||||
_bg_mat.uv1_scale = Vector3(r.size.x / asize.x, r.size.y / asize.y, 1.0)
|
||||
_bg_mat.uv1_offset = Vector3(r.position.x / asize.x, r.position.y / asize.y, 0.0)
|
||||
else:
|
||||
_bg_mat.albedo_texture = tex
|
||||
if bg_cmd.has("uv"):
|
||||
var uvs: PackedVector2Array = bg_cmd["uv"]
|
||||
_bg_mat.uv1_scale = Vector3(uvs[3].x - uvs[0].x, uvs[3].y - uvs[0].y, 1.0)
|
||||
_bg_mat.uv1_offset = Vector3(uvs[0].x, uvs[0].y, 0.0)
|
||||
else:
|
||||
_bg_mat.uv1_scale = Vector3(1.0, 1.0, 1.0)
|
||||
_bg_mat.uv1_offset = Vector3(0.0, 0.0, 0.0)
|
||||
|
||||
var vp_size := get_viewport().get_visible_rect().size
|
||||
var dist := 2800.0
|
||||
var v_size := 2.0 * dist * tan(deg_to_rad(camera.fov) / 2.0)
|
||||
var h_size := v_size * (float(vp_size.x) / float(vp_size.y))
|
||||
_bg_quad.size = Vector2(h_size, v_size)
|
||||
_bg_mesh.transform = Transform3D(Basis(), Vector3(0.0, 0.0, -dist))
|
||||
_bg_mesh.visible = true
|
||||
|
||||
func _load_terrain(draw: Dictionary) -> void:
|
||||
if not ClassDB.class_exists("Metin2World"):
|
||||
return
|
||||
var native_map: String = Metin2PythonHost.current_map_name()
|
||||
if native_map.is_empty():
|
||||
return
|
||||
var camera_world := d3d_transform(draw["view"]).affine_inverse().origin
|
||||
var focus := Vector2(camera_world.x, -camera_world.y)
|
||||
if focus.distance_to(_last_terrain_focus) < 1000.0:
|
||||
return
|
||||
_last_terrain_focus = focus
|
||||
var native_map: String = Metin2PythonHost.current_map_name()
|
||||
map_path = native_map if not native_map.is_empty() else _map_for_position(focus)
|
||||
map_path = native_map
|
||||
terrain = ClassDB.instantiate("Metin2World")
|
||||
terrain.name = "PackTerrain"
|
||||
terrain.set("auto_load", false)
|
||||
@@ -212,11 +278,38 @@ func _place_terrain(view: PackedFloat32Array) -> void:
|
||||
Vector3(0, -100, 0)), Vector3.ZERO)
|
||||
terrain.transform = d3d_transform(view) * from_map
|
||||
|
||||
func _apply_projection(proj: PackedFloat32Array) -> void:
|
||||
func _apply_projection(proj: PackedFloat32Array, draw: Dictionary = {}) -> void:
|
||||
# D3DXMatrixPerspectiveFovRH: _22 = cot(fovy / 2), _33 = zf / (zn - zf), _43 = zn * zf / (zn - zf).
|
||||
camera.fov = rad_to_deg(2.0 * atan(1.0 / proj[5]))
|
||||
camera.near = proj[14] / proj[10]
|
||||
camera.far = proj[14] / (proj[10] + 1.0)
|
||||
var fov_deg := rad_to_deg(2.0 * atan(1.0 / proj[5]))
|
||||
var zn: float = proj[14] / proj[10]
|
||||
var zf: float = proj[14] / (proj[10] + 1.0)
|
||||
camera.fov = fov_deg
|
||||
camera.near = zn
|
||||
camera.far = zf
|
||||
|
||||
var vp_offset := Vector2.ZERO
|
||||
if draw.has("viewport"):
|
||||
var vp: PackedFloat32Array = draw["viewport"]
|
||||
var vp_x: float = vp[0]
|
||||
var vp_y: float = vp[1]
|
||||
var vp_w: float = vp[2]
|
||||
var vp_h: float = vp[3]
|
||||
var vp_size := get_viewport().get_visible_rect().size
|
||||
if vp_w > 0.0 and vp_h > 0.0 and (vp_w < vp_size.x or vp_h < vp_size.y or vp_x > 0.0 or vp_y > 0.0):
|
||||
var offset_px_x: float = (vp_x + vp_w / 2.0) - (vp_size.x / 2.0)
|
||||
var offset_px_y: float = (vp_y + vp_h / 2.0) - (vp_size.y / 2.0)
|
||||
if absf(offset_px_x) > 0.5 or absf(offset_px_y) > 0.5:
|
||||
var v_size_near := 2.0 * zn * tan(deg_to_rad(fov_deg) / 2.0)
|
||||
var h_size_near := v_size_near * (vp_size.x / vp_size.y)
|
||||
var off_near_x := (offset_px_x / vp_size.x) * h_size_near
|
||||
var off_near_y := -(offset_px_y / vp_size.y) * v_size_near
|
||||
vp_offset = Vector2(-off_near_x, off_near_y)
|
||||
|
||||
var v_size_near := 2.0 * zn * tan(deg_to_rad(fov_deg) / 2.0)
|
||||
if vp_offset != Vector2.ZERO:
|
||||
camera.set_frustum(v_size_near, vp_offset, zn, zf)
|
||||
else:
|
||||
camera.set_perspective(fov_deg, zn, zf)
|
||||
|
||||
func _apply_light(draw: Dictionary, view: PackedFloat32Array) -> void:
|
||||
var view_basis := d3d_transform(view).basis
|
||||
@@ -253,6 +346,19 @@ func _mesh_instance(index: int) -> MeshInstance3D:
|
||||
_meshes.append(instance)
|
||||
return _meshes[index]
|
||||
|
||||
func _mesh_for_draw(draw: Dictionary) -> ArrayMesh:
|
||||
var key: int = draw.get("geometry_key", 0)
|
||||
if key == 0:
|
||||
return _build_mesh(draw)
|
||||
var revision: int = draw.get("geometry_revision", 0)
|
||||
var cached: Array = _geometry_cache.get(key, [])
|
||||
if not cached.is_empty() and int(cached[0]) == revision:
|
||||
cached[2] = _geometry_frame
|
||||
return cached[1]
|
||||
var mesh := _build_mesh(draw)
|
||||
_geometry_cache[key] = [revision, mesh, _geometry_frame]
|
||||
return mesh
|
||||
|
||||
func _build_mesh(draw: Dictionary) -> ArrayMesh:
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
@@ -266,15 +372,27 @@ func _build_mesh(draw: Dictionary) -> ArrayMesh:
|
||||
arrays[Mesh.ARRAY_INDEX] = draw["indices"]
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
mesh.surface_set_material(0, _material(draw))
|
||||
return mesh
|
||||
|
||||
func _material(draw: Dictionary) -> StandardMaterial3D:
|
||||
var lit: bool = draw["lighting"]
|
||||
var z_write: int = draw.get("z_write", 1)
|
||||
var tf: int = int(draw.get("texture_factor", 0xFFFFFFFF))
|
||||
var tf_a: float = float((tf >> 24) & 0xFF) / 255.0
|
||||
var tf_r: float = float((tf >> 16) & 0xFF) / 255.0
|
||||
var tf_g: float = float((tf >> 8) & 0xFF) / 255.0
|
||||
var tf_b: float = float(tf & 0xFF) / 255.0
|
||||
var factor_color := Color(tf_r, tf_g, tf_b, tf_a)
|
||||
|
||||
var uses_tf: bool = bool(draw.get("uses_tf", false))
|
||||
var has_tf: bool = uses_tf and (tf != 0xFFFFFFFF and tf != -1)
|
||||
var tf_key := "%02x%02x%02x%02x" % [int(tf_r * 63.0), int(tf_g * 63.0), int(tf_b * 63.0), int(tf_a * 63.0)] if has_tf else ""
|
||||
var has_diffuse: bool = draw.has("diffuse")
|
||||
var is_alpha: bool = draw["alpha_blend"] or (has_tf and factor_color.a < 0.99)
|
||||
# alpha_blend / alpha_test / lit are bools: %d needs them as ints.
|
||||
var key := "%s|%d|%d|%d|%d|%d|%d|%d|%s|%s" % [draw["texture0"], int(draw["alpha_blend"]), draw["src_blend"],
|
||||
var key := "%s|%d|%d|%d|%d|%d|%d|%d|%d|%d|%s|%s|%s" % [draw["texture0"], int(draw["alpha_blend"]), draw["src_blend"],
|
||||
draw["dest_blend"], int(draw["alpha_test"]), draw["alpha_ref"], draw["cull_mode"], int(lit),
|
||||
draw["material_diffuse"], draw["material_emissive"]]
|
||||
int(has_diffuse), z_write, draw["material_diffuse"], draw["material_emissive"], tf_key]
|
||||
if _materials.has(key):
|
||||
return _materials[key]
|
||||
var material := StandardMaterial3D.new()
|
||||
@@ -282,7 +400,14 @@ func _material(draw: Dictionary) -> StandardMaterial3D:
|
||||
if not texture_name.is_empty():
|
||||
# The ported client reads its models from 40250's packs; the texture must come from there too.
|
||||
material.albedo_texture = UiAssets.load_pack_tex(texture_name)
|
||||
material.albedo_color = draw["material_diffuse"] if lit else Color.WHITE
|
||||
|
||||
var base_diffuse: Color = draw["material_diffuse"] if lit else Color.WHITE
|
||||
if has_tf:
|
||||
material.albedo_color = Color(base_diffuse.r * factor_color.r, base_diffuse.g * factor_color.g,
|
||||
base_diffuse.b * factor_color.b, base_diffuse.a * factor_color.a)
|
||||
else:
|
||||
material.albedo_color = base_diffuse
|
||||
|
||||
material.emission_enabled = lit and draw["material_emissive"] != Color(0, 0, 0, 0)
|
||||
if material.emission_enabled:
|
||||
material.emission = draw["material_emissive"]
|
||||
@@ -296,18 +421,28 @@ func _material(draw: Dictionary) -> StandardMaterial3D:
|
||||
else BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.specular_mode = BaseMaterial3D.SPECULAR_DISABLED
|
||||
material.roughness = 1.0
|
||||
material.vertex_color_use_as_albedo = draw.has("diffuse")
|
||||
material.vertex_color_use_as_albedo = has_diffuse
|
||||
match int(draw["cull_mode"]):
|
||||
D3DCULL_NONE:
|
||||
material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
D3DCULL_CW:
|
||||
material.cull_mode = BaseMaterial3D.CULL_BACK
|
||||
D3DCULL_CCW:
|
||||
material.cull_mode = BaseMaterial3D.CULL_FRONT
|
||||
_:
|
||||
material.cull_mode = BaseMaterial3D.CULL_BACK
|
||||
if draw["alpha_blend"]:
|
||||
if z_write == 0:
|
||||
material.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED
|
||||
elif is_alpha:
|
||||
material.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_ALWAYS
|
||||
else:
|
||||
material.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_OPAQUE_ONLY
|
||||
if is_alpha:
|
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
if int(draw["dest_blend"]) == D3DBLEND_ONE:
|
||||
material.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
|
||||
else:
|
||||
material.blend_mode = BaseMaterial3D.BLEND_MODE_MIX
|
||||
elif draw["alpha_test"]:
|
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
|
||||
material.alpha_scissor_threshold = float(draw["alpha_ref"]) / 255.0
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# 登录 → 选人 → 进游戏,把 RenderGame 的绘制命令(python_3d_surface.gd)画出来并截图。
|
||||
#
|
||||
# godot --path project --script res://python_game_render_test.gd
|
||||
# MT_FAKE_MOB_COUNT=24 MT_PROFILE_FRAME=1 MT_PROFILE_COMBAT=1 script/python_game_render_test.sh
|
||||
#
|
||||
# 服务器与账号只从环境变量读(MT_LIVE_HOST / MT_LIVE_LOGIN / MT_LIVE_PASSWORD,可选 MT_LIVE_AUTH_PORT
|
||||
# 11000、MT_LIVE_GAME_PORT 13000、MT_LIVE_SLOT 0);Python 侧也用 os.environ 取,不进源码文本。
|
||||
@@ -72,8 +73,12 @@ func run() -> void:
|
||||
var output := OS.get_environment("MT_RENDER_OUTPUT")
|
||||
if output.is_empty():
|
||||
output = ProjectSettings.globalize_path("res://../build/rendering/python-game-%d" % Time.get_unix_time_from_system())
|
||||
elif not output.is_absolute_path():
|
||||
output = ProjectSettings.globalize_path("res://../" + output)
|
||||
DirAccess.make_dir_recursive_absolute(output)
|
||||
var report := {"output": output, "headless": DisplayServer.get_name() == "headless"}
|
||||
if OS.has_environment("MT_FAKE_MOB_COUNT"):
|
||||
report["fake_mob_count"] = int(OS.get_environment("MT_FAKE_MOB_COUNT"))
|
||||
|
||||
var host := OS.get_environment("MT_LIVE_HOST")
|
||||
if host.is_empty() or OS.get_environment("MT_LIVE_LOGIN").is_empty() or OS.get_environment("MT_LIVE_PASSWORD").is_empty():
|
||||
@@ -132,9 +137,63 @@ func run() -> void:
|
||||
# The main character's draws reach the 3D surface.
|
||||
var drawn := in_game and await _pump_until(10, func(): return world.draw_count > 0)
|
||||
_check(drawn, "RenderGame draws shown (%d)" % world.draw_count)
|
||||
# Let the WAIT motion run a little before the picture.
|
||||
for i in 30:
|
||||
# Let the WAIT motion run a little before the picture, and let GamePhase (3 packets/frame)
|
||||
# finish draining the entergame burst + GC_PING when MT_FAKE_MOB_COUNT is large.
|
||||
var warmup_frames := maxi(30, int(report.get("fake_mob_count", 1)) + 20)
|
||||
for i in warmup_frames:
|
||||
await process_frame
|
||||
if OS.has_environment("MT_PROFILE_FRAME"):
|
||||
var frame_ms: Array[float] = []
|
||||
var ui_update_ms: Array[float] = []
|
||||
var ui_draw_ms: Array[float] = []
|
||||
var world_update_ms: Array[float] = []
|
||||
var previous := Time.get_ticks_usec()
|
||||
for i in 240:
|
||||
await process_frame
|
||||
var current := Time.get_ticks_usec()
|
||||
frame_ms.append(float(current - previous) / 1000.0)
|
||||
ui_update_ms.append(ui.profile_ui_update_ms)
|
||||
ui_draw_ms.append(ui.profile_draw_ms)
|
||||
world_update_ms.append(world.profile_update_ms)
|
||||
previous = current
|
||||
frame_ms.sort()
|
||||
ui_update_ms.sort()
|
||||
ui_draw_ms.sort()
|
||||
world_update_ms.sort()
|
||||
var total := 0.0
|
||||
for sample in frame_ms:
|
||||
total += sample
|
||||
report["frame_profile"] = {
|
||||
"samples": frame_ms.size(),
|
||||
"mean_ms": total / frame_ms.size(),
|
||||
"p50_ms": frame_ms[119],
|
||||
"p95_ms": frame_ms[227],
|
||||
"ui_update_p50_ms": ui_update_ms[119],
|
||||
"ui_draw_p50_ms": ui_draw_ms[119],
|
||||
"world_update_p50_ms": world_update_ms[119],
|
||||
}
|
||||
var command_kinds := {}
|
||||
var image_texts := {}
|
||||
for command in Metin2PythonHost.ui_render_commands():
|
||||
var kind: String = str(command.get("kind", "unknown"))
|
||||
command_kinds[kind] = int(command_kinds.get(kind, 0)) + 1
|
||||
if kind == "image":
|
||||
var image_name: String = str(command.get("text", ""))
|
||||
image_texts[image_name] = int(image_texts.get(image_name, 0)) + 1
|
||||
report["frame_profile"]["ui_commands"] = command_kinds
|
||||
report["frame_profile"]["image_texts"] = image_texts
|
||||
var batched_commands := 0
|
||||
var glyph_batches := 0
|
||||
var batched_glyphs := 0
|
||||
for command in Metin2PythonHost.ui_render_commands_batched():
|
||||
batched_commands += 1
|
||||
if command.get("kind", "") == "glyph_batch":
|
||||
glyph_batches += 1
|
||||
batched_glyphs += int(command.get("glyph_count", 0))
|
||||
report["frame_profile"]["batched_commands"] = batched_commands
|
||||
report["frame_profile"]["glyph_batches"] = glyph_batches
|
||||
report["frame_profile"]["batched_glyphs"] = batched_glyphs
|
||||
print("FRAME_PROFILE ", report["frame_profile"])
|
||||
report["draw_count"] = world.draw_count
|
||||
report["unlit_stand_in_count"] = world.unlit_stand_in_count
|
||||
report["terrain"] = world.terrain_report
|
||||
@@ -170,14 +229,25 @@ func run() -> void:
|
||||
var draws: Array = Metin2PythonHost.render3d_draws()
|
||||
var native_lit_draws := 0
|
||||
var view_finite := true
|
||||
var actor_foot_cm := INF
|
||||
var foot_xy := Vector2.ZERO
|
||||
# Identify character, monster and NPC draws (excluding map objects, buildings, trees and roofs).
|
||||
var actor_draws: Array = []
|
||||
for draw in draws:
|
||||
var tex: String = String(draw.get("texture0", "")).to_lower().replace("\\", "/")
|
||||
for k in ["pc/", "pc2/", "monster/", "npc/"]:
|
||||
if k in tex:
|
||||
actor_draws.append(draw)
|
||||
break
|
||||
|
||||
for draw in draws:
|
||||
if draw["light0"]:
|
||||
native_lit_draws += 1
|
||||
for value in draw["view"]:
|
||||
if not is_finite(value):
|
||||
view_finite = false
|
||||
|
||||
var actor_foot_cm := INF
|
||||
var foot_xy := Vector2.ZERO
|
||||
for draw in actor_draws:
|
||||
var world_xform: Transform3D = world.d3d_transform(draw["world"])
|
||||
for vertex in draw["positions"]:
|
||||
var p: Vector3 = world_xform * vertex
|
||||
@@ -187,14 +257,14 @@ func run() -> void:
|
||||
# Every actor, not only the main one, stands on the map. An actor's parts (body, head, hair, teeth)
|
||||
# share its world matrix; the lowest vertex over those parts meets sample_height.
|
||||
var actor_low := {}
|
||||
for draw in draws:
|
||||
for draw in actor_draws:
|
||||
var draw_xform: Transform3D = world.d3d_transform(draw["world"])
|
||||
var key := str(draw["world"])
|
||||
for vertex in draw["positions"]:
|
||||
var p: Vector3 = draw_xform * vertex
|
||||
if not actor_low.has(key) or p.z < actor_low[key].z:
|
||||
actor_low[key] = p
|
||||
if world.terrain:
|
||||
if world.terrain and not actor_low.is_empty():
|
||||
var worst_actor_gap_cm := 0.0
|
||||
for low in actor_low.values():
|
||||
var ground := float(world.terrain.call("sample_height", low.x / 100.0, -low.y / 100.0)) * 100.0
|
||||
@@ -244,7 +314,7 @@ func run() -> void:
|
||||
var distinct_textures := {}
|
||||
for instance in world.find_children("Draw*", "MeshInstance3D", false, false):
|
||||
if instance.visible and instance.mesh:
|
||||
var material: StandardMaterial3D = instance.mesh.surface_get_material(0)
|
||||
var material: StandardMaterial3D = instance.get_active_material(0)
|
||||
if material and material.albedo_texture:
|
||||
surfaces_textured += 1
|
||||
distinct_textures[material.albedo_texture] = true
|
||||
@@ -353,7 +423,7 @@ func run() -> void:
|
||||
for frame in 600:
|
||||
await process_frame
|
||||
now = dog_screen.call()
|
||||
if now.distance_to(last) < 0.5:
|
||||
if now.x >= 0.0 and now.distance_to(last) < 0.5:
|
||||
break
|
||||
last = now
|
||||
return now
|
||||
@@ -377,15 +447,60 @@ func run() -> void:
|
||||
await process_frame
|
||||
button.call(true, at)
|
||||
var mid_shot := false
|
||||
var dead := await _pump_until(20, func():
|
||||
var combat_condition := func():
|
||||
var now: Vector2 = dog_screen.call()
|
||||
if now.x >= 0.0:
|
||||
at = now
|
||||
move.call(at)
|
||||
if not mid_shot and Metin2PythonHost.evaluate("33 in _target_hp") == "True" and DisplayServer.get_name() != "headless":
|
||||
if not mid_shot and Metin2PythonHost.evaluate("33 in _target_hp") == "True" and DisplayServer.get_name() != "headless" and not OS.has_environment("MT_PROFILE_COMBAT"):
|
||||
mid_shot = true
|
||||
root.get_texture().get_image().save_png(output.path_join("combat_mid.png"))
|
||||
return Metin2PythonHost.evaluate("_target_hp[-1:] == [0]") == "True")
|
||||
return Metin2PythonHost.evaluate("_target_hp[-1:] == [0]") == "True"
|
||||
var dead := false
|
||||
if OS.has_environment("MT_PROFILE_COMBAT"):
|
||||
var combat_frames: Array[float] = []
|
||||
var combat_ui_update: Array[float] = []
|
||||
var combat_ui_draw: Array[float] = []
|
||||
var combat_world_update: Array[float] = []
|
||||
var combat_draw_counts: Array[int] = []
|
||||
var deadline := Time.get_ticks_msec() + 20000
|
||||
var previous := Time.get_ticks_usec()
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
if combat_condition.call():
|
||||
dead = true
|
||||
break
|
||||
await process_frame
|
||||
var current := Time.get_ticks_usec()
|
||||
combat_frames.append(float(current - previous) / 1000.0)
|
||||
combat_ui_update.append(ui.profile_ui_update_ms)
|
||||
combat_ui_draw.append(ui.profile_draw_ms)
|
||||
combat_world_update.append(world.profile_update_ms)
|
||||
combat_draw_counts.append(world.draw_count)
|
||||
previous = current
|
||||
combat_frames.sort()
|
||||
combat_ui_update.sort()
|
||||
combat_ui_draw.sort()
|
||||
combat_world_update.sort()
|
||||
combat_draw_counts.sort()
|
||||
if not combat_frames.is_empty():
|
||||
var n := combat_frames.size()
|
||||
report["combat_profile"] = {
|
||||
"samples": n,
|
||||
"frame_p50_ms": combat_frames[n / 2],
|
||||
"frame_p95_ms": combat_frames[mini(n - 1, int(n * 0.95))],
|
||||
"frame_max_ms": combat_frames[n - 1],
|
||||
"ui_update_p50_ms": combat_ui_update[n / 2],
|
||||
"ui_update_p95_ms": combat_ui_update[mini(n - 1, int(n * 0.95))],
|
||||
"ui_draw_p50_ms": combat_ui_draw[n / 2],
|
||||
"ui_draw_p95_ms": combat_ui_draw[mini(n - 1, int(n * 0.95))],
|
||||
"world_update_p50_ms": combat_world_update[n / 2],
|
||||
"world_update_p95_ms": combat_world_update[mini(n - 1, int(n * 0.95))],
|
||||
"draw_count_p50": combat_draw_counts[n / 2],
|
||||
"draw_count_max": combat_draw_counts[n - 1],
|
||||
}
|
||||
print("COMBAT_PROFILE ", report["combat_profile"])
|
||||
else:
|
||||
dead = await _pump_until(20, combat_condition)
|
||||
button.call(false, at)
|
||||
if DisplayServer.get_name() != "headless":
|
||||
root.get_texture().get_image().save_png(output.path_join("combat.png"))
|
||||
|
||||
+284
-70
@@ -8,16 +8,26 @@ const UiAssets = preload("res://ui/ui_assets.gd")
|
||||
const KEY_TO_DIK := {
|
||||
KEY_ESCAPE: 0x01, KEY_1: 0x02, KEY_2: 0x03, KEY_3: 0x04,
|
||||
KEY_4: 0x05, KEY_5: 0x06, KEY_6: 0x07, KEY_7: 0x08,
|
||||
KEY_8: 0x09, KEY_9: 0x0a, KEY_0: 0x0b, KEY_BACKSPACE: 0x0e,
|
||||
KEY_TAB: 0x0f, KEY_Q: 0x10, KEY_W: 0x11, KEY_E: 0x12,
|
||||
KEY_R: 0x13, KEY_T: 0x14, KEY_Y: 0x15, KEY_U: 0x16,
|
||||
KEY_I: 0x17, KEY_O: 0x18, KEY_P: 0x19, KEY_ENTER: 0x1c,
|
||||
KEY_A: 0x1e, KEY_S: 0x1f, KEY_D: 0x20, KEY_F: 0x21,
|
||||
KEY_G: 0x22, KEY_H: 0x23, KEY_J: 0x24, KEY_K: 0x25,
|
||||
KEY_L: 0x26, KEY_Z: 0x2c, KEY_X: 0x2d, KEY_C: 0x2e,
|
||||
KEY_8: 0x09, KEY_9: 0x0a, KEY_0: 0x0b, KEY_MINUS: 0x0c,
|
||||
KEY_EQUAL: 0x0d, KEY_BACKSPACE: 0x0e, KEY_TAB: 0x0f,
|
||||
KEY_Q: 0x10, KEY_W: 0x11, KEY_E: 0x12, KEY_R: 0x13,
|
||||
KEY_T: 0x14, KEY_Y: 0x15, KEY_U: 0x16, KEY_I: 0x17,
|
||||
KEY_O: 0x18, KEY_P: 0x19, KEY_BRACKETLEFT: 0x1a, KEY_BRACKETRIGHT: 0x1b,
|
||||
KEY_ENTER: 0x1c, KEY_CTRL: 0x1d, KEY_A: 0x1e, KEY_S: 0x1f,
|
||||
KEY_D: 0x20, KEY_F: 0x21, KEY_G: 0x22, KEY_H: 0x23,
|
||||
KEY_J: 0x24, KEY_K: 0x25, KEY_L: 0x26, KEY_SEMICOLON: 0x27,
|
||||
KEY_APOSTROPHE: 0x28, KEY_QUOTELEFT: 0x29, KEY_SHIFT: 0x2a,
|
||||
KEY_BACKSLASH: 0x2b, KEY_Z: 0x2c, KEY_X: 0x2d, KEY_C: 0x2e,
|
||||
KEY_V: 0x2f, KEY_B: 0x30, KEY_N: 0x31, KEY_M: 0x32,
|
||||
KEY_SPACE: 0x39, KEY_UP: 0xc8, KEY_LEFT: 0xcb,
|
||||
KEY_RIGHT: 0xcd, KEY_DOWN: 0xd0,
|
||||
KEY_COMMA: 0x33, KEY_PERIOD: 0x34, KEY_SLASH: 0x35,
|
||||
KEY_ALT: 0x38, KEY_SPACE: 0x39,
|
||||
KEY_F1: 0x3b, KEY_F2: 0x3c, KEY_F3: 0x3d, KEY_F4: 0x3e,
|
||||
KEY_F5: 0x3f, KEY_F6: 0x40, KEY_F7: 0x41, KEY_F8: 0x42,
|
||||
KEY_F9: 0x43, KEY_F10: 0x44, KEY_F11: 0x57, KEY_F12: 0x58,
|
||||
KEY_HOME: 0xc7, KEY_UP: 0xc8, KEY_PAGEUP: 0xc9,
|
||||
KEY_LEFT: 0xcb, KEY_RIGHT: 0xcd, KEY_END: 0xcf,
|
||||
KEY_DOWN: 0xd0, KEY_PAGEDOWN: 0xd1, KEY_INSERT: 0xd2,
|
||||
KEY_DELETE: 0xd3,
|
||||
}
|
||||
|
||||
# Win32 virtual-key codes of the keys EditLine.OnIMEKeyDown handles (WM_KEYDOWN → OnIMEKeyDown);
|
||||
@@ -36,6 +46,11 @@ const KEY_TO_CHAR := {
|
||||
# "mem:<id>" → [revision, ImageTexture]: the CGraphicFontTexture glyph pages, refetched when the
|
||||
# "@<revision>" of a command's name moves on (a glyph was added to the page).
|
||||
var _memory_textures := {}
|
||||
var _tex_info_cache := {}
|
||||
var _color_cache := {}
|
||||
var _white_color_array := PackedColorArray([Color.WHITE])
|
||||
var _asset_root_path := ""
|
||||
var _segments_used := 0
|
||||
|
||||
# True when this surface started system.py itself (run_app) and so owns the interpreter's lifetime.
|
||||
var _owns_app := false
|
||||
@@ -80,6 +95,8 @@ func _exit_tree() -> void:
|
||||
func _ready() -> void:
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
texture_repeat = CanvasItem.TEXTURE_REPEAT_ENABLED
|
||||
_asset_root_path = AssetRoot.path()
|
||||
_sync_size()
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
@@ -100,7 +117,10 @@ func _process(_delta: float) -> void:
|
||||
get_tree().quit(0)
|
||||
return
|
||||
if Metin2PythonHost.is_running():
|
||||
var profile_start := Time.get_ticks_usec() if _profile_frame else 0
|
||||
Metin2PythonHost.ui_update()
|
||||
if profile_start != 0:
|
||||
profile_ui_update_ms = float(Time.get_ticks_usec() - profile_start) / 1000.0
|
||||
queue_redraw()
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
@@ -112,10 +132,30 @@ func _gui_input(event: InputEvent) -> void:
|
||||
if event.button_index >= MOUSE_BUTTON_LEFT and event.button_index <= MOUSE_BUTTON_MIDDLE:
|
||||
Metin2PythonHost.ui_mouse_button(event.button_index, event.pressed,
|
||||
int(event.position.x), int(event.position.y))
|
||||
elif event.pressed:
|
||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||
var factor: float = event.factor if event.factor > 0.0 else 1.0
|
||||
Metin2PythonHost.ui_mouse_wheel(int(round(120.0 * factor)))
|
||||
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||
var factor: float = event.factor if event.factor > 0.0 else 1.0
|
||||
Metin2PythonHost.ui_mouse_wheel(-int(round(120.0 * factor)))
|
||||
elif event is InputEventPanGesture:
|
||||
# macOS trackpad two-finger scroll: scrolling up has negative delta.y, zooming in
|
||||
var delta: int = -int(round(event.delta.y * 30.0))
|
||||
if delta != 0:
|
||||
Metin2PythonHost.ui_mouse_wheel(delta)
|
||||
elif event is InputEventMagnifyGesture:
|
||||
# macOS trackpad pinch to zoom: factor > 1.0 is zoom in
|
||||
var delta: int = int(round((event.factor - 1.0) * 600.0))
|
||||
if delta != 0:
|
||||
Metin2PythonHost.ui_mouse_wheel(delta)
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and Metin2PythonHost.is_running():
|
||||
var dik: int = KEY_TO_DIK.get(event.physical_keycode, 0)
|
||||
var code: int = event.physical_keycode
|
||||
var dik: int = KEY_TO_DIK.get(code, 0)
|
||||
if dik == 0 and event.keycode != 0:
|
||||
dik = KEY_TO_DIK.get(event.keycode, 0)
|
||||
if dik != 0:
|
||||
Metin2PythonHost.ui_key(dik, event.pressed)
|
||||
if event.pressed:
|
||||
@@ -159,79 +199,247 @@ var _segments: Array[RID] = []
|
||||
var _materials: Array[RID] = []
|
||||
var _mask_shader := RID()
|
||||
var _white: ImageTexture
|
||||
var _profile_frame := OS.has_environment("MT_PROFILE_FRAME")
|
||||
var profile_ui_update_ms := 0.0
|
||||
var profile_draw_ms := 0.0
|
||||
|
||||
func _get_color_array(argb: int) -> PackedColorArray:
|
||||
if argb == 0xFFFFFFFF or argb == -1:
|
||||
return _white_color_array
|
||||
var arr: PackedColorArray = _color_cache.get(argb, PackedColorArray())
|
||||
if not arr.is_empty():
|
||||
return arr
|
||||
var c := Color8((argb >> 16) & 255, (argb >> 8) & 255, argb & 255, (argb >> 24) & 255)
|
||||
arr = PackedColorArray([c])
|
||||
_color_cache[argb] = arr
|
||||
return arr
|
||||
|
||||
func _get_tex_draw_info(name: String) -> Array:
|
||||
var info: Array = _tex_info_cache.get(name, [])
|
||||
if not info.is_empty():
|
||||
return info
|
||||
var texture: Texture2D = _texture(name)
|
||||
if texture == null:
|
||||
_tex_info_cache[name] = []
|
||||
return []
|
||||
var tex_rid: RID = texture.get_rid()
|
||||
var is_atlas := false
|
||||
var u0 := 0.0
|
||||
var v0 := 0.0
|
||||
var du := 1.0
|
||||
var dv := 1.0
|
||||
var default_uv := PackedVector2Array()
|
||||
if texture is AtlasTexture:
|
||||
var at: AtlasTexture = texture
|
||||
if at.atlas != null:
|
||||
tex_rid = at.atlas.get_rid()
|
||||
var asize: Vector2 = at.atlas.get_size()
|
||||
if asize.x > 0 and asize.y > 0:
|
||||
is_atlas = true
|
||||
var r: Rect2 = at.region
|
||||
u0 = r.position.x / asize.x
|
||||
v0 = r.position.y / asize.y
|
||||
du = r.size.x / asize.x
|
||||
dv = r.size.y / asize.y
|
||||
default_uv = PackedVector2Array([
|
||||
Vector2(u0, v0),
|
||||
Vector2(u0 + du, v0),
|
||||
Vector2(u0 + du, v0 + dv),
|
||||
Vector2(u0, v0 + dv)
|
||||
])
|
||||
if default_uv.is_empty():
|
||||
default_uv = PackedVector2Array([
|
||||
Vector2(0, 0),
|
||||
Vector2(1, 0),
|
||||
Vector2(1, 1),
|
||||
Vector2(0, 1)
|
||||
])
|
||||
info = [tex_rid, is_atlas, u0, v0, du, dv, default_uv, texture]
|
||||
_tex_info_cache[name] = info
|
||||
return info
|
||||
|
||||
func _draw() -> void:
|
||||
var used := 0
|
||||
var profile_start := Time.get_ticks_usec() if _profile_frame else 0
|
||||
var fg_used := 0
|
||||
var fg_plain := false
|
||||
var masked := 0
|
||||
var plain := false
|
||||
for segment in _segments:
|
||||
RenderingServer.canvas_item_clear(segment)
|
||||
|
||||
for i in _segments_used:
|
||||
RenderingServer.canvas_item_clear(_segments[i])
|
||||
_segments_used = 0
|
||||
|
||||
if not Metin2PythonHost.is_running():
|
||||
return
|
||||
for command in Metin2PythonHost.ui_render_commands():
|
||||
var argb: int = command["argb"]
|
||||
var color := Color8((argb >> 16) & 255, (argb >> 8) & 255,
|
||||
argb & 255, (argb >> 24) & 255)
|
||||
var p1 := Vector2(command["x1"], command["y1"])
|
||||
var p2 := Vector2(command["x2"], command["y2"])
|
||||
var clip := Rect2(Vector2(command["clip_x1"], command["clip_y1"]),
|
||||
Vector2(command["clip_x2"] - command["clip_x1"],
|
||||
command["clip_y2"] - command["clip_y1"]))
|
||||
if command["kind"] == "bar":
|
||||
var visible := Rect2(p1, p2 - p1).intersection(clip)
|
||||
if visible.has_area():
|
||||
if not plain:
|
||||
used = _segment(used, RID())
|
||||
plain = true
|
||||
RenderingServer.canvas_item_add_rect(_segments[used - 1], visible, color)
|
||||
elif command["kind"] == "gradient_bar":
|
||||
var visible := Rect2(p1, p2 - p1).intersection(clip)
|
||||
if visible.has_area():
|
||||
if not plain:
|
||||
used = _segment(used, RID())
|
||||
plain = true
|
||||
var bottom_argb: int = command["end_argb"]
|
||||
|
||||
var has_3d: bool = Metin2PythonHost.has_3d_draws()
|
||||
|
||||
for command in Metin2PythonHost.ui_render_commands_batched():
|
||||
# Skip background UI commands when 3D is active, as the 3D surface renders the background Quad
|
||||
if has_3d and bool(command.get(&"behind_3d", false)):
|
||||
continue
|
||||
var kind: Variant = command[&"kind"]
|
||||
if kind == &"glyph_batch":
|
||||
var info := _get_tex_draw_info(command[&"text"])
|
||||
if info.is_empty():
|
||||
continue
|
||||
if not fg_plain:
|
||||
fg_used = _segment(fg_used, RID())
|
||||
fg_plain = true
|
||||
RenderingServer.canvas_item_add_triangle_array(_segments[fg_used - 1],
|
||||
command[&"indices"], command[&"points"], command[&"colors"],
|
||||
command[&"uvs"], PackedInt32Array(), PackedFloat32Array(), info[0])
|
||||
continue
|
||||
|
||||
var x1: float = command[&"x1"]
|
||||
var y1: float = command[&"y1"]
|
||||
var x2: float = command[&"x2"]
|
||||
var y2: float = command[&"y2"]
|
||||
var cx1: float = command[&"clip_x1"]
|
||||
var cy1: float = command[&"clip_y1"]
|
||||
var cx2: float = command[&"clip_x2"]
|
||||
var cy2: float = command[&"clip_y2"]
|
||||
|
||||
# 1) Completely culled by scissor?
|
||||
if x2 <= cx1 or x1 >= cx2 or y2 <= cy1 or y1 >= cy2:
|
||||
continue
|
||||
|
||||
var argb: int = command[&"argb"]
|
||||
var target_segments := _segments
|
||||
|
||||
if kind == &"image":
|
||||
var text_name: String = command.get(&"text", "")
|
||||
var mask_name: String = command.get(&"mask", "")
|
||||
var mask_tex: Texture2D = _texture(mask_name) if not mask_name.is_empty() else null
|
||||
if text_name.is_empty() and mask_tex != null:
|
||||
text_name = "__white__"
|
||||
|
||||
var info := _get_tex_draw_info(text_name)
|
||||
if info.is_empty():
|
||||
continue
|
||||
var tex_rid: RID = info[0]
|
||||
var is_atlas: bool = info[1]
|
||||
var u0: float = info[2]
|
||||
var v0: float = info[3]
|
||||
var du: float = info[4]
|
||||
var dv: float = info[5]
|
||||
|
||||
var quad: PackedVector2Array = command[&"quad"]
|
||||
var quad_uv: PackedVector2Array = command[&"uv"]
|
||||
|
||||
var needs_clip := x1 < cx1 or x2 > cx2 or y1 < cy1 or y2 > cy2
|
||||
var poly_pts: PackedVector2Array
|
||||
var poly_uv: PackedVector2Array
|
||||
|
||||
if not needs_clip:
|
||||
poly_pts = PackedVector2Array([quad[0], quad[1], quad[3], quad[2]])
|
||||
var is_full_uv := quad_uv[0] == Vector2.ZERO and quad_uv[1] == Vector2(1, 0) and quad_uv[2] == Vector2(0, 1) and quad_uv[3] == Vector2.ONE
|
||||
if is_full_uv:
|
||||
poly_uv = info[6]
|
||||
elif is_atlas:
|
||||
poly_uv = PackedVector2Array([
|
||||
Vector2(u0 + quad_uv[0].x * du, v0 + quad_uv[0].y * dv),
|
||||
Vector2(u0 + quad_uv[1].x * du, v0 + quad_uv[1].y * dv),
|
||||
Vector2(u0 + quad_uv[3].x * du, v0 + quad_uv[3].y * dv),
|
||||
Vector2(u0 + quad_uv[2].x * du, v0 + quad_uv[2].y * dv)
|
||||
])
|
||||
else:
|
||||
poly_uv = PackedVector2Array([quad_uv[0], quad_uv[1], quad_uv[3], quad_uv[2]])
|
||||
else:
|
||||
var mapped_uv: PackedVector2Array
|
||||
if is_atlas:
|
||||
mapped_uv = PackedVector2Array([
|
||||
Vector2(u0 + quad_uv[0].x * du, v0 + quad_uv[0].y * dv),
|
||||
Vector2(u0 + quad_uv[1].x * du, v0 + quad_uv[1].y * dv),
|
||||
Vector2(u0 + quad_uv[2].x * du, v0 + quad_uv[2].y * dv),
|
||||
Vector2(u0 + quad_uv[3].x * du, v0 + quad_uv[3].y * dv)
|
||||
])
|
||||
else:
|
||||
mapped_uv = quad_uv
|
||||
var clip_rect := Rect2(cx1, cy1, cx2 - cx1, cy2 - cy1)
|
||||
var clipped := _clip_quad(quad, mapped_uv, clip_rect)
|
||||
if clipped[0].size() < 3:
|
||||
continue
|
||||
poly_pts = clipped[0]
|
||||
poly_uv = clipped[1]
|
||||
|
||||
if mask_tex != null:
|
||||
var mat_rid := _mask_material(masked, mask_tex, quad_uv, command[&"mask_uv"])
|
||||
fg_used = _segment(fg_used, mat_rid)
|
||||
fg_plain = false
|
||||
masked += 1
|
||||
elif not fg_plain:
|
||||
fg_used = _segment(fg_used, RID())
|
||||
fg_plain = true
|
||||
var seg_idx := fg_used - 1
|
||||
RenderingServer.canvas_item_add_polygon(target_segments[seg_idx], poly_pts,
|
||||
_get_color_array(argb), poly_uv, tex_rid)
|
||||
|
||||
elif kind == &"bar":
|
||||
var bx1 := maxf(x1, cx1)
|
||||
var by1 := maxf(y1, cy1)
|
||||
var bx2 := minf(x2, cx2)
|
||||
var by2 := minf(y2, cy2)
|
||||
if bx2 > bx1 and by2 > by1:
|
||||
if not fg_plain:
|
||||
fg_used = _segment(fg_used, RID())
|
||||
fg_plain = true
|
||||
var seg_idx := fg_used - 1
|
||||
var color := Color8((argb >> 16) & 255, (argb >> 8) & 255,
|
||||
argb & 255, (argb >> 24) & 255)
|
||||
RenderingServer.canvas_item_add_rect(target_segments[seg_idx],
|
||||
Rect2(bx1, by1, bx2 - bx1, by2 - by1), color)
|
||||
|
||||
elif kind == &"gradient_bar":
|
||||
var bx1 := maxf(x1, cx1)
|
||||
var by1 := maxf(y1, cy1)
|
||||
var bx2 := minf(x2, cx2)
|
||||
var by2 := minf(y2, cy2)
|
||||
if bx2 > bx1 and by2 > by1:
|
||||
if not fg_plain:
|
||||
fg_used = _segment(fg_used, RID())
|
||||
fg_plain = true
|
||||
var seg_idx := fg_used - 1
|
||||
var color := Color8((argb >> 16) & 255, (argb >> 8) & 255,
|
||||
argb & 255, (argb >> 24) & 255)
|
||||
var bottom_argb: int = command[&"end_argb"]
|
||||
var bottom := Color8((bottom_argb >> 16) & 255, (bottom_argb >> 8) & 255,
|
||||
bottom_argb & 255, (bottom_argb >> 24) & 255)
|
||||
var top_factor := (visible.position.y - p1.y) / (p2.y - p1.y)
|
||||
var bottom_factor := (visible.end.y - p1.y) / (p2.y - p1.y)
|
||||
RenderingServer.canvas_item_add_polygon(_segments[used - 1],
|
||||
PackedVector2Array([visible.position, Vector2(visible.end.x, visible.position.y),
|
||||
visible.end, Vector2(visible.position.x, visible.end.y)]),
|
||||
var top_factor := (by1 - y1) / (y2 - y1) if y2 != y1 else 0.0
|
||||
var bottom_factor := (by2 - y1) / (y2 - y1) if y2 != y1 else 1.0
|
||||
RenderingServer.canvas_item_add_polygon(target_segments[seg_idx],
|
||||
PackedVector2Array([Vector2(bx1, by1), Vector2(bx2, by1),
|
||||
Vector2(bx2, by2), Vector2(bx1, by2)]),
|
||||
PackedColorArray([color.lerp(bottom, top_factor), color.lerp(bottom, top_factor),
|
||||
color.lerp(bottom, bottom_factor), color.lerp(bottom, bottom_factor)]))
|
||||
elif command["kind"] == "line":
|
||||
var segment := _clip_line(p1, p2, clip)
|
||||
|
||||
elif kind == &"line":
|
||||
var clip_rect := Rect2(cx1, cy1, cx2 - cx1, cy2 - cy1)
|
||||
var p1 := Vector2(x1, y1)
|
||||
var p2 := Vector2(x2, y2)
|
||||
var segment := _clip_line(p1, p2, clip_rect)
|
||||
if segment.size() == 2:
|
||||
if not plain:
|
||||
used = _segment(used, RID())
|
||||
plain = true
|
||||
RenderingServer.canvas_item_add_line(_segments[used - 1], segment[0], segment[1], color)
|
||||
elif command["kind"] == "image":
|
||||
var texture: Texture2D = _texture(command["text"])
|
||||
var mask: Texture2D = _texture(command["mask"]) if command.has("mask") else null
|
||||
if mask != null and texture == null:
|
||||
texture = _white_texture()
|
||||
if texture != null:
|
||||
var clipped := _clip_quad(command["quad"], command["uv"], clip)
|
||||
if clipped[0].size() >= 3:
|
||||
if mask != null:
|
||||
used = _segment(used, _mask_material(masked, mask, command["uv"], command["mask_uv"]))
|
||||
masked += 1
|
||||
plain = false
|
||||
elif not plain:
|
||||
used = _segment(used, RID())
|
||||
plain = true
|
||||
RenderingServer.canvas_item_add_polygon(_segments[used - 1], clipped[0],
|
||||
PackedColorArray([color]), clipped[1], texture.get_rid())
|
||||
if not fg_plain:
|
||||
fg_used = _segment(fg_used, RID())
|
||||
fg_plain = true
|
||||
var seg_idx := fg_used - 1
|
||||
var color := Color8((argb >> 16) & 255, (argb >> 8) & 255,
|
||||
argb & 255, (argb >> 24) & 255)
|
||||
RenderingServer.canvas_item_add_line(target_segments[seg_idx], segment[0], segment[1], color)
|
||||
|
||||
_segments_used = fg_used
|
||||
if profile_start != 0:
|
||||
profile_draw_ms = float(Time.get_ticks_usec() - profile_start) / 1000.0
|
||||
|
||||
# Opens the next pooled child item (draw order = index) with the given material; returns the count used.
|
||||
func _segment(used: int, material: RID) -> int:
|
||||
if used == _segments.size():
|
||||
var pool := _segments
|
||||
var parent_item: RID = get_canvas_item()
|
||||
if used == pool.size():
|
||||
var item := RenderingServer.canvas_item_create()
|
||||
RenderingServer.canvas_item_set_parent(item, get_canvas_item())
|
||||
_segments.push_back(item)
|
||||
var item := _segments[used]
|
||||
RenderingServer.canvas_item_set_parent(item, parent_item)
|
||||
RenderingServer.canvas_item_set_default_texture_repeat(item, RenderingServer.CANVAS_ITEM_TEXTURE_REPEAT_ENABLED)
|
||||
pool.push_back(item)
|
||||
var item := pool[used]
|
||||
RenderingServer.canvas_item_set_draw_index(item, used)
|
||||
RenderingServer.canvas_item_set_material(item, material)
|
||||
return used + 1
|
||||
@@ -260,8 +468,11 @@ func _white_texture() -> Texture2D:
|
||||
return _white
|
||||
|
||||
func _texture(name: String) -> Texture2D:
|
||||
if name == "__white__":
|
||||
return _white_texture()
|
||||
if not name.begins_with("mem:"):
|
||||
return UiAssets.load_tex(AssetRoot.path(), name)
|
||||
var root := _asset_root_path if not _asset_root_path.is_empty() else AssetRoot.path()
|
||||
return UiAssets.load_tex(root, name)
|
||||
var at := name.find("@")
|
||||
var key := name.substr(0, at) if at >= 0 else name
|
||||
var revision := int(name.substr(at + 1)) if at >= 0 else 0
|
||||
@@ -292,6 +503,9 @@ func _clip_quad(quad: PackedVector2Array, uv: PackedVector2Array, clip: Rect2) -
|
||||
var det := edge_u.cross(edge_v)
|
||||
if is_zero_approx(det):
|
||||
return [PackedVector2Array(), PackedVector2Array()]
|
||||
if clip.has_point(quad[0]) and clip.has_point(quad[1]) \
|
||||
and clip.has_point(quad[2]) and clip.has_point(quad[3]):
|
||||
return [outline, PackedVector2Array([uv[0], uv[1], uv[3], uv[2]])]
|
||||
var rect := PackedVector2Array([clip.position, Vector2(clip.end.x, clip.position.y),
|
||||
clip.end, Vector2(clip.position.x, clip.end.y)])
|
||||
var pieces := Geometry2D.intersect_polygons(outline, rect)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
@tool
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
var tw = Metin2World.new()
|
||||
tw.set("assets_root", "pack://")
|
||||
tw.set("map_path", "metin2_map_a1")
|
||||
tw.set("auto_load", false)
|
||||
tw.call("set_focus_tile", Vector2i(2, 2))
|
||||
if tw.call("load_map"):
|
||||
for y in range(68200, 66000, -100):
|
||||
var h = float(tw.call("sample_height", 59700.0 / 100.0, float(y) / 100.0)) * 100.0
|
||||
print("y = %d -> height = %.2f" % [y, h])
|
||||
quit(0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bt4n4goaei8mi
|
||||
@@ -0,0 +1,107 @@
|
||||
extends SceneTree
|
||||
|
||||
const PythonUISurface = preload("res://python_ui_surface.gd")
|
||||
const Python3DSurface = preload("res://python_3d_surface.gd")
|
||||
|
||||
var ui: Control
|
||||
var world: Node3D
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func _py(source: String) -> bool:
|
||||
return Metin2PythonHost.run_line(source) == ""
|
||||
|
||||
func _pump_until(seconds: float, condition: Callable) -> bool:
|
||||
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
if condition.call():
|
||||
return true
|
||||
await process_frame
|
||||
return condition.call()
|
||||
|
||||
func run() -> void:
|
||||
root.size = Vector2i(1024, 768)
|
||||
world = Python3DSurface.new()
|
||||
world.name = "Python3DSurface"
|
||||
ui = PythonUISurface.new()
|
||||
ui.name = "PythonUISurface"
|
||||
root.add_child(ui)
|
||||
root.add_child(world)
|
||||
|
||||
var err: String = ui.run_app()
|
||||
if err != "":
|
||||
print("ERROR: run_app: ", err)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
await _pump_until(15, func(): return _py(
|
||||
"import __main__, os, networkModule, introLogin, introSelect, game, player, background, chr, chrmgr\n"
|
||||
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
|
||||
))
|
||||
|
||||
await _pump_until(20, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
|
||||
))
|
||||
|
||||
var host = OS.get_environment("MT_LIVE_HOST")
|
||||
var login = OS.get_environment("MT_LIVE_LOGIN")
|
||||
var pwd = OS.get_environment("MT_LIVE_PASSWORD")
|
||||
var auth_port = int(OS.get_environment("MT_LIVE_AUTH_PORT"))
|
||||
var game_port = int(OS.get_environment("MT_LIVE_GAME_PORT"))
|
||||
|
||||
_py("_stream.SetConnectInfo('%s', %d, '%s', %d)\n" % [host, game_port, host, auth_port]
|
||||
+ "_stream.curPhaseWindow.Connect('%s', '%s')" % [login, pwd])
|
||||
|
||||
await _pump_until(30, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
|
||||
))
|
||||
|
||||
_py("_stream.curPhaseWindow.SelectSlot(0)\n_stream.curPhaseWindow.StartGame()")
|
||||
|
||||
await _pump_until(60, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()"
|
||||
))
|
||||
|
||||
await _pump_until(10, func(): return world.draw_count > 0)
|
||||
for i in 30:
|
||||
await process_frame
|
||||
|
||||
var draws: Array = Metin2PythonHost.render3d_draws()
|
||||
print("Game 3D draws count: ", draws.size())
|
||||
|
||||
# Find actual actors
|
||||
var actor_draws: Array = []
|
||||
for draw in draws:
|
||||
var tex = draw.get("texture0", "").to_lower()
|
||||
var is_actor = false
|
||||
for k in ["pc/", "pc2/", "monster/", "npc/", "warrior", "assassin", "sura", "shaman", "dog", "wolf"]:
|
||||
if k in tex:
|
||||
is_actor = true
|
||||
break
|
||||
if is_actor:
|
||||
actor_draws.append(draw)
|
||||
|
||||
print("Actual actor draws count: ", actor_draws.size())
|
||||
|
||||
var actor_low := {}
|
||||
for draw in actor_draws:
|
||||
var draw_xform: Transform3D = world.d3d_transform(draw["world"])
|
||||
var key := str(draw["world"])
|
||||
for vertex in draw["positions"]:
|
||||
var p: Vector3 = draw_xform * vertex
|
||||
if not actor_low.has(key) or p.z < actor_low[key].z:
|
||||
actor_low[key] = p
|
||||
|
||||
print("Distinct actors count: ", actor_low.size())
|
||||
if world.terrain:
|
||||
var worst_actor_gap_cm := 0.0
|
||||
for key in actor_low:
|
||||
var low: Vector3 = actor_low[key]
|
||||
var ground := float(world.terrain.call("sample_height", low.x / 100.0, -low.y / 100.0)) * 100.0
|
||||
var gap = absf(low.z - ground)
|
||||
print("Actor at (%f, %f): foot_z=%f, ground_z=%f, gap=%f cm" % [low.x, low.y, low.z, ground, gap])
|
||||
worst_actor_gap_cm = maxf(worst_actor_gap_cm, gap)
|
||||
print("WORST ACTUAL ACTOR GAP: ", worst_actor_gap_cm, " cm")
|
||||
|
||||
quit(0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cmum40u4abuep
|
||||
@@ -0,0 +1,13 @@
|
||||
@tool
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
var b := Basis(Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9))
|
||||
print("b.x = ", b.x)
|
||||
print("b.y = ", b.y)
|
||||
print("b.z = ", b.z)
|
||||
var v := Vector3(1, 0, 0)
|
||||
print("b * Vector3(1, 0, 0) = ", b * v)
|
||||
var v2 := Vector3(0, 1, 0)
|
||||
print("b * Vector3(0, 1, 0) = ", b * v2)
|
||||
quit(0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d17rcsnfyx418
|
||||
@@ -0,0 +1,95 @@
|
||||
extends SceneTree
|
||||
|
||||
const PythonUISurface = preload("res://python_ui_surface.gd")
|
||||
const Python3DSurface = preload("res://python_3d_surface.gd")
|
||||
|
||||
var ui: Control
|
||||
var world: Node3D
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func _py(source: String) -> bool:
|
||||
return Metin2PythonHost.run_line(source) == ""
|
||||
|
||||
func _key(keycode: Key, unicode: int, pressed: bool) -> void:
|
||||
var event := InputEventKey.new()
|
||||
event.keycode = keycode
|
||||
event.physical_keycode = keycode
|
||||
event.unicode = unicode
|
||||
event.pressed = pressed
|
||||
root.push_input(event)
|
||||
|
||||
func _press(keycode: Key) -> void:
|
||||
_key(keycode, 0, true)
|
||||
_key(keycode, 0, false)
|
||||
|
||||
func _type(text: String) -> void:
|
||||
for i in text.length():
|
||||
var ch := text.unicode_at(i)
|
||||
var keycode := OS.find_keycode_from_string(text[i].to_upper())
|
||||
_key(keycode, ch, true)
|
||||
_key(keycode, ch, false)
|
||||
|
||||
func _pump_until(seconds: float, condition: Callable) -> bool:
|
||||
var deadline := Time.get_ticks_msec() + int(seconds * 1000.0)
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
if condition.call():
|
||||
return true
|
||||
await process_frame
|
||||
return condition.call()
|
||||
|
||||
func run() -> void:
|
||||
root.size = Vector2i(1024, 768)
|
||||
|
||||
world = Python3DSurface.new()
|
||||
world.name = "Python3DSurface"
|
||||
ui = PythonUISurface.new()
|
||||
ui.name = "PythonUISurface"
|
||||
root.add_child(ui)
|
||||
root.add_child(world)
|
||||
|
||||
var err: String = ui.run_app()
|
||||
if err != "":
|
||||
quit(1)
|
||||
return
|
||||
|
||||
await _pump_until(15, func(): return _py(
|
||||
"import __main__, os, networkModule, introLogin, introSelect\n"
|
||||
+ "__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"
|
||||
))
|
||||
|
||||
await _pump_until(20, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"
|
||||
))
|
||||
|
||||
_py(
|
||||
"_env = os.environ\n"
|
||||
+ "_stream.SetConnectInfo(_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_GAME_PORT', '13000')), "
|
||||
+ "_env['MT_LIVE_HOST'], int(_env.get('MT_LIVE_AUTH_PORT', '11000')))\n"
|
||||
+ "_w = _stream.curPhaseWindow\n"
|
||||
+ "_w._LoginWindow__OpenLoginBoard()\n"
|
||||
+ "_w.idEditLine.SetText('')\n"
|
||||
+ "_w.pwdEditLine.SetText('')\n"
|
||||
+ "_w.idEditLine.SetFocus()"
|
||||
)
|
||||
_type(OS.get_environment("MT_LIVE_LOGIN"))
|
||||
_press(KEY_TAB)
|
||||
_type(OS.get_environment("MT_LIVE_PASSWORD"))
|
||||
_py("del _env, _w")
|
||||
_press(KEY_ENTER)
|
||||
|
||||
await _pump_until(30, func(): return _py(
|
||||
"assert isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"
|
||||
))
|
||||
|
||||
for i in 15:
|
||||
await process_frame
|
||||
|
||||
var img_final = root.get_texture().get_image()
|
||||
img_final.save_png("/tmp/select_character_final.png")
|
||||
print("Saved /tmp/select_character_final.png")
|
||||
|
||||
quit(0)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dpi4fhed4wc28
|
||||
+99
-20
@@ -1,10 +1,10 @@
|
||||
# UiAssets (P1) —— 解析 uiscript 里的图片路径(`.sub` / `.tga` / `.png` / `.jpg`)。
|
||||
# UiAssets (P1) —— 解析 uiscript 里的图片路径(`.sub` / `.tga` / `.png` / `.jpg` / `.dds`)。
|
||||
#
|
||||
# 路径形如 `d:/ymir work/ui/public/middle_button_01.sub`。解析:
|
||||
# 1) 去掉盘符前缀
|
||||
# 2) 依次试: <assets>/<rel> · <assets>/<pack>/<rel>(散包目录)
|
||||
# 3) `.sub` = 子图描述(title/image/left/top/right/bottom)→ 载入其 image + 裁剪成 AtlasTexture
|
||||
# `.dds` 走 Metin2World.load_dds(C++ dxt 解码)运行时解成 Image。
|
||||
# 1) 优先从 40250 Client/pack(Metin2Pack)取(保持 d:/ 盘符或规范相对路径)
|
||||
# 2) 支持 pack 内 .sub 子图切片(AtlasTexture),支持 version 1.0 / 2.0 及公用 Public.dds / IntroEmpire.dds 母图寻址
|
||||
# 3) 若 pack 找不到且指定了 assets_root,回退到散文件目录
|
||||
# 4) .dds 走 Metin2World.load_dds(C++ dxt 解码)运行时解成 Image。
|
||||
extends RefCounted
|
||||
|
||||
static var _cache := {}
|
||||
@@ -19,21 +19,20 @@ static func load_dds_image(path: String) -> Image:
|
||||
return null
|
||||
if not ClassDB.class_exists("Metin2World"):
|
||||
return null
|
||||
# Metin2World is a Node, not RefCounted; a static reference never frees it.
|
||||
# The decoder returns an independently owned Image, so release the helper
|
||||
# after each decode (textures themselves remain cached by load_tex).
|
||||
var helper: Object = ClassDB.instantiate("Metin2World")
|
||||
var img = helper.call("load_dds", path)
|
||||
helper.free()
|
||||
return img if img is Image else null
|
||||
|
||||
static func load_tex(assets_root: String, vpath: String) -> Texture2D:
|
||||
if vpath == "" or assets_root == "":
|
||||
if vpath == "":
|
||||
return null
|
||||
var key := assets_root + "|" + vpath
|
||||
var key := (assets_root if assets_root != "" else "pack") + "|" + vpath
|
||||
if _cache.has(key):
|
||||
return _cache[key]
|
||||
var tex: Texture2D = _load_uncached(assets_root, vpath)
|
||||
var tex: Texture2D = _load_pack_any(vpath)
|
||||
if tex == null and assets_root != "":
|
||||
tex = _load_uncached(assets_root, vpath)
|
||||
_cache[key] = tex
|
||||
return tex
|
||||
|
||||
@@ -45,8 +44,7 @@ static func load_pack_tex(vpath: String) -> Texture2D:
|
||||
var key := "pack|" + vpath
|
||||
if _cache.has(key):
|
||||
return _cache[key]
|
||||
# 40250 的包名带盘符(d:/ymir work/...),原样交给 CEterPackManager。
|
||||
var tex: Texture2D = _load_pack_image(vpath.replace("\\", "/"))
|
||||
var tex: Texture2D = _load_pack_any(vpath)
|
||||
_cache[key] = tex
|
||||
return tex
|
||||
|
||||
@@ -56,6 +54,93 @@ static func _strip_drive(p: String) -> String:
|
||||
s = s.substr(2)
|
||||
return s.lstrip("/")
|
||||
|
||||
static func _find_pack_path(vpath: String) -> String:
|
||||
if not ClassDB.class_exists("Metin2Pack") or not Metin2Pack.is_ready():
|
||||
return ""
|
||||
var norm := vpath.replace("\\", "/").strip_edges()
|
||||
var stripped := _strip_drive(norm)
|
||||
var candidates: Array[String] = [
|
||||
norm,
|
||||
stripped,
|
||||
"d:/" + stripped,
|
||||
norm.to_lower(),
|
||||
stripped.to_lower(),
|
||||
("d:/" + stripped).to_lower(),
|
||||
]
|
||||
for c in candidates:
|
||||
if Metin2Pack.exists(c):
|
||||
return c
|
||||
return ""
|
||||
|
||||
static func _load_pack_any(vpath: String) -> Texture2D:
|
||||
var pack_path := _find_pack_path(vpath)
|
||||
if pack_path == "":
|
||||
return null
|
||||
var ext := pack_path.get_extension().to_lower()
|
||||
if ext == "sub":
|
||||
return _load_pack_sub(pack_path)
|
||||
return _load_pack_image(pack_path)
|
||||
|
||||
static func _load_pack_sub(pack_path: String) -> Texture2D:
|
||||
if not ClassDB.class_exists("Metin2Pack") or not Metin2Pack.is_ready():
|
||||
return null
|
||||
if not Metin2Pack.exists(pack_path):
|
||||
return null
|
||||
var bytes: PackedByteArray = Metin2Pack.get_bytes(pack_path)
|
||||
if bytes.is_empty():
|
||||
return null
|
||||
var txt := bytes.get_string_from_ascii()
|
||||
var image_name := ""
|
||||
var version := ""
|
||||
var l := 0
|
||||
var t := 0
|
||||
var r := -1
|
||||
var b := -1
|
||||
for line in txt.split("\n"):
|
||||
var parts := line.strip_edges().split(" ", false)
|
||||
if parts.size() < 2:
|
||||
continue
|
||||
match parts[0].to_lower():
|
||||
"version": version = parts[1].strip_edges().trim_prefix('"').trim_suffix('"')
|
||||
"image": image_name = parts[1].strip_edges().trim_prefix('"').trim_suffix('"')
|
||||
"left": l = int(parts[1])
|
||||
"top": t = int(parts[1])
|
||||
"right": r = int(parts[1])
|
||||
"bottom": b = int(parts[1])
|
||||
if image_name == "":
|
||||
return null
|
||||
|
||||
var sub_dir := pack_path.get_base_dir()
|
||||
var candidates: Array[String] = []
|
||||
if version == "2.0":
|
||||
candidates.append(sub_dir.path_join(image_name))
|
||||
candidates.append("d:/ymir work/ui/".path_join(image_name))
|
||||
else:
|
||||
candidates.append("d:/ymir work/ui/".path_join(image_name))
|
||||
candidates.append(sub_dir.path_join(image_name))
|
||||
|
||||
var search_dir := sub_dir
|
||||
for _i in 4:
|
||||
var parent := search_dir.get_base_dir()
|
||||
if parent == search_dir or parent.is_empty():
|
||||
break
|
||||
candidates.append(parent.path_join(image_name))
|
||||
search_dir = parent
|
||||
|
||||
var base_tex: Texture2D = null
|
||||
for cand in candidates:
|
||||
base_tex = _load_pack_any(cand)
|
||||
if base_tex != null:
|
||||
break
|
||||
if base_tex == null:
|
||||
return null
|
||||
if r <= l or b <= t:
|
||||
return base_tex
|
||||
var at := AtlasTexture.new()
|
||||
at.atlas = base_tex
|
||||
at.region = Rect2(l, t, r - l, b - t)
|
||||
return at
|
||||
|
||||
static func _resolve(assets_root: String, rel: String) -> String:
|
||||
var direct := assets_root.path_join(rel)
|
||||
if FileAccess.file_exists(direct):
|
||||
@@ -67,20 +152,18 @@ static func _resolve(assets_root: String, rel: String) -> String:
|
||||
var cand := assets_root.path_join(sub).path_join(rel)
|
||||
if FileAccess.file_exists(cand):
|
||||
return cand
|
||||
# 大小写不敏感兜底:<assets>/**/ymir work/ui/... —— 只按 basename 找
|
||||
return ""
|
||||
|
||||
static func _load_uncached(assets_root: String, vpath: String) -> Texture2D:
|
||||
var rel := _strip_drive(vpath)
|
||||
var real := _resolve(assets_root, rel)
|
||||
if real == "":
|
||||
# 试把 .sub 换成 .tga / .png
|
||||
for ext: String in [".tga", ".png", ".jpg"]:
|
||||
real = _resolve(assets_root, rel.get_basename() + ext)
|
||||
if real != "":
|
||||
break
|
||||
if real == "":
|
||||
return _load_pack_image(rel)
|
||||
return _load_pack_any(vpath)
|
||||
if real.get_extension().to_lower() == "sub":
|
||||
return _load_sub(real)
|
||||
return _load_image_file(real)
|
||||
@@ -112,8 +195,6 @@ static func _load_pack_image(rel: String) -> Texture2D:
|
||||
return ImageTexture.create_from_image(image) if image != null and not image.is_empty() else null
|
||||
|
||||
static func _load_image_file(path: String) -> Texture2D:
|
||||
# .sub 文件可能引用了未随当前资源包发布的共享贴图(例如 Public.tga)。
|
||||
# 先做存在性检查,避免 headless/UI fallback 因缺失可选贴图刷错误日志。
|
||||
if not FileAccess.file_exists(path):
|
||||
return null
|
||||
var ext := path.get_extension().to_lower()
|
||||
@@ -144,8 +225,6 @@ static func _load_sub(path: String) -> Texture2D:
|
||||
"bottom": b = int(parts[1])
|
||||
if image_name == "":
|
||||
return null
|
||||
# Skill .sub files live below ui/skill/<job>, while their shared DDS
|
||||
# lives in ui/. Search the containing directory and its parents.
|
||||
var img_path := ""
|
||||
var search_dir := path.get_base_dir()
|
||||
for _i in 6:
|
||||
|
||||
Reference in New Issue
Block a user