forked from woofmeow/straywild
Expand gameplay systems and interface controls
This commit is contained in:
parent
09b7a78533
commit
2710544070
46 changed files with 2072 additions and 153 deletions
|
|
@ -86,6 +86,11 @@ func _run() -> void:
|
|||
var chat_ui := game_ui.get_node("%ChatUI") as ChatUI
|
||||
assert(player != null and service != null and toolbar != null)
|
||||
assert(chat_ui != null)
|
||||
assert(toolbar.get_parent() == chat_ui.get_parent())
|
||||
assert(
|
||||
toolbar.get_index() > chat_ui.get_index(),
|
||||
"The art-kit toolbar must receive overlapping pointer input before Chat.",
|
||||
)
|
||||
assert(bool(game_ui.call("_can_start_virtual_mouse")))
|
||||
var virtual_pointer_states: Array[bool] = []
|
||||
game_ui.virtual_pointer_mode_changed.connect(
|
||||
|
|
@ -317,6 +322,22 @@ func _run() -> void:
|
|||
assert(not (
|
||||
game_ui.get_node("%ExperiencePresentation") as Control
|
||||
).visible)
|
||||
var hud_fishing_spot := main.get_node("%FishingSpot") as FishingSpot
|
||||
assert(hud_fishing_spot != null)
|
||||
hud_fishing_spot.state = FishingSpot.FishingState.SHOWING_CATCH
|
||||
game_ui.call("_refresh_gameplay_hud_visibility")
|
||||
assert((
|
||||
game_ui.get_node("%GameplayTransientHUD") as Control
|
||||
).visible)
|
||||
assert(not (
|
||||
game_ui.get_node("%ExperiencePresentation") as Control
|
||||
).visible)
|
||||
assert(chat_ui.is_hud_hidden())
|
||||
hud_fishing_spot.state = FishingSpot.FishingState.READY
|
||||
game_ui.call("_refresh_gameplay_hud_visibility")
|
||||
assert(not (
|
||||
game_ui.get_node("%GameplayTransientHUD") as Control
|
||||
).visible)
|
||||
quick_menu.open_menu(true)
|
||||
assert(quick_menu.visible and quick_menu.is_open())
|
||||
quick_menu.close_menu()
|
||||
|
|
@ -365,6 +386,12 @@ func _run() -> void:
|
|||
await process_frame
|
||||
assert(service.is_active() and not service.is_placement_mode())
|
||||
assert(toolbar.visible)
|
||||
chat_ui.open_chat()
|
||||
assert(chat_ui.is_open())
|
||||
assert(not service.is_active() and not toolbar.visible)
|
||||
chat_ui.close_chat()
|
||||
assert(not chat_ui.is_open())
|
||||
assert(service.is_active() and toolbar.visible)
|
||||
var ui_root := game_ui.get_node("%UIRoot") as Control
|
||||
assert(toolbar.get_parent() == ui_root)
|
||||
assert(is_zero_approx(toolbar.position.y))
|
||||
|
|
|
|||
120
tests/chat_privacy_multiplayer_validation.gd
Normal file
120
tests/chat_privacy_multiplayer_validation.gd
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
extends SceneTree
|
||||
|
||||
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||
const TEST_PORT: int = 18152
|
||||
const PREJOIN_MESSAGE: String = "private prejoin message"
|
||||
const LIVE_MESSAGE: String = "live message after join"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var arguments: PackedStringArray = OS.get_cmdline_user_args()
|
||||
if arguments.has("host"):
|
||||
await _run_host()
|
||||
return
|
||||
if arguments.has("client"):
|
||||
await _run_client()
|
||||
return
|
||||
push_error("Chat privacy validation needs host or client mode.")
|
||||
quit(1)
|
||||
|
||||
|
||||
func _run_host() -> void:
|
||||
var main: Node = await _create_initialized_main()
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
assert(session.start_private_host(TEST_PORT))
|
||||
assert(save_manager.initialize_new_game())
|
||||
main.call("_enter_gameplay")
|
||||
for _frame: int in 4:
|
||||
await physics_frame
|
||||
assert(session.set_host_open(true))
|
||||
var chat_service := main.get_node(
|
||||
"%NetworkChatService"
|
||||
) as NetworkChatService
|
||||
assert(chat_service.send_local_message(PREJOIN_MESSAGE))
|
||||
var remote_peer_id: int = await _wait_for_remote_peer(session)
|
||||
assert(remote_peer_id > 1)
|
||||
await create_timer(1.0).timeout
|
||||
assert(chat_service.send_local_message(LIVE_MESSAGE))
|
||||
var disconnect_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while (
|
||||
Time.get_ticks_msec() < disconnect_deadline
|
||||
and session.is_authenticated_peer(remote_peer_id)
|
||||
):
|
||||
await process_frame
|
||||
assert(not session.is_authenticated_peer(remote_peer_id))
|
||||
print("Chat privacy multiplayer host validation: PASS")
|
||||
await _session_cleanup(main, session)
|
||||
|
||||
|
||||
func _run_client() -> void:
|
||||
var main: Node = await _create_initialized_main()
|
||||
main.call("_on_title_join_game_requested", "127.0.0.1:%d" % TEST_PORT)
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||
while Time.get_ticks_msec() < join_deadline:
|
||||
await process_frame
|
||||
if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
|
||||
main.call("_confirm_server_trust")
|
||||
if session.is_joined_client() and bool(main.get("_gameplay_started")):
|
||||
break
|
||||
assert(session.is_joined_client())
|
||||
var chat_service := main.get_node(
|
||||
"%NetworkChatService"
|
||||
) as NetworkChatService
|
||||
await create_timer(0.5).timeout
|
||||
assert(not _history_contains(chat_service, PREJOIN_MESSAGE))
|
||||
var live_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while (
|
||||
Time.get_ticks_msec() < live_deadline
|
||||
and not _history_contains(chat_service, LIVE_MESSAGE)
|
||||
):
|
||||
await process_frame
|
||||
assert(_history_contains(chat_service, LIVE_MESSAGE))
|
||||
assert(not _history_contains(chat_service, PREJOIN_MESSAGE))
|
||||
print("Chat privacy multiplayer client validation: PASS")
|
||||
await _session_cleanup(main, session)
|
||||
|
||||
|
||||
func _history_contains(service: NetworkChatService, body: String) -> bool:
|
||||
return service.get_history().any(
|
||||
func(message: Dictionary) -> bool:
|
||||
return str(message.get("body", "")) == body
|
||||
)
|
||||
|
||||
|
||||
func _create_initialized_main() -> Node:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var main: Node = MainScene.instantiate()
|
||||
root.add_child(main)
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
if not bool(main.get("_application_initialized")):
|
||||
main.call("_activate_selected_data_path", "", true)
|
||||
for _frame: int in 8:
|
||||
await process_frame
|
||||
assert(bool(main.get("_application_initialized")))
|
||||
return main
|
||||
|
||||
|
||||
func _wait_for_remote_peer(session: NetworkSession) -> int:
|
||||
var deadline: int = Time.get_ticks_msec() + 20000
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
await process_frame
|
||||
for peer_id: int in session.get_authenticated_peer_ids():
|
||||
if peer_id != session.get_local_peer_id():
|
||||
return peer_id
|
||||
return 0
|
||||
|
||||
|
||||
func _session_cleanup(main: Node, session: NetworkSession) -> void:
|
||||
session.disconnect_session("")
|
||||
main.queue_free()
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
await create_timer(0.1).timeout
|
||||
quit()
|
||||
1
tests/chat_privacy_multiplayer_validation.gd.uid
Normal file
1
tests/chat_privacy_multiplayer_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ivtqrnppnmli
|
||||
|
|
@ -19,6 +19,12 @@ func _run() -> void:
|
|||
defaults.call("_validate")
|
||||
assert(defaults.is_valid())
|
||||
assert(not bool(defaults.get("public_listing")))
|
||||
assert(not bool(defaults.get("chat_logging")))
|
||||
assert(str(defaults.get("chat_log_path")).is_empty())
|
||||
assert(NetworkChatService.BURST_COUNT == 3)
|
||||
assert(NetworkChatService.WINDOW_COUNT == 5)
|
||||
assert(is_equal_approx(NetworkChatService.WINDOW_SECONDS, 10.0))
|
||||
assert(NetworkChatService.CALL_COOLDOWN_MILLISECONDS == 90)
|
||||
|
||||
var path: String = ProjectSettings.globalize_path(
|
||||
"user://dedicated-server-validation.cfg"
|
||||
|
|
@ -32,6 +38,12 @@ func _run() -> void:
|
|||
file.set_value("server", "public", true)
|
||||
file.set_value("server", "data_directory", "/tmp/configured-server")
|
||||
file.set_value("discovery", "url", "https://discovery.netfishing.org/")
|
||||
file.set_value("privacy", "chat_logging", true)
|
||||
file.set_value(
|
||||
"privacy",
|
||||
"chat_log_path",
|
||||
"/tmp/configured-server/logs/chat.jsonl",
|
||||
)
|
||||
file.set_value(
|
||||
"moderation", "operators", PackedStringArray([OPERATOR_B, OPERATOR_A])
|
||||
)
|
||||
|
|
@ -48,6 +60,11 @@ func _run() -> void:
|
|||
assert(int(configured.get("world_seed")) == 928374)
|
||||
assert(bool(configured.get("public_listing")))
|
||||
assert(str(configured.get("discovery_url")) == "https://discovery.netfishing.org")
|
||||
assert(bool(configured.get("chat_logging")))
|
||||
assert(
|
||||
str(configured.get("chat_log_path"))
|
||||
== "/tmp/configured-server/logs/chat.jsonl"
|
||||
)
|
||||
assert(
|
||||
configured.get("operator_fingerprints")
|
||||
== PackedStringArray([OPERATOR_A, OPERATOR_B])
|
||||
|
|
@ -89,6 +106,51 @@ func _run() -> void:
|
|||
invalid_seed.call("_validate")
|
||||
assert(not invalid_seed.is_valid())
|
||||
|
||||
var default_chat_path := ConfigType.new()
|
||||
default_chat_path.set("data_directory", "/tmp/default-chat-path-server")
|
||||
default_chat_path.set("chat_logging", true)
|
||||
default_chat_path.call("_validate")
|
||||
assert(default_chat_path.is_valid())
|
||||
assert(
|
||||
str(default_chat_path.get("chat_log_path"))
|
||||
== "/tmp/default-chat-path-server/logs/chat.jsonl"
|
||||
)
|
||||
|
||||
var invalid_chat_path := ConfigType.new()
|
||||
invalid_chat_path.set("data_directory", "/tmp/invalid-chat-path-server")
|
||||
invalid_chat_path.set("chat_logging", true)
|
||||
invalid_chat_path.set("chat_log_path", "relative/chat.jsonl")
|
||||
invalid_chat_path.call("_validate")
|
||||
assert(not invalid_chat_path.is_valid())
|
||||
|
||||
var chat_log_validation_path: String = ProjectSettings.globalize_path(
|
||||
"user://dedicated-chat-log-validation.jsonl"
|
||||
)
|
||||
var chat_service := NetworkChatService.new()
|
||||
assert(not chat_service.configure_dedicated_history(
|
||||
true,
|
||||
"relative/chat.jsonl",
|
||||
))
|
||||
assert(chat_service.configure_dedicated_history(
|
||||
true,
|
||||
chat_log_validation_path,
|
||||
))
|
||||
assert(FileAccess.file_exists(chat_log_validation_path))
|
||||
chat_service.call("_append_chat_log", {
|
||||
"sender_display_name": "Privacy Tester",
|
||||
"body": "explicitly retained message",
|
||||
})
|
||||
var logged_message: Variant = JSON.parse_string(
|
||||
FileAccess.get_file_as_string(chat_log_validation_path).strip_edges()
|
||||
)
|
||||
assert(typeof(logged_message) == TYPE_DICTIONARY)
|
||||
assert(str(logged_message.get("recorded_at_utc", "")).ends_with("Z"))
|
||||
assert(logged_message.get("sender_display_name") == "Privacy Tester")
|
||||
assert(logged_message.get("body") == "explicitly retained message")
|
||||
assert(chat_service.configure_dedicated_history(false, ""))
|
||||
assert(DirAccess.remove_absolute(chat_log_validation_path) == OK)
|
||||
chat_service.free()
|
||||
|
||||
var discovery := DiscoveryClient.new()
|
||||
assert(
|
||||
str(discovery.call("_default_room_name", "River"))
|
||||
|
|
|
|||
|
|
@ -54,6 +54,21 @@ func _initialize() -> void:
|
|||
assert(FishingShopStockType.get_price(&"magnet") == 250)
|
||||
assert(FishingShopStockType.get_stock_item_ids().has(&"magnet"))
|
||||
assert(FishingShopStockType.is_permanent_unlock(&"magnet", magnet))
|
||||
var fishing_net: ItemDataType = (
|
||||
ItemCatalogResource.get_available_item_by_id(&"fishing_net")
|
||||
)
|
||||
assert(fishing_net != null)
|
||||
assert(fishing_net.category == ItemDataType.Category.TOOL)
|
||||
assert(fishing_net.icon == null)
|
||||
assert(not fishing_net.usable)
|
||||
assert(not fishing_net.equippable)
|
||||
assert(not fishing_net.hotbar_allowed)
|
||||
assert(FishingShopStockType.get_price(&"fishing_net") == 250)
|
||||
assert(FishingShopStockType.get_stock_item_ids().has(&"fishing_net"))
|
||||
assert(FishingShopStockType.is_permanent_unlock(
|
||||
&"fishing_net", fishing_net
|
||||
))
|
||||
assert(not FishingShopStockType.is_gathering_tool(&"fishing_net"))
|
||||
var crab_net: ItemDataType = ItemCatalogResource.get_available_item_by_id(
|
||||
&"crab_net"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -98,29 +98,27 @@ func _run() -> void:
|
|||
|
||||
assert(save_manager.save_now())
|
||||
var save_path: String = str(save_manager.get("_save_path"))
|
||||
var save_file := FileAccess.open(save_path, FileAccess.READ)
|
||||
assert(save_file != null)
|
||||
var parsed: Variant = JSON.parse_string(save_file.get_as_text())
|
||||
save_file.close()
|
||||
assert(typeof(parsed) == TYPE_DICTIONARY)
|
||||
var hotbar_data: Dictionary = (parsed as Dictionary)["hotbar"]
|
||||
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
|
||||
assert(bool(decoded.get("ok", false)))
|
||||
var parsed: Dictionary = decoded["data"]
|
||||
var hotbar_data: Dictionary = parsed["hotbar"]
|
||||
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
|
||||
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
|
||||
assert(int((parsed as Dictionary)["save_version"]) == 9)
|
||||
assert(int(parsed["save_version"]) == 10)
|
||||
assert(
|
||||
int((parsed as Dictionary)["experience"]["total_experience"])
|
||||
int(parsed["experience"]["total_experience"])
|
||||
== 125
|
||||
)
|
||||
assert(absf(
|
||||
float((parsed as Dictionary)["world"]["time_hours"]) - saved_time_hours
|
||||
float(parsed["world"]["time_hours"]) - saved_time_hours
|
||||
) < 0.01)
|
||||
assert(
|
||||
int((parsed as Dictionary)["world"]["weather"])
|
||||
int(parsed["world"]["weather"])
|
||||
== int(saved_weather)
|
||||
)
|
||||
assert(absf(
|
||||
float(
|
||||
(parsed as Dictionary)["world"][
|
||||
parsed["world"][
|
||||
"weather_seconds_remaining"
|
||||
]
|
||||
) - saved_weather_seconds
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ func _validate_version_four_migration() -> void:
|
|||
version_four,
|
||||
4,
|
||||
)
|
||||
assert(int(migrated.get("save_version", -1)) == 9)
|
||||
assert(int(migrated.get("save_version", -1)) == 10)
|
||||
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
|
||||
assert(
|
||||
is_equal_approx(
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ func _run() -> void:
|
|||
var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
|
||||
var catalog := main.get("fish_catalog") as FishPool
|
||||
assert(player != null and fishing_spot != null and catalog != null)
|
||||
fishing_spot.minimum_showcase_duration = 0.15
|
||||
assert(player.bag.add_item(&"crab_net"))
|
||||
assert(player.hotbar.assign_item(0, &"crab_net"))
|
||||
assert(player.hotbar.select_slot(0))
|
||||
|
|
@ -60,13 +61,20 @@ func _run() -> void:
|
|||
assert(not player.is_movement_enabled())
|
||||
Input.action_release("fish_primary")
|
||||
await process_frame
|
||||
assert(bool(fishing_spot.get("_put_away_press_armed")))
|
||||
assert(not bool(fishing_spot.get("_put_away_press_armed")))
|
||||
|
||||
var pocket_event := InputEventAction.new()
|
||||
pocket_event.action = &"fish_primary"
|
||||
pocket_event.pressed = true
|
||||
Input.parse_input_event(pocket_event)
|
||||
await process_frame
|
||||
assert(fishing_spot.get("_pending_catch") == crab_catch)
|
||||
Input.action_release("fish_primary")
|
||||
await create_timer(0.2).timeout
|
||||
await process_frame
|
||||
assert(bool(fishing_spot.get("_put_away_press_armed")))
|
||||
Input.parse_input_event(pocket_event)
|
||||
await process_frame
|
||||
assert(fishing_spot.get("_pending_catch") == null)
|
||||
|
||||
var pocket_deadline: int = Time.get_ticks_msec() + 6000
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
extends SceneTree
|
||||
|
||||
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||
const ItemCatalogResource: ItemCatalog = preload(
|
||||
"res://items/catalog/item_catalog.tres"
|
||||
)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
|
|
@ -27,29 +30,34 @@ func _run() -> void:
|
|||
save_manager.set_autosave_enabled(true)
|
||||
assert(player.bag.add_item(&"art_kit"))
|
||||
assert(player.bag.add_item(&"coffee"))
|
||||
assert(player.bag.add_item(&"worms"))
|
||||
assert(player.bag.add_item(&"the_standby"))
|
||||
assert(player.equip_bait(ItemCatalogResource.get_item_by_id(&"worms")))
|
||||
assert(player.equip_lure(ItemCatalogResource.get_item_by_id(&"the_standby")))
|
||||
assert(player.bag.move_item_to_storage_slot(&"basic_fishing_rod", 14))
|
||||
assert(player.bag.move_item_to_storage_slot(&"coffee", 12))
|
||||
assert(save_manager.save_now())
|
||||
|
||||
var save_file := FileAccess.open(
|
||||
str(save_manager.get("_save_path")),
|
||||
FileAccess.READ,
|
||||
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
|
||||
str(save_manager.get("_save_path"))
|
||||
)
|
||||
assert(save_file != null)
|
||||
var parsed: Variant = JSON.parse_string(save_file.get_as_text())
|
||||
save_file.close()
|
||||
assert(typeof(parsed) == TYPE_DICTIONARY)
|
||||
var records: Array = (parsed as Dictionary)["bag"]["items"]
|
||||
assert(int((parsed as Dictionary)["world"]["seed"]) == TEST_WORLD_SEED)
|
||||
assert(bool(decoded.get("ok", false)))
|
||||
var parsed: Dictionary = decoded["data"]
|
||||
var records: Array = parsed["bag"]["items"]
|
||||
assert(int(parsed["world"]["seed"]) == TEST_WORLD_SEED)
|
||||
assert(_saved_slot(records, &"basic_fishing_rod") == 14)
|
||||
assert(_saved_slot(records, &"coffee") == 12)
|
||||
|
||||
assert(player.bag.move_item_to_storage_slot(&"basic_fishing_rod", 0))
|
||||
assert(player.bag.move_item_to_storage_slot(&"coffee", 0))
|
||||
player.unequip_bait()
|
||||
player.unequip_lure()
|
||||
assert(save_manager.load_player_data())
|
||||
assert(save_manager.get_world_seed() == TEST_WORLD_SEED)
|
||||
assert(player.bag.get_storage_slot(&"basic_fishing_rod") == 14)
|
||||
assert(player.bag.get_storage_slot(&"coffee") == 12)
|
||||
assert(player.active_bait_id == &"worms")
|
||||
assert(player.active_lure_id == &"the_standby")
|
||||
|
||||
main.queue_free()
|
||||
for _frame: int in 4:
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ func _run() -> void:
|
|||
_validate_weather_guarantees(board)
|
||||
_validate_late_board_weather_fallback()
|
||||
_validate_remote_tamper_rejection(jobs, board)
|
||||
_validate_creature_jobs(jobs)
|
||||
|
||||
var sell_job: Dictionary = _find_job(
|
||||
jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL
|
||||
|
|
@ -195,15 +196,12 @@ func _run() -> void:
|
|||
assert(session.set_host_open(false))
|
||||
|
||||
assert(save_manager.save_now())
|
||||
var save_file := FileAccess.open(
|
||||
str(save_manager.get("_save_path")), FileAccess.READ
|
||||
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
|
||||
str(save_manager.get("_save_path"))
|
||||
)
|
||||
assert(save_file != null)
|
||||
var parsed: Variant = JSON.parse_string(save_file.get_as_text())
|
||||
save_file.close()
|
||||
assert(typeof(parsed) == TYPE_DICTIONARY)
|
||||
var save_data: Dictionary = parsed
|
||||
assert(int(save_data.get("save_version", -1)) == 9)
|
||||
assert(bool(decoded.get("ok", false)))
|
||||
var save_data: Dictionary = decoded["data"]
|
||||
assert(int(save_data.get("save_version", -1)) == 10)
|
||||
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
|
||||
|
||||
_validate_pause_session_switch(main, session)
|
||||
|
|
@ -263,6 +261,100 @@ func _validate_unavailable_fishnet_layout() -> void:
|
|||
await process_frame
|
||||
|
||||
|
||||
func _validate_creature_jobs(jobs: PlayerJobService) -> void:
|
||||
var fish_count: int = 0
|
||||
var insect_count: int = 0
|
||||
var shellfish_count: int = 0
|
||||
var candidates: Array[FishData] = []
|
||||
for fish: FishData in Catalog.candidates:
|
||||
if fish == null or not fish.is_selectable():
|
||||
continue
|
||||
candidates.append(fish)
|
||||
match fish.get_creature_group():
|
||||
FishData.CreatureGroup.FISH:
|
||||
fish_count += 1
|
||||
FishData.CreatureGroup.INSECT:
|
||||
insect_count += 1
|
||||
FishData.CreatureGroup.SHELLFISH:
|
||||
shellfish_count += 1
|
||||
assert(fish_count > 0 and insect_count > 0 and shellfish_count > 0)
|
||||
|
||||
var lifetime: Array[Dictionary] = jobs.get_lifetime_jobs()
|
||||
var fish_discovery: Dictionary = _find_job(
|
||||
lifetime, JobCatalog.Kind.DISCOVER_SPECIES
|
||||
)
|
||||
assert(int(fish_discovery.get("target", -1)) == fish_count)
|
||||
for creature_group: int in [
|
||||
FishData.CreatureGroup.INSECT,
|
||||
FishData.CreatureGroup.SHELLFISH,
|
||||
]:
|
||||
assert(not _find_creature_job(
|
||||
lifetime,
|
||||
JobCatalog.Kind.CATCH_CREATURE_GROUP,
|
||||
creature_group,
|
||||
).is_empty())
|
||||
assert(not _find_creature_job(
|
||||
lifetime,
|
||||
JobCatalog.Kind.DISCOVER_CREATURE_GROUP,
|
||||
creature_group,
|
||||
).is_empty())
|
||||
assert(not _find_creature_job(
|
||||
lifetime,
|
||||
JobCatalog.Kind.MASTER_CREATURE_GROUP,
|
||||
creature_group,
|
||||
).is_empty())
|
||||
|
||||
var daily_groups: Dictionary[int, bool] = {}
|
||||
for seed_index: int in 64:
|
||||
for job: Dictionary in JobCatalog.generate_daily_jobs(
|
||||
"creature-job-validation-%d" % seed_index,
|
||||
candidates,
|
||||
true,
|
||||
):
|
||||
if int(job.get("kind", -1)) == JobCatalog.Kind.CATCH_CREATURE_GROUP:
|
||||
daily_groups[int(job.get("creature_group", -1))] = true
|
||||
assert(daily_groups.has(FishData.CreatureGroup.INSECT))
|
||||
assert(daily_groups.has(FishData.CreatureGroup.SHELLFISH))
|
||||
|
||||
var beetle: FishData = Catalog.get_fish_by_id(&"beetle_stag_common")
|
||||
assert(beetle != null)
|
||||
var beetle_catch := _make_test_catch(beetle, &"job-beetle")
|
||||
jobs.call("_on_authoritative_catch", beetle_catch)
|
||||
var statistics: Dictionary = jobs.to_save_data().get("statistics", {})
|
||||
assert(int(statistics.get("fish_caught", -1)) == 0)
|
||||
assert(int(statistics.get("insects_caught", -1)) == 1)
|
||||
|
||||
|
||||
func _find_creature_job(
|
||||
jobs: Array[Dictionary],
|
||||
kind: JobCatalog.Kind,
|
||||
creature_group: int,
|
||||
) -> Dictionary:
|
||||
for job: Dictionary in jobs:
|
||||
if (
|
||||
int(job.get("kind", -1)) == kind
|
||||
and int(job.get("creature_group", -1)) == creature_group
|
||||
):
|
||||
return job
|
||||
return {}
|
||||
|
||||
|
||||
func _make_test_catch(fish: FishData, catch_id: StringName) -> FishCatch:
|
||||
var fish_catch := FishCatch.new()
|
||||
fish_catch.fish = fish
|
||||
fish_catch.fish_id = fish.id
|
||||
fish_catch.catch_id = catch_id
|
||||
fish_catch.catch_sequence = 1
|
||||
fish_catch.weight_lb = fish.get_minimum_weight()
|
||||
fish_catch.display_scale = fish.get_display_scale_for_weight(
|
||||
fish_catch.weight_lb
|
||||
)
|
||||
fish_catch.quality = FishQuality.Tier.BORING
|
||||
fish_catch.sale_value = fish.get_sale_value_for_weight(fish_catch.weight_lb)
|
||||
assert(fish_catch.is_valid())
|
||||
return fish_catch
|
||||
|
||||
|
||||
func _validate_weather_guarantees(board: Dictionary) -> void:
|
||||
var jobs: Array = board.get("jobs", [])
|
||||
var schedule: Array = board.get("weather_schedule", [])
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ const KeyboardMouseMappingPanelType = preload(
|
|||
"res://ui/keyboard_mouse_mapping_panel.gd"
|
||||
)
|
||||
const SettingsPanelScene = preload("res://ui/settings_panel.tscn")
|
||||
const FishingSpotScene = preload("res://fishing/fishing_spot.tscn")
|
||||
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
||||
const CatchDifficultyProfileType = preload(
|
||||
"res://fishing/catch_difficulty_profile.gd"
|
||||
)
|
||||
const PlayerType = preload("res://player/player.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
|
|
@ -27,6 +33,13 @@ func _run() -> void:
|
|||
str(KeyboardMouseMappingManagerType.ROLE_PRIMARY_ACTION)
|
||||
].get("kind", "")) == "mouse_button"
|
||||
)
|
||||
assert(
|
||||
str(defaults[
|
||||
str(KeyboardMouseMappingManagerType.ROLE_ALTERNATE_REEL)
|
||||
].get("kind", "")) == "key"
|
||||
)
|
||||
assert(_has_physical_key(&"reel_alternate", KEY_QUOTELEFT))
|
||||
assert(_event_count(&"reel_alternate", true) == 0)
|
||||
assert(
|
||||
str(defaults[
|
||||
str(KeyboardMouseMappingManagerType.ROLE_CHAT)
|
||||
|
|
@ -60,6 +73,15 @@ func _run() -> void:
|
|||
))
|
||||
assert(_has_mouse_button(&"fish_primary", MOUSE_BUTTON_XBUTTON1))
|
||||
assert(not _has_mouse_button(&"fish_primary", MOUSE_BUTTON_LEFT))
|
||||
var alternate_reel_key := InputEventKey.new()
|
||||
alternate_reel_key.physical_keycode = KEY_G
|
||||
alternate_reel_key.pressed = true
|
||||
assert(manager.set_binding(
|
||||
KeyboardMouseMappingManagerType.ROLE_ALTERNATE_REEL,
|
||||
manager.binding_from_event(alternate_reel_key),
|
||||
))
|
||||
assert(_has_physical_key(&"reel_alternate", KEY_G))
|
||||
assert(_event_count(&"reel_alternate", true) == 0)
|
||||
|
||||
var joypad_event := InputEventJoypadButton.new()
|
||||
joypad_event.button_index = JOY_BUTTON_A
|
||||
|
|
@ -117,11 +139,64 @@ func _run() -> void:
|
|||
))
|
||||
assert(_has_physical_key(&"jump", KEY_SPACE))
|
||||
assert(_has_mouse_button(&"fish_primary", MOUSE_BUTTON_LEFT))
|
||||
assert(_has_physical_key(&"reel_alternate", KEY_QUOTELEFT))
|
||||
assert(_event_count(&"reel_alternate", true) == 0)
|
||||
assert(_has_physical_key(&"open_chat", KEY_T))
|
||||
assert(_event_count(&"jump", true) == joypad_events_before)
|
||||
|
||||
var legacy_bindings: Dictionary = manager.get_active_bindings()
|
||||
legacy_bindings.erase(str(
|
||||
KeyboardMouseMappingManagerType.ROLE_ALTERNATE_REEL
|
||||
))
|
||||
legacy_bindings[str(KeyboardMouseMappingManagerType.ROLE_JUMP)] = (
|
||||
manager.binding_from_event(rebind_key)
|
||||
)
|
||||
var legacy_file := FileAccess.open(
|
||||
KeyboardMouseMappingManagerType.MAPPING_PATH,
|
||||
FileAccess.WRITE,
|
||||
)
|
||||
assert(legacy_file != null)
|
||||
legacy_file.store_string(JSON.stringify({
|
||||
"format_version": KeyboardMouseMappingManagerType.LEGACY_FORMAT_VERSION,
|
||||
"bindings": legacy_bindings,
|
||||
}))
|
||||
legacy_file.close()
|
||||
var migrated_manager := KeyboardMouseMappingManagerType.new()
|
||||
root.add_child(migrated_manager)
|
||||
await process_frame
|
||||
assert(migrated_manager.has_custom_mapping())
|
||||
assert(_has_physical_key(&"jump", KEY_R))
|
||||
assert(_has_physical_key(&"reel_alternate", KEY_QUOTELEFT))
|
||||
assert(migrated_manager.reset_mapping())
|
||||
|
||||
var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
|
||||
root.add_child(fishing_spot)
|
||||
await process_frame
|
||||
var local_player := PlayerType.new()
|
||||
fishing_spot.set("_local_player", local_player)
|
||||
fishing_spot.set("_gameplay_input_enabled", true)
|
||||
fishing_spot.state = FishingSpotType.FishingState.FIGHTING
|
||||
var catch_controller := fishing_spot.get_node("%CatchController")
|
||||
var catch_profile := CatchDifficultyProfileType.new()
|
||||
catch_profile.barrier_count_min = 0
|
||||
catch_profile.barrier_count_max = 0
|
||||
catch_controller.start_encounter(catch_profile, 1.0, 1)
|
||||
var alternate_press := InputEventAction.new()
|
||||
alternate_press.action = &"reel_alternate"
|
||||
alternate_press.pressed = true
|
||||
fishing_spot._unhandled_input(alternate_press)
|
||||
assert(bool(catch_controller.get("_reel_input_held")))
|
||||
var alternate_release := InputEventAction.new()
|
||||
alternate_release.action = &"reel_alternate"
|
||||
alternate_release.pressed = false
|
||||
fishing_spot._unhandled_input(alternate_release)
|
||||
assert(not bool(catch_controller.get("_reel_input_held")))
|
||||
fishing_spot.free()
|
||||
local_player.free()
|
||||
|
||||
settings_panel.queue_free()
|
||||
panel.queue_free()
|
||||
migrated_manager.queue_free()
|
||||
manager.queue_free()
|
||||
print("Keyboard and mouse mapping validation: PASS")
|
||||
quit(0)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ func _run() -> void:
|
|||
await process_frame
|
||||
assert(bool(normal_main.get("_application_initialized")))
|
||||
_validate_normal_profile(normal_main)
|
||||
_validate_new_game_music_transition(normal_main)
|
||||
_stop_audio_players(normal_main)
|
||||
normal_main.queue_free()
|
||||
for _frame: int in 8:
|
||||
|
|
@ -115,6 +116,7 @@ func _validate_main_profile(main: Node) -> void:
|
|||
assert(player_menu != null)
|
||||
assert(not player_menu.is_cooler_water_effect_enabled())
|
||||
assert((main.get_node("%TitleMusic") as AudioStreamPlayer).stream != null)
|
||||
assert((main.get_node("%NewGameMusic") as AudioStreamPlayer).stream != null)
|
||||
assert((main.get_node("%DuskMusic") as AudioStreamPlayer).stream != null)
|
||||
assert(
|
||||
(main.get_node("%WavesAudio") as AudioStreamPlayer).stream == null
|
||||
|
|
@ -183,6 +185,7 @@ func _validate_normal_profile(main: Node) -> void:
|
|||
assert(clouds.get_node_or_null("CloudField") is MultiMeshInstance3D)
|
||||
assert(clouds.get_node_or_null("CloudCeiling") == null)
|
||||
assert((main.get_node("%TitleMusic") as AudioStreamPlayer).stream != null)
|
||||
assert((main.get_node("%NewGameMusic") as AudioStreamPlayer).stream != null)
|
||||
assert((main.get_node("%DuskMusic") as AudioStreamPlayer).stream != null)
|
||||
assert(
|
||||
(main.get_node("%WavesAudio") as AudioStreamPlayer).stream != null
|
||||
|
|
@ -192,6 +195,18 @@ func _validate_normal_profile(main: Node) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _validate_new_game_music_transition(main: Node) -> void:
|
||||
var title_music := main.get_node("%TitleMusic") as AudioStreamPlayer
|
||||
var new_game_music := main.get_node("%NewGameMusic") as AudioStreamPlayer
|
||||
assert(title_music.playing)
|
||||
main.call("_start_new_game_music")
|
||||
assert(not title_music.playing)
|
||||
assert(new_game_music.playing)
|
||||
main.call("_show_title_music", true)
|
||||
assert(not new_game_music.playing)
|
||||
assert(title_music.playing)
|
||||
|
||||
|
||||
func _first_fresh_water_visual(main: Node) -> MeshInstance3D:
|
||||
var root := main.get_node(
|
||||
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ func _validate_save_migration() -> void:
|
|||
version_five,
|
||||
5,
|
||||
)
|
||||
assert(int(migrated.get("save_version", -1)) == 9)
|
||||
assert(int(migrated.get("save_version", -1)) == 10)
|
||||
var experience_data: Dictionary = migrated.get("experience", {})
|
||||
assert(int(experience_data.get("total_experience", -1)) == 0)
|
||||
var world_data: Dictionary = migrated.get("world", {})
|
||||
|
|
|
|||
|
|
@ -152,15 +152,10 @@ func _run_client() -> void:
|
|||
var chat_service := main.get_node(
|
||||
"%NetworkChatService"
|
||||
) as NetworkChatService
|
||||
var history_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while Time.get_ticks_msec() < history_deadline:
|
||||
if chat_service.get_history().any(
|
||||
func(message: Dictionary) -> bool:
|
||||
return str(message.get("body", "")) == HOST_HISTORY_MESSAGE
|
||||
):
|
||||
break
|
||||
await process_frame
|
||||
assert(chat_service.get_history().any(
|
||||
# Player-hosted rooms deliberately send no prior scrollback to joining or
|
||||
# reconnecting clients. Live messages sent after authentication still work.
|
||||
await create_timer(0.5).timeout
|
||||
assert(not chat_service.get_history().any(
|
||||
func(message: Dictionary) -> bool:
|
||||
return str(message.get("body", "")) == HOST_HISTORY_MESSAGE
|
||||
))
|
||||
|
|
|
|||
135
tests/progression_archive_validation.gd
Normal file
135
tests/progression_archive_validation.gd
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
extends SceneTree
|
||||
|
||||
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var main: Node = MainScene.instantiate()
|
||||
root.add_child(main)
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
if not bool(main.get("_application_initialized")):
|
||||
main.call("_activate_selected_data_path", "", true)
|
||||
for _frame: int in 8:
|
||||
await process_frame
|
||||
assert(bool(main.get("_application_initialized")))
|
||||
|
||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
var player := main.get("_player") as Player
|
||||
var data_root := main.get("_data_root") as PlayerDataRoot
|
||||
assert(save_manager != null and player != null and data_root != null)
|
||||
assert(save_manager.initialize_new_game(864209))
|
||||
save_manager.set_autosave_enabled(true)
|
||||
assert(player.wallet.restore_balance(4321))
|
||||
assert(player.bag.add_item(&"coffee"))
|
||||
assert(save_manager.save_now())
|
||||
|
||||
var save_path: String = str(save_manager.get("_save_path"))
|
||||
assert(save_path.ends_with("player/player_save.nfsave"))
|
||||
assert(FileAccess.file_exists(save_path))
|
||||
assert(not FileAccess.file_exists(save_path.get_base_dir().path_join(
|
||||
"player_save.json"
|
||||
)))
|
||||
_assert_opaque(save_path)
|
||||
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
|
||||
assert(bool(decoded.get("ok", false)))
|
||||
assert(int((decoded["data"] as Dictionary)["save_version"]) == 10)
|
||||
|
||||
var archive_path: String = data_root.progression_backup_directory().path_join(
|
||||
"progression-test.nfsave"
|
||||
)
|
||||
var exported: Dictionary = save_manager.export_progression_archive(
|
||||
archive_path
|
||||
)
|
||||
assert(bool(exported.get("ok", false)))
|
||||
assert(FileAccess.file_exists(archive_path))
|
||||
_assert_opaque(archive_path)
|
||||
var inspected: Dictionary = save_manager.inspect_progression_archive(
|
||||
archive_path
|
||||
)
|
||||
assert(bool(inspected.get("ok", false)))
|
||||
assert(int(inspected.get("wallet_balance", -1)) == 4321)
|
||||
assert(int(inspected.get("world_seed", -1)) == 864209)
|
||||
|
||||
assert(player.wallet.restore_balance(7))
|
||||
assert(save_manager.save_now())
|
||||
var imported: Dictionary = save_manager.import_progression_archive(
|
||||
archive_path
|
||||
)
|
||||
assert(bool(imported.get("ok", false)))
|
||||
assert(save_manager.load_player_data())
|
||||
assert(player.wallet.get_balance() == 4321)
|
||||
assert(save_manager.get_world_seed() == 864209)
|
||||
|
||||
var legacy_data: Dictionary = (
|
||||
ProgressionSaveCodec.read_local_save(save_path)["data"]
|
||||
)
|
||||
assert(save_manager.delete_progression_save())
|
||||
var legacy_path: String = save_path.get_base_dir().path_join(
|
||||
"player_save.json"
|
||||
)
|
||||
var legacy_file := FileAccess.open(legacy_path, FileAccess.WRITE)
|
||||
assert(legacy_file != null)
|
||||
legacy_file.store_string(JSON.stringify(legacy_data, "\t"))
|
||||
legacy_file.close()
|
||||
assert(save_manager.load_player_data())
|
||||
assert(FileAccess.file_exists(save_path))
|
||||
assert(not FileAccess.file_exists(legacy_path))
|
||||
_assert_opaque(save_path)
|
||||
|
||||
var settings_panels: Array[Node] = main.find_children(
|
||||
"*", "SettingsPanel", true, false
|
||||
)
|
||||
assert(settings_panels.size() == 2)
|
||||
for settings_panel: SettingsPanel in settings_panels:
|
||||
var export_button := settings_panel.get_node(
|
||||
"%ExportProgression"
|
||||
) as Button
|
||||
var import_button := settings_panel.get_node(
|
||||
"%ImportProgression"
|
||||
) as Button
|
||||
assert(export_button != null and import_button != null)
|
||||
assert(not export_button.disabled and not import_button.disabled)
|
||||
assert(
|
||||
export_button.get_node(export_button.focus_neighbor_right)
|
||||
== import_button
|
||||
)
|
||||
assert(
|
||||
import_button.get_node(import_button.focus_neighbor_left)
|
||||
== export_button
|
||||
)
|
||||
|
||||
var migrated_root: String = data_root.root_path.get_base_dir().path_join(
|
||||
"progression-migrated-data"
|
||||
)
|
||||
var data_migration: Dictionary = PortableDataMigration.migrate_active_to(
|
||||
data_root, migrated_root
|
||||
)
|
||||
assert(bool(data_migration.get("ok", false)))
|
||||
var migrated_save: String = migrated_root.path_join(
|
||||
"player/player_save.nfsave"
|
||||
)
|
||||
assert(FileAccess.file_exists(migrated_save))
|
||||
assert(bool(
|
||||
ProgressionSaveCodec.read_local_save(migrated_save).get("ok", false)
|
||||
))
|
||||
|
||||
main.queue_free()
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
await create_timer(0.1).timeout
|
||||
print("Progression archive validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _assert_opaque(path: String) -> void:
|
||||
var bytes: PackedByteArray = PortableFileGuard.read_bytes(path)
|
||||
assert(not bytes.is_empty())
|
||||
var encoded_hex: String = bytes.hex_encode()
|
||||
assert("save_version".to_utf8_buffer().hex_encode() not in encoded_hex)
|
||||
assert("wallet".to_utf8_buffer().hex_encode() not in encoded_hex)
|
||||
1
tests/progression_archive_validation.gd.uid
Normal file
1
tests/progression_archive_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ccl47ftc1cu5l
|
||||
Loading…
Add table
Add a link
Reference in a new issue