extends SceneTree const SafeboxUI = preload("res://ui/safebox_ui.gd") const MallUI = preload("res://ui/mall_ui.gd") var _pass := 0 var _fail := 0 func _ck(ok: bool, msg: String) -> void: if ok: _pass += 1 print(" [PASS] %s" % msg) else: _fail += 1 printerr(" [FAIL] %s" % msg) class MockProto extends Node: func item(vnum: int) -> Dictionary: match vnum: 10: return {"name": "Sword+0", "locale_name": "单手剑+0", "type": 1, "sub_type": 0} 19: return {"name": "Sword+9", "locale_name": "单手剑+9", "type": 1, "sub_type": 0} 27001: return {"name": "Red Potion(S)", "locale_name": "小型红药水", "type": 3, "sub_type": 0} 27993: return {"name": "Pearl", "locale_name": "珍珠", "type": 3, "sub_type": 0} _: return {"name": "Item%d" % vnum, "locale_name": "道具%d" % vnum, "type": 3, "sub_type": 0} class MockMouseController extends Node: var _attached: Dictionary = {} var _targets: Dictionary = {} func is_attached() -> bool: return not _attached.is_empty() func attached() -> Dictionary: return _attached func attach_item(win: int, cell: int, vnum: int, count: int, _tex = null, _owner = null) -> void: _attached = {"window": win, "cell": cell, "vnum": vnum, "count": count} func cancel() -> void: _attached.clear() func register_owner(_o) -> void: pass func unregister_owner(_o) -> void: pass func register_target(rect: Control, cb: Callable, _owner = null) -> void: _targets[rect] = cb func unregister_target(rect: Control) -> void: _targets.erase(rect) class MockClient extends Node: signal safebox_opened(size: int) signal safebox_closed() signal safebox_changed() signal safebox_password_required() signal safebox_wrong_password() signal mall_opened(size: int) signal mall_closed() signal mall_changed() signal mall_password_required() signal inventory_changed() var _safebox_open := false var _safebox_size := 1 var _safebox_gold := 0 var _safebox_items := [] var _mall_open := false var _mall_size := 45 var _mall_items := [] var _inventory := [] var _char_pos := Vector3.ZERO var _main_vid := 10001 var calls: Array[Dictionary] = [] func get_main_vid() -> int: return _main_vid func get_entity(vid: int) -> Dictionary: if vid == _main_vid: return {"vid": vid, "pos": _char_pos} return {} func is_safebox_open() -> bool: return _safebox_open func get_safebox_size() -> int: return _safebox_size func get_safebox_gold() -> int: return _safebox_gold func get_safebox_items() -> Array: return _safebox_items func is_mall_open() -> bool: return _mall_open func get_mall_size() -> int: return _mall_size func get_mall_items() -> Array: return _mall_items func get_inventory() -> Array: return _inventory func safebox_checkin(safe_pos: int, inv_window: int, inv_cell: int) -> bool: calls.append({"method": "safebox_checkin", "args": [safe_pos, inv_window, inv_cell]}) return true func safebox_checkout(safe_pos: int, inv_window: int, inv_cell: int) -> bool: calls.append({"method": "safebox_checkout", "args": [safe_pos, inv_window, inv_cell]}) return true func safebox_move(from_cell: int, to_cell: int, count: int) -> bool: calls.append({"method": "safebox_move", "args": [from_cell, to_cell, count]}) return true func safebox_close() -> bool: _safebox_open = false calls.append({"method": "safebox_close", "cmd": "/safebox_close"}) return true func mall_checkout(mall_pos: int, inv_window: int, inv_cell: int) -> bool: calls.append({"method": "mall_checkout", "args": [mall_pos, inv_window, inv_cell]}) return true func mall_close() -> bool: _mall_open = false calls.append({"method": "mall_close", "cmd": "/mall_close"}) return true func safebox_password(pw: String) -> bool: calls.append({"method": "safebox_password", "cmd": "/safebox_password " + pw}) return true func safebox_change_password(old_pw: String, new_pw: String) -> bool: calls.append({"method": "safebox_change_password", "cmd": "/safebox_change_password %s %s" % [old_pw, new_pw]}) return true func mall_password(pw: String) -> bool: calls.append({"method": "mall_password", "cmd": "/mall_password " + pw}) return true func _init() -> void: print("\n=== Running test_safebox_mall_parity (Phase 4 Extension Parity) ===") _run_all() print("\n=======================================================") if _fail == 0: print("=== ALL SAFEBOX & MALL PARITY TESTS PASSED (%d checks) ===" % _pass) quit(0) else: printerr("=== SOME CHECKS FAILED: %d pass, %d fail ===" % [_pass, _fail]) quit(1) func _run_all() -> void: var client := MockClient.new() var proto := MockProto.new() var mouse := MockMouseController.new() var host := Control.new() get_root().add_child(client) get_root().add_child(mouse) get_root().add_child(host) # ------------------------------------------------------------- # Test 1: Safebox Open/Close, Auto-Open, and 10.0m Auto-Close # ------------------------------------------------------------- print("\n--- Test 1: Safebox Open/Close & 10.0m Limit Range (40250 uisafebox.py) ---") var safe := SafeboxUI.new() safe.item_mouse = mouse host.add_child(safe) safe.setup(client, host, proto) _ck(not safe.is_open(), "Safebox initially closed") client._char_pos = Vector3(100.0, 50.0, 0.0) client._safebox_open = true client._safebox_size = 3 client.safebox_opened.emit(3) _ck(safe.is_open(), "Safebox auto-opened on safebox_opened signal") _ck(safe._has_open_pos and safe._open_char_pos == Vector3(100.0, 50.0, 0.0), "Safebox recorded opening character position (100, 50, 0)") _ck(safe._page_buttons.size() == 3, "Safebox has 3 pages configured") # Move within 5m client._char_pos = Vector3(104.0, 50.0, 0.0) safe._process(0.016) _ck(safe.is_open(), "Safebox remains open at 4.0m distance") # Move beyond 10m client._char_pos = Vector3(112.0, 50.0, 0.0) safe._process(0.016) _ck(not safe.is_open(), "Safebox auto-closed after exceeding 10.0m limit range") var has_safe_close := false for c in client.calls: if c.get("method") == "safebox_close" and c.get("cmd") == "/safebox_close": has_safe_close = true break _ck(has_safe_close, "Safebox auto-close sent /safebox_close chat command to server") # Server closes safebox client._safebox_open = true safe.open() _ck(safe.is_open(), "Safebox re-opened") client.calls.clear() client._safebox_open = false client.safebox_closed.emit() _ck(not safe.is_open(), "Safebox closed on safebox_closed server signal") _ck(client.calls.is_empty(), "Safebox closed by server does not send /safebox_close back") # ------------------------------------------------------------- # Test 2: Mall Open/Close & 10.0m Auto-Close # ------------------------------------------------------------- print("\n--- Test 2: Mall Open/Close & 10.0m Limit Range (40250 uisafebox.py MallWindow) ---") var mall := MallUI.new() mall.item_mouse = mouse host.add_child(mall) mall.setup(client, host, proto) _ck(not mall.is_open(), "Mall initially closed") client._char_pos = Vector3(200.0, 80.0, 0.0) client._mall_open = true client._mall_size = 45 client.mall_opened.emit(45) _ck(mall.is_open(), "Mall auto-opened on mall_opened signal") _ck(mall._has_open_pos and mall._open_char_pos == Vector3(200.0, 80.0, 0.0), "Mall recorded opening character position (200, 80, 0)") # Move within 6m client._char_pos = Vector3(200.0, 86.0, 0.0) mall._process(0.016) _ck(mall.is_open(), "Mall remains open at 6.0m distance") # Move beyond 10m client._char_pos = Vector3(200.0, 95.0, 0.0) client.calls.clear() mall._process(0.016) _ck(not mall.is_open(), "Mall auto-closed after exceeding 10.0m limit range") var has_mall_close := false for c in client.calls: if c.get("method") == "mall_close" and c.get("cmd") == "/mall_close": has_mall_close = true break _ck(has_mall_close, "Mall auto-close sent /mall_close chat command to server") # Server closes mall client._mall_open = true mall.open() _ck(mall.is_open(), "Mall re-opened") client.calls.clear() client._mall_open = false client.mall_closed.emit() _ck(not mall.is_open(), "Mall closed on mall_closed server signal") # ------------------------------------------------------------- # Test 3: Sockets & Attrs Data Preservation # ------------------------------------------------------------- print("\n--- Test 3: Sockets & Attrs Data Preservation in Safebox and Mall ---") var rich_item := { "cell": 0, "vnum": 19, "count": 1, "flags": 0, "anti_flags": 0, "sockets": [28430, 28431, 0], "attrs": [ {"type": 1, "value": 500}, {"type": 5, "value": 12} ] } client._safebox_items = [rich_item] client._mall_items = [rich_item] safe.refresh() mall.refresh() var safe_tt := safe._tooltip_for(rich_item) var mall_tt := mall._tooltip_for(rich_item) _ck(safe_tt.contains("500") or safe_tt.contains("HP") or safe_tt.contains("生命"), "Safebox tooltip includes attribute 1 (HP +500)") _ck(safe_tt.contains("12") or safe_tt.contains("STR") or safe_tt.contains("力量"), "Safebox tooltip includes attribute 2 (STR +12)") _ck(mall_tt.contains("500") or mall_tt.contains("HP") or mall_tt.contains("生命"), "Mall tooltip includes attribute 1 (HP +500)") _ck(mall_tt.contains("12") or mall_tt.contains("STR") or mall_tt.contains("力量"), "Mall tooltip includes attribute 2 (STR +12)") # ------------------------------------------------------------- # Test 4: Safebox Operations (Checkin, Checkout, Move) # ------------------------------------------------------------- print("\n--- Test 4: Safebox Operations (40250 Checkin, Checkout, Move) ---") client._safebox_open = true safe.open() client.calls.clear() client._inventory = [{"cell": 5, "vnum": 27001, "count": 20}] # Free slot 0 is empty # Drag drop from inventory (win 1, cell 5) into empty safebox slot 8 var drop_payload := {"window": 1, "cell": 5, "vnum": 27001, "count": 20} var drop_res := safe._drop_to_slot(drop_payload, 8) _ck(drop_res, "Dropping inventory item into empty Safebox slot succeeds") _ck(client.calls.size() > 0 and client.calls[0]["method"] == "safebox_checkin" and client.calls[0]["args"] == [8, 1, 5], "Drop sent safebox_checkin(safe_pos=8, inv_win=1, inv_cell=5)") # Right-click inventory item -> deposit to next free safebox slot client.calls.clear() client._safebox_items = [{"cell": 0, "vnum": 10, "count": 1}] # Next free is 1 safe.deposit(1, 5) _ck(client.calls.size() > 0 and client.calls[0]["method"] == "safebox_checkin" and client.calls[0]["args"] == [1, 1, 5], "Right-clicking inventory item triggers deposit into next free safebox slot 1") # Right-click safebox slot -> checkout to first free inventory slot client.calls.clear() var ev_rc := InputEventMouseButton.new() ev_rc.button_index = MOUSE_BUTTON_RIGHT ev_rc.pressed = true safe._on_cell_input(0, ev_rc) _ck(client.calls.size() > 0 and client.calls[0]["method"] == "safebox_checkout" and client.calls[0]["args"] == [0, 1, 0], "Right-clicking safebox slot 0 triggers checkout to inventory free slot 0") # Safebox internal slot move (e.g. from cell 0 to cell 6) client.calls.clear() var safe_move_payload := {"window": 3, "cell": 0, "vnum": 10, "count": 1} var move_res := safe._drop_to_slot(safe_move_payload, 6) _ck(move_res, "Moving item between safebox slots succeeds") _ck(client.calls.size() > 0 and client.calls[0]["method"] == "safebox_move" and client.calls[0]["args"] == [0, 6, 1], "Internal safebox move sent safebox_move(from=0, to=6, count=1)") # ------------------------------------------------------------- # Test 5: Mall Operations and MALL_CANNOT_INSERT Rejection # ------------------------------------------------------------- print("\n--- Test 5: Mall Operations & MALL_CANNOT_INSERT (40250 uisafebox.py:640) ---") client.calls.clear() client._inventory = [{"cell": 2, "vnum": 10, "count": 1}] # Free slot 0 is empty client._mall_items = [{"cell": 4, "vnum": 27993, "count": 5}] mall.refresh() # Right-click mall slot 4 -> checkout to inventory free slot 0 var ev_mall_rc := InputEventMouseButton.new() ev_mall_rc.button_index = MOUSE_BUTTON_RIGHT ev_mall_rc.pressed = true mall._on_grid_input(4, ev_mall_rc) _ck(client.calls.size() > 0 and client.calls[0]["method"] == "mall_checkout" and client.calls[0]["args"] == [4, 1, 0], "Right-clicking mall slot 4 triggers mall_checkout(mall_pos=4, inv_win=1, inv_cell=0)") # Attempt to deposit inventory item into mall slot -> rejected client.calls.clear() mouse.attach_item(1, 2, 10, 1) var ev_mall_drop := InputEventMouseButton.new() ev_mall_drop.button_index = MOUSE_BUTTON_LEFT ev_mall_drop.pressed = true mall._on_grid_input(7, ev_mall_drop) _ck(not mouse.is_attached(), "Dragging item into Mall cancels attached mouse item") _ck(mall._status.text == "无法将物品放入商城仓库!", "Mall rejects insertion with exact 40250 warning (MALL_CANNOT_INSERT)") _ck(client.calls.is_empty(), "No packet sent on rejected mall deposit attempt") # Attempt drop via _drop_to_slot var mall_drop_res := mall._drop_to_slot({"window": 1, "cell": 2, "vnum": 10, "count": 1}, 7) _ck(not mall_drop_res, "mall._drop_to_slot strictly returns false") # ------------------------------------------------------------- # Test 6: Password Protocols and Error Notifications # ------------------------------------------------------------- print("\n--- Test 6: Password Protocols & Wrong Password (40250 /safebox_password) ---") client.calls.clear() client.safebox_password("000000") _ck(client.calls.size() > 0 and client.calls[0]["cmd"] == "/safebox_password 000000", "Safebox password sends /safebox_password 000000") client.calls.clear() client.safebox_change_password("000000", "123456") _ck(client.calls.size() > 0 and client.calls[0]["cmd"] == "/safebox_change_password 000000 123456", "Safebox change password sends /safebox_change_password 000000 123456") client.calls.clear() client.mall_password("888888") _ck(client.calls.size() > 0 and client.calls[0]["cmd"] == "/mall_password 888888", "Mall password sends /mall_password 888888") client.safebox_wrong_password.emit() _ck(safe._status.text == "仓库密码错误", "safebox_wrong_password signal shows 仓库密码错误") # Clean up proto.queue_free() safe.queue_free() mall.queue_free() client.queue_free() mouse.queue_free() host.queue_free()