extends SceneTree const MainScene: PackedScene = preload("res://main/main.tscn") const Catalog: FishPool = preload("res://fish/pools/fish_catalog.tres") 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 session := main.get_node("%NetworkSession") as NetworkSession assert(session != null) var occupied_port := PacketPeerUDP.new() assert(occupied_port.bind(18137, "127.0.0.1") == OK) assert(session.start_private_host(18137, 3)) assert(session.get_host_port() == 18138) occupied_port.close() var save_manager := main.get("_save_manager") as PlayerSaveManager assert(save_manager.initialize_new_game()) main.call("_enter_gameplay") await physics_frame await physics_frame var jobs := main.get_node("%PlayerJobService") as PlayerJobService var weather := main.get_node("%WorldWeatherService") as WorldWeatherService var network_fishing := main.get( "_network_fishing" ) as NetworkFishingService var network_sale := main.get("_network_sale") as NetworkSaleService var player := main.get("_player") as Player assert(jobs != null and weather != null and player != null) assert(network_fishing != null and network_sale != null) var board: Dictionary = jobs.get_host_board_network_data() assert(PlayerJobService.validate_board(board)) assert(jobs.get_daily_jobs().size() == JobCatalog.DAILY_JOB_COUNT) assert(weather.get_daily_plan_id() == jobs.get_plan_id()) _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 ) assert(not sell_job.is_empty()) var sell_target: int = int(sell_job.get("target", 0)) var sold_ids: Array[StringName] = [] for index: int in sell_target * 2: sold_ids.append(StringName("job-sale-%d" % index)) network_sale.local_sale_finished.emit( "job-test", true, "sold", sold_ids, 1 ) await process_frame var pending: Array[Dictionary] = jobs.get_pending_rewards() assert(pending.size() == 1) var refreshed_sell: Dictionary = _find_job( jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL ) assert(int(refreshed_sell.get("progress", -1)) == sell_target) assert(int(refreshed_sell.get("completed_count", 0)) == 1) var game_ui := main.get_node("%GameUI") as GameUI var player_menu := game_ui.get("_player_menu") as PlayerMenu assert(player_menu != null) player_menu.open_menu() for _frame: int in 24: await process_frame assert(player_menu.visible) player_menu.call("_show_section_immediate", PlayerMenu.Section.NET) var net_page := player_menu.get_node("%TheNetPage") as TheNetPage assert(net_page.visible) assert((player_menu.get_node("%TheNetTab") as Button).button_pressed) assert( (player_menu.get_node("%NavigationCluster") as Control).get_child_count() == 7 ) net_page.call("_select_view", TheNetPage.View.LIFETIME) await process_frame _validate_fishnet_bounds(net_page) await _validate_compact_value_tooltips(net_page) await _validate_unavailable_fishnet_layout() var wallet_before: int = player.wallet.get_balance() var experience_before: int = player.experience.get_total_experience() var first_claim_id: String = str(pending[0].get("claim_id", "")) net_page.call("_claim", first_claim_id) await process_frame await process_frame assert(bool(game_ui.get("_experience_animation_active"))) var experience_presentation := ( game_ui.get_node("%ExperiencePresentation") as Control ) assert(experience_presentation.visible) assert((game_ui.get_node("%ExperienceProgressPanel") as Control).visible) assert((game_ui.get_node("%ExperienceBubble") as Control).visible) assert( experience_presentation.get_index() == experience_presentation.get_parent().get_child_count() - 1 ) assert(not (game_ui.get_node("%GameplayTransientHUD") as Control).visible) assert(player_menu.visible) assert(not jobs.claim(first_claim_id)) assert( player.wallet.get_balance() == wallet_before + int(pending[0].get("fish_coin", 0)) ) assert( player.experience.get_total_experience() == experience_before + int(pending[0].get("experience", 0)) ) refreshed_sell = _find_job( jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL ) assert(int(refreshed_sell.get("progress", -1)) == sell_target) assert(int(refreshed_sell.get("completed_count", 0)) == 1) assert(not bool(refreshed_sell.get("claimable", true))) network_sale.local_sale_finished.emit( "job-test-repeat", true, "sold", sold_ids, 1 ) await process_frame assert(jobs.get_pending_rewards().is_empty()) refreshed_sell = _find_job( jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL ) assert(int(refreshed_sell.get("progress", -1)) == sell_target) assert(int(refreshed_sell.get("completed_count", 0)) == 1) var fresh_job: Dictionary = _find_job( jobs.get_daily_jobs(), JobCatalog.Kind.CATCH_WATER ) assert(not fresh_job.is_empty()) var bluegill: FishData = Catalog.get_fish_by_id(&"bluegill") assert(bluegill != null) for index: int in int(fresh_job.get("target", 0)): var fish_catch := FishCatch.new() fish_catch.fish = bluegill fish_catch.fish_id = bluegill.id fish_catch.catch_id = StringName("job-catch-%d" % index) fish_catch.catch_sequence = index + 1 fish_catch.weight_lb = bluegill.get_minimum_weight() fish_catch.display_scale = bluegill.get_display_scale_for_weight( fish_catch.weight_lb ) fish_catch.quality = FishQuality.Tier.BORING fish_catch.sale_value = bluegill.get_sale_value_for_weight( fish_catch.weight_lb ) assert(fish_catch.is_valid()) network_fishing.local_catch_received.emit(fish_catch) await process_frame var refreshed_fresh: Dictionary = _find_job( jobs.get_daily_jobs(), JobCatalog.Kind.CATCH_WATER ) assert(int(refreshed_fresh.get("completed_count", 0)) == 1) var pause_menu := game_ui.get_pause_menu() var join_page := pause_menu.get_node("%JoinGamePage") as JoinGamePage assert(join_page != null) pause_menu.show() join_page.set_status("Connection timed out.") join_page.show() join_page.call( "_on_discovery_status_changed", "2 public rooms found", false ) var join_status := join_page.get_node("%Status") as Label assert(join_status.text == "Could not reach the server.") join_page.hide() join_page.call("_set_mode", JoinGamePage.Mode.DIRECT) join_page.call("_set_mode", JoinGamePage.Mode.DISCOVER) join_page.show() join_page.call( "_on_discovery_status_changed", "2 public rooms found", false ) assert(join_status.text == "2 public rooms found") join_page.close_page() pause_menu.hide() join_page.call("_refresh") var session_summary := ( join_page.get_node("%SessionSummary") as Label ) assert(not join_page.has_node( "Paper/Margin/Layout/Actions/OpenCloseButton" )) assert("UDP 18138" in session_summary.text) assert(session.set_host_open(true)) await process_frame assert("UDP 18138" in session_summary.text) assert(session.set_host_open(false)) assert(save_manager.save_now()) var decoded: Dictionary = ProgressionSaveCodec.read_local_save( str(save_manager.get("_save_path")) ) assert(bool(decoded.get("ok", false))) var save_data: Dictionary = decoded["data"] assert(int(save_data.get("save_version", -1)) == 11) assert(PlayerJobService.validate_save_data(save_data.get("jobs", {}))) assert(not PlayerHomeState.sanitize_save_data( save_data.get("home", {}) ).is_empty()) _validate_pause_session_switch(main, session) main.queue_free() for _frame: int in 4: await process_frame await create_timer(0.1).timeout print("Job system validation: PASS") quit() func _validate_pause_session_switch( main: Node, session: NetworkSession, ) -> void: var discovery := main.get_node("%DiscoveryClient") as DiscoveryClient discovery.set("_pending_join_endpoint", "127.0.0.1:18141") discovery.set("_pending_join_room_id", "session-switch-room") discovery.set("_pending_join_token", "session-switch-token") discovery.call( "_set_public_join_state", DiscoveryClient.PublicJoinState.CONNECTING, ) main.call("_on_pause_join_game_requested", "127.0.0.1:18141") assert(session.state == NetworkSession.State.CONNECTING) assert(discovery.get("_pending_join_token") == "session-switch-token") var join_probe_timer := discovery.get("_join_probe_timer") as Timer assert(join_probe_timer != null and not join_probe_timer.is_stopped()) session.cancel_connection() assert(session.state == NetworkSession.State.INACTIVE) assert(str(discovery.get("_pending_join_token")).is_empty()) func _validate_unavailable_fishnet_layout() -> void: var unavailable_page := TheNetPage.new() unavailable_page.size = Vector2(1280.0, 720.0) root.add_child(unavailable_page) for _frame: int in 3: await process_frame unavailable_page.call("_refresh_forecast", true) await process_frame var forecast_list := unavailable_page.get("_forecast_list") as HBoxContainer assert(forecast_list != null) assert(forecast_list.size.x >= 272.0) assert(forecast_list.size.y <= 66.0) assert(forecast_list.get_child_count() == 1) var placeholder := forecast_list.get_child(0) as Label assert(placeholder != null) assert(placeholder.size.x >= 252.0) assert(placeholder.size.y <= 66.0) assert(placeholder.autowrap_mode == TextServer.AUTOWRAP_OFF) var tabs := unavailable_page.get("_tabs") as HBoxContainer assert(tabs != null) assert(tabs.position.y + tabs.size.y < UtilityPageStyle.LAPTOP_RECT.end.y) unavailable_page.queue_free() for _frame: int in 2: await process_frame func _validate_fishnet_bounds(page: TheNetPage) -> void: var laptop := page.get_node("UtilityMainBox") as PanelContainer var forecast_column := page.find_child( "ForecastColumn", true, false ) as VBoxContainer var refresh_label := page.find_child( "DailyRefreshLabel", true, false ) as Label var jobs_scroll := page.find_child("JobsScroll", true, false) as ScrollContainer assert(laptop != null) assert(forecast_column != null and refresh_label != null) assert(jobs_scroll != null) var laptop_rect: Rect2 = laptop.get_global_rect() assert(forecast_column.get_global_rect().end.x <= laptop_rect.end.x) assert(refresh_label.get_global_rect().end.x <= laptop_rect.end.x) assert(refresh_label.get_global_rect().end.y <= laptop_rect.end.y) assert(jobs_scroll.horizontal_scroll_mode == ScrollContainer.SCROLL_MODE_DISABLED) var list := page.get("_list") as VBoxContainer assert(list != null) for row: Control in list.get_children(): assert(row.size.x <= jobs_scroll.size.x + 0.5) func _validate_compact_value_tooltips(page: TheNetPage) -> void: assert(str(page.call("_compact_integer", 2400)) == "2.4k") assert(str(page.call("_compact_integer", 2500)) == "2.5k") var list := page.get("_list") as VBoxContainer assert(list != null) var jobs: Array[Dictionary] = [{ "title": "compact tooltip check", "description": "compact values keep their exact values on hover", "target": 2500, "progress": 2400, "fish_coin": 1200, "experience": 3400, }] page.call("_build_job_rows", jobs, "") await process_frame var row := list.get_child(list.get_child_count() - 1) as PanelContainer assert(row != null) var progress_bar: ProgressBar var progress_count: Label var reward: VBoxContainer for child: Node in row.find_children("*", "ProgressBar", true, false): progress_bar = child as ProgressBar break for child: Node in row.find_children("*", "Label", true, false): var label := child as Label if label != null and label.text == "2.4k / 2.5k": progress_count = label break for child: Node in row.find_children("*", "VBoxContainer", true, false): var candidate := child as VBoxContainer if candidate != null and candidate.tooltip_text == ( "1200 fish coins ยท 3400 xp" ): reward = candidate break assert(progress_bar != null) assert(progress_bar.tooltip_text == "2400 / 2500") assert(progress_bar.mouse_filter == Control.MOUSE_FILTER_STOP) assert(progress_count != null) assert(progress_count.tooltip_text == "2400 / 2500") assert(progress_count.mouse_filter == Control.MOUSE_FILTER_STOP) assert(reward != null) assert(reward.mouse_filter == Control.MOUSE_FILTER_STOP) row.queue_free() 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", []) var anchor_index: int = int(board.get("schedule_anchor_index", 0)) for required_weather: int in JobCatalog.weather_requirements(jobs): var occurrences: int = 0 var has_later_window: bool = false for index: int in schedule.size(): var entry: Dictionary = schedule[index] if int(entry.get("weather", -1)) != required_weather: continue occurrences += 1 if index > anchor_index: has_later_window = true assert(occurrences >= 2) assert(has_later_window) func _validate_late_board_weather_fallback() -> void: var late_jobs: Array[Dictionary] = JobCatalog.generate_daily_jobs( "late-board-validation", [], false ) assert(JobCatalog.weather_requirements(late_jobs).is_empty()) var anchored_jobs: Array[Dictionary] = JobCatalog.generate_daily_jobs( "anchored-board-validation", [], true ) var anchored_schedule: Array[Dictionary] = ( JobCatalog.generate_weather_schedule( "anchored-board-validation", anchored_jobs, 9 ) ) for required_weather: int in JobCatalog.weather_requirements(anchored_jobs): var occurrence_indices: Array[int] = [] for index: int in anchored_schedule.size(): var entry: Dictionary = anchored_schedule[index] if ( index >= 9 and int(entry.get("weather", -1)) == required_weather ): occurrence_indices.append(index) assert(occurrence_indices.size() >= 2) assert(occurrence_indices[0] >= 9) assert(occurrence_indices[-1] > 9) func _validate_remote_tamper_rejection( jobs: PlayerJobService, board: Dictionary, ) -> void: var tampered: Dictionary = board.duplicate(true) var tampered_jobs: Array = tampered.get("jobs", []) var first_job: Dictionary = tampered_jobs[0] first_job["fish_coin"] = int(first_job.get("fish_coin", 0)) + 1 tampered_jobs[0] = first_job tampered["jobs"] = tampered_jobs assert(PlayerJobService.validate_board(tampered)) assert(not jobs.apply_remote_board(tampered)) func _find_job(jobs: Array[Dictionary], kind: JobCatalog.Kind) -> Dictionary: for job: Dictionary in jobs: if int(job.get("kind", -1)) == int(kind): return job return {}