diff --git a/.github/workflows/ps2.yml b/.github/workflows/ps2.yml new file mode 100644 index 00000000..c7a9ad44 --- /dev/null +++ b/.github/workflows/ps2.yml @@ -0,0 +1,68 @@ +name: Build PlayStation 2 ISO + +on: + push: + branches: + - master + paths: + - '.github/workflows/ps2.yml' + - 'CMakeLists.txt' + - 'src/**' + - 'thirdparty/**' + - 'tools/ps2/**' + pull_request: + paths: + - '.github/workflows/ps2.yml' + - 'CMakeLists.txt' + - 'src/**' + - 'thirdparty/**' + - 'tools/ps2/**' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + env: + PS2DEV: /tmp/ps2dev + + steps: + - name: Check out the repository + uses: actions/checkout@v6 + + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install --yes cmake genisoimage ninja-build + cmake --version + ninja --version + genisoimage --version + + - name: Install PS2DEV + run: | + mkdir -p "$PS2DEV" + curl --fail --location --retry 3 \ + --output /tmp/ps2dev.tar.gz \ + https://github.com/ps2dev/ps2dev/releases/download/latest/ps2dev-ubuntu-latest.tar.gz + tar --extract --file /tmp/ps2dev.tar.gz --strip-components=1 --directory "$PS2DEV" + + - name: Build the data-free ISO + run: sh tools/ps2/build-iso.sh + + - name: Verify the ISO + run: | + test -s build/ps2/openshadowflare.iso + ! isoinfo -i build/ps2/openshadowflare.iso -f | grep -qi 'SFGAME.BIN' + + - name: Upload the ISO + uses: actions/upload-artifact@v4 + with: + name: openshadowflare-ps2-release + path: build/ps2/openshadowflare.iso + if-no-files-found: error diff --git a/CMakeLists.txt b/CMakeLists.txt index fa0f2a4b..1171bea0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,7 +44,9 @@ add_subdirectory(thirdparty/lwl) add_subdirectory(thirdparty/lal) add_subdirectory(thirdparty/twl) add_subdirectory(thirdparty/tal) -if(OPENSHADOWFLARE_PRESENTATION_BACKEND STREQUAL "lgl" OR BUILD_TESTING) +if( + NOT PLATFORM_PS2 + AND (OPENSHADOWFLARE_PRESENTATION_BACKEND STREQUAL "lgl" OR BUILD_TESTING)) add_subdirectory(thirdparty/lgl) endif() diff --git a/documentation/ps2-port.md b/documentation/ps2-port.md new file mode 100644 index 00000000..e53b0320 --- /dev/null +++ b/documentation/ps2-port.md @@ -0,0 +1,79 @@ +# PS2 port + +## Build + +Install PS2DEV and `genisoimage`, then set its environment variables: + +```sh +export PS2DEV="$HOME/ps2dev" +export PS2SDK="$PS2DEV/ps2sdk" +export GSKIT="$PS2DEV/gsKit" +export PATH="$PATH:$PS2DEV/bin:$PS2DEV/ee/bin:$PS2DEV/iop/bin" +``` + +On Ubuntu or WSL, install the PS2DEV prebuilt toolchain and build: + +```sh +sudo apt-get update && sudo apt-get install -y curl cmake genisoimage +mkdir -p "$PS2DEV" +curl -fL https://github.com/ps2dev/ps2dev/releases/download/latest/ps2dev-ubuntu-latest.tar.gz \ + | tar -xz --strip-components=1 -C "$PS2DEV" +sh tools/ps2/build-iso.sh +``` + +The archive is published on [PS2DEV releases](https://github.com/ps2dev/ps2dev/releases). + +The default output, `build/ps2/openshadowflare.iso`, is a small data-free ISO +suitable for CI and distribution. GitHub Actions builds and uploads this +variant, and verifies it does not contain game data. It loads an owned +`ShadowFlare` data directory placed beside the ISO in PCSX2, or at the root of +a FAT32 USB drive on PS2 hardware: + +```text +/ + openshadowflare.iso + ShadowFlare/ + SFlare.Cfg + System/ + Scenario/ + Save/ + Player/ + Map/ + Character/ + +FAT32 USB drive/ + ShadowFlare/ + SFlare.Cfg + ... +``` + +In PCSX2, enable **Settings > Emulation > Enable Host Filesystem**. PCSX2 +then maps its ISO folder to the `host:` device. The PS2 ISO includes the USB +drivers required to use `mass:` on hardware. Copy the owned game data: + +```sh +# PCSX2: copy next to build/ps2/openshadowflare.iso. +cp -a /path/to/ShadowFlare build/ps2/ + +# PS2 hardware: copy to the root of a mounted FAT32 USB drive. +cp -a /path/to/ShadowFlare /media/$USER/PS2USB/ +``` + +To create a private all-in-one disc from an owned retail installation, pass +the path to its `ShadowFlare` directory. This replaces the same output ISO with +a version containing a packed `SFGAME.BIN` archive, so it must not be +distributed: + +```sh +sh tools/ps2/build-iso.sh --data-dir /path/to/ShadowFlare + +# Example when the data is on the Windows E: drive in WSL. +sh tools/ps2/build-iso.sh --data-dir /mnt/e/Games/pcsx2/ps2/ShadowFlare +``` + +## Type portability + +On PS2DEV, `std::int32_t` is `long`, not `int`. Use typed literals such as +`std::int32_t{0}` with `std::min`, `std::max`, and `std::clamp`; their +arguments must have the same type. Format fixed-width integers with `PRId32` +from `` rather than `%d`. diff --git a/src/SF_EXE/CMakeLists.txt b/src/SF_EXE/CMakeLists.txt index 2cf9a359..1209498c 100644 --- a/src/SF_EXE/CMakeLists.txt +++ b/src/SF_EXE/CMakeLists.txt @@ -1,423 +1,423 @@ -function(osf_configure_game_library target) - target_compile_features(${target} PUBLIC cxx_std_17) - target_include_directories( - ${target} - PUBLIC - "${CMAKE_CURRENT_SOURCE_DIR}" - ) - set_target_properties(${target} PROPERTIES CXX_EXTENSIONS OFF) - target_compile_definitions( - ${target} - PUBLIC - OSF_ENABLE_DEBUG_TOOLS=$ - ) - - if(MSVC) - target_compile_options(${target} PRIVATE /W4 /permissive-) - else() - target_compile_options( - ${target} PRIVATE -Wall -Wextra -Wpedantic) - endif() -endfunction() - -if(OPENSHADOWFLARE_ENABLE_DEBUG_TOOLS) - add_library( - osf_debug_tools STATIC - debug/frame_profiler.cpp - ) - add_library(OpenShadowFlare::DebugTools ALIAS osf_debug_tools) - osf_configure_game_library(osf_debug_tools) -endif() - -include(cmake/configure_platform.cmake) -include(cmake/configure_presentation.cmake) - -add_library( - osf_gapi STATIC - gapi/bit_mask_image.cpp - gapi/gapi.cpp -) -add_library(OpenShadowFlare::Gapi ALIAS osf_gapi) -osf_configure_game_library(osf_gapi) - -add_subdirectory(libs/RK_FUNCTION) -add_subdirectory(libs/RKC_DIB) -add_subdirectory(libs/RKC_DSOUND) -add_subdirectory(libs/RKC_UPDIB) -add_subdirectory(libs/RKC_RPGSCRN) -add_subdirectory(libs/RKC_RPG_AICONTROL) -add_subdirectory(libs/RKC_RPG_SCRIPT) -add_subdirectory(libs/RKC_RPG_TABLE) -add_subdirectory(libs/RKC_DBFCONTROL) - -add_library( - osf_core STATIC - core/command_line.cpp - core/game_config.cpp - core/retail_random.cpp -) -add_library(OpenShadowFlare::Core ALIAS osf_core) -osf_configure_game_library(osf_core) - -add_library( - osf_items STATIC - items/item_database.cpp - items/item_audio.cpp - items/item_appearance.cpp - items/item_condition.cpp - items/item_instance_values.cpp - items/item_grid.cpp - items/item_information.cpp - items/item_instance_factory.cpp - items/item_repair.cpp - items/new_player_loadout.cpp - items/player_automatic_items.cpp - items/player_belt.cpp - items/player_equipment.cpp - items/player_giant_warehouse.cpp - items/player_inventory.cpp - items/player_special_items.cpp - items/vendor_inventory.cpp -) -add_library(OpenShadowFlare::Items ALIAS osf_items) -osf_configure_game_library(osf_items) -target_link_libraries( - osf_items - PRIVATE - OpenShadowFlare::RK_FUNCTION - OpenShadowFlare::RKC_RPG_TABLE -) - -add_library( - osf_resources STATIC - resources/character_visual_resource.cpp - resources/effect_pattern_resource.cpp - resources/effect_visual_resource.cpp - resources/font_resource.cpp - resources/item_inventory_resource.cpp - resources/item_world_resource.cpp - resources/object_visual_resource.cpp - resources/resource_manager.cpp - resources/resource_memory.cpp - resources/retail_filesystem.cpp - resources/save_catalog.cpp -) -add_library(OpenShadowFlare::Resources ALIAS osf_resources) -osf_configure_game_library(osf_resources) -target_link_libraries( - osf_resources - PUBLIC - OpenShadowFlare::RKC_DIB - OpenShadowFlare::RKC_RPGSCRN - OpenShadowFlare::RKC_UPDIB -) - -add_library( - osf_states STATIC - states/game_state.cpp - states/gameplay_blackjack.cpp - states/gameplay_equipment_color.cpp - states/gameplay_inventory.cpp - states/gameplay_magic.cpp - states/gameplay_map.cpp - states/gameplay_mission_list.cpp - states/gameplay_options_menu.cpp - states/gameplay_status.cpp - states/gameplay_state.cpp - states/gameplay_transport.cpp - states/gameplay_vendor.cpp - states/save_slot.cpp - states/character_select_state.cpp - states/character_select/network_flow.cpp - states/character_select/new_character_flow.cpp - states/character_select/saved_game_flow.cpp - states/title_state.cpp -) -if(OPENSHADOWFLARE_ENABLE_DEBUG_TOOLS) - target_sources( - osf_states - PRIVATE - states/gameplay_debug_menu.cpp - ) -endif() -add_library(OpenShadowFlare::States ALIAS osf_states) -osf_configure_game_library(osf_states) -target_link_libraries( - osf_states - PUBLIC - OpenShadowFlare::Core - OpenShadowFlare::Items -) - -add_library( - osf_world STATIC - world/actor_direction.cpp - world/companion_actor.cpp - world/companion_attack_action.cpp - world/companion_attack_impact.cpp - world/companion_damage_receiver.cpp - world/companion_explosion_action.cpp - world/companion_profile.cpp - world/companion_status_message.cpp - world/companion_respawn.cpp - world/companion_target_selector.cpp - world/combat_damage.cpp - world/combat_effect_actor.cpp - world/combat_hit_chance.cpp - world/enemy_actor.cpp - world/enemy_ai_action.cpp - world/enemy_ai_evaluator.cpp - world/enemy_damage_receiver.cpp - world/enemy_death_rewards.cpp - world/enemy_direct_impact.cpp - world/enemy_effect_controller.cpp - world/enemy_effect_impact.cpp - world/enemy_presentation.cpp - world/enemy_presentation_audio.cpp - world/enemy_target_selector.cpp - world/generic_effect_actor.cpp - world/ground_item.cpp - world/map_exploration.cpp - world/miss_effect_actor.cpp - world/mission_catalog.cpp - world/movement_controller.cpp - world/movement_destination_selector.cpp - world/npc_actor.cpp - world/npc_script_action.cpp - world/player_appearance.cpp - world/player_actor.cpp - world/player_attack_action.cpp - world/player_attack_impact.cpp - world/player_attack_target.cpp - world/player_ranged_attack.cpp - world/player_combat_defense.cpp - world/player_counter_burst.cpp - world/player_damage_receiver.cpp - world/player_element_condition.cpp - world/player_data.cpp - world/player_job.cpp - world/player_energy_shield.cpp - world/player_experience_award.cpp - world/player_heal_spell.cpp - world/player_increased_power.cpp - world/player_increased_power_attack.cpp - world/player_item_controller.cpp - world/player_land_mine.cpp - world/player_level_up_notice.cpp - world/player_magic_shield.cpp - world/player_magic.cpp - world/player_moon_spell.cpp - world/player_resource_rate.cpp - world/player_runtime_profile.cpp - world/player_spell_action.cpp - world/player_spell_cast.cpp - world/player_spell_parameters.cpp - world/player_sustained_spell.cpp - world/player_transport_spell.cpp - world/player_voice.cpp - world/quest_state.cpp - world/retail_save_automatic_items.cpp - world/retail_save_companion_progress.cpp - world/retail_save_extension.cpp - world/retail_save_file.cpp - world/retail_save_giant_warehouse.cpp - world/retail_save_items.cpp - world/retail_save_magic.cpp - world/retail_save_mines.cpp - world/retail_save_preview.cpp - world/retail_save_progress.cpp - world/retail_save_world_state.cpp - world/runtime_effect_actor.cpp - world/runtime_effect_system.cpp - world/runtime_effect_target.cpp - world/scenario_data.cpp - world/scenario_entity_state.cpp - world/scenario_object_actor.cpp - world/scenario_screen_particles.cpp - world/scenario_visual_presentation.cpp - world/scenario_world.cpp - world/transport_catalog.cpp - world/vendor_stock_generator.cpp - world/script/scenario_attached_effect_command.cpp - world/script/scenario_effect_command.cpp - world/script/scenario_numeric_label_command.cpp - world/script/scenario_placed_effect_command.cpp - world/script/scenario_script_runtime.cpp - world/world_pointer.cpp - world/world_scene.cpp - world/world_scene_companion_combat.cpp - world/world_scene_companion_explosion.cpp - world/world_scene_companion_switch.cpp - world/world_scene_combat.cpp - world/world_scene_enemy_combat.cpp - world/world_scene_effects.cpp - world/world_scene_interaction.cpp - world/world_scene_items.cpp - world/world_scene_loading.cpp - world/world_scene_moon.cpp - world/world_scene_player_profile.cpp - world/world_scene_player_experience.cpp - world/world_scene_presentation.cpp - world/world_scene_player_resources.cpp - world/world_scene_script.cpp - world/world_scene_script_items.cpp - world/world_scene_transport_spell.cpp -) -add_library(OpenShadowFlare::World ALIAS osf_world) -osf_configure_game_library(osf_world) -target_link_libraries( - osf_world - PUBLIC - OpenShadowFlare::Core - OpenShadowFlare::Gapi - OpenShadowFlare::Items - OpenShadowFlare::Resources - OpenShadowFlare::RKC_DIB - OpenShadowFlare::RKC_RPGSCRN - OpenShadowFlare::RKC_RPG_AICONTROL - OpenShadowFlare::RKC_RPG_SCRIPT - OpenShadowFlare::RKC_RPG_TABLE - OpenShadowFlare::RKC_UPDIB -) - -add_library( - osf_ui STATIC - ui/companion_hud_input.cpp - ui/conversation_layout.cpp - ui/gameplay_hud_input.cpp - ui/player_level_up_notice_input.cpp - ui/player_level_up_notice_layout.cpp - ui/pointer_input_guard.cpp - ui/quest_notice_layout.cpp -) -add_library(OpenShadowFlare::Ui ALIAS osf_ui) -osf_configure_game_library(osf_ui) -target_link_libraries( - osf_ui - PUBLIC - OpenShadowFlare::World - OpenShadowFlare::RKC_RPGSCRN -) - -add_library( - osf_render STATIC - render/character_renderer.cpp - render/character_select_renderer.cpp - render/enemy_nameplate_renderer.cpp - render/gameplay_blackjack_renderer.cpp - render/gameplay_equipment_color_renderer.cpp - render/gameplay_help_renderer.cpp - render/gameplay_hud_renderer.cpp - render/gameplay_inventory_renderer.cpp - render/gameplay_map_renderer.cpp - render/gameplay_magic_renderer.cpp - render/gameplay_mission_list_renderer.cpp - render/gameplay_options_renderer.cpp - render/gameplay_overlay_renderer.cpp - render/gameplay_renderer.cpp - render/gameplay_status_renderer.cpp - render/gameplay_transport_renderer.cpp - render/gameplay_vendor_renderer.cpp - render/item_information_renderer.cpp - render/loading_renderer.cpp - render/player_level_up_notice_renderer.cpp - render/quest_notice_renderer.cpp - render/scenario_presentation_renderer.cpp - render/system_cursor_renderer.cpp - render/title_renderer.cpp -) -if(OPENSHADOWFLARE_ENABLE_DEBUG_TOOLS) - target_sources( - osf_render - PRIVATE - render/gameplay_debug_renderer.cpp - ) -endif() -add_library(OpenShadowFlare::Render ALIAS osf_render) -osf_configure_game_library(osf_render) -target_link_libraries( - osf_render - PUBLIC - OpenShadowFlare::Gapi - OpenShadowFlare::States - OpenShadowFlare::Ui - OpenShadowFlare::World - OpenShadowFlare::RKC_DIB - OpenShadowFlare::RKC_RPGSCRN - OpenShadowFlare::RKC_UPDIB -) -if(OPENSHADOWFLARE_ENABLE_DEBUG_TOOLS) - target_link_libraries( - osf_render - PUBLIC - OpenShadowFlare::DebugTools - ) -endif() - -add_library(sf_game_core INTERFACE) -add_library(OpenShadowFlare::GameCore ALIAS sf_game_core) -target_link_libraries( - sf_game_core - INTERFACE - OpenShadowFlare::Core - OpenShadowFlare::Items - OpenShadowFlare::Resources - OpenShadowFlare::States - OpenShadowFlare::Ui - OpenShadowFlare::World - OpenShadowFlare::Render -) - -add_executable( - ShadowFlare_rebuilt - runtime/audio_system.cpp - runtime/gameplay_artwork.cpp - runtime/game_runtime.cpp - runtime/gameplay_ui_controller.cpp - runtime/input_adapter.cpp - runtime/main.cpp - runtime/runtime_renderer.cpp - runtime/state_bindings.cpp -) - -target_compile_features(ShadowFlare_rebuilt PRIVATE cxx_std_17) -set_target_properties(ShadowFlare_rebuilt PROPERTIES CXX_EXTENSIONS OFF) -target_compile_definitions( - ShadowFlare_rebuilt - PRIVATE - OSF_ENABLE_DEBUG_TOOLS=$ -) - -target_link_libraries( - ShadowFlare_rebuilt - PRIVATE - OpenShadowFlare::GameCore - OpenShadowFlare::RKC_DBFCONTROL - OpenShadowFlare::RKC_DSOUND - Lwl::Lwl - Lal::Lal -) - -osf_configure_platform(ShadowFlare_rebuilt) -osf_configure_presentation(ShadowFlare_rebuilt) - -if(MINGW) - target_link_options( - ShadowFlare_rebuilt - PRIVATE - -static-libgcc - -static-libstdc++ - ) -endif() - -if(MSVC) - target_compile_options(ShadowFlare_rebuilt PRIVATE /W4 /permissive-) -else() - target_compile_options( - ShadowFlare_rebuilt - PRIVATE - -Wall - -Wextra - -Wpedantic - ) -endif() +function(osf_configure_game_library target) + target_compile_features(${target} PUBLIC cxx_std_17) + target_include_directories( + ${target} + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + ) + set_target_properties(${target} PROPERTIES CXX_EXTENSIONS OFF) + target_compile_definitions( + ${target} + PUBLIC + OSF_ENABLE_DEBUG_TOOLS=$ + ) + + if(MSVC) + target_compile_options(${target} PRIVATE /W4 /permissive-) + else() + target_compile_options( + ${target} PRIVATE -Wall -Wextra -Wpedantic) + endif() +endfunction() + +if(OPENSHADOWFLARE_ENABLE_DEBUG_TOOLS) + add_library( + osf_debug_tools STATIC + debug/frame_profiler.cpp + ) + add_library(OpenShadowFlare::DebugTools ALIAS osf_debug_tools) + osf_configure_game_library(osf_debug_tools) +endif() + +include(cmake/configure_platform.cmake) +include(cmake/configure_presentation.cmake) + +add_library( + osf_gapi STATIC + gapi/bit_mask_image.cpp + gapi/gapi.cpp +) +add_library(OpenShadowFlare::Gapi ALIAS osf_gapi) +osf_configure_game_library(osf_gapi) + +add_subdirectory(libs/RK_FUNCTION) +add_subdirectory(libs/RKC_DIB) +add_subdirectory(libs/RKC_DSOUND) +add_subdirectory(libs/RKC_UPDIB) +add_subdirectory(libs/RKC_RPGSCRN) +add_subdirectory(libs/RKC_RPG_AICONTROL) +add_subdirectory(libs/RKC_RPG_SCRIPT) +add_subdirectory(libs/RKC_RPG_TABLE) +add_subdirectory(libs/RKC_DBFCONTROL) + +add_library( + osf_core STATIC + core/command_line.cpp + core/game_config.cpp + core/retail_random.cpp +) +add_library(OpenShadowFlare::Core ALIAS osf_core) +osf_configure_game_library(osf_core) + +add_library( + osf_items STATIC + items/item_database.cpp + items/item_audio.cpp + items/item_appearance.cpp + items/item_condition.cpp + items/item_instance_values.cpp + items/item_grid.cpp + items/item_information.cpp + items/item_instance_factory.cpp + items/item_repair.cpp + items/new_player_loadout.cpp + items/player_automatic_items.cpp + items/player_belt.cpp + items/player_equipment.cpp + items/player_giant_warehouse.cpp + items/player_inventory.cpp + items/player_special_items.cpp + items/vendor_inventory.cpp +) +add_library(OpenShadowFlare::Items ALIAS osf_items) +osf_configure_game_library(osf_items) +target_link_libraries( + osf_items + PRIVATE + OpenShadowFlare::RK_FUNCTION + OpenShadowFlare::RKC_RPG_TABLE +) + +add_library( + osf_resources STATIC + resources/character_visual_resource.cpp + resources/effect_pattern_resource.cpp + resources/effect_visual_resource.cpp + resources/font_resource.cpp + resources/item_inventory_resource.cpp + resources/item_world_resource.cpp + resources/object_visual_resource.cpp + resources/resource_manager.cpp + resources/resource_memory.cpp + resources/retail_filesystem.cpp + resources/save_catalog.cpp +) +add_library(OpenShadowFlare::Resources ALIAS osf_resources) +osf_configure_game_library(osf_resources) +target_link_libraries( + osf_resources + PUBLIC + OpenShadowFlare::RKC_DIB + OpenShadowFlare::RKC_RPGSCRN + OpenShadowFlare::RKC_UPDIB +) + +add_library( + osf_states STATIC + states/game_state.cpp + states/gameplay_blackjack.cpp + states/gameplay_equipment_color.cpp + states/gameplay_inventory.cpp + states/gameplay_magic.cpp + states/gameplay_map.cpp + states/gameplay_mission_list.cpp + states/gameplay_options_menu.cpp + states/gameplay_status.cpp + states/gameplay_state.cpp + states/gameplay_transport.cpp + states/gameplay_vendor.cpp + states/save_slot.cpp + states/character_select_state.cpp + states/character_select/network_flow.cpp + states/character_select/new_character_flow.cpp + states/character_select/saved_game_flow.cpp + states/title_state.cpp +) +if(OPENSHADOWFLARE_ENABLE_DEBUG_TOOLS) + target_sources( + osf_states + PRIVATE + states/gameplay_debug_menu.cpp + ) +endif() +add_library(OpenShadowFlare::States ALIAS osf_states) +osf_configure_game_library(osf_states) +target_link_libraries( + osf_states + PUBLIC + OpenShadowFlare::Core + OpenShadowFlare::Items +) + +add_library( + osf_world STATIC + world/actor_direction.cpp + world/companion_actor.cpp + world/companion_attack_action.cpp + world/companion_attack_impact.cpp + world/companion_damage_receiver.cpp + world/companion_explosion_action.cpp + world/companion_profile.cpp + world/companion_status_message.cpp + world/companion_respawn.cpp + world/companion_target_selector.cpp + world/combat_damage.cpp + world/combat_effect_actor.cpp + world/combat_hit_chance.cpp + world/enemy_actor.cpp + world/enemy_ai_action.cpp + world/enemy_ai_evaluator.cpp + world/enemy_damage_receiver.cpp + world/enemy_death_rewards.cpp + world/enemy_direct_impact.cpp + world/enemy_effect_controller.cpp + world/enemy_effect_impact.cpp + world/enemy_presentation.cpp + world/enemy_presentation_audio.cpp + world/enemy_target_selector.cpp + world/generic_effect_actor.cpp + world/ground_item.cpp + world/map_exploration.cpp + world/miss_effect_actor.cpp + world/mission_catalog.cpp + world/movement_controller.cpp + world/movement_destination_selector.cpp + world/npc_actor.cpp + world/npc_script_action.cpp + world/player_appearance.cpp + world/player_actor.cpp + world/player_attack_action.cpp + world/player_attack_impact.cpp + world/player_attack_target.cpp + world/player_ranged_attack.cpp + world/player_combat_defense.cpp + world/player_counter_burst.cpp + world/player_damage_receiver.cpp + world/player_element_condition.cpp + world/player_data.cpp + world/player_job.cpp + world/player_energy_shield.cpp + world/player_experience_award.cpp + world/player_heal_spell.cpp + world/player_increased_power.cpp + world/player_increased_power_attack.cpp + world/player_item_controller.cpp + world/player_land_mine.cpp + world/player_level_up_notice.cpp + world/player_magic_shield.cpp + world/player_magic.cpp + world/player_moon_spell.cpp + world/player_resource_rate.cpp + world/player_runtime_profile.cpp + world/player_spell_action.cpp + world/player_spell_cast.cpp + world/player_spell_parameters.cpp + world/player_sustained_spell.cpp + world/player_transport_spell.cpp + world/player_voice.cpp + world/quest_state.cpp + world/retail_save_automatic_items.cpp + world/retail_save_companion_progress.cpp + world/retail_save_extension.cpp + world/retail_save_file.cpp + world/retail_save_giant_warehouse.cpp + world/retail_save_items.cpp + world/retail_save_magic.cpp + world/retail_save_mines.cpp + world/retail_save_preview.cpp + world/retail_save_progress.cpp + world/retail_save_world_state.cpp + world/runtime_effect_actor.cpp + world/runtime_effect_system.cpp + world/runtime_effect_target.cpp + world/scenario_data.cpp + world/scenario_entity_state.cpp + world/scenario_object_actor.cpp + world/scenario_screen_particles.cpp + world/scenario_visual_presentation.cpp + world/scenario_world.cpp + world/transport_catalog.cpp + world/vendor_stock_generator.cpp + world/script/scenario_attached_effect_command.cpp + world/script/scenario_effect_command.cpp + world/script/scenario_numeric_label_command.cpp + world/script/scenario_placed_effect_command.cpp + world/script/scenario_script_runtime.cpp + world/world_pointer.cpp + world/world_scene.cpp + world/world_scene_companion_combat.cpp + world/world_scene_companion_explosion.cpp + world/world_scene_companion_switch.cpp + world/world_scene_combat.cpp + world/world_scene_enemy_combat.cpp + world/world_scene_effects.cpp + world/world_scene_interaction.cpp + world/world_scene_items.cpp + world/world_scene_loading.cpp + world/world_scene_moon.cpp + world/world_scene_player_profile.cpp + world/world_scene_player_experience.cpp + world/world_scene_presentation.cpp + world/world_scene_player_resources.cpp + world/world_scene_script.cpp + world/world_scene_script_items.cpp + world/world_scene_transport_spell.cpp +) +add_library(OpenShadowFlare::World ALIAS osf_world) +osf_configure_game_library(osf_world) +target_link_libraries( + osf_world + PUBLIC + OpenShadowFlare::Core + OpenShadowFlare::Gapi + OpenShadowFlare::Items + OpenShadowFlare::Resources + OpenShadowFlare::RKC_DIB + OpenShadowFlare::RKC_RPGSCRN + OpenShadowFlare::RKC_RPG_AICONTROL + OpenShadowFlare::RKC_RPG_SCRIPT + OpenShadowFlare::RKC_RPG_TABLE + OpenShadowFlare::RKC_UPDIB +) + +add_library( + osf_ui STATIC + ui/companion_hud_input.cpp + ui/conversation_layout.cpp + ui/gameplay_hud_input.cpp + ui/player_level_up_notice_input.cpp + ui/player_level_up_notice_layout.cpp + ui/pointer_input_guard.cpp + ui/quest_notice_layout.cpp +) +add_library(OpenShadowFlare::Ui ALIAS osf_ui) +osf_configure_game_library(osf_ui) +target_link_libraries( + osf_ui + PUBLIC + OpenShadowFlare::World + OpenShadowFlare::RKC_RPGSCRN +) + +add_library( + osf_render STATIC + render/character_renderer.cpp + render/character_select_renderer.cpp + render/enemy_nameplate_renderer.cpp + render/gameplay_blackjack_renderer.cpp + render/gameplay_equipment_color_renderer.cpp + render/gameplay_help_renderer.cpp + render/gameplay_hud_renderer.cpp + render/gameplay_inventory_renderer.cpp + render/gameplay_map_renderer.cpp + render/gameplay_magic_renderer.cpp + render/gameplay_mission_list_renderer.cpp + render/gameplay_options_renderer.cpp + render/gameplay_overlay_renderer.cpp + render/gameplay_renderer.cpp + render/gameplay_status_renderer.cpp + render/gameplay_transport_renderer.cpp + render/gameplay_vendor_renderer.cpp + render/item_information_renderer.cpp + render/loading_renderer.cpp + render/player_level_up_notice_renderer.cpp + render/quest_notice_renderer.cpp + render/scenario_presentation_renderer.cpp + render/system_cursor_renderer.cpp + render/title_renderer.cpp +) +if(OPENSHADOWFLARE_ENABLE_DEBUG_TOOLS) + target_sources( + osf_render + PRIVATE + render/gameplay_debug_renderer.cpp + ) +endif() +add_library(OpenShadowFlare::Render ALIAS osf_render) +osf_configure_game_library(osf_render) +target_link_libraries( + osf_render + PUBLIC + OpenShadowFlare::Gapi + OpenShadowFlare::States + OpenShadowFlare::Ui + OpenShadowFlare::World + OpenShadowFlare::RKC_DIB + OpenShadowFlare::RKC_RPGSCRN + OpenShadowFlare::RKC_UPDIB +) +if(OPENSHADOWFLARE_ENABLE_DEBUG_TOOLS) + target_link_libraries( + osf_render + PUBLIC + OpenShadowFlare::DebugTools + ) +endif() + +add_library(sf_game_core INTERFACE) +add_library(OpenShadowFlare::GameCore ALIAS sf_game_core) +target_link_libraries( + sf_game_core + INTERFACE + OpenShadowFlare::Core + OpenShadowFlare::Items + OpenShadowFlare::Resources + OpenShadowFlare::States + OpenShadowFlare::Ui + OpenShadowFlare::World + OpenShadowFlare::Render +) + +add_executable( + ShadowFlare_rebuilt + runtime/audio_system.cpp + runtime/gameplay_artwork.cpp + runtime/game_runtime.cpp + runtime/gameplay_ui_controller.cpp + runtime/input_adapter.cpp + runtime/main.cpp + runtime/runtime_renderer.cpp + runtime/state_bindings.cpp +) + +target_compile_features(ShadowFlare_rebuilt PRIVATE cxx_std_17) +set_target_properties(ShadowFlare_rebuilt PROPERTIES CXX_EXTENSIONS OFF) +target_compile_definitions( + ShadowFlare_rebuilt + PRIVATE + OSF_ENABLE_DEBUG_TOOLS=$ +) + +target_link_libraries( + ShadowFlare_rebuilt + PRIVATE + OpenShadowFlare::GameCore + OpenShadowFlare::RKC_DBFCONTROL + OpenShadowFlare::RKC_DSOUND + Lwl::Lwl + Lal::Lal +) + +osf_configure_platform(ShadowFlare_rebuilt) +osf_configure_presentation(ShadowFlare_rebuilt) + +if(MINGW) + target_link_options( + ShadowFlare_rebuilt + PRIVATE + -static-libgcc + -static-libstdc++ + ) +endif() + +if(MSVC) + target_compile_options(ShadowFlare_rebuilt PRIVATE /W4 /permissive-) +else() + target_compile_options( + ShadowFlare_rebuilt + PRIVATE + -Wall + -Wextra + -Wpedantic + ) +endif() diff --git a/src/SF_EXE/cmake/configure_platform.cmake b/src/SF_EXE/cmake/configure_platform.cmake index 456288fc..7b1ca7cf 100644 --- a/src/SF_EXE/cmake/configure_platform.cmake +++ b/src/SF_EXE/cmake/configure_platform.cmake @@ -2,6 +2,9 @@ function(osf_configure_platform target) if(EMSCRIPTEN) include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/Emscripten.cmake") osf_configure_emscripten_platform(${target}) + elseif(PLATFORM_PS2) + include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/PS2.cmake") + osf_configure_ps2_platform(${target}) elseif(NINTENDO_SWITCH) include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/platforms/Switch.cmake") osf_configure_switch_platform(${target}) diff --git a/src/SF_EXE/cmake/configure_presentation.cmake b/src/SF_EXE/cmake/configure_presentation.cmake index 8d324e16..7db99e67 100644 --- a/src/SF_EXE/cmake/configure_presentation.cmake +++ b/src/SF_EXE/cmake/configure_presentation.cmake @@ -1,5 +1,6 @@ function(osf_configure_presentation target) - if(OPENSHADOWFLARE_PRESENTATION_BACKEND STREQUAL "lgl") + if(PLATFORM_PS2) + elseif(OPENSHADOWFLARE_PRESENTATION_BACKEND STREQUAL "lgl") target_sources( ${target} PRIVATE diff --git a/src/SF_EXE/cmake/platforms/PS2.cmake b/src/SF_EXE/cmake/platforms/PS2.cmake new file mode 100644 index 00000000..aea68fe9 --- /dev/null +++ b/src/SF_EXE/cmake/platforms/PS2.cmake @@ -0,0 +1,10 @@ +function(osf_configure_ps2_platform target) + target_sources( + ${target} + PRIVATE + runtime/platform/ps2/application_loop.cpp + runtime/platform/ps2/ps2_data_backend.cpp + runtime/platform/ps2/surface_presenter.cpp + ) + target_link_libraries(${target} PRIVATE gskit dmakit) +endfunction() diff --git a/src/SF_EXE/gapi/gapi.cpp b/src/SF_EXE/gapi/gapi.cpp index 89a87038..ca259df7 100644 --- a/src/SF_EXE/gapi/gapi.cpp +++ b/src/SF_EXE/gapi/gapi.cpp @@ -32,8 +32,8 @@ Viewport fitViewport( static_cast(target_height) * source_width / source_height); } - result.width = std::max(result.width, 1); - result.height = std::max(result.height, 1); + result.width = std::max(result.width, std::int32_t{1}); + result.height = std::max(result.height, std::int32_t{1}); result.x = (target_width - result.width) / 2; result.y = (target_height - result.height) / 2; return result; diff --git a/src/SF_EXE/items/item_condition.cpp b/src/SF_EXE/items/item_condition.cpp index 0f83fb8e..c628b611 100644 --- a/src/SF_EXE/items/item_condition.cpp +++ b/src/SF_EXE/items/item_condition.cpp @@ -30,7 +30,7 @@ bool itemConditionWarningVisible( const std::int32_t durability = std::clamp( itemCurrentDurability(item, definition), - 0, + std::int32_t{0}, definition.maximum_durability); if (durability == 0) { return true; diff --git a/src/SF_EXE/items/item_information.cpp b/src/SF_EXE/items/item_information.cpp index 171ec21f..a0568927 100644 --- a/src/SF_EXE/items/item_information.cpp +++ b/src/SF_EXE/items/item_information.cpp @@ -42,7 +42,7 @@ std::int32_t itemSalePrice( const InventoryItem& item, const ItemDefinition& definition) { std::int64_t price = - std::max(definition.base_price, 0); + std::max(definition.base_price, std::int32_t{0}); if ((definition.category == 0 || definition.category == 1) && definition.maximum_durability > 0) { @@ -50,7 +50,7 @@ std::int32_t itemSalePrice( price * std::clamp( itemCurrentDurability(item, definition), - 0, + std::int32_t{0}, definition.maximum_durability) / definition.maximum_durability; } @@ -62,7 +62,7 @@ std::int32_t itemSalePrice( std::int32_t itemPurchasePrice( const InventoryItem&, const ItemDefinition& definition) { - return std::max(definition.base_price, 0); + return std::max(definition.base_price, std::int32_t{0}); } std::string itemInformationText( diff --git a/src/SF_EXE/items/item_repair.cpp b/src/SF_EXE/items/item_repair.cpp index 16c83c31..db2d4a32 100644 --- a/src/SF_EXE/items/item_repair.cpp +++ b/src/SF_EXE/items/item_repair.cpp @@ -111,7 +111,7 @@ std::int32_t retailItemRepairPrice( } const std::int32_t current = std::clamp( itemCurrentDurability(item, definition), - 0, + std::int32_t{0}, definition.maximum_durability); if (current == definition.maximum_durability) { return 0; diff --git a/src/SF_EXE/libs/RKC_DBFCONTROL/software_backend.cpp b/src/SF_EXE/libs/RKC_DBFCONTROL/software_backend.cpp index 3666493f..75d372b8 100644 --- a/src/SF_EXE/libs/RKC_DBFCONTROL/software_backend.cpp +++ b/src/SF_EXE/libs/RKC_DBFCONTROL/software_backend.cpp @@ -14,7 +14,7 @@ namespace { std::uint8_t applyBrightness( std::uint8_t value, std::int32_t brightness) { - brightness = std::clamp(brightness, 0, 1000); + brightness = std::clamp(brightness, std::int32_t{0}, std::int32_t{1000}); return static_cast( static_cast(value) * brightness / 1000); } @@ -23,7 +23,7 @@ std::uint8_t applyColorStrength( std::uint8_t value, std::int32_t strength) { const std::int32_t amount = - std::clamp(strength, 0, 2000) - 1000; + std::clamp(strength, std::int32_t{0}, std::int32_t{2000}) - 1000; if (amount < 0) { return static_cast( static_cast(value) + @@ -39,6 +39,7 @@ std::uint8_t blendChannel( std::uint8_t destination, std::uint8_t source, std::int32_t opacity) { + opacity = std::clamp(opacity, std::int32_t{0}, std::int32_t{1000}); return static_cast( (static_cast(source) * opacity + static_cast(destination) * @@ -60,7 +61,7 @@ std::uint8_t addChannel( std::min( static_cast(destination) + source_amount, - 255)); + std::int32_t{255})); } std::uint8_t readPixelIndex( @@ -91,8 +92,8 @@ SoftwareBackend::SoftwareBackend( std::int32_t width, std::int32_t height, PresentCallback present) - : width_(std::max(width, 0)), - height_(std::max(height, 0)), + : width_(std::max(width, std::int32_t{0})), + height_(std::max(height, std::int32_t{0})), pixels_( static_cast(width_) * static_cast(height_)), @@ -157,7 +158,9 @@ bool SoftwareBackend::drawPattern( const bool additive = draw.blend_mode == PatternBlendMode::additive; const std::int32_t opacity = std::clamp( - draw.opacity, 0, additive ? 2000 : 1000); + draw.opacity, + std::int32_t{0}, + additive ? std::int32_t{2000} : std::int32_t{1000}); for (const NjpPatternPart& item : pattern.parts) { if (item.part_index < 0 || @@ -207,11 +210,11 @@ bool SoftwareBackend::drawPattern( } std::int32_t first_y = - std::max(0, -destination_y); + std::max(std::int32_t{0}, -destination_y); std::int32_t last_y = std::min(destination_height, height_ - destination_y); std::int32_t first_x = - std::max(0, -destination_x); + std::max(std::int32_t{0}, -destination_x); std::int32_t last_x = std::min(destination_width, width_ - destination_x); if (draw.clip.width > 0 && draw.clip.height > 0) { @@ -479,7 +482,11 @@ bool SoftwareBackend::drawBitMask( } const std::int32_t opacity = - std::clamp(draw.opacity, 0, 1000) * draw.color.alpha / 255; + std::clamp( + draw.opacity, + std::int32_t{0}, + std::int32_t{1000}) * + draw.color.alpha / std::int32_t{255}; if (opacity <= 0) { return true; } @@ -673,7 +680,10 @@ bool SoftwareBackend::drawRectangle( return true; } const std::int32_t opacity = - std::clamp(draw.opacity, 0, 1000); + std::clamp( + draw.opacity, + std::int32_t{0}, + std::int32_t{1000}); if (opacity >= 1000) { for (std::int32_t y = top; y < bottom; ++y) { Color* row = pixels_.data() + @@ -712,7 +722,10 @@ bool SoftwareBackend::drawLine(const LineDraw& draw) { const bool clipped = draw.clip.width > 0 && draw.clip.height > 0; const std::int32_t opacity = - std::clamp(draw.opacity, 0, 1000); + std::clamp( + draw.opacity, + std::int32_t{0}, + std::int32_t{1000}); const auto draw_point = [this, &draw, color, clipped, opacity]( std::int32_t x, std::int32_t y) { diff --git a/src/SF_EXE/libs/RKC_DIB/bitmap.cpp b/src/SF_EXE/libs/RKC_DIB/bitmap.cpp index 18cf3412..31091a5a 100644 --- a/src/SF_EXE/libs/RKC_DIB/bitmap.cpp +++ b/src/SF_EXE/libs/RKC_DIB/bitmap.cpp @@ -224,8 +224,8 @@ void BitmapImage::fillRectangle( width_ <= 0 || height_ <= 0) { return; } - const std::int32_t left = std::clamp(x, 0, width_); - const std::int32_t top = std::clamp(y, 0, height_); + const std::int32_t left = std::clamp(x, std::int32_t{0}, width_); + const std::int32_t top = std::clamp(y, std::int32_t{0}, height_); const std::int64_t raw_right = static_cast(x) + width; const std::int64_t raw_bottom = diff --git a/src/SF_EXE/libs/RKC_RPGSCRN/display_hit_test.cpp b/src/SF_EXE/libs/RKC_RPGSCRN/display_hit_test.cpp index 2e2b4f7a..01dff990 100644 --- a/src/SF_EXE/libs/RKC_RPGSCRN/display_hit_test.cpp +++ b/src/SF_EXE/libs/RKC_RPGSCRN/display_hit_test.cpp @@ -148,7 +148,7 @@ bool displayAnimationIntersectsRectangle( } chart_index = std::clamp( chart_index, - 0, + std::int32_t{0}, static_cast( animation.charts().size() - 1)); const gapi::CafChart& chart = diff --git a/src/SF_EXE/render/character_renderer.cpp b/src/SF_EXE/render/character_renderer.cpp index a8c37ca6..8d785502 100644 --- a/src/SF_EXE/render/character_renderer.cpp +++ b/src/SF_EXE/render/character_renderer.cpp @@ -34,7 +34,7 @@ void renderCharacterAnimationPass( const std::int32_t selected_chart_index = std::clamp( chart_index, - 0, + std::int32_t{0}, static_cast( animation.charts().size() - 1)); const gapi::CafChart& chart = @@ -123,10 +123,10 @@ void renderCharacterAnimationPass( 1000, 1000, shadow - ? std::clamp(shadow_opacity, 0, 1000) + ? std::clamp(shadow_opacity, std::int32_t{0}, std::int32_t{1000}) : std::clamp( cell->transparency * - std::clamp(opacity, 0, 1000) / + std::clamp(opacity, std::int32_t{0}, std::int32_t{1000}) / 1000, 0, 1000), diff --git a/src/SF_EXE/render/character_select_renderer.cpp b/src/SF_EXE/render/character_select_renderer.cpp index c06b09f2..b8e08214 100644 --- a/src/SF_EXE/render/character_select_renderer.cpp +++ b/src/SF_EXE/render/character_select_renderer.cpp @@ -73,7 +73,7 @@ void renderNameEditor( {64, 64, 64, 255}, brightness}); const std::int32_t caretColumn = - std::min(textCellCount(data.character_name), 20); + std::min(textCellCount(data.character_name), std::int32_t{20}); renderer.drawRectangle( {kEditorX + kTextInset + caretColumn * kFontWidth, kEditorY + kTextInset, @@ -322,12 +322,13 @@ void renderSavedGames( const std::int32_t itemBrightness = index == static_cast( - std::max(selected, 0)) + std::max(selected, std::int32_t{0})) ? brightness : brightness / 2; const bool selectedItem = index == - static_cast(std::max(selected, 0)); + static_cast( + std::max(selected, std::int32_t{0})); const gapi::Color labelColor = selectedItem ? gapi::Color{224, 192, 128, 255} : gapi::Color{112, 96, 64, 255}; @@ -347,7 +348,7 @@ void renderSavedGames( static_cast(index) == selected ? 0 : 3; if (hovered) { const std::int32_t renderedCounter = - std::max(data.save_hover_animation - 1, 0); + std::max(data.save_hover_animation - 1, std::int32_t{0}); numberFrame = hoverFrames[(renderedCounter / 4) & 7]; } diff --git a/src/SF_EXE/render/enemy_nameplate_renderer.cpp b/src/SF_EXE/render/enemy_nameplate_renderer.cpp index 2302f9c9..38a21b8c 100644 --- a/src/SF_EXE/render/enemy_nameplate_renderer.cpp +++ b/src/SF_EXE/render/enemy_nameplate_renderer.cpp @@ -37,10 +37,10 @@ void renderEnemyNameplate( 800, }); const std::int32_t maximum_life = - std::max(nameplate.maximum_life, 0); + std::max(nameplate.maximum_life, std::int32_t{0}); const std::int32_t current_life = std::clamp( - nameplate.current_life, 0, maximum_life); + nameplate.current_life, std::int32_t{0}, maximum_life); const std::int32_t life_width = maximum_life == 0 ? 0 diff --git a/src/SF_EXE/render/gameplay_hud_renderer.cpp b/src/SF_EXE/render/gameplay_hud_renderer.cpp index 7ab84b0f..6a57e8fd 100644 --- a/src/SF_EXE/render/gameplay_hud_renderer.cpp +++ b/src/SF_EXE/render/gameplay_hud_renderer.cpp @@ -36,7 +36,7 @@ void drawLevel( gapi::Backend& renderer, const gapi::NjpImage& patterns, std::int32_t level) { - level = std::clamp(level, 0, 999); + level = std::clamp(level, std::int32_t{0}, std::int32_t{999}); const std::int32_t digits = level > 99 ? 3 : (level > 9 ? 2 : 1); constexpr std::array, 3> @@ -127,7 +127,7 @@ std::int32_t gameplayHudExperienceBarWidth( static_cast( static_cast(experience) * kRetailExperienceWidth / threshold), - 1); + std::int32_t{1}); } std::int32_t gameplayHudBarWidth( @@ -143,7 +143,7 @@ std::int32_t gameplayHudBarWidth( static_cast( static_cast(current) * kRetailBarWidth / maximum), - 1); + std::int32_t{1}); } void renderGameplayHud( diff --git a/src/SF_EXE/render/gameplay_magic_renderer.cpp b/src/SF_EXE/render/gameplay_magic_renderer.cpp index 9147e054..ba2fbfd1 100644 --- a/src/SF_EXE/render/gameplay_magic_renderer.cpp +++ b/src/SF_EXE/render/gameplay_magic_renderer.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -88,7 +89,7 @@ void drawShadowedText( std::string number(std::int32_t value) { char buffer[32]{}; - std::snprintf(buffer, sizeof(buffer), "%d", value); + std::snprintf(buffer, sizeof(buffer), "%" PRId32, value); return buffer; } @@ -168,11 +169,11 @@ void renderDescription( static_cast(lines.size()) * 12 + 8; const std::int32_t x = std::clamp( panel.pointerX() - width / 2, - 1, + std::int32_t{1}, 639 - width); const std::int32_t y = std::clamp( panel.pointerY() + 8, - 1, + std::int32_t{1}, 479 - height); renderer.drawRectangle({ x, diff --git a/src/SF_EXE/render/gameplay_overlay_renderer.cpp b/src/SF_EXE/render/gameplay_overlay_renderer.cpp index 545c6093..147f96bc 100644 --- a/src/SF_EXE/render/gameplay_overlay_renderer.cpp +++ b/src/SF_EXE/render/gameplay_overlay_renderer.cpp @@ -103,7 +103,8 @@ gapi::Color scenarioObjectNameColor( std::uint8_t colorChannel(std::int32_t value) { return static_cast( - std::clamp(value, 0, 255)); + std::clamp( + value, std::int32_t{0}, std::int32_t{255})); } void drawScenarioTextLabels( @@ -140,7 +141,10 @@ void drawScenarioTextLabels( height + kMargin * 2, {0, 0, 0, 255}, 1000, - std::clamp(label.background_opacity, 0, 1000), + std::clamp( + label.background_opacity, + std::int32_t{0}, + std::int32_t{1000}), }); renderer.drawText( *font, diff --git a/src/SF_EXE/render/gameplay_renderer.cpp b/src/SF_EXE/render/gameplay_renderer.cpp index 3931da98..f9d9f122 100644 --- a/src/SF_EXE/render/gameplay_renderer.cpp +++ b/src/SF_EXE/render/gameplay_renderer.cpp @@ -578,9 +578,9 @@ void renderScenarioObjectPass( 1000, 1000, shadow - ? std::clamp(shadow_opacity, 0, 1000) + ? std::clamp(shadow_opacity, std::int32_t{0}, std::int32_t{1000}) : std::clamp( - object.drawStrength(), 0, 1000), + object.drawStrength(), std::int32_t{0}, std::int32_t{1000}), shadow ? 1000 : object.redDrawStrength() + @@ -714,7 +714,7 @@ void drawMapObject( 1000, 1000, shadow - ? std::clamp(shadow_opacity, 0, 1000) + ? std::clamp(shadow_opacity, std::int32_t{0}, std::int32_t{1000}) : std::clamp( semi_transparent ? std::min( @@ -1045,7 +1045,7 @@ void drawGroundItem( 1000, shadow ? std::clamp( - shadow_opacity, 0, 1000) + shadow_opacity, std::int32_t{0}, std::int32_t{1000}) : 1000, shadow ? 1000 @@ -1324,9 +1324,9 @@ void renderWorldGeometry( const std::int32_t camera_y = world.renderCameraScreenY(interpolation); const std::int32_t start_x = - std::max(camera_x / ground.chipWidth(), 0); + std::max(camera_x / ground.chipWidth(), std::int32_t{0}); const std::int32_t start_y = - std::max(camera_y / ground.chipHeight(), 0); + std::max(camera_y / ground.chipHeight(), std::int32_t{0}); const std::int32_t end_x = std::min( (camera_x + kScreenWidth) / ground.chipWidth(), ground.width() - 1); diff --git a/src/SF_EXE/render/gameplay_status_renderer.cpp b/src/SF_EXE/render/gameplay_status_renderer.cpp index a2a5d07d..22fdb337 100644 --- a/src/SF_EXE/render/gameplay_status_renderer.cpp +++ b/src/SF_EXE/render/gameplay_status_renderer.cpp @@ -199,8 +199,8 @@ void renderGameplayStatusPanel( static_cast( 36 + std::clamp( affinities[element] + 10, - 0, - 20)), + std::int32_t{0}, + std::int32_t{20})), {0, static_cast(element) * 16}); } diff --git a/src/SF_EXE/render/item_information_renderer.cpp b/src/SF_EXE/render/item_information_renderer.cpp index 1033760c..06c7c5e2 100644 --- a/src/SF_EXE/render/item_information_renderer.cpp +++ b/src/SF_EXE/render/item_information_renderer.cpp @@ -68,12 +68,14 @@ void renderInformation( kInformationPadding * 2; const std::int32_t x = std::clamp( pointer_x - width / 2, - 1, - std::max(1, kScreenWidth - width)); + std::int32_t{1}, + std::max( + std::int32_t{1}, kScreenWidth - width)); const std::int32_t y = std::clamp( pointer_y + 8, - 1, - std::max(1, kScreenHeight - height)); + std::int32_t{1}, + std::max( + std::int32_t{1}, kScreenHeight - height)); const gapi::Color black{0, 0, 0, 255}; const gapi::Color white{255, 255, 255, 255}; renderer.drawRectangle({ diff --git a/src/SF_EXE/render/loading_renderer.cpp b/src/SF_EXE/render/loading_renderer.cpp index 2e48b816..a88c52d2 100644 --- a/src/SF_EXE/render/loading_renderer.cpp +++ b/src/SF_EXE/render/loading_renderer.cpp @@ -20,7 +20,7 @@ void renderInitialLoadingScreen( } const std::int32_t arrow_offset = - std::max(counter, 0) % 16; + std::max(counter, std::int32_t{0}) % 16; renderer.drawPattern( waiting, 2, {592 + arrow_offset, 450}); } diff --git a/src/SF_EXE/resources/character_visual_resource.cpp b/src/SF_EXE/resources/character_visual_resource.cpp index 26fcdf09..af7d8baa 100644 --- a/src/SF_EXE/resources/character_visual_resource.cpp +++ b/src/SF_EXE/resources/character_visual_resource.cpp @@ -1,260 +1,260 @@ -#include "character_visual_resource.hpp" - -#include "resource_memory.hpp" - -#include -#include -#include -#include - -namespace osf { -namespace { - -void setError(std::string* error, std::string message) { - if (error) { - *error = std::move(message); - } -} - -std::string resourceDirectory(std::int32_t resource_id) { - std::ostringstream name; - name << std::setfill('0') << std::setw(8) << resource_id; - return name.str(); -} - -struct SelectedPatterns { - std::vector normal; - std::vector shadow; -}; - -void enablePattern( - std::vector& patterns, - std::int32_t pattern_index) { - if (pattern_index < 0) { - return; - } - const std::size_t index = - static_cast(pattern_index); - if (patterns.size() <= index) { - patterns.resize(index + 1, 0); - } - patterns[index] = 1; -} - -SelectedPatterns patternsForParts( - const gapi::CafAnimation& animation, - const std::vector& enabled_parts) { - SelectedPatterns selected; - for (const gapi::CafChart& chart : animation.charts()) { - for (const gapi::CafDirection& direction : - chart.directions) { - const std::size_t part_count = std::min( - direction.parts.size(), enabled_parts.size()); - for (std::size_t part_index = 0; - part_index < part_count; - ++part_index) { - if (enabled_parts[part_index] == 0) { - continue; - } - for (const gapi::CafCell& cell : - direction.parts[part_index]) { - enablePattern( - selected.normal, cell.pattern_index); - if ((cell.status & 8) != 0) { - enablePattern( - selected.shadow, cell.pattern_index); - } - } - } - } - } - return selected; -} - -} // namespace - -bool CharacterVisualResource::load( - const std::filesystem::path& directory, - const std::string& stem, - std::string* error) { - clear(); - std::string resource_error; - if (!animation_.load( - directory / (stem + ".Caf"), &resource_error) || - !patterns_.load( - directory / (stem + ".Njp"), &resource_error) || - !shadow_patterns_.load( - directory / (stem + ".Sdw"), &resource_error)) { - setError(error, resource_error); - clear(); - return false; - } - directory_ = directory; - stem_ = stem; - selected_parts_.assign(animation_.maxPartCount(), 1); - patterns_loaded_ = true; - if (error) { - error->clear(); - } - return true; -} - -bool CharacterVisualResource::loadAnimation( - const std::filesystem::path& directory, - const std::string& stem, - std::string* error) { - clear(); - if (!animation_.load(directory / (stem + ".Caf"), error)) { - clear(); - return false; - } - directory_ = directory; - stem_ = stem; - if (error) { - error->clear(); - } - return true; -} - -bool CharacterVisualResource::loadSelectedParts( - const std::vector& enabled_parts, - std::string* error) { - if (directory_.empty() || stem_.empty()) { - setError(error, "The character animation source is not loaded."); - return false; - } - std::vector selection( - animation_.maxPartCount(), 0); - std::copy_n( - enabled_parts.begin(), - std::min(enabled_parts.size(), selection.size()), - selection.begin()); - if (patterns_loaded_ && selection == selected_parts_) { - if (error) { - error->clear(); - } - return true; - } - - const SelectedPatterns selected = - patternsForParts(animation_, selection); - patterns_.clear(); - shadow_patterns_.clear(); - selected_parts_.clear(); - patterns_loaded_ = false; - std::string resource_error; - if (!patterns_.loadSelectedPatterns( - directory_ / (stem_ + ".Njp"), - selected.normal, - &resource_error) || - !shadow_patterns_.loadSelectedPatterns( - directory_ / (stem_ + ".Sdw"), - selected.shadow, - &resource_error)) { - patterns_.clear(); - shadow_patterns_.clear(); - setError(error, resource_error); - return false; - } - selected_parts_ = std::move(selection); - patterns_loaded_ = true; - if (error) { - error->clear(); - } - return true; -} - -void CharacterVisualResource::clear() { - patterns_.clear(); - shadow_patterns_.clear(); - animation_.clear(); - directory_.clear(); - stem_.clear(); - selected_parts_.clear(); - patterns_loaded_ = false; -} - -const gapi::NjpImage& -CharacterVisualResource::patterns() const { - return patterns_; -} - -const gapi::NjpImage& -CharacterVisualResource::shadowPatterns() const { - return shadow_patterns_; -} - -const gapi::CafAnimation& -CharacterVisualResource::animation() const { - return animation_; -} - -std::uint64_t CharacterVisualResource::memoryUsageBytes() const { - return decodedMemoryUsageBytes(patterns_) + - decodedMemoryUsageBytes(shadow_patterns_) + - decodedMemoryUsageBytes(animation_); -} - -CharacterVisualResources::CharacterVisualResources( - std::string category) - : category_(std::move(category)) {} - -const CharacterVisualResource* CharacterVisualResources::load( - const std::filesystem::path& data_root, - std::int32_t resource_id, - std::string* error) { - if (resource_id < 0 || resource_id > 99999999) { - setError( - error, - "The " + category_ + - " animation resource ID is invalid."); - return nullptr; - } - const auto found = resources_.find(resource_id); - if (found != resources_.end()) { - return found->second.get(); - } - - auto resource = std::make_unique(); - std::string resource_error; - if (!resource->load( - data_root / "Character" / category_ / - resourceDirectory(resource_id), - "Animation", - &resource_error)) { - setError( - error, - "The " + category_ + - " animation could not be loaded: " + - resource_error); - return nullptr; - } - const CharacterVisualResource* result = resource.get(); - resources_.emplace(resource_id, std::move(resource)); - if (error) { - error->clear(); - } - return result; -} - -const CharacterVisualResource* CharacterVisualResources::find( - std::int32_t resource_id) const { - const auto found = resources_.find(resource_id); - return found == resources_.end() - ? nullptr - : found->second.get(); -} - -void CharacterVisualResources::clear() { - resources_.clear(); -} - -std::uint64_t CharacterVisualResources::memoryUsageBytes() const { - std::uint64_t bytes = 0; - for (const auto& entry : resources_) { - bytes += entry.second->memoryUsageBytes(); - } - return bytes; -} - -} // namespace osf +#include "character_visual_resource.hpp" + +#include "resource_memory.hpp" + +#include +#include +#include +#include + +namespace osf { +namespace { + +void setError(std::string* error, std::string message) { + if (error) { + *error = std::move(message); + } +} + +std::string resourceDirectory(std::int32_t resource_id) { + std::ostringstream name; + name << std::setfill('0') << std::setw(8) << resource_id; + return name.str(); +} + +struct SelectedPatterns { + std::vector normal; + std::vector shadow; +}; + +void enablePattern( + std::vector& patterns, + std::int32_t pattern_index) { + if (pattern_index < 0) { + return; + } + const std::size_t index = + static_cast(pattern_index); + if (patterns.size() <= index) { + patterns.resize(index + 1, 0); + } + patterns[index] = 1; +} + +SelectedPatterns patternsForParts( + const gapi::CafAnimation& animation, + const std::vector& enabled_parts) { + SelectedPatterns selected; + for (const gapi::CafChart& chart : animation.charts()) { + for (const gapi::CafDirection& direction : + chart.directions) { + const std::size_t part_count = std::min( + direction.parts.size(), enabled_parts.size()); + for (std::size_t part_index = 0; + part_index < part_count; + ++part_index) { + if (enabled_parts[part_index] == 0) { + continue; + } + for (const gapi::CafCell& cell : + direction.parts[part_index]) { + enablePattern( + selected.normal, cell.pattern_index); + if ((cell.status & 8) != 0) { + enablePattern( + selected.shadow, cell.pattern_index); + } + } + } + } + } + return selected; +} + +} // namespace + +bool CharacterVisualResource::load( + const std::filesystem::path& directory, + const std::string& stem, + std::string* error) { + clear(); + std::string resource_error; + if (!animation_.load( + directory / (stem + ".Caf"), &resource_error) || + !patterns_.load( + directory / (stem + ".Njp"), &resource_error) || + !shadow_patterns_.load( + directory / (stem + ".Sdw"), &resource_error)) { + setError(error, resource_error); + clear(); + return false; + } + directory_ = directory; + stem_ = stem; + selected_parts_.assign(animation_.maxPartCount(), 1); + patterns_loaded_ = true; + if (error) { + error->clear(); + } + return true; +} + +bool CharacterVisualResource::loadAnimation( + const std::filesystem::path& directory, + const std::string& stem, + std::string* error) { + clear(); + if (!animation_.load(directory / (stem + ".Caf"), error)) { + clear(); + return false; + } + directory_ = directory; + stem_ = stem; + if (error) { + error->clear(); + } + return true; +} + +bool CharacterVisualResource::loadSelectedParts( + const std::vector& enabled_parts, + std::string* error) { + if (directory_.empty() || stem_.empty()) { + setError(error, "The character animation source is not loaded."); + return false; + } + std::vector selection( + animation_.maxPartCount(), 0); + std::copy_n( + enabled_parts.begin(), + std::min(enabled_parts.size(), selection.size()), + selection.begin()); + if (patterns_loaded_ && selection == selected_parts_) { + if (error) { + error->clear(); + } + return true; + } + + const SelectedPatterns selected = + patternsForParts(animation_, selection); + patterns_.clear(); + shadow_patterns_.clear(); + selected_parts_.clear(); + patterns_loaded_ = false; + std::string resource_error; + if (!patterns_.loadSelectedPatterns( + directory_ / (stem_ + ".Njp"), + selected.normal, + &resource_error) || + !shadow_patterns_.loadSelectedPatterns( + directory_ / (stem_ + ".Sdw"), + selected.shadow, + &resource_error)) { + patterns_.clear(); + shadow_patterns_.clear(); + setError(error, resource_error); + return false; + } + selected_parts_ = std::move(selection); + patterns_loaded_ = true; + if (error) { + error->clear(); + } + return true; +} + +void CharacterVisualResource::clear() { + patterns_.clear(); + shadow_patterns_.clear(); + animation_.clear(); + directory_.clear(); + stem_.clear(); + selected_parts_.clear(); + patterns_loaded_ = false; +} + +const gapi::NjpImage& +CharacterVisualResource::patterns() const { + return patterns_; +} + +const gapi::NjpImage& +CharacterVisualResource::shadowPatterns() const { + return shadow_patterns_; +} + +const gapi::CafAnimation& +CharacterVisualResource::animation() const { + return animation_; +} + +std::uint64_t CharacterVisualResource::memoryUsageBytes() const { + return decodedMemoryUsageBytes(patterns_) + + decodedMemoryUsageBytes(shadow_patterns_) + + decodedMemoryUsageBytes(animation_); +} + +CharacterVisualResources::CharacterVisualResources( + std::string category) + : category_(std::move(category)) {} + +const CharacterVisualResource* CharacterVisualResources::load( + const std::filesystem::path& data_root, + std::int32_t resource_id, + std::string* error) { + if (resource_id < 0 || resource_id > 99999999) { + setError( + error, + "The " + category_ + + " animation resource ID is invalid."); + return nullptr; + } + const auto found = resources_.find(resource_id); + if (found != resources_.end()) { + return found->second.get(); + } + + auto resource = std::make_unique(); + std::string resource_error; + if (!resource->load( + data_root / "Character" / category_ / + resourceDirectory(resource_id), + "Animation", + &resource_error)) { + setError( + error, + "The " + category_ + + " animation could not be loaded: " + + resource_error); + return nullptr; + } + const CharacterVisualResource* result = resource.get(); + resources_.emplace(resource_id, std::move(resource)); + if (error) { + error->clear(); + } + return result; +} + +const CharacterVisualResource* CharacterVisualResources::find( + std::int32_t resource_id) const { + const auto found = resources_.find(resource_id); + return found == resources_.end() + ? nullptr + : found->second.get(); +} + +void CharacterVisualResources::clear() { + resources_.clear(); +} + +std::uint64_t CharacterVisualResources::memoryUsageBytes() const { + std::uint64_t bytes = 0; + for (const auto& entry : resources_) { + bytes += entry.second->memoryUsageBytes(); + } + return bytes; +} + +} // namespace osf diff --git a/src/SF_EXE/resources/character_visual_resource.hpp b/src/SF_EXE/resources/character_visual_resource.hpp index b9823946..6ab17a4f 100644 --- a/src/SF_EXE/resources/character_visual_resource.hpp +++ b/src/SF_EXE/resources/character_visual_resource.hpp @@ -1,69 +1,69 @@ -#ifndef OPENSHADOWFLARE_CHARACTER_VISUAL_RESOURCE_HPP -#define OPENSHADOWFLARE_CHARACTER_VISUAL_RESOURCE_HPP - -#include "libs/RKC_RPGSCRN/rkc_rpgscrn.hpp" -#include "libs/RKC_UPDIB/rkc_updib.hpp" - -#include -#include -#include -#include -#include -#include - -namespace osf { - -class CharacterVisualResource { -public: - bool load( - const std::filesystem::path& directory, - const std::string& stem, - std::string* error = nullptr); - bool loadAnimation( - const std::filesystem::path& directory, - const std::string& stem, - std::string* error = nullptr); - bool loadSelectedParts( - const std::vector& enabled_parts, - std::string* error = nullptr); - void clear(); - - const gapi::NjpImage& patterns() const; - const gapi::NjpImage& shadowPatterns() const; - const gapi::CafAnimation& animation() const; - std::uint64_t memoryUsageBytes() const; - -private: - gapi::NjpImage patterns_; - gapi::NjpImage shadow_patterns_; - gapi::CafAnimation animation_; - std::filesystem::path directory_; - std::string stem_; - std::vector selected_parts_; - bool patterns_loaded_ = false; -}; - -class CharacterVisualResources { -public: - explicit CharacterVisualResources( - std::string category); - - const CharacterVisualResource* load( - const std::filesystem::path& data_root, - std::int32_t resource_id, - std::string* error = nullptr); - const CharacterVisualResource* find( - std::int32_t resource_id) const; - void clear(); - std::uint64_t memoryUsageBytes() const; - -private: - std::string category_; - std::unordered_map< - std::int32_t, - std::unique_ptr> resources_; -}; - -} // namespace osf - -#endif +#ifndef OPENSHADOWFLARE_CHARACTER_VISUAL_RESOURCE_HPP +#define OPENSHADOWFLARE_CHARACTER_VISUAL_RESOURCE_HPP + +#include "libs/RKC_RPGSCRN/rkc_rpgscrn.hpp" +#include "libs/RKC_UPDIB/rkc_updib.hpp" + +#include +#include +#include +#include +#include +#include + +namespace osf { + +class CharacterVisualResource { +public: + bool load( + const std::filesystem::path& directory, + const std::string& stem, + std::string* error = nullptr); + bool loadAnimation( + const std::filesystem::path& directory, + const std::string& stem, + std::string* error = nullptr); + bool loadSelectedParts( + const std::vector& enabled_parts, + std::string* error = nullptr); + void clear(); + + const gapi::NjpImage& patterns() const; + const gapi::NjpImage& shadowPatterns() const; + const gapi::CafAnimation& animation() const; + std::uint64_t memoryUsageBytes() const; + +private: + gapi::NjpImage patterns_; + gapi::NjpImage shadow_patterns_; + gapi::CafAnimation animation_; + std::filesystem::path directory_; + std::string stem_; + std::vector selected_parts_; + bool patterns_loaded_ = false; +}; + +class CharacterVisualResources { +public: + explicit CharacterVisualResources( + std::string category); + + const CharacterVisualResource* load( + const std::filesystem::path& data_root, + std::int32_t resource_id, + std::string* error = nullptr); + const CharacterVisualResource* find( + std::int32_t resource_id) const; + void clear(); + std::uint64_t memoryUsageBytes() const; + +private: + std::string category_; + std::unordered_map< + std::int32_t, + std::unique_ptr> resources_; +}; + +} // namespace osf + +#endif diff --git a/src/SF_EXE/resources/retail_filesystem.cpp b/src/SF_EXE/resources/retail_filesystem.cpp index f3eddc09..f7ecb240 100644 --- a/src/SF_EXE/resources/retail_filesystem.cpp +++ b/src/SF_EXE/resources/retail_filesystem.cpp @@ -1,6 +1,7 @@ #include "retail_filesystem.hpp" #include +#include #include #include @@ -87,7 +88,8 @@ std::int32_t countRetailSaves( std::int32_t count = 0; for (std::int32_t index = 0; index < 6; ++index) { char path[32]{}; - std::snprintf(path, sizeof(path), "Save\\%04d.Ssv", index); + std::snprintf( + path, sizeof(path), "Save\\%04" PRId32 ".Ssv", index); if (retailFileExists(root, path)) { ++count; } @@ -104,7 +106,10 @@ bool deleteRetailSave( for (std::int32_t slot = 0; slot < 6; ++slot) { char save_path[32]{}; std::snprintf( - save_path, sizeof(save_path), "Save\\%04d.Ssv", slot); + save_path, + sizeof(save_path), + "Save\\%04" PRId32 ".Ssv", + slot); if (!retailFileExists(root, save_path)) { continue; } @@ -119,7 +124,7 @@ bool deleteRetailSave( std::snprintf( preview_path, sizeof(preview_path), - "Save\\%04d.Bmp", + "Save\\%04" PRId32 ".Bmp", slot); error.clear(); std::filesystem::remove( diff --git a/src/SF_EXE/resources/save_catalog.cpp b/src/SF_EXE/resources/save_catalog.cpp index b550bfb0..6e27c304 100644 --- a/src/SF_EXE/resources/save_catalog.cpp +++ b/src/SF_EXE/resources/save_catalog.cpp @@ -1,6 +1,7 @@ #include "save_catalog.hpp" #include +#include #include #include #include @@ -41,7 +42,7 @@ std::vector loadRetailSaveCatalog( for (std::int32_t slot = 0; slot < 6; ++slot) { char filename[16]{}; std::snprintf( - filename, sizeof(filename), "%04d.Ssv", slot); + filename, sizeof(filename), "%04" PRId32 ".Ssv", slot); const std::filesystem::path savePath = game_root / "Save" / filename; std::ifstream stream(savePath, std::ios::binary); @@ -59,7 +60,7 @@ std::vector loadRetailSaveCatalog( } std::snprintf( - filename, sizeof(filename), "%04d.Bmp", slot); + filename, sizeof(filename), "%04" PRId32 ".Bmp", slot); RetailSaveSummary summary; summary.slot = slot; summary.save_path = savePath; diff --git a/src/SF_EXE/runtime/audio_system.cpp b/src/SF_EXE/runtime/audio_system.cpp index 7a59297f..9c13882f 100644 --- a/src/SF_EXE/runtime/audio_system.cpp +++ b/src/SF_EXE/runtime/audio_system.cpp @@ -5,6 +5,7 @@ #include "states/character_select_state.hpp" #include "states/title_state.hpp" +#include #include namespace osf::runtime { @@ -113,7 +114,7 @@ void AudioSystem::startWorldMusic(std::int32_t track) { std::snprintf( path, sizeof(path), - "System\\Game\\Music\\BGM%02d.Voc", + "System\\Game\\Music\\BGM%02" PRId32 ".Voc", track); if (loadVoc(world_music_, path)) { world_music_.play(0, true, bgm_volume_); diff --git a/src/SF_EXE/runtime/game_runtime.cpp b/src/SF_EXE/runtime/game_runtime.cpp index 087d3689..f7f04012 100644 --- a/src/SF_EXE/runtime/game_runtime.cpp +++ b/src/SF_EXE/runtime/game_runtime.cpp @@ -1,591 +1,591 @@ -#include "game_runtime.hpp" - -#include "lwl.h" -#include "core/retail_random.hpp" -#include "resources/resource_manager.hpp" -#include "resources/retail_filesystem.hpp" -#include "runtime/application_loop.hpp" -#include "runtime/audio_system.hpp" -#include "runtime/gameplay_artwork.hpp" -#include "runtime/gameplay_ui_controller.hpp" -#include "runtime/input_adapter.hpp" -#include "runtime/presentation/surface_presenter.hpp" -#include "runtime/runtime_renderer.hpp" -#include "runtime/state_bindings.hpp" -#include "states/game_state.hpp" -#include "states/gameplay_state.hpp" -#include "states/character_select_state.hpp" -#include "states/title_state.hpp" -#include "ui/companion_hud_input.hpp" -#include "ui/player_level_up_notice_input.hpp" -#include "world/retail_save_preview.hpp" -#include "world/world_scene.hpp" -#if OSF_ENABLE_DEBUG_TOOLS -#include "debug/frame_profiler.hpp" -#endif - -#include -#include -#include -#include -#include -#include - -namespace { - -constexpr int kVirtualWidth = 640; -constexpr int kVirtualHeight = 480; - -class Runtime final : public osf::runtime::FrameApplication { -public: - explicit Runtime(std::filesystem::path dataRoot) - : dataRoot_(std::move(dataRoot)), - resources_(dataRoot_), - surfacePresenter_( - osf::runtime::createSurfacePresenter()), - renderer_(kVirtualWidth, kVirtualHeight), - input_(kVirtualWidth, kVirtualHeight), - titleState_( - random_, - osf::runtime::makeTitleStateHooks( - dataRoot_, resources_, audio_)), - characterSelectState_( - osf::runtime::makeCharacterSelectStateHooks( - dataRoot_, - resources_, - audio_, - window_)), - gameplayState_( - osf::runtime::makeGameplayStateHooks( - dataRoot_, - gameplayPlayer_, - resources_, - audio_, - world_)), - gameState_(makeGameStateCallbacks()) {} - - ~Runtime() { - saveConfigIfDirty(); - surfacePresenter_.reset(); - lwl_window_destroy(window_); - if (windowingInitialized_) { - lwl_shutdown(); - } - } - - bool initialize(const osf::GameConfig& gameConfig) { - gameConfig_ = gameConfig; - if (!lwl_init()) { - std::fprintf(stderr, "Could not initialize LWL.\n"); - return false; - } - windowingInitialized_ = true; - - window_ = lwl_window_create( - "OpenShadowFlare", kVirtualWidth, kVirtualHeight); - if (!window_) { - std::fprintf(stderr, "Could not create the game window.\n"); - return false; - } - lwl_window_set_mode(window_, LWL_WINDOW_NORMAL); - lwl_window_show(window_); - - std::string presenterError; - if (!surfacePresenter_ || - !surfacePresenter_->initialize( - window_, &presenterError)) { - std::fprintf( - stderr, - "Could not initialize surface presentation: %s\n", - presenterError.c_str()); - return false; - } - std::string audioError; - if (!audio_.initialize( - dataRoot_, - gameConfig_.effect_volume, - gameConfig_.bgm_volume, - &audioError)) { - std::fprintf( - stderr, - "Warning: audio is unavailable: %s\n", - audioError.c_str()); - } - shadowOpacity_ = - gameConfig_.semi_transparent_shadow ? 500 : 1000; - world_.configurePointer({ - gameConfig_.click_range, - gameConfig_.click_range_enabled, - gameConfig_.click_priority, - }); - - if (!resources_.loadCommonPattern( - 3, - "System\\Common\\Pattern\\System.njp")) { - return false; - } - lwl_window_set_cursor_visible(window_, false); - gameState_.transition(osf::GameState::title); - return true; - } - - void start(bool smokeTest) { - smokeTest_ = smokeTest; - running_ = true; - renderedFrames_ = 0; - previousTime_ = lwl_time_seconds(); - nextFrame_ = previousTime_; -#if OSF_ENABLE_DEBUG_TOOLS - fpsWindowStart_ = previousTime_; - fpsWindowFrames_ = 0; - framesPerSecond_ = 0; - profiler_.setEnabled(false, previousTime_); -#endif - gameAccumulator_ = kGameStep; - } - - bool frame() override { - tickOnce(); - if (!running_) { - saveConfigIfDirty(); - } - return running_; - } - -private: - static constexpr double kRenderStep = 1.0 / 60.0; - static constexpr double kGameStep = 1.0 / 30.0; - static constexpr double kMaximumElapsed = 0.25; - - void tickOnce() { - const double currentTime = lwl_time_seconds(); - gameAccumulator_ += std::clamp( - currentTime - previousTime_, - 0.0, - kMaximumElapsed); - previousTime_ = currentTime; - - LwlEvent event{}; - while (lwl_poll_event(window_, &event)) { - if (!input_.handleEvent( - window_, - event, - gameState_.currentState())) { - running_ = false; - } - } - - while (running_ && gameAccumulator_ >= kGameStep) { - updateGame(running_); - gameAccumulator_ -= kGameStep; - } - - const double interpolation = - std::clamp( - gameAccumulator_ / kGameStep, - 0.0, - 1.0); -#if OSF_ENABLE_DEBUG_TOOLS - const bool profilingEnabled = - gameplayUi_.debug().profilingEnabled(); - profiler_.setEnabled(profilingEnabled, currentTime); - const bool synchronizeDisplay = !profilingEnabled; - if (synchronizeDisplay != displaySynchronizationEnabled_) { - surfacePresenter_->setDisplaySynchronization( - synchronizeDisplay); - displaySynchronizationEnabled_ = synchronizeDisplay; - } - if (profiler_.memorySampleDue(currentTime)) { - const std::uint64_t audioMemory = - audio_.memoryUsageBytes(); - const std::uint64_t gameMemory = - resources_.memoryUsageBytes() + - world_.resourceMemoryUsageBytes() + - renderer_.memoryUsageBytes(); - profiler_.recordMemoryUsage( - currentTime, - gameMemory, - audioMemory, - surfacePresenter_->videoMemoryUsageBytes()); - } - const double framebufferFillStart = lwl_time_seconds(); -#endif - const osf::gapi::SurfaceView surface = renderer_.render( - { - gameState_.currentState(), - titleFrame_, - characterFrame_, - gameplayFrame_, - characterSelectState_, - world_, - resources_, - savePreview_, - gameplayUi_.options(), - gameplayUi_.blackjack(), -#if OSF_ENABLE_DEBUG_TOOLS - gameplayUi_.debug(), -#endif - gameplayUi_.equipmentColor(), - gameplayUi_.inventory(), - gameplayUi_.map(), - gameplayUi_.magic(), - gameplayUi_.status(), - gameplayUi_.missionList(), - gameplayUi_.transport(), - gameplayUi_.vendor(), - gameConfig_, - shadowOpacity_, - gameplayCounter_, -#if OSF_ENABLE_DEBUG_TOOLS - framesPerSecond_, - profiler_.metrics(), -#endif - input_.menu().pointer_x, - input_.menu().pointer_y, - }, - interpolation); -#if OSF_ENABLE_DEBUG_TOOLS - profiler_.recordFramebufferFill( - lwl_time_seconds() - framebufferFillStart); - const double presentStart = lwl_time_seconds(); -#endif - surfacePresenter_->prepareFrame(surface); -#if OSF_ENABLE_DEBUG_TOOLS - profiler_.recordPresent( - lwl_time_seconds() - presentStart); -#endif - surfacePresenter_->displayFrame(); - if (gameState_.currentState() == - osf::GameState::gameplay && - gameplayFrame_.phase == osf::GameplayPhase::world) { - world_.advanceScenarioVisualFrame(); - } - - ++renderedFrames_; -#if OSF_ENABLE_DEBUG_TOOLS - ++fpsWindowFrames_; - const double fps_elapsed = - currentTime - fpsWindowStart_; - if (fps_elapsed >= 0.5) { - framesPerSecond_ = static_cast( - static_cast(fpsWindowFrames_) / - fps_elapsed + - 0.5); - fpsWindowStart_ = currentTime; - fpsWindowFrames_ = 0; - } -#endif - if (smokeTest_ && renderedFrames_ >= 3) { - running_ = false; - } - - nextFrame_ += kRenderStep; - if (nextFrame_ < currentTime - kMaximumElapsed) { - nextFrame_ = currentTime; - } - lwl_sleep_until_seconds(nextFrame_); - } - - void saveConfigIfDirty() { - if (configDirty_) { - osf::saveGameConfigFile( - (dataRoot_ / "SFlare.Cfg").string(), - gameConfig_); - configDirty_ = false; - } - } - - void completeScenarioChange() { - gameplayUi_.reset(); - world_.setCameraAnchor(320, 240); - audio_.startWorldMusic(world_.musicTrack()); - } - - void updateGame(bool& running) { - switch (gameState_.currentState()) { - case osf::GameState::title: { - for (std::size_t index = 0; - index < 10; - ++index) { - const auto* animation = - resources_.titleAnimation(index); - const auto& charts = animation->charts(); - input_.menu().smoke_frame_counts[index] = - charts.empty() - ? 0 - : charts.front().directions[8].frame_count; - } - titleFrame_ = - titleState_.update(input_.menu()); - audio_.playTitleFrame(titleFrame_); - if (titleFrame_.action == - osf::TitleAction::open_character_select) { - gameState_.transition( - osf::GameState::character_select, - titleFrame_.character_select_argument); - } else if ( - titleFrame_.action == osf::TitleAction::exit_game) { - running = false; - } - break; - } - case osf::GameState::character_select: { - input_.characterSelect().saved_game_count = - resources_.savedGameCount(); - characterFrame_ = - characterSelectState_.update( - input_.characterSelect()); - audio_.playCharacterSelectFrame(characterFrame_); - if (characterFrame_.action == - osf::CharacterSelectAction::return_to_title) { - gameState_.transition(osf::GameState::title); - } else if ( - characterFrame_.action == - osf::CharacterSelectAction::enter_gameplay) { - const auto& selection = - characterSelectState_.data(); - gameplayPlayer_ = {}; - if (selection.mode == - osf::CharacterSelectMode::saved_game) { - gameplayPlayer_.source = - osf::PlayerDataSource::retail_save; - const auto& saved_games = - resources_.savedGames(); - if (selection.selected_saved_game >= 0 && - static_cast( - selection.selected_saved_game) < - saved_games.size()) { - gameplayPlayer_.save_path = - saved_games[ - static_cast( - selection.selected_saved_game)] - .save_path; - } - } else { - gameplayPlayer_.source = - osf::PlayerDataSource::new_character; - gameplayPlayer_.name = - selection.character_name; - gameplayPlayer_.gender = - selection.character_gender; - gameplayPlayer_.save_path = - osf::resolveRetailPath( - dataRoot_, - selection.next_save_path); - } - gameState_.transition(osf::GameState::gameplay); - } else if ( - characterFrame_.action == - osf::CharacterSelectAction::exit_game) { - running = false; - } - break; - } - case osf::GameState::gameplay: { - ++gameplayCounter_; - const bool scenario_visual_active = - world_.scenarioVisualActive(); - const bool notice_consumed = - !scenario_visual_active && - !gameplayUi_.options().active() && - osf::dismissPlayerLevelUpNoticeAtPointer( - input_.menu() - .pointer_primary_pressed, - input_.menu().pointer_x, - input_.menu().pointer_y, - resources_.pattern(1), - world_); - const bool ui_consumed = - !scenario_visual_active && - !notice_consumed && - gameplayUi_.update( - gameplayFrame_, - input_, - world_, - audio_, - gameConfig_, - configDirty_, - random_, - gameplayPlayer_, - savePreview_, - gameState_, - running, - shadowOpacity_); - if (world_.takeScenarioChanged()) { - completeScenarioChange(); - } else if (!ui_consumed) { - const bool map_active = - gameplayUi_.map().active(); - const bool inventory_active = - gameplayUi_.inventory().active(); - const bool special_items_active = - gameplayUi_.inventory() - .leftStorageActive(); - const bool magic_active = - gameplayUi_.magic().active(); - const bool status_active = - gameplayUi_.status().active(); - const bool transport_active = - gameplayUi_.transport().active(); - const bool vendor_active = - gameplayUi_.vendor().active(); - osf::GameplayFrameInput world_input; - world_input.confirm_pressed = - input_.menu().confirm_pressed && - !map_active; - world_input.pointer_primary_pressed = - input_.menu().pointer_primary_pressed && - !notice_consumed; - world_input.pointer_x = input_.menu().pointer_x; - world_input.pointer_y = input_.menu().pointer_y; - world_input.pointer_primary_down = - input_.pointerPrimaryDown() && - !notice_consumed; - world_input.run_toggle_pressed = - input_.runTogglePressed(); - world_input.increased_power_pressed = - input_.increasedPowerPressed(); - world_input.land_mine_pressed = - input_.landMinePressed(); - world_input.world_view_left = - map_active || magic_active || status_active || - special_items_active || transport_active || - vendor_active - ? 320 - : 0; - world_input.world_view_right = - inventory_active ? 320 : 640; - world_input.pointer_secondary_pressed = - input_.pointerSecondaryPressed(); - world_input.companion_toggle_pressed = - input_.companionTogglePressed(); - world_input.companion_hud_pressed = - world_.hasCompanion() && - osf::companionHudToggleAtPointer( - world_input.pointer_primary_pressed, - world_input.pointer_x, - world_input.pointer_y); - world_input.cancel_pressed = - input_.gameplayOptionsPressed(); - gameplayFrame_ = - gameplayState_.update(world_input); - if (world_.takeScenarioChanged()) { - completeScenarioChange(); - } - } - break; - } - default: - break; - } - - if (gameState_.currentState() == osf::GameState::gameplay && - gameplayFrame_.phase == osf::GameplayPhase::world && - world_.hasPlayer()) { - std::string artwork_error; - const bool artwork_ready = - osf::runtime::synchronizeGameplayArtwork( - resources_, - world_, - gameplayUi_, - &artwork_error); - if (!artwork_ready && !artworkSyncFailed_) { - std::fprintf( - stderr, - "Could not prepare gameplay artwork: %s\n", - artwork_error.c_str()); - } - artworkSyncFailed_ = !artwork_ready; - } else { - artworkSyncFailed_ = false; - } - - input_.clearTransientInput(); - } - - osf::GameStateDispatcherCallbacks makeGameStateCallbacks() { - osf::GameStateDispatcherCallbacks callbacks; - callbacks.title.enter = [this](std::int32_t) { - titleState_.enter(); - }; - callbacks.title.leave = [this] { - titleState_.leave(); - resources_.releaseTitleResources(); - }; - callbacks.character_select.enter = [this](std::int32_t argument) { - characterSelectState_.enter(argument); - }; - callbacks.character_select.leave = [this] { - characterSelectState_.leave(); - resources_.releaseCharacterSelectResources(); - }; - callbacks.gameplay.enter = [this](std::int32_t) { - gameplayFrame_ = {}; - gameplayCounter_ = 0; - gameplayUi_.reset(); - savePreview_.clear(); - gameplayState_.enter(); - }; - callbacks.gameplay.leave = [this] { - gameplayUi_.reset(); - gameplayState_.leave(); - }; - return callbacks; - } - - LwlWindow* window_ = nullptr; - bool windowingInitialized_ = false; - bool configDirty_ = false; - bool running_ = false; - bool smokeTest_ = false; - bool artworkSyncFailed_ = false; - int renderedFrames_ = 0; - double previousTime_ = 0.0; - double nextFrame_ = 0.0; - double gameAccumulator_ = 0.0; - std::int32_t shadowOpacity_ = 500; - std::uint32_t gameplayCounter_ = 0; -#if OSF_ENABLE_DEBUG_TOOLS - double fpsWindowStart_ = 0.0; - std::uint32_t fpsWindowFrames_ = 0; - std::int32_t framesPerSecond_ = 0; - osf::debug::FrameProfiler profiler_; -#endif - osf::GameConfig gameConfig_; - osf::PlayerLoadRequest gameplayPlayer_; - std::filesystem::path dataRoot_; - osf::ResourceManager resources_; - std::unique_ptr - surfacePresenter_; -#if OSF_ENABLE_DEBUG_TOOLS - bool displaySynchronizationEnabled_ = true; -#endif - osf::runtime::RuntimeRenderer renderer_; - osf::TitleFrameResult titleFrame_; - osf::CharacterSelectFrameResult characterFrame_; - osf::GameplayFrameResult gameplayFrame_; - osf::runtime::AudioSystem audio_; - osf::runtime::InputAdapter input_; - osf::RetailRandom random_; - osf::TitleState titleState_; - osf::CharacterSelectState characterSelectState_; - osf::WorldScene world_; - osf::RetailSavePreview savePreview_; - osf::runtime::GameplayUiController gameplayUi_; - osf::GameplayState gameplayState_; - osf::GameStateDispatcher gameState_; -}; - -} // namespace - -int osf::runtime::runGame( - const std::filesystem::path& data_root, - const GameConfig& game_config, - bool smoke_test) { - auto runtime = std::make_unique(data_root); - if (!runtime->initialize(game_config)) { - return 1; - } - runtime->start(smoke_test); - return runApplicationLoop(std::move(runtime)); -} +#include "game_runtime.hpp" + +#include "lwl.h" +#include "core/retail_random.hpp" +#include "resources/resource_manager.hpp" +#include "resources/retail_filesystem.hpp" +#include "runtime/application_loop.hpp" +#include "runtime/audio_system.hpp" +#include "runtime/gameplay_artwork.hpp" +#include "runtime/gameplay_ui_controller.hpp" +#include "runtime/input_adapter.hpp" +#include "runtime/presentation/surface_presenter.hpp" +#include "runtime/runtime_renderer.hpp" +#include "runtime/state_bindings.hpp" +#include "states/game_state.hpp" +#include "states/gameplay_state.hpp" +#include "states/character_select_state.hpp" +#include "states/title_state.hpp" +#include "ui/companion_hud_input.hpp" +#include "ui/player_level_up_notice_input.hpp" +#include "world/retail_save_preview.hpp" +#include "world/world_scene.hpp" +#if OSF_ENABLE_DEBUG_TOOLS +#include "debug/frame_profiler.hpp" +#endif + +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int kVirtualWidth = 640; +constexpr int kVirtualHeight = 480; + +class Runtime final : public osf::runtime::FrameApplication { +public: + explicit Runtime(std::filesystem::path dataRoot) + : dataRoot_(std::move(dataRoot)), + resources_(dataRoot_), + surfacePresenter_( + osf::runtime::createSurfacePresenter()), + renderer_(kVirtualWidth, kVirtualHeight), + input_(kVirtualWidth, kVirtualHeight), + titleState_( + random_, + osf::runtime::makeTitleStateHooks( + dataRoot_, resources_, audio_)), + characterSelectState_( + osf::runtime::makeCharacterSelectStateHooks( + dataRoot_, + resources_, + audio_, + window_)), + gameplayState_( + osf::runtime::makeGameplayStateHooks( + dataRoot_, + gameplayPlayer_, + resources_, + audio_, + world_)), + gameState_(makeGameStateCallbacks()) {} + + ~Runtime() { + saveConfigIfDirty(); + surfacePresenter_.reset(); + lwl_window_destroy(window_); + if (windowingInitialized_) { + lwl_shutdown(); + } + } + + bool initialize(const osf::GameConfig& gameConfig) { + gameConfig_ = gameConfig; + if (!lwl_init()) { + std::fprintf(stderr, "Could not initialize LWL.\n"); + return false; + } + windowingInitialized_ = true; + + window_ = lwl_window_create( + "OpenShadowFlare", kVirtualWidth, kVirtualHeight); + if (!window_) { + std::fprintf(stderr, "Could not create the game window.\n"); + return false; + } + lwl_window_set_mode(window_, LWL_WINDOW_NORMAL); + lwl_window_show(window_); + + std::string presenterError; + if (!surfacePresenter_ || + !surfacePresenter_->initialize( + window_, &presenterError)) { + std::fprintf( + stderr, + "Could not initialize surface presentation: %s\n", + presenterError.c_str()); + return false; + } + std::string audioError; + if (!audio_.initialize( + dataRoot_, + gameConfig_.effect_volume, + gameConfig_.bgm_volume, + &audioError)) { + std::fprintf( + stderr, + "Warning: audio is unavailable: %s\n", + audioError.c_str()); + } + shadowOpacity_ = + gameConfig_.semi_transparent_shadow ? 500 : 1000; + world_.configurePointer({ + gameConfig_.click_range, + gameConfig_.click_range_enabled, + gameConfig_.click_priority, + }); + + if (!resources_.loadCommonPattern( + 3, + "System\\Common\\Pattern\\System.njp")) { + return false; + } + lwl_window_set_cursor_visible(window_, false); + gameState_.transition(osf::GameState::title); + return true; + } + + void start(bool smokeTest) { + smokeTest_ = smokeTest; + running_ = true; + renderedFrames_ = 0; + previousTime_ = lwl_time_seconds(); + nextFrame_ = previousTime_; +#if OSF_ENABLE_DEBUG_TOOLS + fpsWindowStart_ = previousTime_; + fpsWindowFrames_ = 0; + framesPerSecond_ = 0; + profiler_.setEnabled(false, previousTime_); +#endif + gameAccumulator_ = kGameStep; + } + + bool frame() override { + tickOnce(); + if (!running_) { + saveConfigIfDirty(); + } + return running_; + } + +private: + static constexpr double kRenderStep = 1.0 / 60.0; + static constexpr double kGameStep = 1.0 / 30.0; + static constexpr double kMaximumElapsed = 0.25; + + void tickOnce() { + const double currentTime = lwl_time_seconds(); + gameAccumulator_ += std::clamp( + currentTime - previousTime_, + 0.0, + kMaximumElapsed); + previousTime_ = currentTime; + + LwlEvent event{}; + while (lwl_poll_event(window_, &event)) { + if (!input_.handleEvent( + window_, + event, + gameState_.currentState())) { + running_ = false; + } + } + + while (running_ && gameAccumulator_ >= kGameStep) { + updateGame(running_); + gameAccumulator_ -= kGameStep; + } + + const double interpolation = + std::clamp( + gameAccumulator_ / kGameStep, + 0.0, + 1.0); +#if OSF_ENABLE_DEBUG_TOOLS + const bool profilingEnabled = + gameplayUi_.debug().profilingEnabled(); + profiler_.setEnabled(profilingEnabled, currentTime); + const bool synchronizeDisplay = !profilingEnabled; + if (synchronizeDisplay != displaySynchronizationEnabled_) { + surfacePresenter_->setDisplaySynchronization( + synchronizeDisplay); + displaySynchronizationEnabled_ = synchronizeDisplay; + } + if (profiler_.memorySampleDue(currentTime)) { + const std::uint64_t audioMemory = + audio_.memoryUsageBytes(); + const std::uint64_t gameMemory = + resources_.memoryUsageBytes() + + world_.resourceMemoryUsageBytes() + + renderer_.memoryUsageBytes(); + profiler_.recordMemoryUsage( + currentTime, + gameMemory, + audioMemory, + surfacePresenter_->videoMemoryUsageBytes()); + } + const double framebufferFillStart = lwl_time_seconds(); +#endif + const osf::gapi::SurfaceView surface = renderer_.render( + { + gameState_.currentState(), + titleFrame_, + characterFrame_, + gameplayFrame_, + characterSelectState_, + world_, + resources_, + savePreview_, + gameplayUi_.options(), + gameplayUi_.blackjack(), +#if OSF_ENABLE_DEBUG_TOOLS + gameplayUi_.debug(), +#endif + gameplayUi_.equipmentColor(), + gameplayUi_.inventory(), + gameplayUi_.map(), + gameplayUi_.magic(), + gameplayUi_.status(), + gameplayUi_.missionList(), + gameplayUi_.transport(), + gameplayUi_.vendor(), + gameConfig_, + shadowOpacity_, + gameplayCounter_, +#if OSF_ENABLE_DEBUG_TOOLS + framesPerSecond_, + profiler_.metrics(), +#endif + input_.menu().pointer_x, + input_.menu().pointer_y, + }, + interpolation); +#if OSF_ENABLE_DEBUG_TOOLS + profiler_.recordFramebufferFill( + lwl_time_seconds() - framebufferFillStart); + const double presentStart = lwl_time_seconds(); +#endif + surfacePresenter_->prepareFrame(surface); +#if OSF_ENABLE_DEBUG_TOOLS + profiler_.recordPresent( + lwl_time_seconds() - presentStart); +#endif + surfacePresenter_->displayFrame(); + if (gameState_.currentState() == + osf::GameState::gameplay && + gameplayFrame_.phase == osf::GameplayPhase::world) { + world_.advanceScenarioVisualFrame(); + } + + ++renderedFrames_; +#if OSF_ENABLE_DEBUG_TOOLS + ++fpsWindowFrames_; + const double fps_elapsed = + currentTime - fpsWindowStart_; + if (fps_elapsed >= 0.5) { + framesPerSecond_ = static_cast( + static_cast(fpsWindowFrames_) / + fps_elapsed + + 0.5); + fpsWindowStart_ = currentTime; + fpsWindowFrames_ = 0; + } +#endif + if (smokeTest_ && renderedFrames_ >= 3) { + running_ = false; + } + + nextFrame_ += kRenderStep; + if (nextFrame_ < currentTime - kMaximumElapsed) { + nextFrame_ = currentTime; + } + lwl_sleep_until_seconds(nextFrame_); + } + + void saveConfigIfDirty() { + if (configDirty_) { + osf::saveGameConfigFile( + (dataRoot_ / "SFlare.Cfg").string(), + gameConfig_); + configDirty_ = false; + } + } + + void completeScenarioChange() { + gameplayUi_.reset(); + world_.setCameraAnchor(320, 240); + audio_.startWorldMusic(world_.musicTrack()); + } + + void updateGame(bool& running) { + switch (gameState_.currentState()) { + case osf::GameState::title: { + for (std::size_t index = 0; + index < 10; + ++index) { + const auto* animation = + resources_.titleAnimation(index); + const auto& charts = animation->charts(); + input_.menu().smoke_frame_counts[index] = + charts.empty() + ? 0 + : charts.front().directions[8].frame_count; + } + titleFrame_ = + titleState_.update(input_.menu()); + audio_.playTitleFrame(titleFrame_); + if (titleFrame_.action == + osf::TitleAction::open_character_select) { + gameState_.transition( + osf::GameState::character_select, + titleFrame_.character_select_argument); + } else if ( + titleFrame_.action == osf::TitleAction::exit_game) { + running = false; + } + break; + } + case osf::GameState::character_select: { + input_.characterSelect().saved_game_count = + resources_.savedGameCount(); + characterFrame_ = + characterSelectState_.update( + input_.characterSelect()); + audio_.playCharacterSelectFrame(characterFrame_); + if (characterFrame_.action == + osf::CharacterSelectAction::return_to_title) { + gameState_.transition(osf::GameState::title); + } else if ( + characterFrame_.action == + osf::CharacterSelectAction::enter_gameplay) { + const auto& selection = + characterSelectState_.data(); + gameplayPlayer_ = {}; + if (selection.mode == + osf::CharacterSelectMode::saved_game) { + gameplayPlayer_.source = + osf::PlayerDataSource::retail_save; + const auto& saved_games = + resources_.savedGames(); + if (selection.selected_saved_game >= 0 && + static_cast( + selection.selected_saved_game) < + saved_games.size()) { + gameplayPlayer_.save_path = + saved_games[ + static_cast( + selection.selected_saved_game)] + .save_path; + } + } else { + gameplayPlayer_.source = + osf::PlayerDataSource::new_character; + gameplayPlayer_.name = + selection.character_name; + gameplayPlayer_.gender = + selection.character_gender; + gameplayPlayer_.save_path = + osf::resolveRetailPath( + dataRoot_, + selection.next_save_path); + } + gameState_.transition(osf::GameState::gameplay); + } else if ( + characterFrame_.action == + osf::CharacterSelectAction::exit_game) { + running = false; + } + break; + } + case osf::GameState::gameplay: { + ++gameplayCounter_; + const bool scenario_visual_active = + world_.scenarioVisualActive(); + const bool notice_consumed = + !scenario_visual_active && + !gameplayUi_.options().active() && + osf::dismissPlayerLevelUpNoticeAtPointer( + input_.menu() + .pointer_primary_pressed, + input_.menu().pointer_x, + input_.menu().pointer_y, + resources_.pattern(1), + world_); + const bool ui_consumed = + !scenario_visual_active && + !notice_consumed && + gameplayUi_.update( + gameplayFrame_, + input_, + world_, + audio_, + gameConfig_, + configDirty_, + random_, + gameplayPlayer_, + savePreview_, + gameState_, + running, + shadowOpacity_); + if (world_.takeScenarioChanged()) { + completeScenarioChange(); + } else if (!ui_consumed) { + const bool map_active = + gameplayUi_.map().active(); + const bool inventory_active = + gameplayUi_.inventory().active(); + const bool special_items_active = + gameplayUi_.inventory() + .leftStorageActive(); + const bool magic_active = + gameplayUi_.magic().active(); + const bool status_active = + gameplayUi_.status().active(); + const bool transport_active = + gameplayUi_.transport().active(); + const bool vendor_active = + gameplayUi_.vendor().active(); + osf::GameplayFrameInput world_input; + world_input.confirm_pressed = + input_.menu().confirm_pressed && + !map_active; + world_input.pointer_primary_pressed = + input_.menu().pointer_primary_pressed && + !notice_consumed; + world_input.pointer_x = input_.menu().pointer_x; + world_input.pointer_y = input_.menu().pointer_y; + world_input.pointer_primary_down = + input_.pointerPrimaryDown() && + !notice_consumed; + world_input.run_toggle_pressed = + input_.runTogglePressed(); + world_input.increased_power_pressed = + input_.increasedPowerPressed(); + world_input.land_mine_pressed = + input_.landMinePressed(); + world_input.world_view_left = + map_active || magic_active || status_active || + special_items_active || transport_active || + vendor_active + ? 320 + : 0; + world_input.world_view_right = + inventory_active ? 320 : 640; + world_input.pointer_secondary_pressed = + input_.pointerSecondaryPressed(); + world_input.companion_toggle_pressed = + input_.companionTogglePressed(); + world_input.companion_hud_pressed = + world_.hasCompanion() && + osf::companionHudToggleAtPointer( + world_input.pointer_primary_pressed, + world_input.pointer_x, + world_input.pointer_y); + world_input.cancel_pressed = + input_.gameplayOptionsPressed(); + gameplayFrame_ = + gameplayState_.update(world_input); + if (world_.takeScenarioChanged()) { + completeScenarioChange(); + } + } + break; + } + default: + break; + } + + if (gameState_.currentState() == osf::GameState::gameplay && + gameplayFrame_.phase == osf::GameplayPhase::world && + world_.hasPlayer()) { + std::string artwork_error; + const bool artwork_ready = + osf::runtime::synchronizeGameplayArtwork( + resources_, + world_, + gameplayUi_, + &artwork_error); + if (!artwork_ready && !artworkSyncFailed_) { + std::fprintf( + stderr, + "Could not prepare gameplay artwork: %s\n", + artwork_error.c_str()); + } + artworkSyncFailed_ = !artwork_ready; + } else { + artworkSyncFailed_ = false; + } + + input_.clearTransientInput(); + } + + osf::GameStateDispatcherCallbacks makeGameStateCallbacks() { + osf::GameStateDispatcherCallbacks callbacks; + callbacks.title.enter = [this](std::int32_t) { + titleState_.enter(); + }; + callbacks.title.leave = [this] { + titleState_.leave(); + resources_.releaseTitleResources(); + }; + callbacks.character_select.enter = [this](std::int32_t argument) { + characterSelectState_.enter(argument); + }; + callbacks.character_select.leave = [this] { + characterSelectState_.leave(); + resources_.releaseCharacterSelectResources(); + }; + callbacks.gameplay.enter = [this](std::int32_t) { + gameplayFrame_ = {}; + gameplayCounter_ = 0; + gameplayUi_.reset(); + savePreview_.clear(); + gameplayState_.enter(); + }; + callbacks.gameplay.leave = [this] { + gameplayUi_.reset(); + gameplayState_.leave(); + }; + return callbacks; + } + + LwlWindow* window_ = nullptr; + bool windowingInitialized_ = false; + bool configDirty_ = false; + bool running_ = false; + bool smokeTest_ = false; + bool artworkSyncFailed_ = false; + int renderedFrames_ = 0; + double previousTime_ = 0.0; + double nextFrame_ = 0.0; + double gameAccumulator_ = 0.0; + std::int32_t shadowOpacity_ = 500; + std::uint32_t gameplayCounter_ = 0; +#if OSF_ENABLE_DEBUG_TOOLS + double fpsWindowStart_ = 0.0; + std::uint32_t fpsWindowFrames_ = 0; + std::int32_t framesPerSecond_ = 0; + osf::debug::FrameProfiler profiler_; +#endif + osf::GameConfig gameConfig_; + osf::PlayerLoadRequest gameplayPlayer_; + std::filesystem::path dataRoot_; + osf::ResourceManager resources_; + std::unique_ptr + surfacePresenter_; +#if OSF_ENABLE_DEBUG_TOOLS + bool displaySynchronizationEnabled_ = true; +#endif + osf::runtime::RuntimeRenderer renderer_; + osf::TitleFrameResult titleFrame_; + osf::CharacterSelectFrameResult characterFrame_; + osf::GameplayFrameResult gameplayFrame_; + osf::runtime::AudioSystem audio_; + osf::runtime::InputAdapter input_; + osf::RetailRandom random_; + osf::TitleState titleState_; + osf::CharacterSelectState characterSelectState_; + osf::WorldScene world_; + osf::RetailSavePreview savePreview_; + osf::runtime::GameplayUiController gameplayUi_; + osf::GameplayState gameplayState_; + osf::GameStateDispatcher gameState_; +}; + +} // namespace + +int osf::runtime::runGame( + const std::filesystem::path& data_root, + const GameConfig& game_config, + bool smoke_test) { + auto runtime = std::make_unique(data_root); + if (!runtime->initialize(game_config)) { + return 1; + } + runtime->start(smoke_test); + return runApplicationLoop(std::move(runtime)); +} diff --git a/src/SF_EXE/runtime/main.cpp b/src/SF_EXE/runtime/main.cpp index 947875c5..f2e8aa2f 100644 --- a/src/SF_EXE/runtime/main.cpp +++ b/src/SF_EXE/runtime/main.cpp @@ -1,105 +1,131 @@ -#include "lwl.h" -#include "core/command_line.hpp" -#include "core/game_config.hpp" -#include "runtime/game_runtime.hpp" - -#include -#include - -namespace { - -bool isSmokeTest(int argc, char** argv) { - for (int index = 1; index < argc; ++index) { - if (std::strcmp(argv[index], "--smoke-test") == 0) { - return true; - } - } - return false; -} - -std::filesystem::path findDataRoot() { - const auto isDataRoot = - [](const std::filesystem::path& candidate) { - std::error_code error; - const bool hasConfig = std::filesystem::is_regular_file( - candidate / "SFlare.Cfg", error); - error.clear(); - const bool hasTitle = std::filesystem::is_regular_file( - candidate / "System" / "Title" / "Pattern" / - "Title.njp", - error); - return hasConfig && hasTitle; - }; - const auto searchParents = - [&isDataRoot](std::filesystem::path directory) { - for (;;) { - const std::filesystem::path candidates[] = { - directory, - directory / "ShadowFlare", - directory / "tmp" / "ShadowFlare", - }; - for (const std::filesystem::path& candidate : - candidates) { - if (isDataRoot(candidate)) { - return candidate; - } - } - - const std::filesystem::path parent = - directory.parent_path(); - if (parent.empty() || parent == directory) { - break; - } - directory = parent; - } - return std::filesystem::path{}; - }; - - std::error_code error; - const std::filesystem::path fromWorkingDirectory = - searchParents(std::filesystem::current_path(error)); - if (!fromWorkingDirectory.empty()) { - return fromWorkingDirectory; - } - - char executablePath[4096]{}; - if (lwl_exe_path( - executablePath, - static_cast(sizeof(executablePath)))) { - const std::filesystem::path fromExecutable = - searchParents( - std::filesystem::absolute( - executablePath, error).parent_path()); - if (!fromExecutable.empty()) { - return fromExecutable; - } - } - - const std::filesystem::path fallbacks[] = { - ".", - std::filesystem::path("tmp") / "ShadowFlare", - }; - for (const std::filesystem::path& candidate : fallbacks) { - if (isDataRoot(candidate)) { - return candidate; - } - } - return "."; -} - -} // namespace - -int main(int argc, char** argv) { - const std::filesystem::path dataRoot = findDataRoot(); - osf::GameConfig gameConfig; - - // Retail ignores config-load failure and retains its constructor defaults. - osf::loadGameConfigFile( - (dataRoot / "SFlare.Cfg").string(), gameConfig); - for (int index = 1; index < argc; ++index) { - osf::applyRetailCommandLine(argv[index], gameConfig); - } - - return osf::runtime::runGame( - dataRoot, gameConfig, isSmokeTest(argc, argv)); -} +#include "lwl.h" +#include "core/command_line.hpp" +#include "core/game_config.hpp" +#include "runtime/game_runtime.hpp" + +#if defined(__PS2__) +#include "runtime/platform/ps2/ps2_data_backend.hpp" +#endif + +#include +#include +#include + +namespace { + +bool isSmokeTest(int argc, char** argv) { + for (int index = 1; index < argc; ++index) { + if (std::strcmp(argv[index], "--smoke-test") == 0) { + return true; + } + } + return false; +} + +std::filesystem::path findDataRoot() { +#if defined(__PS2__) + return std::filesystem::path( + osf::runtime::platform::ps2::dataRoot()); +#else + const auto isDataRoot = + [](const std::filesystem::path& candidate) { + std::error_code error; + const bool hasConfig = std::filesystem::is_regular_file( + candidate / "SFlare.Cfg", error); + error.clear(); + const bool hasTitle = std::filesystem::is_regular_file( + candidate / "System" / "Title" / "Pattern" / + "Title.njp", + error); + return hasConfig && hasTitle; + }; + const auto searchParents = + [&isDataRoot](std::filesystem::path directory) { + for (;;) { + const std::filesystem::path candidates[] = { + directory, + directory / "ShadowFlare", + directory / "tmp" / "ShadowFlare", + }; + for (const std::filesystem::path& candidate : + candidates) { + if (isDataRoot(candidate)) { + return candidate; + } + } + + const std::filesystem::path parent = + directory.parent_path(); + if (parent.empty() || parent == directory) { + break; + } + directory = parent; + } + return std::filesystem::path{}; + }; + + std::error_code error; + const std::filesystem::path fromWorkingDirectory = + searchParents(std::filesystem::current_path(error)); + if (!fromWorkingDirectory.empty()) { + return fromWorkingDirectory; + } + + char executablePath[4096]{}; + if (lwl_exe_path( + executablePath, + static_cast(sizeof(executablePath)))) { + const std::filesystem::path fromExecutable = + searchParents( + std::filesystem::absolute( + executablePath, error).parent_path()); + if (!fromExecutable.empty()) { + return fromExecutable; + } + } + + const std::filesystem::path fallbacks[] = { + ".", + std::filesystem::path("tmp") / "ShadowFlare", + }; + for (const std::filesystem::path& candidate : fallbacks) { + if (isDataRoot(candidate)) { + return candidate; + } + } + return "."; +#endif +} + +} // namespace + +int main(int argc, char** argv) { +#if defined(__PS2__) + if (osf::runtime::platform::ps2::initDataBackend() != 0) { + std::fprintf(stderr, "Failed to initialize PS2 data backend.\n"); + return 1; + } +#endif + const std::filesystem::path dataRoot = findDataRoot(); + osf::GameConfig gameConfig; + + // Retail ignores config-load failure and retains its constructor defaults. + const bool config_loaded = osf::loadGameConfigFile( + (dataRoot / "SFlare.Cfg").string(), gameConfig); +#if defined(__PS2__) + if (!config_loaded) { + std::fprintf( + stderr, + "Warning: could not load %s; using defaults.\n", + (dataRoot / "SFlare.Cfg").string().c_str()); + } +#else + (void) config_loaded; +#endif + for (int index = 1; index < argc; ++index) { + osf::applyRetailCommandLine(argv[index], gameConfig); + } + + return osf::runtime::runGame( + dataRoot, gameConfig, isSmokeTest(argc, argv)); +} diff --git a/src/SF_EXE/runtime/platform/ps2/application_loop.cpp b/src/SF_EXE/runtime/platform/ps2/application_loop.cpp new file mode 100644 index 00000000..0e158012 --- /dev/null +++ b/src/SF_EXE/runtime/platform/ps2/application_loop.cpp @@ -0,0 +1,11 @@ +#include "runtime/application_loop.hpp" + +namespace osf::runtime { + +int runApplicationLoop(std::unique_ptr application) { + while (application->frame()) { + } + return 0; +} + +} // namespace osf::runtime diff --git a/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.cpp b/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.cpp new file mode 100644 index 00000000..458df91a --- /dev/null +++ b/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.cpp @@ -0,0 +1,765 @@ +#define _GNU_SOURCE +#define NEWLIB_PORT_AWARE + +#include "ps2_data_backend.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace osf { +namespace runtime { +namespace platform { +namespace ps2 { +namespace { + +constexpr char kArchiveName[] = "cdrom0:\\SFGAME.BIN"; +constexpr char kArchiveDataRoot[] = "cdrom0:\\ShadowFlare"; +constexpr char kHostDataRoot[] = "host0:ShadowFlare"; +constexpr char kHostConfigPath[] = "host0:ShadowFlare/SFlare.Cfg"; +constexpr char kLegacyHostDataRoot[] = "host:ShadowFlare"; +constexpr char kLegacyHostConfigPath[] = "host:ShadowFlare/SFlare.Cfg"; +constexpr char kMassDataRoot[] = "mass:/ShadowFlare"; +constexpr char kMassConfigPath[] = "mass:/ShadowFlare/SFlare.Cfg"; +constexpr char kUsbDriverPath[] = "cdrom0:\\USBD.IRX;1"; +constexpr char kUsbMassDriverPath[] = "cdrom0:\\USBHDFSD.IRX;1"; +constexpr char kDataRootComponent[] = "ShadowFlare"; +constexpr std::uint32_t kMagic = 0x53464231u; +constexpr std::uint32_t kVersion = 1u; +constexpr std::uint32_t kHeaderSize = 16u; +constexpr std::uint32_t kEntrySize = 16u; + +constexpr int kReadOnlyMode = S_IRUSR | S_IRGRP | S_IROTH; + +struct DataEntry { + const char* name; + std::uint32_t name_size; + std::uint32_t data_offset; + std::uint32_t data_size; +}; + +struct FileHandle { + int archive_fd; + std::uint32_t base; + std::uint32_t size; + std::uint32_t position; + char* filename; +}; + +struct DirHandle { + char* children; + std::uint32_t* child_offsets; + std::uint32_t count; + std::uint32_t position; + char* relative; + char* filename; +}; + +bool s_initialized = false; +const char* s_data_root = kArchiveDataRoot; +DataEntry* s_entries = nullptr; +std::uint32_t s_entry_count = 0; +_libcglue_fdman_path_ops_t* s_default_path_ops = nullptr; +_libcglue_fdman_fd_ops_t s_file_ops{}; +_libcglue_fdman_fd_ops_t s_dir_ops{}; +_libcglue_fdman_path_ops_t s_path_ops{}; + +char asciiLower(char value) { + return value >= 'A' && value <= 'Z' + ? static_cast(value - 'A' + 'a') + : value; +} + +bool nameEquals( + const char* first, + std::size_t first_size, + const char* second, + std::size_t second_size) { + if (first_size != second_size) { + return false; + } + for (std::size_t index = 0; index < first_size; ++index) { + if (asciiLower(first[index]) != asciiLower(second[index])) { + return false; + } + } + return true; +} + +bool nameStartsWith(const char* name, const char* prefix, std::size_t prefix_size) { + return std::strncmp(name, prefix, prefix_size) == 0; +} + +int normalizeRelative(const char* absolute, char* out, std::size_t out_size) { + const char* cursor = absolute; + + const char* colon = std::strchr(cursor, ':'); + if (colon) { + if (std::strncmp(cursor, "cdrom", 5) != 0) { + return -1; + } + cursor = colon + 1; + } + + while (*cursor == '/' || *cursor == '\\') { + ++cursor; + } + + std::size_t component = 0; + while (cursor[component] && cursor[component] != '/' && + cursor[component] != '\\') { + ++component; + } + if (component != 0) { + if (component != std::strlen(kDataRootComponent) || + !nameEquals( + cursor, component, kDataRootComponent, + std::strlen(kDataRootComponent))) { + return -1; + } + cursor += component; + if (*cursor == '/' || *cursor == '\\') { + ++cursor; + } + } + + std::size_t out_length = 0; + bool pending_separator = false; + for (; *cursor && out_length + 1 < out_size; ++cursor) { + if (*cursor == '/' || *cursor == '\\') { + if (out_length != 0) { + pending_separator = true; + } + continue; + } + if (pending_separator) { + out[out_length++] = '/'; + pending_separator = false; + } + out[out_length++] = *cursor; + } + out[out_length] = '\0'; + return static_cast(out_length); +} + +const DataEntry* findEntry(const char* relative) { + const std::size_t length = std::strlen(relative); + for (std::uint32_t index = 0; index < s_entry_count; ++index) { + const DataEntry& entry = s_entries[index]; + if (nameEquals(entry.name, entry.name_size, relative, length)) { + return &entry; + } + } + return nullptr; +} + +bool isDirectoryPath(const char* relative, std::size_t length) { + if (length == 0) { + return true; + } + char prefix[512]; + if (length >= sizeof(prefix)) { + return false; + } + std::memcpy(prefix, relative, length); + prefix[length] = '/'; + prefix[length + 1] = '\0'; + for (std::uint32_t index = 0; index < s_entry_count; ++index) { + const DataEntry& entry = s_entries[index]; + if (entry.name_size > length && + nameStartsWith(entry.name, prefix, length + 1)) { + return true; + } + } + return false; +} + +bool isChildDirectory(const char* directory, const char* child) { + char prefix[512]; + const std::size_t directory_size = std::strlen(directory); + const std::size_t child_size = std::strlen(child); + if (directory_size + child_size + 2 >= sizeof(prefix)) { + return false; + } + std::size_t length = 0; + if (directory_size != 0) { + std::memcpy(prefix, directory, directory_size); + length = directory_size; + } + prefix[length++] = '/'; + std::memcpy(prefix + length, child, child_size); + length += child_size; + prefix[length] = '\0'; + + for (std::uint32_t index = 0; index < s_entry_count; ++index) { + const DataEntry& entry = s_entries[index]; + if (entry.name_size > length && + nameStartsWith(entry.name, prefix, length)) { + return true; + } + } + return false; +} + +char* duplicateString(const char* text) { + const std::size_t length = std::strlen(text); + char* copy = static_cast(std::malloc(length + 1)); + if (copy) { + std::memcpy(copy, text, length + 1); + } + return copy; +} + +int openDirectory(_libcglue_fdman_fd_info_t* info, const char* filename, const char* relative) { + DirHandle* handle = static_cast(std::calloc(1, sizeof(DirHandle))); + if (!handle) { + return -ENOMEM; + } + + const std::size_t base_size = std::strlen(relative); + char directory[512]; + std::memcpy(directory, relative, base_size); + if (base_size != 0 && base_size < sizeof(directory)) { + directory[base_size] = '/'; + directory[base_size + 1] = '\0'; + } else if (base_size >= sizeof(directory)) { + std::free(handle); + return -ENAMETOOLONG; + } else { + directory[0] = '\0'; + } + const std::size_t directory_prefix_size = base_size == 0 ? 0 : base_size + 1; + + handle->filename = duplicateString(filename); + handle->relative = duplicateString(relative); + if (!handle->filename || !handle->relative) { + std::free(handle->filename); + std::free(handle->relative); + std::free(handle); + return -ENOMEM; + } + handle->children = static_cast(std::malloc(32768)); + handle->child_offsets = static_cast(std::malloc(4096 * sizeof(std::uint32_t))); + if (!handle->children || !handle->child_offsets) { + std::free(handle->children); + std::free(handle->child_offsets); + std::free(handle->filename); + std::free(handle->relative); + std::free(handle); + return -ENOMEM; + } + + std::size_t name_cursor = 0; + for (std::uint32_t index = 0; index < s_entry_count; ++index) { + const DataEntry& entry = s_entries[index]; + if (entry.name_size <= directory_prefix_size || + !nameStartsWith(entry.name, directory, directory_prefix_size)) { + continue; + } + const char* child = entry.name + directory_prefix_size; + const std::size_t child_name_size = entry.name_size - directory_prefix_size; + const char* child_separator = static_cast( + std::memchr(child, '/', child_name_size)); + const std::size_t child_size = child_separator + ? static_cast(child_separator - child) + : child_name_size; + if (child_size == 0) { + continue; + } + + bool duplicate = false; + for (std::uint32_t seen = 0; seen < handle->count; ++seen) { + const char* existing = handle->children + handle->child_offsets[seen]; + if (std::memcmp(existing, child, child_size) == 0 && + existing[child_size] == '\0') { + duplicate = true; + break; + } + } + if (duplicate) { + continue; + } + + if (handle->count >= 4096 || name_cursor + child_size + 1 > 32768) { + continue; + } + handle->child_offsets[handle->count++] = static_cast(name_cursor); + std::memcpy(handle->children + name_cursor, child, child_size); + handle->children[name_cursor + child_size] = '\0'; + name_cursor += child_size + 1; + } + + info->userdata = handle; + info->ops = &s_dir_ops; + return 0; +} + +int openFile( + _libcglue_fdman_fd_info_t* info, + const char* filename, + const char* relative, + const DataEntry* entry) { + (void) relative; + FileHandle* handle = static_cast(std::calloc(1, sizeof(FileHandle))); + if (!handle) { + return -ENOMEM; + } + handle->filename = duplicateString(filename); + if (!handle->filename) { + std::free(handle); + return -ENOMEM; + } + handle->archive_fd = fioOpen(kArchiveName, FIO_O_RDONLY); + if (handle->archive_fd < 0) { + const int error = handle->archive_fd; + std::free(handle->filename); + std::free(handle); + return error < 0 ? error : -EIO; + } + handle->base = entry->data_offset; + handle->size = entry->data_size; + handle->position = 0; + + info->userdata = handle; + info->ops = &s_file_ops; + return 0; +} + +int pathOpen(_libcglue_fdman_fd_info_t* info, const char* buf, int flags, mode_t mode) { + char relative[512]; + const int relative_length = normalizeRelative(buf, relative, sizeof(relative)); + if (relative_length < 0) { + return s_default_path_ops->open(info, buf, flags, mode); + } + + if ((flags & (O_WRONLY | O_RDWR)) != 0) { + return -EROFS; + } + + if ((flags & O_DIRECTORY) != 0 || relative_length == 0) { + return openDirectory(info, buf, relative); + } + + const DataEntry* entry = findEntry(relative); + if (!entry) { + if (isDirectoryPath(relative, static_cast(relative_length))) { + return openDirectory(info, buf, relative); + } + return -ENOENT; + } + return openFile(info, buf, relative, entry); +} + +int pathStat(const char* path, struct stat* buf) { + char relative[512]; + const int relative_length = normalizeRelative(path, relative, sizeof(relative)); + if (relative_length < 0) { + return s_default_path_ops->stat(path, buf); + } + + std::memset(buf, 0, sizeof(*buf)); + const std::size_t length = static_cast(relative_length); + const DataEntry* entry = findEntry(relative); + if (entry) { + buf->st_mode = S_IFREG | kReadOnlyMode; + buf->st_size = entry->data_size; + return 0; + } + if (isDirectoryPath(relative, length)) { + buf->st_mode = S_IFDIR | S_IRWXU; + return 0; + } + return -ENOENT; +} + +int pathRemove(const char* path) { + char relative[512]; + if (normalizeRelative(path, relative, sizeof(relative)) < 0) { + return s_default_path_ops->remove(path); + } + return -EROFS; +} + +int pathRename(const char* old_path, const char* new_path) { + char relative[512]; + if (normalizeRelative(old_path, relative, sizeof(relative)) < 0) { + return s_default_path_ops->rename(old_path, new_path); + } + return -EROFS; +} + +int pathMkdir(const char* path, int mode) { + char relative[512]; + if (normalizeRelative(path, relative, sizeof(relative)) < 0) { + return s_default_path_ops->mkdir(path, mode); + } + return -EROFS; +} + +int pathRmdir(const char* path) { + char relative[512]; + if (normalizeRelative(path, relative, sizeof(relative)) < 0) { + return s_default_path_ops->rmdir(path); + } + return -EROFS; +} + +int pathReadlink(const char* path, char* buf, std::size_t bufsiz) { + char relative[512]; + if (normalizeRelative(path, relative, sizeof(relative)) < 0) { + return s_default_path_ops->readlink(path, buf, bufsiz); + } + return -EROFS; +} + +int pathSymlink(const char* target, const char* linkpath) { + char relative[512]; + if (normalizeRelative(target, relative, sizeof(relative)) < 0) { + return s_default_path_ops->symlink(target, linkpath); + } + return -EROFS; +} + +int fileClose(void* userdata) { + FileHandle* handle = static_cast(userdata); + if (!handle) { + return -EBADF; + } + int result = 0; + if (handle->archive_fd >= 0) { + result = fioClose(handle->archive_fd); + } + std::free(handle->filename); + std::free(handle); + return result; +} + +int fileRead(void* userdata, void* buf, int nbytes) { + FileHandle* handle = static_cast(userdata); + if (!handle) { + return -EBADF; + } + if (handle->position >= handle->size) { + return 0; + } + int want = nbytes; + const std::uint32_t remaining = handle->size - handle->position; + if (want < 0 || static_cast(want) > remaining) { + want = static_cast(remaining); + } + if (fioLseek(handle->archive_fd, static_cast(handle->base + handle->position), 0) < 0) { + return -EIO; + } + const int read = fioRead(handle->archive_fd, buf, want); + if (read > 0) { + handle->position += static_cast(read); + } + return read; +} + +int fileWrite(void* userdata, const void* buf, int nbytes) { + (void)userdata; + (void)buf; + (void)nbytes; + return -EROFS; +} + +int fileLseek(void* userdata, int offset, int whence) { + FileHandle* handle = static_cast(userdata); + if (!handle) { + return -EBADF; + } + long long position = 0; + switch (whence) { + case SEEK_SET: + position = offset; + break; + case SEEK_CUR: + position = static_cast(handle->position) + offset; + break; + case SEEK_END: + position = static_cast(handle->size) + offset; + break; + default: + return -EINVAL; + } + if (position < 0 || position > static_cast(handle->size)) { + return -EINVAL; + } + handle->position = static_cast(position); + return static_cast(handle->position); +} + +int64_t fileLseek64(void* userdata, int64_t offset, int whence) { + return fileLseek(userdata, static_cast(offset), whence); +} + +int fileGetfd(void* userdata) { + FileHandle* handle = static_cast(userdata); + return handle ? handle->archive_fd : -EBADF; +} + +char* fileGetfilename(void* userdata) { + FileHandle* handle = static_cast(userdata); + return handle ? handle->filename : nullptr; +} + +int fileIoctl(void* userdata, int request, void* data) { + (void)userdata; + (void)request; + (void)data; + return -EINVAL; +} + +int fileIoctl2(void* userdata, int request, void* arg, unsigned int arglen, void* buf, unsigned int buflen) { + (void)userdata; + (void)request; + (void)arg; + (void)arglen; + (void)buf; + (void)buflen; + return -EINVAL; +} + +int dirClose(void* userdata) { + DirHandle* handle = static_cast(userdata); + if (!handle) { + return -EBADF; + } + std::free(handle->children); + std::free(handle->child_offsets); + std::free(handle->relative); + std::free(handle->filename); + std::free(handle); + return 0; +} + +int dirRead(void* userdata, struct dirent* dirp) { + DirHandle* handle = static_cast(userdata); + if (!handle || !dirp) { + return -EBADF; + } + if (handle->position >= handle->count) { + return 0; + } + const char* child = handle->children + handle->child_offsets[handle->position]; + dirp->d_type = isChildDirectory(handle->relative, child) ? DT_DIR : DT_REG; + std::strncpy(dirp->d_name, child, MAXNAMLEN); + dirp->d_name[MAXNAMLEN] = '\0'; + ++handle->position; + return 1; +} + +int dirGetfd(void* userdata) { + (void)userdata; + return 0; +} + +char* dirGetfilename(void* userdata) { + DirHandle* handle = static_cast(userdata); + return handle ? handle->filename : nullptr; +} + +int dirIoctl(void* userdata, int request, void* data) { + (void)userdata; + (void)request; + (void)data; + return -EINVAL; +} + +int dirIoctl2(void* userdata, int request, void* arg, unsigned int arglen, void* buf, unsigned int buflen) { + (void)userdata; + (void)request; + (void)arg; + (void)arglen; + (void)buf; + (void)buflen; + return -EINVAL; +} + +bool readFull(int fd, void* buffer, int size) { + char* cursor = static_cast(buffer); + int remaining = size; + while (remaining > 0) { + const int read = fioRead(fd, cursor, remaining); + if (read <= 0) { + return false; + } + cursor += read; + remaining -= read; + } + return true; +} + +bool fileExists(const char* path) { + const int fd = fioOpen(path, FIO_O_RDONLY); + if (fd < 0) { + return false; + } + fioClose(fd); + return true; +} + +void loadUsbMassStorageDrivers() { + // USBHDFSD registers the mass: filesystem after USBD is available. Ignore + // load failures so an archive-only disc still boots on every PS2 setup. + SifLoadFileInit(); + SifLoadModule(kUsbDriverPath, 0, nullptr); + SifLoadModule(kUsbMassDriverPath, 0, nullptr); +} + +} // namespace + +int initDataBackend() { + if (s_initialized) { + return 0; + } + + fioInit(); + if (fileExists(kHostConfigPath)) { + s_data_root = kHostDataRoot; + s_initialized = true; + std::fprintf(stderr, "ps2 data: using %s\n", s_data_root); + return 0; + } + if (fileExists(kLegacyHostConfigPath)) { + s_data_root = kLegacyHostDataRoot; + s_initialized = true; + std::fprintf(stderr, "ps2 data: using %s\n", s_data_root); + return 0; + } + + loadUsbMassStorageDrivers(); + if (fileExists(kMassConfigPath)) { + s_data_root = kMassDataRoot; + s_initialized = true; + std::fprintf(stderr, "ps2 data: using %s\n", s_data_root); + return 0; + } + + s_default_path_ops = _libcglue_fdman_path_ops; + + const int fd = fioOpen(kArchiveName, FIO_O_RDONLY); + if (fd < 0) { + std::fprintf(stderr, "ps2 data: cannot open %s\n", kArchiveName); + return -1; + } + + std::uint8_t header[kHeaderSize]; + if (!readFull(fd, header, sizeof(header))) { + std::fprintf(stderr, "ps2 data: cannot read archive header\n"); + fioClose(fd); + return -1; + } + const auto readU32 = [&header](std::size_t offset) { + return static_cast(header[offset]) | + (static_cast(header[offset + 1]) << 8u) | + (static_cast(header[offset + 2]) << 16u) | + (static_cast(header[offset + 3]) << 24u); + }; + if (readU32(0) != kMagic || readU32(4) != kVersion) { + std::fprintf(stderr, "ps2 data: bad archive header\n"); + fioClose(fd); + return -1; + } + s_entry_count = readU32(8); + const std::uint32_t index_size = readU32(12); + if (s_entry_count == 0 || s_entry_count > 65536 || + index_size < kHeaderSize + s_entry_count * kEntrySize) { + std::fprintf(stderr, "ps2 data: bad archive index\n"); + fioClose(fd); + return -1; + } + + const std::uint32_t index_data_size = index_size - kHeaderSize; + std::uint8_t* index = static_cast(std::malloc(index_data_size)); + if (!index || !readFull(fd, index, static_cast(index_data_size))) { + std::fprintf(stderr, "ps2 data: cannot read archive index\n"); + std::free(index); + fioClose(fd); + return -1; + } + fioClose(fd); + + s_entries = static_cast(std::calloc(s_entry_count, sizeof(DataEntry))); + if (!s_entries) { + std::fprintf(stderr, "ps2 data: out of memory\n"); + std::free(index); + return -1; + } + const char* index_names = reinterpret_cast(index); + for (std::uint32_t entry_index = 0; entry_index < s_entry_count; ++entry_index) { + const std::uint8_t* raw = index + entry_index * kEntrySize; + const auto rawU32 = [raw](std::size_t offset) { + return static_cast(raw[offset]) | + (static_cast(raw[offset + 1]) << 8u) | + (static_cast(raw[offset + 2]) << 16u) | + (static_cast(raw[offset + 3]) << 24u); + }; + const std::uint32_t name_offset = rawU32(0); + const std::uint32_t name_size = rawU32(4); + DataEntry& entry = s_entries[entry_index]; + entry.name = index_names + name_offset; + entry.name_size = name_size; + entry.data_offset = rawU32(8); + entry.data_size = rawU32(12); + } + + s_file_ops.getfd = fileGetfd; + s_file_ops.getfilename = fileGetfilename; + s_file_ops.close = fileClose; + s_file_ops.read = fileRead; + s_file_ops.lseek = fileLseek; + s_file_ops.lseek64 = fileLseek64; + s_file_ops.write = fileWrite; + s_file_ops.ioctl = fileIoctl; + s_file_ops.ioctl2 = fileIoctl2; + + s_dir_ops.getfd = dirGetfd; + s_dir_ops.getfilename = dirGetfilename; + s_dir_ops.close = dirClose; + s_dir_ops.dread = dirRead; + s_dir_ops.ioctl = dirIoctl; + s_dir_ops.ioctl2 = dirIoctl2; + + s_path_ops.open = pathOpen; + s_path_ops.remove = pathRemove; + s_path_ops.rename = pathRename; + s_path_ops.mkdir = pathMkdir; + s_path_ops.rmdir = pathRmdir; + s_path_ops.stat = pathStat; + s_path_ops.readlink = pathReadlink; + s_path_ops.symlink = pathSymlink; + + _libcglue_fdman_path_ops = &s_path_ops; + s_initialized = true; + s_data_root = kArchiveDataRoot; + std::fprintf( + stderr, + "ps2 data: %lu files from %s\n", + static_cast(s_entry_count), + kArchiveName); + return 0; +} + +const char* dataRoot() { + return s_data_root; +} + +} // namespace ps2 +} // namespace platform +} // namespace runtime +} // namespace osf diff --git a/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.hpp b/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.hpp new file mode 100644 index 00000000..ab43bbea --- /dev/null +++ b/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.hpp @@ -0,0 +1,24 @@ +// PlayStation 2 game-data backend. +// +// The BIOS fileio module strips '\' and '/' from cdrom paths and cdvdman +// searches ISO9660 names case-sensitively, so only flat, uppercase, short +// root files are reachable through fileio. The game data therefore ships as a +// single root file (SFGAME.BIN) packed by tools/ps2/pack.c. + +#ifndef OSF_PS2_DATA_BACKEND_HPP_ +#define OSF_PS2_DATA_BACKEND_HPP_ + +namespace osf { +namespace runtime { +namespace platform { +namespace ps2 { + +int initDataBackend(); +const char* dataRoot(); + +} // namespace ps2 +} // namespace platform +} // namespace runtime +} // namespace osf + +#endif // OSF_PS2_DATA_BACKEND_HPP_ diff --git a/src/SF_EXE/runtime/platform/ps2/surface_presenter.cpp b/src/SF_EXE/runtime/platform/ps2/surface_presenter.cpp new file mode 100644 index 00000000..7aba7a14 --- /dev/null +++ b/src/SF_EXE/runtime/platform/ps2/surface_presenter.cpp @@ -0,0 +1,205 @@ +#include "runtime/presentation/surface_presenter.hpp" + +#include "gsKit.h" +#include "dmaKit.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +using osf::gapi::SurfaceView; + +constexpr std::int32_t kTextureWidth = 640; +constexpr std::int32_t kTextureHeight = 512; + +class Ps2SurfacePresenter final + : public osf::runtime::SurfacePresenter { +public: + ~Ps2SurfacePresenter() override { + shutdown(); + } + + bool initialize( + LwlWindow* window, + std::string* error) override; + void prepareFrame(SurfaceView surface) override; + void displayFrame() override; +#if OSF_ENABLE_DEBUG_TOOLS + std::optional + videoMemoryUsageBytes() const override; +#endif + +private: + void shutdown(); + void uploadSurface(const SurfaceView& surface); + + GSGLOBAL* gsGlobal_ = nullptr; + GSTEXTURE texture_{}; + std::unique_ptr textureMemory_; + std::int32_t textureWidth_ = 0; + std::int32_t textureHeight_ = 0; + bool framePrepared_ = false; +}; + +void setError(std::string* error, const char* message) { + if (error) { + *error = message; + } +} + +bool Ps2SurfacePresenter::initialize( + LwlWindow* window, + std::string* error) { + (void) window; + shutdown(); + + gsGlobal_ = gsKit_init_global(); + if (!gsGlobal_) { + setError(error, "Could not initialize gsKit."); + return false; + } + gsGlobal_->Mode = GS_MODE_NTSC; + gsGlobal_->Interlace = GS_INTERLACED; + gsGlobal_->Field = GS_FIELD; + gsGlobal_->Width = 640; + gsGlobal_->Height = 448; + gsGlobal_->PSM = GS_PSM_CT32; + gsGlobal_->DoubleBuffering = GS_SETTING_ON; + gsGlobal_->ZBuffering = GS_SETTING_OFF; + + dmaKit_init( + D_CTRL_RELE_OFF, + D_CTRL_MFD_OFF, + D_CTRL_STS_UNSPEC, + D_CTRL_STD_OFF, + D_CTRL_RCYC_8, + 1 << DMA_CHANNEL_GIF); + dmaKit_chan_init(DMA_CHANNEL_GIF); + + gsKit_init_screen(gsGlobal_); + gsKit_mode_switch(gsGlobal_, GS_PERSISTENT); + gsKit_set_clamp(gsGlobal_, GS_CMODE_CLAMP); + + textureMemory_ = + std::make_unique( + static_cast(kTextureWidth) * + static_cast(kTextureHeight)); + texture_.Width = static_cast(kTextureWidth); + texture_.Height = static_cast(kTextureHeight); + texture_.PSM = GS_PSM_CT32; + texture_.Filter = GS_FILTER_NEAREST; + texture_.Mem = reinterpret_cast(textureMemory_.get()); + texture_.Vram = gsKit_vram_alloc( + gsGlobal_, + static_cast(kTextureWidth) * + static_cast(kTextureHeight) * 4u, + GSKIT_ALLOC_USERBUFFER); + if (texture_.Vram == 0) { + setError(error, "Could not allocate texture VRAM."); + shutdown(); + return false; + } + textureWidth_ = kTextureWidth; + textureHeight_ = kTextureHeight; + return true; +} + +void Ps2SurfacePresenter::shutdown() { + framePrepared_ = false; + textureWidth_ = 0; + textureHeight_ = 0; + textureMemory_.reset(); + gsGlobal_ = nullptr; +} + +void Ps2SurfacePresenter::uploadSurface(const SurfaceView& surface) { + if (surface.width > textureWidth_ || + surface.height > textureHeight_) { + return; + } + const std::uint32_t* source = + reinterpret_cast(surface.pixels); + std::uint32_t* destination = textureMemory_.get(); + for (std::int32_t row = 0; row < surface.height; ++row) { + std::memcpy( + destination, + source, + static_cast(surface.width) * 4u); + destination += textureWidth_; + source += surface.width; + } + gsKit_texture_upload(gsGlobal_, &texture_); +} + +void Ps2SurfacePresenter::prepareFrame(SurfaceView surface) { + framePrepared_ = false; + if (!gsGlobal_ || !textureMemory_ || !surface.pixels || + surface.width <= 0 || surface.height <= 0) { + return; + } + + uploadSurface(surface); + + const float scale = std::min( + static_cast(gsGlobal_->Width) / + static_cast(surface.width), + static_cast(gsGlobal_->Height) / + static_cast(surface.height)); + const std::int32_t draw_width = + static_cast(surface.width * scale); + const std::int32_t draw_height = + static_cast(surface.height * scale); + const std::int32_t offset_x = + (static_cast(gsGlobal_->Width) - draw_width) / 2; + const std::int32_t offset_y = + (static_cast(gsGlobal_->Height) - draw_height) / 2; + gsKit_clear( + gsGlobal_, GS_SETREG_RGBAQ(0x00, 0x00, 0x00, 0x00, 0x00)); + gsKit_prim_sprite_striped_texture( + gsGlobal_, + &texture_, + static_cast(offset_x), + static_cast(offset_y), + 0.0f, + 0.0f, + static_cast(offset_x + draw_width), + static_cast(offset_y + draw_height), + static_cast(surface.width), + static_cast(surface.height), + 0, + GS_SETREG_RGBAQ(0x80, 0x80, 0x80, 0x80, 0x00)); + gsKit_queue_exec(gsGlobal_); + framePrepared_ = true; +} + +void Ps2SurfacePresenter::displayFrame() { + if (!gsGlobal_ || !framePrepared_) { + return; + } + gsKit_sync_flip(gsGlobal_); + framePrepared_ = false; +} + +#if OSF_ENABLE_DEBUG_TOOLS +std::optional +Ps2SurfacePresenter::videoMemoryUsageBytes() const { + if (!gsGlobal_ || texture_.Vram == 0) { + return std::nullopt; + } + return static_cast(kTextureWidth) * + static_cast(kTextureHeight) * + sizeof(std::uint32_t); +} +#endif + +} // namespace + +std::unique_ptr +osf::runtime::createSurfacePresenter() { + return std::make_unique(); +} diff --git a/src/SF_EXE/runtime/state_bindings.cpp b/src/SF_EXE/runtime/state_bindings.cpp index 36fe129e..dbf9882a 100644 --- a/src/SF_EXE/runtime/state_bindings.cpp +++ b/src/SF_EXE/runtime/state_bindings.cpp @@ -1,290 +1,290 @@ -#include "state_bindings.hpp" - -#include "lwl.h" -#include "resources/font_resource.hpp" -#include "resources/resource_manager.hpp" -#include "ui/conversation_layout.hpp" -#include "resources/retail_filesystem.hpp" -#include "runtime/audio_system.hpp" -#include "world/player_data.hpp" -#include "world/world_scene.hpp" - -#include -#include -#include -#include - -namespace osf::runtime { - -TitleStateHooks makeTitleStateHooks( - const std::filesystem::path& data_root, - ResourceManager& resources, - AudioSystem& audio) { - TitleStateHooks hooks; - hooks.load_pattern = - [&resources]( - std::int32_t id, - std::string_view path) { - return resources.loadTitlePattern(id, path); - }; - hooks.load_animation = - [&resources]( - std::size_t index, - std::int32_t, - std::string_view path) { - return resources.loadTitleAnimation(index, path); - }; - hooks.release_pattern = [&resources](std::int32_t id) { - resources.releaseTitlePattern(id); - }; - hooks.release_animation = [&resources](std::size_t index) { - resources.releaseTitleAnimation(index); - }; - hooks.load_voice = - [&audio]( - std::string_view path, - std::int32_t slot) { - if (slot == 500) { - audio.loadMenuMusic(path); - } - }; - hooks.files_exist = - [&data_root](std::string_view pattern) { - return pattern == "Save\\*.Ssv" && - countRetailSaves(data_root) != 0; - }; - hooks.file_exists = - [&data_root](std::string_view path) { - return retailFileExists(data_root, path); - }; - return hooks; -} - -CharacterSelectStateHooks makeCharacterSelectStateHooks( - const std::filesystem::path& data_root, - ResourceManager& resources, - AudioSystem& audio, - LwlWindow*& window) { - CharacterSelectStateHooks hooks; - hooks.begin_scene = [&resources] { - resources.loadCommonPattern( - 0, - "System\\Common\\Pattern\\Font00.njp", - englishRetailFontPatternSelection()); - }; - hooks.clear_scene = [&resources] { - resources.releaseCommonPattern(0); - }; - hooks.load_pattern = - [&resources]( - std::int32_t id, - std::string_view path) { - return resources.loadCharacterSelectPattern(id, path); - }; - hooks.release_pattern = [&resources](std::int32_t id) { - resources.releaseCharacterSelectPattern(id); - }; - hooks.file_exists = - [&data_root](std::string_view path) { - return retailFileExists(data_root, path); - }; - hooks.load_saved_characters = [&resources] { - resources.loadSavedCharacters(); - }; - hooks.delete_saved_character = - [&data_root, &resources](std::int32_t index) { - deleteRetailSave(data_root, index); - resources.loadSavedCharacters(); - }; - hooks.read_clipboard = [&window] { - char* text = lwl_clipboard_get(window); - std::string result = text ? text : ""; - lwl_free(text); - return result; - }; - hooks.voice_is_playing = - [&audio](std::int32_t slot) { - return slot == 500 && - audio.menuMusicIsPlaying(); - }; - hooks.play_voice = - [&audio](std::int32_t slot, bool loop) { - if (slot == 500) { - audio.playMenuMusic(loop); - } - }; - hooks.release_voice = - [&audio](std::int32_t slot) { - if (slot == 500) { - audio.releaseMenuMusic(); - } - }; - return hooks; -} - -GameplayStateHooks makeGameplayStateHooks( - const std::filesystem::path& data_root, - PlayerLoadRequest& player, - ResourceManager& resources, - AudioSystem& audio, - WorldScene& world) { - GameplayStateHooks hooks; - hooks.prepare_interface = [&resources] { - const bool ready = resources.loadCommonPattern( - 1, - "System\\Common\\Pattern\\Font01.njp", - englishRetailFontPatternSelection()) && - resources.loadCommonPattern( - 2, - "System\\Common\\Pattern\\Waiting.njp") && - resources.loadGameplayPattern( - 5, "System\\Game\\Pattern\\Bar.njp") && - resources.loadGameplayPattern( - 8, "System\\Game\\Pattern\\StatusIcon.njp") && - resources.loadGameplayPattern( - 9, "System\\Game\\Pattern\\MagicIcon.njp") && - resources.loadGameplayPattern( - 10, - "System\\Game\\Pattern\\MagicBarIcon.njp"); - if (!ready) { - resources.releaseCommonPattern(1); - resources.releaseCommonPattern(2); - resources.releaseGameplayResources(); - return false; - } - return true; - }; - hooks.release_loading_artwork = [&resources] { - resources.releaseCommonPattern(2); - }; - hooks.release_interface = [&resources] { - resources.releaseCommonPattern(1); - resources.releaseCommonPattern(2); - resources.releaseGameplayResources(); - }; - hooks.prepare_world = - [&data_root, &player, &world] { - std::string error; - const bool ready = - world.loadInitialScenario( - data_root, player, &error); - if (!ready) { - std::fprintf( - stderr, - "Could not load the initial world: %s\n", - error.c_str()); - } - return ready; - }; - hooks.release_world = [&world] { - world.clear(); - }; - hooks.start_world_music = [&audio, &world] { - audio.startWorldMusic(world.musicTrack()); - }; - hooks.stop_world_music = [&audio] { - audio.stopWorldMusic(); - }; - hooks.command_player_movement = - [&world](std::int32_t x, std::int32_t y) { - world.commandPlayerMovement(x, y); - }; - hooks.cancel_player_movement = [&world] { - world.cancelPlayerMovement(); - }; - hooks.update_pointer_hover = - [&resources, &world]( - std::int32_t x, - std::int32_t y) { - world.updatePointerHover(x, y); - const auto* font = resources.pattern(1); - if (!font || - !world.conversationRequiresSelection()) { - return; - } - const std::int32_t option = - conversationChoiceAtScreenPosition( - world, - *font, - world.cameraScreenX(), - world.cameraScreenY(), - x, - y); - if (option >= 0) { - world.selectConversationOption(option); - } - }; - hooks.clear_pointer_hover = [&world] { - world.clearPointerHover(); - }; - hooks.command_world_interaction = - [&world](std::int32_t x, std::int32_t y) { - return world.commandWorldInteraction(x, y); - }; - hooks.command_player_magic = - [&world](std::int32_t x, std::int32_t y) { - return world.commandPlayerMagic(x, y); - }; - hooks.world_interaction_pending = [&world] { - return world.interactionPending(); - }; - hooks.conversation_active = [&world] { - return world.conversationActive(); - }; - hooks.scenario_visual_active = [&world] { - return world.scenarioVisualActive(); - }; - hooks.advance_scenario_visual = [&world] { - world.requestScenarioVisualAdvance(); - }; - hooks.conversation_requires_selection = [&world] { - return world.conversationRequiresSelection(); - }; - hooks.choose_conversation_option = - [&resources, &world]( - std::int32_t x, - std::int32_t y) { - const auto* font = resources.pattern(1); - if (!font) { - return false; - } - const std::int32_t option = - conversationChoiceAtScreenPosition( - world, - *font, - world.cameraScreenX(), - world.cameraScreenY(), - x, - y); - if (option < 0) { - return false; - } - world.chooseConversationOption(option); - return true; - }; - hooks.advance_conversation = [&world] { - world.advanceConversation(); - }; - hooks.toggle_player_run = [&world] { - world.togglePlayerRun(); - }; - hooks.toggle_companion_activity = [&world] { - world.toggleOwnedCompanionActivity(); - }; - hooks.activate_increased_power = [&world] { - world.activatePlayerIncreasedPower(); - }; - hooks.place_land_mine = [&world] { - world.placePlayerLandMine(); - }; - hooks.update_world = [&audio, &world] { - world.update(); - for (const std::int32_t sample : - world.takeAudioSamples()) { - audio.playGameplayEffect(sample); - } - }; - return hooks; -} - -} // namespace osf::runtime +#include "state_bindings.hpp" + +#include "lwl.h" +#include "resources/font_resource.hpp" +#include "resources/resource_manager.hpp" +#include "ui/conversation_layout.hpp" +#include "resources/retail_filesystem.hpp" +#include "runtime/audio_system.hpp" +#include "world/player_data.hpp" +#include "world/world_scene.hpp" + +#include +#include +#include +#include + +namespace osf::runtime { + +TitleStateHooks makeTitleStateHooks( + const std::filesystem::path& data_root, + ResourceManager& resources, + AudioSystem& audio) { + TitleStateHooks hooks; + hooks.load_pattern = + [&resources]( + std::int32_t id, + std::string_view path) { + return resources.loadTitlePattern(id, path); + }; + hooks.load_animation = + [&resources]( + std::size_t index, + std::int32_t, + std::string_view path) { + return resources.loadTitleAnimation(index, path); + }; + hooks.release_pattern = [&resources](std::int32_t id) { + resources.releaseTitlePattern(id); + }; + hooks.release_animation = [&resources](std::size_t index) { + resources.releaseTitleAnimation(index); + }; + hooks.load_voice = + [&audio]( + std::string_view path, + std::int32_t slot) { + if (slot == 500) { + audio.loadMenuMusic(path); + } + }; + hooks.files_exist = + [&data_root](std::string_view pattern) { + return pattern == "Save\\*.Ssv" && + countRetailSaves(data_root) != 0; + }; + hooks.file_exists = + [&data_root](std::string_view path) { + return retailFileExists(data_root, path); + }; + return hooks; +} + +CharacterSelectStateHooks makeCharacterSelectStateHooks( + const std::filesystem::path& data_root, + ResourceManager& resources, + AudioSystem& audio, + LwlWindow*& window) { + CharacterSelectStateHooks hooks; + hooks.begin_scene = [&resources] { + resources.loadCommonPattern( + 0, + "System\\Common\\Pattern\\Font00.njp", + englishRetailFontPatternSelection()); + }; + hooks.clear_scene = [&resources] { + resources.releaseCommonPattern(0); + }; + hooks.load_pattern = + [&resources]( + std::int32_t id, + std::string_view path) { + return resources.loadCharacterSelectPattern(id, path); + }; + hooks.release_pattern = [&resources](std::int32_t id) { + resources.releaseCharacterSelectPattern(id); + }; + hooks.file_exists = + [&data_root](std::string_view path) { + return retailFileExists(data_root, path); + }; + hooks.load_saved_characters = [&resources] { + resources.loadSavedCharacters(); + }; + hooks.delete_saved_character = + [&data_root, &resources](std::int32_t index) { + deleteRetailSave(data_root, index); + resources.loadSavedCharacters(); + }; + hooks.read_clipboard = [&window] { + char* text = lwl_clipboard_get(window); + std::string result = text ? text : ""; + lwl_free(text); + return result; + }; + hooks.voice_is_playing = + [&audio](std::int32_t slot) { + return slot == 500 && + audio.menuMusicIsPlaying(); + }; + hooks.play_voice = + [&audio](std::int32_t slot, bool loop) { + if (slot == 500) { + audio.playMenuMusic(loop); + } + }; + hooks.release_voice = + [&audio](std::int32_t slot) { + if (slot == 500) { + audio.releaseMenuMusic(); + } + }; + return hooks; +} + +GameplayStateHooks makeGameplayStateHooks( + const std::filesystem::path& data_root, + PlayerLoadRequest& player, + ResourceManager& resources, + AudioSystem& audio, + WorldScene& world) { + GameplayStateHooks hooks; + hooks.prepare_interface = [&resources] { + const bool ready = resources.loadCommonPattern( + 1, + "System\\Common\\Pattern\\Font01.njp", + englishRetailFontPatternSelection()) && + resources.loadCommonPattern( + 2, + "System\\Common\\Pattern\\Waiting.njp") && + resources.loadGameplayPattern( + 5, "System\\Game\\Pattern\\Bar.njp") && + resources.loadGameplayPattern( + 8, "System\\Game\\Pattern\\StatusIcon.njp") && + resources.loadGameplayPattern( + 9, "System\\Game\\Pattern\\MagicIcon.njp") && + resources.loadGameplayPattern( + 10, + "System\\Game\\Pattern\\MagicBarIcon.njp"); + if (!ready) { + resources.releaseCommonPattern(1); + resources.releaseCommonPattern(2); + resources.releaseGameplayResources(); + return false; + } + return true; + }; + hooks.release_loading_artwork = [&resources] { + resources.releaseCommonPattern(2); + }; + hooks.release_interface = [&resources] { + resources.releaseCommonPattern(1); + resources.releaseCommonPattern(2); + resources.releaseGameplayResources(); + }; + hooks.prepare_world = + [&data_root, &player, &world] { + std::string error; + const bool ready = + world.loadInitialScenario( + data_root, player, &error); + if (!ready) { + std::fprintf( + stderr, + "Could not load the initial world: %s\n", + error.c_str()); + } + return ready; + }; + hooks.release_world = [&world] { + world.clear(); + }; + hooks.start_world_music = [&audio, &world] { + audio.startWorldMusic(world.musicTrack()); + }; + hooks.stop_world_music = [&audio] { + audio.stopWorldMusic(); + }; + hooks.command_player_movement = + [&world](std::int32_t x, std::int32_t y) { + world.commandPlayerMovement(x, y); + }; + hooks.cancel_player_movement = [&world] { + world.cancelPlayerMovement(); + }; + hooks.update_pointer_hover = + [&resources, &world]( + std::int32_t x, + std::int32_t y) { + world.updatePointerHover(x, y); + const auto* font = resources.pattern(1); + if (!font || + !world.conversationRequiresSelection()) { + return; + } + const std::int32_t option = + conversationChoiceAtScreenPosition( + world, + *font, + world.cameraScreenX(), + world.cameraScreenY(), + x, + y); + if (option >= 0) { + world.selectConversationOption(option); + } + }; + hooks.clear_pointer_hover = [&world] { + world.clearPointerHover(); + }; + hooks.command_world_interaction = + [&world](std::int32_t x, std::int32_t y) { + return world.commandWorldInteraction(x, y); + }; + hooks.command_player_magic = + [&world](std::int32_t x, std::int32_t y) { + return world.commandPlayerMagic(x, y); + }; + hooks.world_interaction_pending = [&world] { + return world.interactionPending(); + }; + hooks.conversation_active = [&world] { + return world.conversationActive(); + }; + hooks.scenario_visual_active = [&world] { + return world.scenarioVisualActive(); + }; + hooks.advance_scenario_visual = [&world] { + world.requestScenarioVisualAdvance(); + }; + hooks.conversation_requires_selection = [&world] { + return world.conversationRequiresSelection(); + }; + hooks.choose_conversation_option = + [&resources, &world]( + std::int32_t x, + std::int32_t y) { + const auto* font = resources.pattern(1); + if (!font) { + return false; + } + const std::int32_t option = + conversationChoiceAtScreenPosition( + world, + *font, + world.cameraScreenX(), + world.cameraScreenY(), + x, + y); + if (option < 0) { + return false; + } + world.chooseConversationOption(option); + return true; + }; + hooks.advance_conversation = [&world] { + world.advanceConversation(); + }; + hooks.toggle_player_run = [&world] { + world.togglePlayerRun(); + }; + hooks.toggle_companion_activity = [&world] { + world.toggleOwnedCompanionActivity(); + }; + hooks.activate_increased_power = [&world] { + world.activatePlayerIncreasedPower(); + }; + hooks.place_land_mine = [&world] { + world.placePlayerLandMine(); + }; + hooks.update_world = [&audio, &world] { + world.update(); + for (const std::int32_t sample : + world.takeAudioSamples()) { + audio.playGameplayEffect(sample); + } + }; + return hooks; +} + +} // namespace osf::runtime diff --git a/src/SF_EXE/states/character_select/new_character_flow.cpp b/src/SF_EXE/states/character_select/new_character_flow.cpp index 7f7ba354..6b073b7c 100644 --- a/src/SF_EXE/states/character_select/new_character_flow.cpp +++ b/src/SF_EXE/states/character_select/new_character_flow.cpp @@ -112,7 +112,7 @@ void updateNewCharacterModeImpl( data.launch_counter == 0 ? kFullBrightness : std::max( - 0, + std::int32_t{0}, kFullBrightness - phase * 50); if (phase > 5) { data.fade_value = diff --git a/src/SF_EXE/states/character_select_state.cpp b/src/SF_EXE/states/character_select_state.cpp index bf5f0fcc..8282ed6b 100644 --- a/src/SF_EXE/states/character_select_state.cpp +++ b/src/SF_EXE/states/character_select_state.cpp @@ -1,280 +1,280 @@ -#include "character_select_state.hpp" - -#include "character_select/character_select_flow.hpp" -#include "states/save_slot.hpp" - -#include - -namespace osf { -namespace { - -constexpr std::int32_t kMenuMusicSlot = 500; - -constexpr std::array kCharacterSelectInputBindings{{ - 1, 2, 16, 17, 38, 40, 37, 39, 9, 27, 13, 46, - 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, - 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, - 87, 88, 89, 90, 48, 49, 50, 51, 52, 53, 54, - 55, 56, 57, 96, 97, 98, 99, 100, 101, 102, - 103, 104, 105, -}}; - -bool loadPattern( - const std::function& callback, - std::int32_t id, - std::string_view path) { - return !callback || callback(id, path); -} - -bool pointerInside( - const CharacterSelectFrameInput& input, - std::int32_t left, - std::int32_t right, - std::int32_t top, - std::int32_t bottom) { - return input.pointer_x > left && - input.pointer_x < right && - input.pointer_y > top && - input.pointer_y < bottom; -} - -void updateVisualState( - CharacterSelectFrameResult& result, - const CharacterSelectFrameInput& input) { - result.host_connect_hovered = - pointerInside(input, 0x171, 0x196, 0x114, 0x120); - result.host_back_hovered = - pointerInside(input, 0xe9, 0x132, 0x114, 0x120); - result.host_paste_hovered = - pointerInside(input, 0x175, 0x189, 0xe3, 0xf5); - result.name_confirm_hovered = - pointerInside(input, 0x237, 0x25b, 0x1c3, 0x1ce); - for (std::size_t index = 0; - index < result.save_slot_hovered.size(); - ++index) { - const std::int32_t x = - 32 + static_cast(index % 2) * 304; - const std::int32_t y = - 188 + static_cast(index / 2) * 88; - result.save_slot_hovered[index] = - pointerInside(input, x, x + 287, y, y + 76); - } -} - -} // namespace - - -CharacterSelectState::CharacterSelectState( - CharacterSelectStateHooks hooks) - : hooks_(std::move(hooks)) {} - -void CharacterSelectState::enter(std::int32_t retail_argument) { - if (hooks_.begin_scene) { - hooks_.begin_scene(); - } - // Like retail function 0x00421920, failure is not checked here. - loadPattern( - hooks_.load_pattern, 4, - "System\\Select\\Pattern\\Select.njp"); - if (hooks_.configure_input) { - hooks_.configure_input( - kCharacterSelectInputBindings.data(), - kCharacterSelectInputBindings.size()); - } - - if (data_.mode == CharacterSelectMode::new_character && - data_.new_character_data_loaded) { - if (hooks_.release_new_character) { - hooks_.release_new_character(); - } - data_.new_character_data_loaded = false; - } - - data_.screen = 0; - data_.input_latch = 1; - if (retail_argument == 0) { - data_.mode = CharacterSelectMode::new_character; - data_.new_character_data_loaded = true; - data_.next_save_path = - findNextRetailSavePath(hooks_.file_exists).path; - if (hooks_.prepare_new_character) { - hooks_.prepare_new_character(data_.next_save_path); - } - data_.character_gender = 1; - data_.character_name.clear(); - data_.name_entry_active = false; - data_.host_address.clear(); - data_.host_entry_active = false; - data_.dialog_selection = 0; - data_.dialog_input_armed = 1; - } else { - data_.mode = CharacterSelectMode::saved_game; - data_.next_save_path.clear(); - if (hooks_.load_saved_characters) { - hooks_.load_saved_characters(); - } - data_.save_hover_animation = 0; - data_.saved_game_selection = 0; - data_.dialog_selection = 0; - data_.dialog_input_armed = 1; - } - - data_.fade_steps_remaining = 0x14; - data_.launch_counter = 0; - data_.character_transition_counter = 0; - data_.brightness_increasing = 1; - data_.selection_result = -1; - data_.selected_saved_game = -1; - if (hooks_.set_cursor_state) { - hooks_.set_cursor_state(-1); - } - if ((!hooks_.voice_is_playing || - !hooks_.voice_is_playing(kMenuMusicSlot)) && - hooks_.play_voice) { - hooks_.play_voice(kMenuMusicSlot, true); - } - data_.input_latch = 1; - data_.active = true; -} - -void CharacterSelectState::leave() { - data_.temporary_buffer.clear(); - if (hooks_.release_voice) { - hooks_.release_voice(kMenuMusicSlot); - } - if (hooks_.clear_scene) { - hooks_.clear_scene(); - } - if (hooks_.release_pattern) { - hooks_.release_pattern(4); - } - data_.active = false; -} - -CharacterSelectFrameResult CharacterSelectState::update( - const CharacterSelectFrameInput& input) { - CharacterSelectFrameResult result; - result.mode = data_.mode; - updateVisualState(result, input); - if (input.input_suspended) { - result.processed = false; - return result; - } - - if (data_.fade_steps_remaining > 0) { - const std::int32_t brightness = - (21 - data_.fade_steps_remaining) * 50; - data_.fade_value = brightness; - data_.fade_target = brightness; - --data_.fade_steps_remaining; - } - result.background_brightness = data_.fade_value; - - if (data_.brightness_increasing == 0) { - if (data_.fade_target > 500) { - data_.fade_target -= 80; - } - } else if ( - data_.fade_steps_remaining == 0 && - data_.fade_target < 1000) { - data_.fade_target += 80; - } - - const bool valid_mode = - data_.mode == CharacterSelectMode::new_character || - data_.mode == CharacterSelectMode::saved_game; - if (valid_mode) { - data_.rendered_mode = data_.mode; - - if (data_.mode == CharacterSelectMode::saved_game && - data_.screen == 1) { - data_.input_latch = 1; - if (!character_select::updateSavedGameDeleteDialog( - data_, - input, - result, - hooks_.delete_saved_character)) { - data_.brightness_increasing = 1; - data_.screen = 0; - data_.input_latch = 1; - } - } - - // Both retail mode renderers contain this same transition-counter - // prelude before their mode-specific interaction and drawing. - bool mode_returned_early = false; - if (data_.launch_counter == 1022) { - result.action = CharacterSelectAction::return_to_title; - mode_returned_early = true; - } else if (data_.launch_counter == 2022) { - result.action = CharacterSelectAction::exit_game; - mode_returned_early = true; - } else if (data_.launch_counter > 0) { - ++data_.launch_counter; - } - - if (!mode_returned_early && - data_.mode == CharacterSelectMode::new_character) { - character_select::updateNewCharacterMode( - data_, input, result); - } else if ( - !mode_returned_early && - data_.mode == CharacterSelectMode::saved_game) { - character_select::updateSavedGameMode( - data_, input, result); - } - - switch (data_.screen) { - case 10: - result.screen_update = - CharacterSelectScreenUpdate::screen_10; - character_select::updateGameModeScreen( - data_, input, result); - break; - case 11: - result.screen_update = - CharacterSelectScreenUpdate::screen_11; - character_select::updateNetworkModeScreen( - data_, input, result); - break; - case 12: - result.screen_update = - CharacterSelectScreenUpdate::screen_12; - character_select::updateHostScreen( - data_, input, result, hooks_.read_clipboard); - break; - case 20: - if (data_.launch_counter == 0) { - data_.launch_counter = 5010; - } - if (data_.launch_counter == 5024) { - result.action = - CharacterSelectAction::enter_gameplay; - return result; - } - break; - default: - break; - } - } - - if (data_.input_latch == 1) { - data_.input_latch = 0; - } - return result; -} - -const CharacterSelectStateData& CharacterSelectState::data() const { - return data_; -} - -CharacterSelectStateData& CharacterSelectState::data() { - return data_; -} - -const std::array& -retailCharacterSelectInputBindings() { - return kCharacterSelectInputBindings; -} - -} // namespace osf +#include "character_select_state.hpp" + +#include "character_select/character_select_flow.hpp" +#include "states/save_slot.hpp" + +#include + +namespace osf { +namespace { + +constexpr std::int32_t kMenuMusicSlot = 500; + +constexpr std::array kCharacterSelectInputBindings{{ + 1, 2, 16, 17, 38, 40, 37, 39, 9, 27, 13, 46, + 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, + 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, + 87, 88, 89, 90, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 96, 97, 98, 99, 100, 101, 102, + 103, 104, 105, +}}; + +bool loadPattern( + const std::function& callback, + std::int32_t id, + std::string_view path) { + return !callback || callback(id, path); +} + +bool pointerInside( + const CharacterSelectFrameInput& input, + std::int32_t left, + std::int32_t right, + std::int32_t top, + std::int32_t bottom) { + return input.pointer_x > left && + input.pointer_x < right && + input.pointer_y > top && + input.pointer_y < bottom; +} + +void updateVisualState( + CharacterSelectFrameResult& result, + const CharacterSelectFrameInput& input) { + result.host_connect_hovered = + pointerInside(input, 0x171, 0x196, 0x114, 0x120); + result.host_back_hovered = + pointerInside(input, 0xe9, 0x132, 0x114, 0x120); + result.host_paste_hovered = + pointerInside(input, 0x175, 0x189, 0xe3, 0xf5); + result.name_confirm_hovered = + pointerInside(input, 0x237, 0x25b, 0x1c3, 0x1ce); + for (std::size_t index = 0; + index < result.save_slot_hovered.size(); + ++index) { + const std::int32_t x = + 32 + static_cast(index % 2) * 304; + const std::int32_t y = + 188 + static_cast(index / 2) * 88; + result.save_slot_hovered[index] = + pointerInside(input, x, x + 287, y, y + 76); + } +} + +} // namespace + + +CharacterSelectState::CharacterSelectState( + CharacterSelectStateHooks hooks) + : hooks_(std::move(hooks)) {} + +void CharacterSelectState::enter(std::int32_t retail_argument) { + if (hooks_.begin_scene) { + hooks_.begin_scene(); + } + // Like retail function 0x00421920, failure is not checked here. + loadPattern( + hooks_.load_pattern, 4, + "System\\Select\\Pattern\\Select.njp"); + if (hooks_.configure_input) { + hooks_.configure_input( + kCharacterSelectInputBindings.data(), + kCharacterSelectInputBindings.size()); + } + + if (data_.mode == CharacterSelectMode::new_character && + data_.new_character_data_loaded) { + if (hooks_.release_new_character) { + hooks_.release_new_character(); + } + data_.new_character_data_loaded = false; + } + + data_.screen = 0; + data_.input_latch = 1; + if (retail_argument == 0) { + data_.mode = CharacterSelectMode::new_character; + data_.new_character_data_loaded = true; + data_.next_save_path = + findNextRetailSavePath(hooks_.file_exists).path; + if (hooks_.prepare_new_character) { + hooks_.prepare_new_character(data_.next_save_path); + } + data_.character_gender = 1; + data_.character_name.clear(); + data_.name_entry_active = false; + data_.host_address.clear(); + data_.host_entry_active = false; + data_.dialog_selection = 0; + data_.dialog_input_armed = 1; + } else { + data_.mode = CharacterSelectMode::saved_game; + data_.next_save_path.clear(); + if (hooks_.load_saved_characters) { + hooks_.load_saved_characters(); + } + data_.save_hover_animation = 0; + data_.saved_game_selection = 0; + data_.dialog_selection = 0; + data_.dialog_input_armed = 1; + } + + data_.fade_steps_remaining = 0x14; + data_.launch_counter = 0; + data_.character_transition_counter = 0; + data_.brightness_increasing = 1; + data_.selection_result = -1; + data_.selected_saved_game = -1; + if (hooks_.set_cursor_state) { + hooks_.set_cursor_state(-1); + } + if ((!hooks_.voice_is_playing || + !hooks_.voice_is_playing(kMenuMusicSlot)) && + hooks_.play_voice) { + hooks_.play_voice(kMenuMusicSlot, true); + } + data_.input_latch = 1; + data_.active = true; +} + +void CharacterSelectState::leave() { + data_.temporary_buffer.clear(); + if (hooks_.release_voice) { + hooks_.release_voice(kMenuMusicSlot); + } + if (hooks_.clear_scene) { + hooks_.clear_scene(); + } + if (hooks_.release_pattern) { + hooks_.release_pattern(4); + } + data_.active = false; +} + +CharacterSelectFrameResult CharacterSelectState::update( + const CharacterSelectFrameInput& input) { + CharacterSelectFrameResult result; + result.mode = data_.mode; + updateVisualState(result, input); + if (input.input_suspended) { + result.processed = false; + return result; + } + + if (data_.fade_steps_remaining > 0) { + const std::int32_t brightness = + (21 - data_.fade_steps_remaining) * 50; + data_.fade_value = brightness; + data_.fade_target = brightness; + --data_.fade_steps_remaining; + } + result.background_brightness = data_.fade_value; + + if (data_.brightness_increasing == 0) { + if (data_.fade_target > 500) { + data_.fade_target -= 80; + } + } else if ( + data_.fade_steps_remaining == 0 && + data_.fade_target < 1000) { + data_.fade_target += 80; + } + + const bool valid_mode = + data_.mode == CharacterSelectMode::new_character || + data_.mode == CharacterSelectMode::saved_game; + if (valid_mode) { + data_.rendered_mode = data_.mode; + + if (data_.mode == CharacterSelectMode::saved_game && + data_.screen == 1) { + data_.input_latch = 1; + if (!character_select::updateSavedGameDeleteDialog( + data_, + input, + result, + hooks_.delete_saved_character)) { + data_.brightness_increasing = 1; + data_.screen = 0; + data_.input_latch = 1; + } + } + + // Both retail mode renderers contain this same transition-counter + // prelude before their mode-specific interaction and drawing. + bool mode_returned_early = false; + if (data_.launch_counter == 1022) { + result.action = CharacterSelectAction::return_to_title; + mode_returned_early = true; + } else if (data_.launch_counter == 2022) { + result.action = CharacterSelectAction::exit_game; + mode_returned_early = true; + } else if (data_.launch_counter > 0) { + ++data_.launch_counter; + } + + if (!mode_returned_early && + data_.mode == CharacterSelectMode::new_character) { + character_select::updateNewCharacterMode( + data_, input, result); + } else if ( + !mode_returned_early && + data_.mode == CharacterSelectMode::saved_game) { + character_select::updateSavedGameMode( + data_, input, result); + } + + switch (data_.screen) { + case 10: + result.screen_update = + CharacterSelectScreenUpdate::screen_10; + character_select::updateGameModeScreen( + data_, input, result); + break; + case 11: + result.screen_update = + CharacterSelectScreenUpdate::screen_11; + character_select::updateNetworkModeScreen( + data_, input, result); + break; + case 12: + result.screen_update = + CharacterSelectScreenUpdate::screen_12; + character_select::updateHostScreen( + data_, input, result, hooks_.read_clipboard); + break; + case 20: + if (data_.launch_counter == 0) { + data_.launch_counter = 5010; + } + if (data_.launch_counter == 5024) { + result.action = + CharacterSelectAction::enter_gameplay; + return result; + } + break; + default: + break; + } + } + + if (data_.input_latch == 1) { + data_.input_latch = 0; + } + return result; +} + +const CharacterSelectStateData& CharacterSelectState::data() const { + return data_; +} + +CharacterSelectStateData& CharacterSelectState::data() { + return data_; +} + +const std::array& +retailCharacterSelectInputBindings() { + return kCharacterSelectInputBindings; +} + +} // namespace osf diff --git a/src/SF_EXE/states/gameplay_inventory.cpp b/src/SF_EXE/states/gameplay_inventory.cpp index 3782b2a7..51f77521 100644 --- a/src/SF_EXE/states/gameplay_inventory.cpp +++ b/src/SF_EXE/states/gameplay_inventory.cpp @@ -767,7 +767,7 @@ void GameplayInventory::updateHover( (next_item_index >= 0 || next_equipment_slot >= 0)) { item_hover_updates_ = - std::min(item_hover_updates_ + 1, 3); + std::min(item_hover_updates_ + 1, std::int32_t{3}); } else { item_hover_updates_ = next_item_index >= 0 || @@ -811,7 +811,7 @@ void GameplayInventory::updateSpecialHover( hovered_special_item_index_ && next_special_item_index >= 0) { item_hover_updates_ = - std::min(item_hover_updates_ + 1, 3); + std::min(item_hover_updates_ + 1, std::int32_t{3}); } else { item_hover_updates_ = next_special_item_index >= 0 ? 1 : 0; diff --git a/src/SF_EXE/states/gameplay_transport.cpp b/src/SF_EXE/states/gameplay_transport.cpp index a2efe52c..dee5b12a 100644 --- a/src/SF_EXE/states/gameplay_transport.cpp +++ b/src/SF_EXE/states/gameplay_transport.cpp @@ -108,7 +108,7 @@ std::int32_t GameplayTransport::page() const { std::int32_t GameplayTransport::pageCount( std::size_t enabled_destination_count) const { return std::max( - 1, + std::int32_t{1}, (static_cast( enabled_destination_count) + entries_per_page - 1) / diff --git a/src/SF_EXE/states/gameplay_vendor.cpp b/src/SF_EXE/states/gameplay_vendor.cpp index adf2332a..75fda2cd 100644 --- a/src/SF_EXE/states/gameplay_vendor.cpp +++ b/src/SF_EXE/states/gameplay_vendor.cpp @@ -184,7 +184,9 @@ void GameplayVendor::updateHover( } if (next >= 0 && next == hovered_item_index_) { item_hover_updates_ = - std::min(item_hover_updates_ + 1, 3); + std::min( + item_hover_updates_ + std::int32_t{1}, + std::int32_t{3}); } else { item_hover_updates_ = next >= 0 ? 1 : 0; } diff --git a/src/SF_EXE/ui/conversation_layout.cpp b/src/SF_EXE/ui/conversation_layout.cpp index e0d6cd3f..d4155dc7 100644 --- a/src/SF_EXE/ui/conversation_layout.cpp +++ b/src/SF_EXE/ui/conversation_layout.cpp @@ -55,7 +55,9 @@ ConversationTextLayout layoutConversationText( result.choices.size()), choice_line, choice_column, - std::max(column - choice_column, 0), + std::max( + column - choice_column, + std::int32_t{0}), choice_byte_offset, result.text.size() - choice_byte_offset, }); diff --git a/src/SF_EXE/ui/player_level_up_notice_layout.cpp b/src/SF_EXE/ui/player_level_up_notice_layout.cpp index a0e95428..e417b4a8 100644 --- a/src/SF_EXE/ui/player_level_up_notice_layout.cpp +++ b/src/SF_EXE/ui/player_level_up_notice_layout.cpp @@ -72,7 +72,8 @@ bool buildPlayerLevelUpNoticeLayout( kSlideUpdates; layout.x = std::min( layout.x, kScreenWidth - layout.width); - layout.y = std::max(layout.y, 1); + layout.y = std::max( + layout.y, std::int32_t{1}); } layout.text_x = layout.x + kTextPadding; diff --git a/src/SF_EXE/world/combat_effect_actor.cpp b/src/SF_EXE/world/combat_effect_actor.cpp index d0691701..9f413183 100644 --- a/src/SF_EXE/world/combat_effect_actor.cpp +++ b/src/SF_EXE/world/combat_effect_actor.cpp @@ -149,7 +149,7 @@ void CombatEffectActor::update() { animation_frame_ = counter_; animation_frame_ = std::clamp( animation_frame_, - 0, + std::int32_t{0}, static_cast( direction->frame_count - 1)); diff --git a/src/SF_EXE/world/combat_hit_chance.cpp b/src/SF_EXE/world/combat_hit_chance.cpp index b898684e..f99bef42 100644 --- a/src/SF_EXE/world/combat_hit_chance.cpp +++ b/src/SF_EXE/world/combat_hit_chance.cpp @@ -9,8 +9,8 @@ std::int32_t retailCombatHitChance( std::int32_t defense_value) { return std::clamp( attack_value - defense_value, - 20, - 98); + std::int32_t{20}, + std::int32_t{98}); } } // namespace osf diff --git a/src/SF_EXE/world/companion_actor.cpp b/src/SF_EXE/world/companion_actor.cpp index 578e5fe7..008ddb0d 100644 --- a/src/SF_EXE/world/companion_actor.cpp +++ b/src/SF_EXE/world/companion_actor.cpp @@ -431,7 +431,7 @@ CompanionActor::updateDamagePresentation( presentation_animation_frame_ = std::clamp( presentation_animation_frame_, - 0, + std::int32_t{0}, count - 1); if (!reaction_displacement_suppressed_ && @@ -658,7 +658,7 @@ void CompanionActor::applyRuntimeProfile( } profile_ = profile; current_life_ = std::clamp( - current_life_, 0, profile_.maximum_life); + current_life_, std::int32_t{0}, profile_.maximum_life); } bool CompanionActor::restoreLife( diff --git a/src/SF_EXE/world/companion_attack_action.cpp b/src/SF_EXE/world/companion_attack_action.cpp index a2b68f90..6306235f 100644 --- a/src/SF_EXE/world/companion_attack_action.cpp +++ b/src/SF_EXE/world/companion_attack_action.cpp @@ -59,7 +59,7 @@ bool buildCompanionAttackAnimationTiming( std::int32_t retailCompanionAttackSpeedTier( std::int32_t attack_speed_rating) { return std::clamp( - attack_speed_rating / 32, 0, 9); + attack_speed_rating / 32, std::int32_t{0}, std::int32_t{9}); } bool CompanionAttackActionController::start( diff --git a/src/SF_EXE/world/companion_explosion_action.cpp b/src/SF_EXE/world/companion_explosion_action.cpp index e11c30cc..cbe0e6fc 100644 --- a/src/SF_EXE/world/companion_explosion_action.cpp +++ b/src/SF_EXE/world/companion_explosion_action.cpp @@ -187,7 +187,8 @@ CombatPacket buildCompanionExplosionPacket( packet.write(2, input.source_character_number); // FUN_00461c40 deliberately uses the owner's magical-defense field as // Explosion's base damage and leaves packet defense at zero. - packet.write(4, std::max(input.damage_value, 1)); + packet.write( + 4, std::max(input.damage_value, std::int32_t{1})); for (std::size_t index = 0; index < input.element_affinities.size(); ++index) { diff --git a/src/SF_EXE/world/companion_status_message.cpp b/src/SF_EXE/world/companion_status_message.cpp index 942d5b92..7e984067 100644 --- a/src/SF_EXE/world/companion_status_message.cpp +++ b/src/SF_EXE/world/companion_status_message.cpp @@ -92,7 +92,7 @@ bool buildRetailCompanionStatusMessage( << profile.walking_speed_raw << '\n'; const std::int32_t level_limit = std::min( - player.level() / 3 + 2, 35); + player.level() / 3 + 2, std::int32_t{35}); if (player.companionLevel() == level_limit) { output << (level_limit == 35 ? "Experience Max\n" diff --git a/src/SF_EXE/world/enemy_actor.cpp b/src/SF_EXE/world/enemy_actor.cpp index 844799b3..afa91de9 100644 --- a/src/SF_EXE/world/enemy_actor.cpp +++ b/src/SF_EXE/world/enemy_actor.cpp @@ -369,7 +369,7 @@ EnemyActorUpdate EnemyActor::update( animation_frame_ = frame_count - 1; } animation_frame_ = std::clamp( - animation_frame_, 0, frame_count - 1); + animation_frame_, std::int32_t{0}, frame_count - 1); } else { animation_frame_ = 0; reaction_duration_ = 1; diff --git a/src/SF_EXE/world/enemy_death_rewards.cpp b/src/SF_EXE/world/enemy_death_rewards.cpp index d3e4b51b..c287c269 100644 --- a/src/SF_EXE/world/enemy_death_rewards.cpp +++ b/src/SF_EXE/world/enemy_death_rewards.cpp @@ -144,8 +144,8 @@ EnemyKillAccountingResult accountRetailEnemyKill( static_cast(local_player_slot)] * 10 / enemy.maximum_life, - 0, - 10); + std::int32_t{0}, + std::int32_t{10}); result.experience_awarded = shares->value(bucket, 0) * experience_reward / @@ -206,7 +206,7 @@ std::vector createRetailEnemyDrops( if (attempts == 0) { attempts = active_player_count; } - attempts = std::max(attempts, 0); + attempts = std::max(attempts, std::int32_t{0}); std::array variants{}; std::int32_t successful = 0; for (std::int32_t attempt = 0; @@ -303,7 +303,7 @@ std::vector createRetailEnemyDrops( gold_minimum) * multiplier / 100; - quantity = std::max(quantity, 1); + quantity = std::max(quantity, std::int32_t{1}); drops.push_back({ makeRetailInventoryItem( *gold, diff --git a/src/SF_EXE/world/movement_controller.cpp b/src/SF_EXE/world/movement_controller.cpp index 8b368087..6b3f2275 100644 --- a/src/SF_EXE/world/movement_controller.cpp +++ b/src/SF_EXE/world/movement_controller.cpp @@ -656,7 +656,7 @@ std::int32_t distanceBetweenBounds( return 0; } if (horizontal == 0 || vertical == 0) { - return std::max(horizontal + vertical - 1, 0); + return std::max(horizontal + vertical - 1, std::int32_t{0}); } return std::max( static_cast( @@ -664,7 +664,7 @@ std::int32_t distanceBetweenBounds( static_cast(horizontal), static_cast(vertical))) - 1, - 0); + std::int32_t{0}); } MovementStepResult advanceMovement( @@ -681,7 +681,7 @@ MovementStepResult advanceMovement( } const WorldPosition candidate = movementCandidate( - position, destination, std::max(speed, 0)); + position, destination, std::max(speed, std::int32_t{0})); const SweepResult movement = sweepMovement( ground, diff --git a/src/SF_EXE/world/npc_actor.cpp b/src/SF_EXE/world/npc_actor.cpp index 3f5eea72..217fb038 100644 --- a/src/SF_EXE/world/npc_actor.cpp +++ b/src/SF_EXE/world/npc_actor.cpp @@ -60,9 +60,9 @@ bool NpcActor::initialize( person.judgement_bottom, }; direction_ = person.direction; - walk_speed_ = std::max(person.walk_speed, 0); - walk_duration_ = std::max(person.walk_duration, 0); - idle_duration_ = std::max(person.idle_duration, 0); + walk_speed_ = std::max(person.walk_speed, std::int32_t{0}); + walk_duration_ = std::max(person.walk_duration, std::int32_t{0}); + idle_duration_ = std::max(person.idle_duration, std::int32_t{0}); wander_min_ = { person.wander_left, person.wander_top, diff --git a/src/SF_EXE/world/player_actor.cpp b/src/SF_EXE/world/player_actor.cpp index 7baf1111..12c057c4 100644 --- a/src/SF_EXE/world/player_actor.cpp +++ b/src/SF_EXE/world/player_actor.cpp @@ -26,7 +26,7 @@ constexpr std::array kSpeedFactors{{ double speedFactor(std::int32_t tier) { return kSpeedFactors[ static_cast( - std::clamp(tier, 0, 9))]; + std::clamp(tier, std::int32_t{0}, std::int32_t{9}))]; } std::int32_t walkingSpeedForTier(std::int32_t tier) { @@ -72,7 +72,7 @@ void PlayerActor::reset( destination_ = position; direction_ = direction; walking_speed_tier_ = - std::clamp(walking_speed_tier, 0, 9); + std::clamp(walking_speed_tier, std::int32_t{0}, std::int32_t{9}); walking_speed_ = walkingSpeedForTier(walking_speed_tier_); running_speed_ = walking_speed_ * 2; @@ -298,7 +298,7 @@ void PlayerActor::toggleMovementPace() { } void PlayerActor::setWalkingSpeedTier(std::int32_t tier) { - walking_speed_tier_ = std::clamp(tier, 0, 9); + walking_speed_tier_ = std::clamp(tier, std::int32_t{0}, std::int32_t{9}); walking_speed_ = walkingSpeedForTier(walking_speed_tier_); running_speed_ = walking_speed_ * 2; } @@ -348,7 +348,7 @@ void PlayerActor::update( animation_frame_ = count - 1; } animation_frame_ = std::clamp( - animation_frame_, 0, count - 1); + animation_frame_, std::int32_t{0}, count - 1); if (damage_presentation_.reaction_stage == 2) { animation_frame_ = 0; } diff --git a/src/SF_EXE/world/player_attack_action.cpp b/src/SF_EXE/world/player_attack_action.cpp index 972413b0..38f598fd 100644 --- a/src/SF_EXE/world/player_attack_action.cpp +++ b/src/SF_EXE/world/player_attack_action.cpp @@ -213,18 +213,18 @@ std::int32_t retailPlayerAttackSpeedTier( return 0; } const std::int32_t clamped_speed = - std::clamp(derived_attack_speed, 0, 255); + std::clamp(derived_attack_speed, std::int32_t{0}, std::int32_t{255}); const std::int32_t row = clamped_speed / 32; const std::int32_t table_value = speed_table ? speed_table->value(row, 0) : -1; - return std::clamp(table_value + 1, 0, 9); + return std::clamp(table_value + 1, std::int32_t{0}, std::int32_t{9}); } double retailPlayerMeleeAttackAnimationSpeed( std::int32_t attack_speed_tier) { return kAttackSpeedFactors[ static_cast( - std::clamp(attack_speed_tier, 0, 9))]; + std::clamp(attack_speed_tier, std::int32_t{0}, std::int32_t{9}))]; } bool PlayerAttackActionController::start( @@ -245,7 +245,7 @@ bool PlayerAttackActionController::start( action_ = action; target_id_ = target_id; attack_speed_tier_ = - std::clamp(attack_speed_tier, 0, 9); + std::clamp(attack_speed_tier, std::int32_t{0}, std::int32_t{9}); timing_ = std::move(timing); action_counter_ = 0; displayed_frame_ = 0; @@ -316,7 +316,10 @@ bool PlayerAttackActionController::startCombo( combo_phases_.push_back(std::move(phase)); } attack_speed_tier_ = - std::clamp(attack_speed_tier, 0, 9); + std::clamp( + attack_speed_tier, + std::int32_t{0}, + std::int32_t{9}); target_id_ = -1; combo_phase_ = 0; combo_lunge_distance_ = 0; @@ -338,7 +341,7 @@ PlayerAttackActionEvent PlayerAttackActionController::update( } if (attack_speed_tier >= 0) { attack_speed_tier_ = - std::clamp(attack_speed_tier, 0, 9); + std::clamp(attack_speed_tier, std::int32_t{0}, std::int32_t{9}); } const bool combo = !combo_phases_.empty(); diff --git a/src/SF_EXE/world/player_data.cpp b/src/SF_EXE/world/player_data.cpp index e9371931..9c5571c5 100644 --- a/src/SF_EXE/world/player_data.cpp +++ b/src/SF_EXE/world/player_data.cpp @@ -235,7 +235,7 @@ std::int32_t PlayerData::jobLevel( std::int32_t count = 0; const std::int32_t history_count = std::clamp( - level(), 0, + level(), std::int32_t{0}, static_cast(kJobHistoryCount)); for (std::int32_t index = 0; index < history_count; @@ -383,8 +383,8 @@ void PlayerData::setCurrentLife( 0x34, std::clamp( value, - 0, - std::max(0, maximum_life))); + std::int32_t{0}, + std::max(std::int32_t{0}, maximum_life))); } void PlayerData::setCurrentMana(std::int32_t value) { @@ -398,8 +398,8 @@ void PlayerData::setCurrentMana( 0x3c, std::clamp( value, - 0, - std::max(0, maximum_mana))); + std::int32_t{0}, + std::max(std::int32_t{0}, maximum_mana))); } void PlayerData::restoreForRespawn() { @@ -424,7 +424,7 @@ bool PlayerData::restoreLife( std::clamp( restored, 0, - std::max(0, baseMaximumLife())))); + std::max(std::int32_t{0}, baseMaximumLife())))); return currentLife() != before; } @@ -443,7 +443,7 @@ bool PlayerData::restoreMana( std::clamp( restored, 0, - std::max(0, baseMaximumMana())))); + std::max(std::int32_t{0}, baseMaximumMana())))); return currentMana() != before; } @@ -749,8 +749,8 @@ std::int32_t PlayerData::walkingSpeedTier() const { // 32, and clamps the resulting movement tier to the retail 0..9 range. return std::clamp( (initialParameter(1) + 32) / 32, - 0, - 9); + std::int32_t{0}, + std::int32_t{9}); } const std::array& diff --git a/src/SF_EXE/world/player_increased_power.cpp b/src/SF_EXE/world/player_increased_power.cpp index 1efe31f8..eebed857 100644 --- a/src/SF_EXE/world/player_increased_power.cpp +++ b/src/SF_EXE/world/player_increased_power.cpp @@ -84,7 +84,12 @@ std::int32_t PlayerIncreasedPower::auraFrame() const { std::int32_t PlayerIncreasedPower::movementSpeedTier( std::int32_t ordinary_tier) const { - return active() ? 9 : std::clamp(ordinary_tier, 0, 9); + return active() + ? 9 + : std::clamp( + ordinary_tier, + std::int32_t{0}, + std::int32_t{9}); } bool PlayerIncreasedPower::blocksSpell( diff --git a/src/SF_EXE/world/player_item_controller.cpp b/src/SF_EXE/world/player_item_controller.cpp index d13406f9..460d2599 100644 --- a/src/SF_EXE/world/player_item_controller.cpp +++ b/src/SF_EXE/world/player_item_controller.cpp @@ -45,7 +45,7 @@ bool applyMedicine( static_cast(current) + flat + percentage, 0, - std::max(0, maximum))); + std::max(std::int32_t{0}, maximum))); }; const std::int32_t old_life = targets.player.currentLife(); @@ -102,7 +102,7 @@ void PlayerItemController::initializeNew() { void PlayerItemController::restoreMineCount( std::int32_t count) { - mine_count_ = std::max(count, 0); + mine_count_ = std::max(count, std::int32_t{0}); } bool PlayerItemController::consumeMine() { @@ -115,7 +115,8 @@ bool PlayerItemController::consumeMine() { bool PlayerItemController::collectMine( std::int32_t maximum_count) { - if (mine_count_ >= std::max(maximum_count, 0)) { + if (mine_count_ >= + std::max(maximum_count, std::int32_t{0})) { return false; } ++mine_count_; diff --git a/src/SF_EXE/world/player_land_mine.cpp b/src/SF_EXE/world/player_land_mine.cpp index 37d91043..27d02c1b 100644 --- a/src/SF_EXE/world/player_land_mine.cpp +++ b/src/SF_EXE/world/player_land_mine.cpp @@ -52,7 +52,7 @@ CombatPacket minePacket( packet.write(1, 0); packet.write(2, source_character_number); packet.write(3, 0); - packet.write(4, std::max(damage, 1)); + packet.write(4, std::max(damage, std::int32_t{1})); packet.write(31, player_level); packet.write(34, random.next() % 4 + 21000); packet.write(35, 8); @@ -74,12 +74,13 @@ std::int32_t mineDamage( std::int32_t player_level, std::int32_t bonus) { const TableData* damage = tables.find(23); - const std::int32_t row = std::max(player_level - 1, 0); + const std::int32_t row = + std::max(player_level - 1, std::int32_t{0}); return std::max( damage && damage->contains(row, 0) ? retailAdd(damage->value(row, 0), bonus) : retailAdd(1, bonus), - 1); + std::int32_t{1}); } bool mineTriggered( @@ -132,7 +133,7 @@ bool PlayerLandMineSystem::place( } mines_.push_back({ position, - std::max(player_level, 1), + std::max(player_level, std::int32_t{1}), source_character_number, }); PlayerLandMineVisual visual; @@ -158,7 +159,7 @@ void PlayerLandMineSystem::addAnimatedVisual( visual.position = position; visual.previous_position = position; visual.judgement = judgement; - visual.lifetime = std::max(lifetime, 1); + visual.lifetime = std::max(lifetime, std::int32_t{1}); visual.vertical_velocity = vertical_velocity; visual.vertical_acceleration = vertical_acceleration; visuals_.push_back(visual); diff --git a/src/SF_EXE/world/player_moon_spell.cpp b/src/SF_EXE/world/player_moon_spell.cpp index 7abe5798..fb72b768 100644 --- a/src/SF_EXE/world/player_moon_spell.cpp +++ b/src/SF_EXE/world/player_moon_spell.cpp @@ -39,18 +39,18 @@ CompanionProfile applyPlayerMoonCompanionModifiers( result.attack_speed_rating = std::clamp( adjustedParameter( base.attack_speed_rating, *table, 1, column), - 0, - 255); + std::int32_t{0}, + std::int32_t{255}); const std::int32_t walking_speed_raw = std::clamp( adjustedParameter( base.walking_speed_raw, *table, 2, column), - 0, - 255); + std::int32_t{0}, + std::int32_t{255}); const std::int32_t running_speed_raw = std::clamp( adjustedParameter( base.running_speed_raw, *table, 3, column), - 0, - 255); + std::int32_t{0}, + std::int32_t{255}); result.walking_speed_raw = walking_speed_raw; result.running_speed_raw = running_speed_raw; result.walking_speed = walking_speed_raw / 5; @@ -58,42 +58,42 @@ CompanionProfile applyPlayerMoonCompanionModifiers( result.physical_attack = std::max( adjustedParameter( base.physical_attack, *table, 4, column), - 1); + std::int32_t{1}); result.maximum_life = std::max( adjustedParameter( base.maximum_life, *table, 5, column), - 1); + std::int32_t{1}); result.hit_rate = std::max( adjustedParameter(base.hit_rate, *table, 6, column), - 1); + std::int32_t{1}); result.physical_defense = std::max( adjustedParameter( base.physical_defense, *table, 7, column), - 1); + std::int32_t{1}); result.physical_evasion = std::max( adjustedParameter( base.physical_evasion, *table, 8, column), - 1); + std::int32_t{1}); result.magical_attack = std::max( adjustedParameter( base.magical_attack, *table, 9, column), - 1); + std::int32_t{1}); result.magical_hit_rate = std::max( adjustedParameter( base.magical_hit_rate, *table, 10, column), - 1); + std::int32_t{1}); result.magical_evasion = std::max( adjustedParameter( base.magical_evasion, *table, 11, column), - 1); + std::int32_t{1}); result.magical_defense = std::max( adjustedParameter( base.magical_defense, *table, 12, column), - 1); + std::int32_t{1}); result.parameter_17 = std::max( adjustedParameter( base.parameter_17, *table, 13, column), - 1); + std::int32_t{1}); return result; } diff --git a/src/SF_EXE/world/player_ranged_attack.cpp b/src/SF_EXE/world/player_ranged_attack.cpp index fbad6e81..998113bc 100644 --- a/src/SF_EXE/world/player_ranged_attack.cpp +++ b/src/SF_EXE/world/player_ranged_attack.cpp @@ -128,15 +128,15 @@ std::int32_t retailRangedPhysicalAttack( std::int32_t current_job, std::int32_t ranged_job_level) { if (current_job == kRangedJob) { - return std::max(physical_attack, 1); + return std::max(physical_attack, std::int32_t{1}); } const std::int32_t percent = std::min( ranged_job_level * 50 / 30 + 40, - 90); + std::int32_t{90}); return std::max( retailMultiply(physical_attack, percent) / 100, - 1); + std::int32_t{1}); } PlayerRangedAttackResult resolvePlayerRangedAttack( diff --git a/src/SF_EXE/world/player_runtime_profile.cpp b/src/SF_EXE/world/player_runtime_profile.cpp index 4fe74302..5786745f 100644 --- a/src/SF_EXE/world/player_runtime_profile.cpp +++ b/src/SF_EXE/world/player_runtime_profile.cpp @@ -29,7 +29,7 @@ std::int32_t adjustedParameter( } // namespace std::int32_t PlayerRuntimeProfile::walkingSpeedTier() const { - return std::clamp((walking_speed_raw + 32) / 32, 0, 9); + return std::clamp((walking_speed_raw + 32) / 32, std::int32_t{0}, std::int32_t{9}); } PlayerRuntimeProfile buildPlayerRuntimeProfile( @@ -46,60 +46,60 @@ PlayerRuntimeProfile buildPlayerRuntimeProfile( retailAdd( player.baseAttackSpeed(), equipment_bonuses[8]), - 0, - 255); + std::int32_t{0}, + std::int32_t{255}); result.walking_speed_raw = std::clamp( retailAdd( player.initialParameter(1), equipment_bonuses[9]), - 0, - 255); + std::int32_t{0}, + std::int32_t{255}); result.maximum_life = - std::max(player.baseMaximumLife(), 1); + std::max(player.baseMaximumLife(), std::int32_t{1}); result.maximum_mana = - std::max(player.baseMaximumMana(), 1); + std::max(player.baseMaximumMana(), std::int32_t{1}); result.weight_capacity = - std::max(player.baseWeightCapacity(), 0); + std::max(player.baseWeightCapacity(), std::int32_t{0}); result.physical_attack = std::max( retailAdd( player.basePhysicalAttack(), equipment_bonuses[0]), - 1); + std::int32_t{1}); result.physical_defense = std::max( retailAdd( player.basePhysicalDefense(), equipment_bonuses[2]), - 1); + std::int32_t{1}); result.hit_rate = std::max( retailAdd( player.baseHitRate(), equipment_bonuses[1]), - 1); + std::int32_t{1}); result.physical_evasion = std::max( retailAdd( player.baseEvasionRate(), equipment_bonuses[3]), - 1); + std::int32_t{1}); result.magical_attack = std::max( retailAdd( player.baseMagicalAttack(), equipment_bonuses[4]), - 1); + std::int32_t{1}); result.magical_defense = std::max( retailAdd( player.baseMagicalDefense(), equipment_bonuses[6]), - 1); + std::int32_t{1}); result.magical_hit_rate = std::max( retailAdd( player.baseMagicalHitRate(), equipment_bonuses[5]), - 1); + std::int32_t{1}); result.magical_evasion = std::max( retailAdd( player.baseMagicalEvasionRate(), equipment_bonuses[7]), - 1); + std::int32_t{1}); const TableData* table = tables.find(kBerserkerTable); const std::int32_t column = berserker.effectiveLevel() - 1; @@ -111,53 +111,53 @@ PlayerRuntimeProfile buildPlayerRuntimeProfile( result.attack_speed_raw = std::clamp( adjustedParameter( result.attack_speed_raw, *table, 1, column), - 0, - 255); + std::int32_t{0}, + std::int32_t{255}); result.walking_speed_raw = std::clamp( adjustedParameter( result.walking_speed_raw, *table, 2, column), - 0, - 255); + std::int32_t{0}, + std::int32_t{255}); result.maximum_life = std::max( adjustedParameter( result.maximum_life, *table, 3, column), - 1); + std::int32_t{1}); result.maximum_mana = std::max( adjustedParameter( result.maximum_mana, *table, 4, column), - 1); + std::int32_t{1}); result.physical_attack = std::max( adjustedParameter( result.physical_attack, *table, 5, column), - 1); + std::int32_t{1}); result.physical_defense = std::max( adjustedParameter( result.physical_defense, *table, 6, column), - 1); + std::int32_t{1}); result.hit_rate = std::max( adjustedParameter( result.hit_rate, *table, 7, column), - 1); + std::int32_t{1}); result.physical_evasion = std::max( adjustedParameter( result.physical_evasion, *table, 8, column), - 1); + std::int32_t{1}); result.magical_attack = std::max( adjustedParameter( result.magical_attack, *table, 9, column), - 1); + std::int32_t{1}); result.magical_defense = std::max( adjustedParameter( result.magical_defense, *table, 10, column), - 1); + std::int32_t{1}); result.magical_hit_rate = std::max( adjustedParameter( result.magical_hit_rate, *table, 11, column), - 1); + std::int32_t{1}); result.magical_evasion = std::max( adjustedParameter( result.magical_evasion, *table, 12, column), - 1); + std::int32_t{1}); return result; } diff --git a/src/SF_EXE/world/player_spell_action.cpp b/src/SF_EXE/world/player_spell_action.cpp index c29d50af..491f3a59 100644 --- a/src/SF_EXE/world/player_spell_action.cpp +++ b/src/SF_EXE/world/player_spell_action.cpp @@ -291,7 +291,7 @@ double retailPlayerSpellAnimationSpeed( return static_cast(base) * kSpellSpeedFactors[ static_cast( - std::clamp(speed_tier, 0, 9))] * + std::clamp(speed_tier, std::int32_t{0}, std::int32_t{9}))] * 0.001; } @@ -521,7 +521,7 @@ PlayerSpellActionController::effectDelay() const { void PlayerSpellActionController::refreshSpeed( std::int32_t speed_tier, const TableData* speed_table) { - speed_tier_ = std::clamp(speed_tier, 0, 9); + speed_tier_ = std::clamp(speed_tier, std::int32_t{0}, std::int32_t{9}); animation_speed_ = action_ == PlayerSpellAction::sonic_blade ? retailPlayerSonicBladeAnimationSpeed( @@ -543,7 +543,7 @@ void PlayerSpellActionController::selectRenderedFrame() { if (displayed_frame_ < timing_.first_frame_count) { animation_chart_ = timing_.first_chart; animation_frame_ = std::max( - displayed_frame_, 0); + displayed_frame_, std::int32_t{0}); return; } if (action_ == PlayerSpellAction::sonic_blade) { diff --git a/src/SF_EXE/world/player_spell_parameters.cpp b/src/SF_EXE/world/player_spell_parameters.cpp index 9b1df8a7..cd0f6bef 100644 --- a/src/SF_EXE/world/player_spell_parameters.cpp +++ b/src/SF_EXE/world/player_spell_parameters.cpp @@ -30,9 +30,9 @@ PlayerSpellParameters playerSpellParameters( if (increased_power) { level += 2; } - level = std::clamp(level, 1, 20); + level = std::clamp(level, std::int32_t{1}, std::int32_t{20}); result.effective_level = std::clamp( - level + magic_level_modifier, 1, 30); + level + magic_level_modifier, std::int32_t{1}, std::int32_t{30}); const std::int32_t base_mana = retailEffectParameter( @@ -47,20 +47,20 @@ PlayerSpellParameters playerSpellParameters( base_mana - equipment.instanceParameterBonus( 19, items), - 1); + std::int32_t{1}); result.effect_value = std::max( retailEffectParameter( tables, spell, result.effective_level, 0), - 0); + std::int32_t{0}); result.maximum_level = magic.level(spell) == 20; if (!result.maximum_level) { const TableData* experience = tables.find(27); const std::int32_t column = - std::clamp(magic.level(spell), 1, 20) - 1; + std::clamp(magic.level(spell), std::int32_t{1}, std::int32_t{20}) - 1; if (experience && experience->contains(spell, column)) { result.experience_threshold = diff --git a/src/SF_EXE/world/player_transport_spell.cpp b/src/SF_EXE/world/player_transport_spell.cpp index b00c438a..14f33ffe 100644 --- a/src/SF_EXE/world/player_transport_spell.cpp +++ b/src/SF_EXE/world/player_transport_spell.cpp @@ -183,8 +183,11 @@ PlayerTransportSpell::updatePresentation( if (beam.delay > presentation_counter_) { continue; } - beam.height = std::max(beam.height - 50, 0); - beam.strength = std::min(beam.strength + 200, 1000); + beam.height = std::max( + beam.height - std::int32_t{50}, std::int32_t{0}); + beam.strength = std::min( + beam.strength + std::int32_t{200}, + std::int32_t{1000}); } ++presentation_counter_; return result; diff --git a/src/SF_EXE/world/player_voice.cpp b/src/SF_EXE/world/player_voice.cpp index 6e360e5d..9b90e31c 100644 --- a/src/SF_EXE/world/player_voice.cpp +++ b/src/SF_EXE/world/player_voice.cpp @@ -15,7 +15,10 @@ std::int32_t retailPlayerComboVoiceSample( // The three linked right-click actions use consecutive Voice00 samples. // Retail stores female as zero and male as one. const std::int32_t first = retail_gender == 1 ? 96 : 99; - return first + std::clamp(combo_step, 0, 2); + return first + std::clamp( + combo_step, + std::int32_t{0}, + std::int32_t{2}); } std::int32_t retailPlayerDeathVoiceSample( diff --git a/src/SF_EXE/world/retail_save_mines.cpp b/src/SF_EXE/world/retail_save_mines.cpp index c73a8e08..ed066a83 100644 --- a/src/SF_EXE/world/retail_save_mines.cpp +++ b/src/SF_EXE/world/retail_save_mines.cpp @@ -62,7 +62,8 @@ bool restoreRetailMineCount( } if (companion_progress_end == suffix_end) { if (extension.has_mine_count) { - mine_count = std::max(extension.mine_count, 0); + mine_count = std::max( + extension.mine_count, std::int32_t{0}); } if (serialized_end) { *serialized_end = companion_progress_end; @@ -83,7 +84,7 @@ bool restoreRetailMineCount( "The retail mine-count stream is truncated."); return false; } - mine_count = std::max(restored, 0); + mine_count = std::max(restored, std::int32_t{0}); if (serialized_end) { *serialized_end = companion_progress_end + 4u; } @@ -109,7 +110,7 @@ bool replaceRetailMineCount( "The retail mine stream begins outside the save payload."); return false; } - mine_count = std::max(mine_count, 0); + mine_count = std::max(mine_count, std::int32_t{0}); std::size_t end = companion_progress_end + 4u; if (companion_progress_end == suffix_end) { payload.insert( diff --git a/src/SF_EXE/world/runtime_effect_actor.cpp b/src/SF_EXE/world/runtime_effect_actor.cpp index afed3fab..8b69751d 100644 --- a/src/SF_EXE/world/runtime_effect_actor.cpp +++ b/src/SF_EXE/world/runtime_effect_actor.cpp @@ -240,7 +240,7 @@ RuntimeEffectActorUpdate RuntimeEffectActor::update( const std::int32_t remaining = std::max( request_.target_approach_updates - movement_counter_, - 0); + std::int32_t{0}); const std::int32_t offset = retailMultiply( remaining, request_.travel_speed) / diff --git a/src/SF_EXE/world/runtime_effect_system.cpp b/src/SF_EXE/world/runtime_effect_system.cpp index 0a8368cc..a69106c5 100644 --- a/src/SF_EXE/world/runtime_effect_system.cpp +++ b/src/SF_EXE/world/runtime_effect_system.cpp @@ -71,12 +71,12 @@ bool RuntimeEffectSystem::queue( } bool RuntimeEffectSystem::queueActor( - RuntimeEffectActorSpawnRequest request) { + const RuntimeEffectActorSpawnRequest& request) { if (request.resource_id < 0 && request.visible) { return false; } - pending_actors_.push_back(std::move(request)); + pending_actors_.push_back(request); return true; } diff --git a/src/SF_EXE/world/runtime_effect_system.hpp b/src/SF_EXE/world/runtime_effect_system.hpp index 92e5f7f8..7220491c 100644 --- a/src/SF_EXE/world/runtime_effect_system.hpp +++ b/src/SF_EXE/world/runtime_effect_system.hpp @@ -64,7 +64,7 @@ class RuntimeEffectSystem { const CombatEffectSpawnRequest& request, const TableDatabase* tables = nullptr); bool queueActor( - RuntimeEffectActorSpawnRequest request); + const RuntimeEffectActorSpawnRequest& request); RuntimeEffectSystemUpdate update( const RuntimeEffectSystemContext& context); diff --git a/src/SF_EXE/world/scenario_screen_particles.cpp b/src/SF_EXE/world/scenario_screen_particles.cpp index 7266e504..4142206d 100644 --- a/src/SF_EXE/world/scenario_screen_particles.cpp +++ b/src/SF_EXE/world/scenario_screen_particles.cpp @@ -16,7 +16,8 @@ std::int32_t retailProjection(double value) { } std::int32_t clampColor(std::int32_t value) { - return std::clamp(value, 0, 255); + return std::clamp( + value, std::int32_t{0}, std::int32_t{255}); } } // namespace diff --git a/src/SF_EXE/world/scenario_visual_presentation.cpp b/src/SF_EXE/world/scenario_visual_presentation.cpp index b6a83b8c..bfcaf6cb 100644 --- a/src/SF_EXE/world/scenario_visual_presentation.cpp +++ b/src/SF_EXE/world/scenario_visual_presentation.cpp @@ -86,7 +86,8 @@ std::int32_t ScenarioVisualPresentation::fadeStrength() const { if (counter_ >= kFadeUpdates) { return 1000; } - return std::max(counter_, 0) * 1000 / kFadeUpdates; + return std::max(counter_, std::int32_t{0}) * + std::int32_t{1000} / kFadeUpdates; } bool ScenarioVisualPresentation::continueVisible() const { @@ -96,7 +97,9 @@ bool ScenarioVisualPresentation::continueVisible() const { std::int32_t ScenarioVisualPresentation::continueOffset() const { const std::int32_t phase = - (std::max(counter_, 0) + 1) % 15; + (std::max(counter_, std::int32_t{0}) + + std::int32_t{1}) % + std::int32_t{15}; if (phase < 5) { return 0; } diff --git a/src/SF_EXE/world/scenario_world.cpp b/src/SF_EXE/world/scenario_world.cpp index 1bf62982..5b9112b5 100644 --- a/src/SF_EXE/world/scenario_world.cpp +++ b/src/SF_EXE/world/scenario_world.cpp @@ -1,541 +1,542 @@ -#include "scenario_world.hpp" - -#include "resources/resource_memory.hpp" - -#include -#include -#include -#include -#include -#include -#include - -namespace osf { -namespace { - -void setError(std::string* error, std::string message) { - if (error) { - *error = std::move(message); - } -} - -bool endsWithIgnoreCase( - const std::string& value, - const std::string& suffix) { - if (value.size() < suffix.size()) { - return false; - } - return std::equal( - suffix.rbegin(), - suffix.rend(), - value.rbegin(), - [](char left, char right) { - return std::tolower( - static_cast(left)) == - std::tolower( - static_cast(right)); - }); -} - -void enablePattern( - std::vector& patterns, - std::int32_t pattern) { - if (pattern < 0) { - return; - } - const std::size_t index = static_cast(pattern); - if (patterns.size() <= index) { - patterns.resize(index + 1, 0); - } - patterns[index] = 1; -} - -std::vector readPatternList( - const std::filesystem::path& path) { - std::ifstream file(path); - std::vector result; - std::string line; - while (std::getline(file, line)) { - if (!line.empty() && line.back() == '\r') { - line.pop_back(); - } - result.push_back(std::move(line)); - } - return result; -} - -std::string mapStem(const std::string& map_path) { - std::string normalized = map_path; - std::replace(normalized.begin(), normalized.end(), '\\', '/'); - return std::filesystem::path(normalized).stem().string(); -} - -std::filesystem::path scenarioDirectory( - const std::filesystem::path& data_root, - std::int32_t scenario_id) { - char directory[16]{}; - std::snprintf( - directory, - sizeof(directory), - "%08d", - scenario_id); - return data_root / "Scenario" / directory; -} - -bool validStart(const ScenarioStart& start) { - return start.scenario_id >= 0 && - start.entry_value >= 0 && - start.local_player_number >= 0 && - start.local_player_number <= 3 && - start.entry_value <= - (std::numeric_limits::max() - - start.local_player_number) / - 4; -} - -} // namespace - -bool ScenarioWorld::load( - const std::filesystem::path& data_root, - const ScenarioStart& start, - const AiControlDatabase& ai_control, - RetailRandom& item_random, - std::string* error) { - clear(); - if (error) { - error->clear(); - } - if (!validStart(start)) { - setError(error, "The scenario start request is invalid."); - return false; - } - const std::filesystem::path scenario_root = - scenarioDirectory(data_root, start.scenario_id); - if (!data_.load( - scenario_root / "Scenario.Mct", - error) || - !script_data_.load( - scenario_root / "Scenario.Scs", - error)) { - clear(); - return false; - } - - const std::int32_t entry_key = - start.local_player_number + - start.entry_value * 4; - const ScenarioEntry* selected_entry = - data_.findEntry(entry_key); - if (!selected_entry) { - setError( - error, - "The scenario does not contain entry key " + - std::to_string(entry_key) + "."); - clear(); - return false; - } - - const std::string map_name = mapStem(data_.mapPath()); - if (map_name.empty()) { - setError(error, "The scenario does not name a map."); - clear(); - return false; - } - const std::filesystem::path map_root = data_root / "Map"; - if (!ground_.load( - map_root / "Ground" / (map_name + ".Gnd"), - error) || - !object_map_.load( - map_root / "Object" / (map_name + ".Obl"), - error) || - !map_overview_patterns_.load( - scenario_root / "Scenario.Njp", - error) || - !map_exploration_.initialize(ground_)) { - if (error && error->empty()) { - *error = "The scenario map could not be prepared."; - } - clear(); - return false; - } - - const std::vector pattern_names = - readPatternList( - map_root / "Pattern" / (map_name + ".Lst")); - if (pattern_names.empty()) { - setError(error, "The map pattern list could not be read."); - clear(); - return false; - } - std::vector> pattern_selection( - pattern_names.size()); - for (std::int32_t y = 0; y < ground_.height(); ++y) { - for (std::int32_t x = 0; x < ground_.width(); ++x) { - const GroundCell* cell = ground_.cell(x, y); - if (!cell || cell->pattern_set < 0 || - static_cast(cell->pattern_set) >= - pattern_selection.size()) { - continue; - } - enablePattern( - pattern_selection[static_cast( - cell->pattern_set)], - cell->pattern); - } - } - for (const MapObject& object : object_map_.objects()) { - if (object.pattern_set < 0 || - static_cast(object.pattern_set) >= - pattern_selection.size()) { - continue; - } - const std::size_t normal_set = - static_cast(object.pattern_set); - enablePattern( - pattern_selection[normal_set], object.pattern); - - // Retail pairs an object's normal pattern set with the following - // shadow sheet. Keep the same pattern from that sheet when present. - const std::size_t shadow_set = normal_set + 1; - if (shadow_set < pattern_names.size() && - endsWithIgnoreCase(pattern_names[shadow_set], ".sdw")) { - enablePattern( - pattern_selection[shadow_set], object.pattern); - } - } - map_patterns_.resize(pattern_names.size()); - for (std::size_t index = 0; - index < pattern_names.size(); - ++index) { - if (!endsWithIgnoreCase(pattern_names[index], ".njp") && - !endsWithIgnoreCase(pattern_names[index], ".sdw")) { - continue; - } - if (pattern_selection[index].empty()) { - continue; - } - auto image = std::make_unique(); - std::string image_error; - if (!image->loadSelectedPatterns( - map_root / "Pattern" / pattern_names[index], - pattern_selection[index], - &image_error)) { - setError( - error, - "A map pattern could not be loaded: " + - pattern_names[index] + " (" + - image_error + ")"); - clear(); - return false; - } - map_patterns_[index] = std::move(image); - } - - for (std::int32_t resource_id : - data_.objectResourceIds()) { - std::string object_resource_error; - if (!object_visuals_.load( - data_root, - resource_id, - &object_resource_error)) { - setError( - error, - "Scenario object resource " + - std::to_string(resource_id) + - " could not be loaded: " + - object_resource_error); - clear(); - return false; - } - } - objects_.reserve(data_.objects().size()); - for (const ScenarioObject& object : data_.objects()) { - ScenarioObjectActor actor; - std::string object_error; - const ObjectVisualResource* visual = - object.resource_id < 0 - ? nullptr - : object_visuals_.find(object.resource_id); - if (!actor.initialize( - object, visual, &object_error)) { - setError( - error, - "Scenario object " + - std::to_string(object.id) + - " could not be loaded: " + - object_error); - clear(); - return false; - } - objects_.push_back(std::move(actor)); - } - - people_.reserve(data_.people().size()); - for (const ScenarioPerson& person : data_.people()) { - NpcActor actor; - std::string actor_error; - const CharacterVisualResource* visual = - people_visuals_.load( - data_root, - person.resource_id, - &actor_error); - if (!visual || - !actor.initialize( - person, *visual, &actor_error)) { - setError( - error, - "Scenario person " + - std::to_string(person.id) + - " could not be loaded: " + - actor_error); - clear(); - return false; - } - people_.push_back(std::move(actor)); - } - - enemies_.reserve(data_.enemies().size()); - for (const ScenarioEnemy& enemy : - data_.enemies()) { - EnemyActor actor; - std::string actor_error; - const CharacterVisualResource* visual = - enemy.resource_id < 0 - ? nullptr - : enemy_visuals_.load( - data_root, - enemy.resource_id, - &actor_error); - const AiControlList* control = - ai_control.find(enemy.ai_control_name); - const std::int32_t control_index = - ai_control.indexOf(control); - if (!control) { - actor_error = - "The AI-control list could not be resolved."; - } - if ((enemy.resource_id >= 0 && !visual) || - !control || - !actor.initialize( - enemy, - visual, - *control, - control_index, - &actor_error)) { - setError( - error, - "Scenario enemy " + - std::to_string(enemy.id) + - " could not be loaded: " + - actor_error); - clear(); - return false; - } - const std::size_t index = enemies_.size(); - if (!enemy_indices_.emplace( - actor.characterNumber(), index).second) { - setError( - error, - "The scenario contains a duplicate enemy character " - "number."); - clear(); - return false; - } - enemies_.push_back(std::move(actor)); - } - - ground_items_.reserve(data_.items().size()); - for (const ScenarioItem& item : data_.items()) { - if (!createScenarioGroundItem( - ground_items_, item_random, item)) { - setError( - error, - "Scenario item " + - std::to_string(item.id) + - " could not be initialized."); - clear(); - return false; - } - } - - id_ = start.scenario_id; - music_track_ = data_.musicTrack(); - local_player_number_ = - start.local_player_number; - entry_value_ = start.entry_value; - entry_ = *selected_entry; - map_exploration_.reveal( - {entry_.world_x, entry_.world_y}); - if (error) { - error->clear(); - } - return true; -} - -void ScenarioWorld::clear() { - id_ = -1; - music_track_ = -1; - local_player_number_ = 0; - entry_value_ = 0; - entry_ = {}; - data_.clear(); - script_data_.clear(); - ground_.clear(); - object_map_.clear(); - map_patterns_.clear(); - object_visuals_.clear(); - people_visuals_.clear(); - enemy_visuals_.clear(); - map_overview_patterns_.clear(); - map_exploration_.clear(); - objects_.clear(); - people_.clear(); - enemies_.clear(); - enemy_indices_.clear(); - ground_items_.clear(); -} - -std::uint64_t ScenarioWorld::resourceMemoryUsageBytes() const { - std::uint64_t bytes = ground_.memoryUsageBytes() + - object_map_.memoryUsageBytes() + - decodedMemoryUsageBytes(map_overview_patterns_) + - map_exploration_.memoryUsageBytes() + - object_visuals_.memoryUsageBytes() + - people_visuals_.memoryUsageBytes() + - enemy_visuals_.memoryUsageBytes(); - for (const auto& patterns : map_patterns_) { - if (patterns) { - bytes += decodedMemoryUsageBytes(*patterns); - } - } - return bytes; -} - -std::int32_t ScenarioWorld::id() const { - return id_; -} - -std::int32_t ScenarioWorld::musicTrack() const { - return music_track_; -} - -std::int32_t ScenarioWorld::localPlayerNumber() const { - return local_player_number_; -} - -std::int32_t ScenarioWorld::entryValue() const { - return entry_value_; -} - -const ScenarioEntry& ScenarioWorld::entry() const { - return entry_; -} - -void ScenarioWorld::setEntry( - std::int32_t entry_value, - const ScenarioEntry& entry) { - entry_value_ = entry_value; - entry_ = entry; -} - -const ScenarioData& ScenarioWorld::data() const { - return data_; -} - -ScenarioData& ScenarioWorld::data() { - return data_; -} - -script::ScriptData ScenarioWorld::takeScriptData() { - return std::move(script_data_); -} - -const GroundMap& ScenarioWorld::ground() const { - return ground_; -} - -GroundMap& ScenarioWorld::ground() { - return ground_; -} - -const ObjectMap& ScenarioWorld::objectMap() const { - return object_map_; -} - -ObjectMap& ScenarioWorld::objectMap() { - return object_map_; -} - -const std::vector>& -ScenarioWorld::mapPatterns() const { - return map_patterns_; -} - -const gapi::NjpImage& -ScenarioWorld::mapOverviewPatterns() const { - return map_overview_patterns_; -} - -MapExploration& ScenarioWorld::mapExploration() { - return map_exploration_; -} - -const MapExploration& -ScenarioWorld::mapExploration() const { - return map_exploration_; -} - -std::vector& -ScenarioWorld::objects() { - return objects_; -} - -const std::vector& -ScenarioWorld::objects() const { - return objects_; -} - -std::vector& ScenarioWorld::people() { - return people_; -} - -const std::vector& -ScenarioWorld::people() const { - return people_; -} - -std::vector& ScenarioWorld::enemies() { - return enemies_; -} - -const std::vector& -ScenarioWorld::enemies() const { - return enemies_; -} - -EnemyActor* ScenarioWorld::findEnemyByCharacterNumber( - std::int32_t character_number) { - const auto found = enemy_indices_.find(character_number); - return found == enemy_indices_.end() - ? nullptr - : &enemies_[found->second]; -} - -const EnemyActor* ScenarioWorld::findEnemyByCharacterNumber( - std::int32_t character_number) const { - const auto found = enemy_indices_.find(character_number); - return found == enemy_indices_.end() - ? nullptr - : &enemies_[found->second]; -} - -std::vector& ScenarioWorld::groundItems() { - return ground_items_; -} - -const std::vector& -ScenarioWorld::groundItems() const { - return ground_items_; -} - -} // namespace osf +#include "scenario_world.hpp" + +#include "resources/resource_memory.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace osf { +namespace { + +void setError(std::string* error, std::string message) { + if (error) { + *error = std::move(message); + } +} + +bool endsWithIgnoreCase( + const std::string& value, + const std::string& suffix) { + if (value.size() < suffix.size()) { + return false; + } + return std::equal( + suffix.rbegin(), + suffix.rend(), + value.rbegin(), + [](char left, char right) { + return std::tolower( + static_cast(left)) == + std::tolower( + static_cast(right)); + }); +} + +void enablePattern( + std::vector& patterns, + std::int32_t pattern) { + if (pattern < 0) { + return; + } + const std::size_t index = static_cast(pattern); + if (patterns.size() <= index) { + patterns.resize(index + 1, 0); + } + patterns[index] = 1; +} + +std::vector readPatternList( + const std::filesystem::path& path) { + std::ifstream file(path); + std::vector result; + std::string line; + while (std::getline(file, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + result.push_back(std::move(line)); + } + return result; +} + +std::string mapStem(const std::string& map_path) { + std::string normalized = map_path; + std::replace(normalized.begin(), normalized.end(), '\\', '/'); + return std::filesystem::path(normalized).stem().string(); +} + +std::filesystem::path scenarioDirectory( + const std::filesystem::path& data_root, + std::int32_t scenario_id) { + char directory[16]{}; + std::snprintf( + directory, + sizeof(directory), + "%08" PRId32, + scenario_id); + return data_root / "Scenario" / directory; +} + +bool validStart(const ScenarioStart& start) { + return start.scenario_id >= 0 && + start.entry_value >= 0 && + start.local_player_number >= 0 && + start.local_player_number <= 3 && + start.entry_value <= + (std::numeric_limits::max() - + start.local_player_number) / + 4; +} + +} // namespace + +bool ScenarioWorld::load( + const std::filesystem::path& data_root, + const ScenarioStart& start, + const AiControlDatabase& ai_control, + RetailRandom& item_random, + std::string* error) { + clear(); + if (error) { + error->clear(); + } + if (!validStart(start)) { + setError(error, "The scenario start request is invalid."); + return false; + } + const std::filesystem::path scenario_root = + scenarioDirectory(data_root, start.scenario_id); + if (!data_.load( + scenario_root / "Scenario.Mct", + error) || + !script_data_.load( + scenario_root / "Scenario.Scs", + error)) { + clear(); + return false; + } + + const std::int32_t entry_key = + start.local_player_number + + start.entry_value * 4; + const ScenarioEntry* selected_entry = + data_.findEntry(entry_key); + if (!selected_entry) { + setError( + error, + "The scenario does not contain entry key " + + std::to_string(entry_key) + "."); + clear(); + return false; + } + + const std::string map_name = mapStem(data_.mapPath()); + if (map_name.empty()) { + setError(error, "The scenario does not name a map."); + clear(); + return false; + } + const std::filesystem::path map_root = data_root / "Map"; + if (!ground_.load( + map_root / "Ground" / (map_name + ".Gnd"), + error) || + !object_map_.load( + map_root / "Object" / (map_name + ".Obl"), + error) || + !map_overview_patterns_.load( + scenario_root / "Scenario.Njp", + error) || + !map_exploration_.initialize(ground_)) { + if (error && error->empty()) { + *error = "The scenario map could not be prepared."; + } + clear(); + return false; + } + + const std::vector pattern_names = + readPatternList( + map_root / "Pattern" / (map_name + ".Lst")); + if (pattern_names.empty()) { + setError(error, "The map pattern list could not be read."); + clear(); + return false; + } + std::vector> pattern_selection( + pattern_names.size()); + for (std::int32_t y = 0; y < ground_.height(); ++y) { + for (std::int32_t x = 0; x < ground_.width(); ++x) { + const GroundCell* cell = ground_.cell(x, y); + if (!cell || cell->pattern_set < 0 || + static_cast(cell->pattern_set) >= + pattern_selection.size()) { + continue; + } + enablePattern( + pattern_selection[static_cast( + cell->pattern_set)], + cell->pattern); + } + } + for (const MapObject& object : object_map_.objects()) { + if (object.pattern_set < 0 || + static_cast(object.pattern_set) >= + pattern_selection.size()) { + continue; + } + const std::size_t normal_set = + static_cast(object.pattern_set); + enablePattern( + pattern_selection[normal_set], object.pattern); + + // Retail pairs an object's normal pattern set with the following + // shadow sheet. Keep the same pattern from that sheet when present. + const std::size_t shadow_set = normal_set + 1; + if (shadow_set < pattern_names.size() && + endsWithIgnoreCase(pattern_names[shadow_set], ".sdw")) { + enablePattern( + pattern_selection[shadow_set], object.pattern); + } + } + map_patterns_.resize(pattern_names.size()); + for (std::size_t index = 0; + index < pattern_names.size(); + ++index) { + if (!endsWithIgnoreCase(pattern_names[index], ".njp") && + !endsWithIgnoreCase(pattern_names[index], ".sdw")) { + continue; + } + if (pattern_selection[index].empty()) { + continue; + } + auto image = std::make_unique(); + std::string image_error; + if (!image->loadSelectedPatterns( + map_root / "Pattern" / pattern_names[index], + pattern_selection[index], + &image_error)) { + setError( + error, + "A map pattern could not be loaded: " + + pattern_names[index] + " (" + + image_error + ")"); + clear(); + return false; + } + map_patterns_[index] = std::move(image); + } + + for (std::int32_t resource_id : + data_.objectResourceIds()) { + std::string object_resource_error; + if (!object_visuals_.load( + data_root, + resource_id, + &object_resource_error)) { + setError( + error, + "Scenario object resource " + + std::to_string(resource_id) + + " could not be loaded: " + + object_resource_error); + clear(); + return false; + } + } + objects_.reserve(data_.objects().size()); + for (const ScenarioObject& object : data_.objects()) { + ScenarioObjectActor actor; + std::string object_error; + const ObjectVisualResource* visual = + object.resource_id < 0 + ? nullptr + : object_visuals_.find(object.resource_id); + if (!actor.initialize( + object, visual, &object_error)) { + setError( + error, + "Scenario object " + + std::to_string(object.id) + + " could not be loaded: " + + object_error); + clear(); + return false; + } + objects_.push_back(std::move(actor)); + } + + people_.reserve(data_.people().size()); + for (const ScenarioPerson& person : data_.people()) { + NpcActor actor; + std::string actor_error; + const CharacterVisualResource* visual = + people_visuals_.load( + data_root, + person.resource_id, + &actor_error); + if (!visual || + !actor.initialize( + person, *visual, &actor_error)) { + setError( + error, + "Scenario person " + + std::to_string(person.id) + + " could not be loaded: " + + actor_error); + clear(); + return false; + } + people_.push_back(std::move(actor)); + } + + enemies_.reserve(data_.enemies().size()); + for (const ScenarioEnemy& enemy : + data_.enemies()) { + EnemyActor actor; + std::string actor_error; + const CharacterVisualResource* visual = + enemy.resource_id < 0 + ? nullptr + : enemy_visuals_.load( + data_root, + enemy.resource_id, + &actor_error); + const AiControlList* control = + ai_control.find(enemy.ai_control_name); + const std::int32_t control_index = + ai_control.indexOf(control); + if (!control) { + actor_error = + "The AI-control list could not be resolved."; + } + if ((enemy.resource_id >= 0 && !visual) || + !control || + !actor.initialize( + enemy, + visual, + *control, + control_index, + &actor_error)) { + setError( + error, + "Scenario enemy " + + std::to_string(enemy.id) + + " could not be loaded: " + + actor_error); + clear(); + return false; + } + const std::size_t index = enemies_.size(); + if (!enemy_indices_.emplace( + actor.characterNumber(), index).second) { + setError( + error, + "The scenario contains a duplicate enemy character " + "number."); + clear(); + return false; + } + enemies_.push_back(std::move(actor)); + } + + ground_items_.reserve(data_.items().size()); + for (const ScenarioItem& item : data_.items()) { + if (!createScenarioGroundItem( + ground_items_, item_random, item)) { + setError( + error, + "Scenario item " + + std::to_string(item.id) + + " could not be initialized."); + clear(); + return false; + } + } + + id_ = start.scenario_id; + music_track_ = data_.musicTrack(); + local_player_number_ = + start.local_player_number; + entry_value_ = start.entry_value; + entry_ = *selected_entry; + map_exploration_.reveal( + {entry_.world_x, entry_.world_y}); + if (error) { + error->clear(); + } + return true; +} + +void ScenarioWorld::clear() { + id_ = -1; + music_track_ = -1; + local_player_number_ = 0; + entry_value_ = 0; + entry_ = {}; + data_.clear(); + script_data_.clear(); + ground_.clear(); + object_map_.clear(); + map_patterns_.clear(); + object_visuals_.clear(); + people_visuals_.clear(); + enemy_visuals_.clear(); + map_overview_patterns_.clear(); + map_exploration_.clear(); + objects_.clear(); + people_.clear(); + enemies_.clear(); + enemy_indices_.clear(); + ground_items_.clear(); +} + +std::uint64_t ScenarioWorld::resourceMemoryUsageBytes() const { + std::uint64_t bytes = ground_.memoryUsageBytes() + + object_map_.memoryUsageBytes() + + decodedMemoryUsageBytes(map_overview_patterns_) + + map_exploration_.memoryUsageBytes() + + object_visuals_.memoryUsageBytes() + + people_visuals_.memoryUsageBytes() + + enemy_visuals_.memoryUsageBytes(); + for (const auto& patterns : map_patterns_) { + if (patterns) { + bytes += decodedMemoryUsageBytes(*patterns); + } + } + return bytes; +} + +std::int32_t ScenarioWorld::id() const { + return id_; +} + +std::int32_t ScenarioWorld::musicTrack() const { + return music_track_; +} + +std::int32_t ScenarioWorld::localPlayerNumber() const { + return local_player_number_; +} + +std::int32_t ScenarioWorld::entryValue() const { + return entry_value_; +} + +const ScenarioEntry& ScenarioWorld::entry() const { + return entry_; +} + +void ScenarioWorld::setEntry( + std::int32_t entry_value, + const ScenarioEntry& entry) { + entry_value_ = entry_value; + entry_ = entry; +} + +const ScenarioData& ScenarioWorld::data() const { + return data_; +} + +ScenarioData& ScenarioWorld::data() { + return data_; +} + +script::ScriptData ScenarioWorld::takeScriptData() { + return std::move(script_data_); +} + +const GroundMap& ScenarioWorld::ground() const { + return ground_; +} + +GroundMap& ScenarioWorld::ground() { + return ground_; +} + +const ObjectMap& ScenarioWorld::objectMap() const { + return object_map_; +} + +ObjectMap& ScenarioWorld::objectMap() { + return object_map_; +} + +const std::vector>& +ScenarioWorld::mapPatterns() const { + return map_patterns_; +} + +const gapi::NjpImage& +ScenarioWorld::mapOverviewPatterns() const { + return map_overview_patterns_; +} + +MapExploration& ScenarioWorld::mapExploration() { + return map_exploration_; +} + +const MapExploration& +ScenarioWorld::mapExploration() const { + return map_exploration_; +} + +std::vector& +ScenarioWorld::objects() { + return objects_; +} + +const std::vector& +ScenarioWorld::objects() const { + return objects_; +} + +std::vector& ScenarioWorld::people() { + return people_; +} + +const std::vector& +ScenarioWorld::people() const { + return people_; +} + +std::vector& ScenarioWorld::enemies() { + return enemies_; +} + +const std::vector& +ScenarioWorld::enemies() const { + return enemies_; +} + +EnemyActor* ScenarioWorld::findEnemyByCharacterNumber( + std::int32_t character_number) { + const auto found = enemy_indices_.find(character_number); + return found == enemy_indices_.end() + ? nullptr + : &enemies_[found->second]; +} + +const EnemyActor* ScenarioWorld::findEnemyByCharacterNumber( + std::int32_t character_number) const { + const auto found = enemy_indices_.find(character_number); + return found == enemy_indices_.end() + ? nullptr + : &enemies_[found->second]; +} + +std::vector& ScenarioWorld::groundItems() { + return ground_items_; +} + +const std::vector& +ScenarioWorld::groundItems() const { + return ground_items_; +} + +} // namespace osf diff --git a/src/SF_EXE/world/world_pointer.cpp b/src/SF_EXE/world/world_pointer.cpp index 0daa0a20..268c1400 100644 --- a/src/SF_EXE/world/world_pointer.cpp +++ b/src/SF_EXE/world/world_pointer.cpp @@ -43,7 +43,7 @@ void WorldPointer::configure( const WorldPointerConfiguration& configuration) { configuration_ = configuration; configuration_.range = - std::clamp(configuration_.range, 0, 4); + std::clamp(configuration_.range, std::int32_t{0}, std::int32_t{4}); } void WorldPointer::reset() { diff --git a/src/SF_EXE/world/world_scene.hpp b/src/SF_EXE/world/world_scene.hpp index d1b6ec92..bf5deccc 100644 --- a/src/SF_EXE/world/world_scene.hpp +++ b/src/SF_EXE/world/world_scene.hpp @@ -440,7 +440,7 @@ class WorldScene { const PlayerLevelUpResult& result); void handleEnemyDeathStart( EnemyActor& enemy, - CombatEffectSpawnRequest effect); + const CombatEffectSpawnRequest& effect); EnemyActorUpdate updateEnemyActor( EnemyActor& enemy, const std::vector& blockers); diff --git a/src/SF_EXE/world/world_scene_combat.cpp b/src/SF_EXE/world/world_scene_combat.cpp index f5bf0574..3e0ce39f 100644 --- a/src/SF_EXE/world/world_scene_combat.cpp +++ b/src/SF_EXE/world/world_scene_combat.cpp @@ -9,7 +9,7 @@ namespace osf { void WorldScene::handleEnemyDeathStart( EnemyActor& enemy, - CombatEffectSpawnRequest effect) { + const CombatEffectSpawnRequest& effect) { constexpr std::int32_t kEpisodeOneMask = 1; const std::vector drops = createRetailEnemyDrops( @@ -51,8 +51,9 @@ void WorldScene::handleEnemyDeathStart( // Retail creates all item and Gold instances first. Its death-effect // direction is the next PRNG draw after those constructors finish. if (effect.valid) { - effect.packet_kind = item_random_.next() % 8; - queueCombatEffect(effect); + CombatEffectSpawnRequest death_effect = effect; + death_effect.packet_kind = item_random_.next() % 8; + queueCombatEffect(death_effect); } } diff --git a/src/SF_EXE/world/world_scene_effects.cpp b/src/SF_EXE/world/world_scene_effects.cpp index 88c7b9c9..9030a6b0 100644 --- a/src/SF_EXE/world/world_scene_effects.cpp +++ b/src/SF_EXE/world/world_scene_effects.cpp @@ -426,7 +426,7 @@ void WorldScene::updateRuntimeEffects() { effect_visuals_.find(resource_id); if (!visual || visual->animation().charts().empty()) { - return 1; + return std::int32_t{1}; } return std::max( visual->animation() diff --git a/src/SF_EXE/world/world_scene_interaction.cpp b/src/SF_EXE/world/world_scene_interaction.cpp index 64edbc79..d404ebc2 100644 --- a/src/SF_EXE/world/world_scene_interaction.cpp +++ b/src/SF_EXE/world/world_scene_interaction.cpp @@ -423,7 +423,8 @@ WorldScene::takeGameplayServiceRequest() { } void WorldScene::completeBlackjack(std::int32_t result) { - blackjack_result_ = std::clamp(result, 0, 2); + blackjack_result_ = std::clamp( + result, std::int32_t{0}, std::int32_t{2}); scenario_script_.runStatusKind(8); } diff --git a/src/SF_EXE/world/world_scene_player_profile.cpp b/src/SF_EXE/world/world_scene_player_profile.cpp index 60c87073..a80c9314 100644 --- a/src/SF_EXE/world/world_scene_player_profile.cpp +++ b/src/SF_EXE/world/world_scene_player_profile.cpp @@ -33,7 +33,7 @@ std::int32_t WorldScene::playerMaximumMineCount() const { return std::max( 10 + player_equipment_.instanceParameterBonus( 84, item_database_), - 1); + std::int32_t{1}); } std::int32_t WorldScene::playerMineDamageBonus() const { diff --git a/src/SF_EXE/world/world_scene_presentation.cpp b/src/SF_EXE/world/world_scene_presentation.cpp index 4d91b7ed..cd8e5139 100644 --- a/src/SF_EXE/world/world_scene_presentation.cpp +++ b/src/SF_EXE/world/world_scene_presentation.cpp @@ -1,5 +1,6 @@ #include "world_scene.hpp" +#include #include namespace osf { @@ -13,7 +14,7 @@ void WorldScene::beginScenarioVisual(std::int32_t visual_id) { std::snprintf( filename, sizeof(filename), - "Visual%02d.njp", + "Visual%02" PRId32 ".njp", visual_id); } const std::filesystem::path pattern_root = diff --git a/src/SF_EXE/world/world_scene_transport_spell.cpp b/src/SF_EXE/world/world_scene_transport_spell.cpp index b154da88..65dfd235 100644 --- a/src/SF_EXE/world/world_scene_transport_spell.cpp +++ b/src/SF_EXE/world/world_scene_transport_spell.cpp @@ -1,5 +1,6 @@ #include "world_scene.hpp" +#include #include namespace osf { @@ -12,7 +13,10 @@ std::filesystem::path scenarioPath( std::int32_t scenario_id) { char directory[16]{}; std::snprintf( - directory, sizeof(directory), "%08d", scenario_id); + directory, + sizeof(directory), + "%08" PRId32, + scenario_id); return data_root / "Scenario" / directory / "Scenario.Mct"; } diff --git a/tests/native/check_source_boundaries.cmake b/tests/native/check_source_boundaries.cmake index 2819d328..2a5fc1c5 100644 --- a/tests/native/check_source_boundaries.cmake +++ b/tests/native/check_source_boundaries.cmake @@ -147,12 +147,12 @@ endforeach() foreach(source_file IN LISTS portable_game_sources) file(READ "${source_file}" source_text) if(source_text MATCHES - "#[ \t]*include[ \t]*[<\"](windows\\.h|lwl\\.h|lal\\.h|lgl\\.h)[>\"]") + "#[ \t]*include[ \t]*[<\"](windows\\.h|lwl\\.h|lal\\.h|lgl\\.h|gsKit\\.h|dmaKit\\.h|eekernel\\.h|fileXio\\.h|audsrv\\.h)[>\"]") message(FATAL_ERROR "Platform integration escaped src/SF_EXE/runtime or libs: ${source_file}") endif() if(source_text MATCHES - "(^|[^A-Za-z0-9_])(_WIN32|WINAPI|HWND|lwl_|lal_|lgl_)") + "(^|[^A-Za-z0-9_])(_WIN32|WINAPI|HWND|lwl_|lal_|lgl_|_EE|__PS2__|PS2SDK|GSKIT)") message(FATAL_ERROR "Platform-specific code escaped src/SF_EXE/runtime or libs: ${source_file}") endif() @@ -215,12 +215,12 @@ foreach(source_file IN LISTS runtime_sources) if(NOT relative_source MATCHES "^runtime/platform/") if(source_text MATCHES - "#[ \t]*include[ \t]*[<\"](emscripten[^>\"]*|jni\\.h|android/[^>\"]*)[>\"]") + "#[ \t]*include[ \t]*[<\"](emscripten[^>\"]*|jni\\.h|android/[^>\"]*|gsKit\\.h|dmaKit\\.h|eekernel\\.h|fileXio\\.h|audsrv\\.h)[>\"]") message(FATAL_ERROR "Platform SDK header escaped runtime/platform: ${source_file}") endif() if(source_text MATCHES - "(^|[^A-Za-z0-9_])(__EMSCRIPTEN__|__ANDROID__|ANDROID|JNIEnv|_WIN32|WINAPI|HWND|__ORBIS__|__PROSPERO__|__NX__|NN_NINTENDO_SDK)([^A-Za-z0-9_]|$)") + "(^|[^A-Za-z0-9_])(__EMSCRIPTEN__|__ANDROID__|ANDROID|JNIEnv|_WIN32|WINAPI|HWND|__ORBIS__|__PROSPERO__|__NX__|NN_NINTENDO_SDK|_EE|__PS2__|PS2SDK|GSKIT)([^A-Za-z0-9_]|$)") message(FATAL_ERROR "Platform-specific code escaped runtime/platform: ${source_file}") endif() diff --git a/tests/native/enemy_effect_controller_test.cpp b/tests/native/enemy_effect_controller_test.cpp index 9ffa9827..8e3027c2 100644 --- a/tests/native/enemy_effect_controller_test.cpp +++ b/tests/native/enemy_effect_controller_test.cpp @@ -1003,7 +1003,9 @@ bool testTypeTenWaves() { static_cast(wave_count) && shake_count == static_cast( - std::min(wave_count, 11)) && + std::min( + wave_count, + std::int32_t{11})) && random.state() == 1 && wave_updates.size() == static_cast(wave_count) && diff --git a/tests/native/player_spell_cast_test.cpp b/tests/native/player_spell_cast_test.cpp index 98b083cf..925197c8 100644 --- a/tests/native/player_spell_cast_test.cpp +++ b/tests/native/player_spell_cast_test.cpp @@ -675,7 +675,7 @@ bool testRetailHealResolution( const std::int32_t expected_amount = std::min( heal_percent * 140 / 100, - 90); + std::int32_t{90}); if (!check( damaged.valid && damaged.healed_amount == expected_amount && @@ -774,35 +774,35 @@ bool testRetailMoonRules( }; if (!check( modified.attack_speed_rating == - std::clamp(adjusted(90, 1), 0, 255) && + std::clamp(adjusted(90, 1), std::int32_t{0}, std::int32_t{255}) && modified.walking_speed_raw == - std::clamp(adjusted(105, 2), 0, 255) && + std::clamp(adjusted(105, 2), std::int32_t{0}, std::int32_t{255}) && modified.running_speed_raw == - std::clamp(adjusted(155, 3), 0, 255) && + std::clamp(adjusted(155, 3), std::int32_t{0}, std::int32_t{255}) && modified.walking_speed == modified.walking_speed_raw / 5 && modified.running_speed == modified.running_speed_raw / 5 && modified.physical_attack == - std::max(adjusted(40, 4), 1) && + std::max(adjusted(40, 4), std::int32_t{1}) && modified.maximum_life == - std::max(adjusted(120, 5), 1) && + std::max(adjusted(120, 5), std::int32_t{1}) && modified.hit_rate == - std::max(adjusted(50, 6), 1) && + std::max(adjusted(50, 6), std::int32_t{1}) && modified.physical_defense == - std::max(adjusted(30, 7), 1) && + std::max(adjusted(30, 7), std::int32_t{1}) && modified.physical_evasion == - std::max(adjusted(25, 8), 1) && + std::max(adjusted(25, 8), std::int32_t{1}) && modified.magical_attack == - std::max(adjusted(20, 9), 1) && + std::max(adjusted(20, 9), std::int32_t{1}) && modified.magical_hit_rate == - std::max(adjusted(35, 10), 1) && + std::max(adjusted(35, 10), std::int32_t{1}) && modified.magical_evasion == - std::max(adjusted(18, 11), 1) && + std::max(adjusted(18, 11), std::int32_t{1}) && modified.magical_defense == - std::max(adjusted(15, 12), 1) && + std::max(adjusted(15, 12), std::int32_t{1}) && modified.parameter_17 == - std::max(adjusted(12, 13), 1), + std::max(adjusted(12, 13), std::int32_t{1}), "Moon did not apply the thirteen retail companion modifiers.")) { return false; } @@ -893,30 +893,30 @@ bool testRetailBerserkerRules( if (!check( modified.attack_speed_raw == std::clamp(adjusted( - base.attack_speed_raw, 1), 0, 255) && + base.attack_speed_raw, 1), std::int32_t{0}, std::int32_t{255}) && modified.walking_speed_raw == std::clamp(adjusted( - base.walking_speed_raw, 2), 0, 255) && + base.walking_speed_raw, 2), std::int32_t{0}, std::int32_t{255}) && modified.maximum_life == - std::max(adjusted(base.maximum_life, 3), 1) && + std::max(adjusted(base.maximum_life, 3), std::int32_t{1}) && modified.maximum_mana == - std::max(adjusted(base.maximum_mana, 4), 1) && + std::max(adjusted(base.maximum_mana, 4), std::int32_t{1}) && modified.physical_attack == - std::max(adjusted(base.physical_attack, 5), 1) && + std::max(adjusted(base.physical_attack, 5), std::int32_t{1}) && modified.physical_defense == - std::max(adjusted(base.physical_defense, 6), 1) && + std::max(adjusted(base.physical_defense, 6), std::int32_t{1}) && modified.hit_rate == - std::max(adjusted(base.hit_rate, 7), 1) && + std::max(adjusted(base.hit_rate, 7), std::int32_t{1}) && modified.physical_evasion == - std::max(adjusted(base.physical_evasion, 8), 1) && + std::max(adjusted(base.physical_evasion, 8), std::int32_t{1}) && modified.magical_attack == - std::max(adjusted(base.magical_attack, 9), 1) && + std::max(adjusted(base.magical_attack, 9), std::int32_t{1}) && modified.magical_defense == - std::max(adjusted(base.magical_defense, 10), 1) && + std::max(adjusted(base.magical_defense, 10), std::int32_t{1}) && modified.magical_hit_rate == - std::max(adjusted(base.magical_hit_rate, 11), 1) && + std::max(adjusted(base.magical_hit_rate, 11), std::int32_t{1}) && modified.magical_evasion == - std::max(adjusted(base.magical_evasion, 12), 1), + std::max(adjusted(base.magical_evasion, 12), std::int32_t{1}), "Berserker did not apply all twelve retail player modifiers.")) { return false; } @@ -1232,13 +1232,13 @@ bool testShippedWorldCast( continue; } for (std::int32_t y = - std::max(0, anchor_y - 140); - y < std::min(400, anchor_y + 30) && + std::max(std::int32_t{0}, anchor_y - 140); + y < std::min(std::int32_t{400}, anchor_y + 30) && pointer_x < 0; ++y) { for (std::int32_t x = - std::max(0, anchor_x - 80); - x < std::min(640, anchor_x + 81); + std::max(std::int32_t{0}, anchor_x - 80); + x < std::min(std::int32_t{640}, anchor_x + 81); ++x) { world.updatePointerHover(x, y); if (world.hoveredEnemyId() == enemy.id()) { @@ -1512,11 +1512,11 @@ bool testShippedSonicBladeCast( } std::int32_t candidate_x = -1; std::int32_t candidate_y = -1; - for (std::int32_t y = std::max(0, anchor_y - 140); - y < std::min(400, anchor_y + 30) && candidate_x < 0; + for (std::int32_t y = std::max(std::int32_t{0}, anchor_y - 140); + y < std::min(std::int32_t{400}, anchor_y + 30) && candidate_x < 0; ++y) { - for (std::int32_t x = std::max(0, anchor_x - 80); - x < std::min(640, anchor_x + 81); + for (std::int32_t x = std::max(std::int32_t{0}, anchor_x - 80); + x < std::min(std::int32_t{640}, anchor_x + 81); ++x) { world.updatePointerHover(x, y); if (world.hoveredEnemyId() == enemy.id()) { @@ -1730,13 +1730,13 @@ bool testShippedHellFireCast( const std::int32_t anchor_y = projected.y - world.cameraScreenY(); for (std::int32_t y = - std::max(0, anchor_y - 140); - y < std::min(400, anchor_y + 30) && + std::max(std::int32_t{0}, anchor_y - 140); + y < std::min(std::int32_t{400}, anchor_y + 30) && pointer_x < 0; ++y) { for (std::int32_t x = - std::max(0, anchor_x - 80); - x < std::min(640, anchor_x + 81); + std::max(std::int32_t{0}, anchor_x - 80); + x < std::min(std::int32_t{640}, anchor_x + 81); ++x) { world.updatePointerHover(x, y); if (world.hoveredEnemyId() == enemy.id()) { @@ -1952,13 +1952,13 @@ bool testShippedIceBlastCast( const std::int32_t anchor_y = projected.y - world.cameraScreenY(); for (std::int32_t y = - std::max(0, anchor_y - 140); - y < std::min(400, anchor_y + 30) && + std::max(std::int32_t{0}, anchor_y - 140); + y < std::min(std::int32_t{400}, anchor_y + 30) && pointer_x < 0; ++y) { for (std::int32_t x = - std::max(0, anchor_x - 80); - x < std::min(640, anchor_x + 81); + std::max(std::int32_t{0}, anchor_x - 80); + x < std::min(std::int32_t{640}, anchor_x + 81); ++x) { world.updatePointerHover(x, y); if (world.hoveredEnemyId() == enemy.id()) { @@ -2927,7 +2927,7 @@ bool testShippedIdentifyCast( } const_cast(world.playerData()).setCurrentMana( - std::max(parameters.mana_cost - 1, 0)); + std::max(parameters.mana_cost - 1, std::int32_t{0})); const std::int32_t insufficient_mana = world.playerData().currentMana(); return check( @@ -3586,11 +3586,11 @@ bool testTargetedSpellInsufficientMana( anchor_y < -160 || anchor_y > 440) { continue; } - for (std::int32_t y = std::max(0, anchor_y - 140); - y < std::min(400, anchor_y + 30) && pointer_x < 0; + for (std::int32_t y = std::max(std::int32_t{0}, anchor_y - 140); + y < std::min(std::int32_t{400}, anchor_y + 30) && pointer_x < 0; ++y) { - for (std::int32_t x = std::max(0, anchor_x - 80); - x < std::min(640, anchor_x + 81); + for (std::int32_t x = std::max(std::int32_t{0}, anchor_x - 80); + x < std::min(std::int32_t{640}, anchor_x + 81); ++x) { world.updatePointerHover(x, y); if (world.hoveredEnemyId() == enemy.id()) { @@ -3622,7 +3622,7 @@ bool testTargetedSpellInsufficientMana( // underlying scene is mutable here; lower only this fixture's MP so the // command guard can be exercised without changing the runtime API. const_cast(world.playerData()).setCurrentMana( - std::max(0, parameters.mana_cost - 1)); + std::max(std::int32_t{0}, parameters.mana_cost - 1)); const std::int32_t mana_before = world.playerData().currentMana(); const std::size_t controllers_before = diff --git a/tests/native/resource_manager_test.cpp b/tests/native/resource_manager_test.cpp index 8ed4cb68..e32120fa 100644 --- a/tests/native/resource_manager_test.cpp +++ b/tests/native/resource_manager_test.cpp @@ -1,351 +1,351 @@ -#include "resources/resource_manager.hpp" -#include "resources/character_visual_resource.hpp" -#include "resources/font_resource.hpp" -#include "resources/item_inventory_resource.hpp" -#include "resources/effect_pattern_resource.hpp" -#include "resources/effect_visual_resource.hpp" - -#include -#include -#include -#include - -namespace { - -bool check(bool condition, const char* message) { - if (!condition) { - std::cerr << message << '\n'; - } - return condition; -} - -bool patternDecoded( - const osf::gapi::NjpImage& image, - std::int32_t pattern_index) { - if (pattern_index < 0 || - static_cast(pattern_index) >= - image.patterns().size() || - !image.patternDecoded(static_cast( - pattern_index))) { - return false; - } - for (const osf::gapi::NjpPatternPart& part : - image.patterns()[static_cast( - pattern_index)].parts) { - if (part.part_index < 0 || - static_cast(part.part_index) >= - image.parts().size() || - !image.parts()[static_cast( - part.part_index)].hasDecodedPixels()) { - return false; - } - } - return true; -} - -bool selectedAnimationPatternsDecoded( - const osf::CharacterVisualResource& visual, - const osf::CharacterVisualResource& reference, - const std::vector& enabled_parts) { - for (const osf::gapi::CafChart& chart : - visual.animation().charts()) { - for (const osf::gapi::CafDirection& direction : - chart.directions) { - for (std::size_t part_index = 0; - part_index < direction.parts.size(); - ++part_index) { - if (part_index >= enabled_parts.size() || - enabled_parts[part_index] == 0) { - continue; - } - for (const osf::gapi::CafCell& cell : - direction.parts[part_index]) { - if (cell.pattern_index < 0) { - continue; - } - const bool normal_required = patternDecoded( - reference.patterns(), cell.pattern_index); - const bool shadow_required = - (cell.status & 8) != 0 && - patternDecoded( - reference.shadowPatterns(), - cell.pattern_index); - if ((normal_required && - !patternDecoded( - visual.patterns(), - cell.pattern_index)) || - (shadow_required && - !patternDecoded( - visual.shadowPatterns(), - cell.pattern_index))) { - return false; - } - } - } - } - } - return true; -} - -} // namespace - -int main() { -#ifndef OPENSHADOWFLARE_SOURCE_DIR - return 0; -#else - const std::filesystem::path data_root = - std::filesystem::path(OPENSHADOWFLARE_SOURCE_DIR) / - "tmp/ShadowFlare"; - if (!std::filesystem::is_directory(data_root)) { - return 0; - } - - const std::filesystem::path female_root = - data_root / "Player" / "Female"; - osf::CharacterVisualResource full_player; - osf::CharacterVisualResource selected_player; - std::string player_error; - if (!check( - full_player.load( - female_root, "Animation00", &player_error) && - selected_player.loadAnimation( - female_root, "Animation00", &player_error), - player_error.c_str())) { - return 1; - } - std::vector selected_parts( - selected_player.animation().maxPartCount(), 0); - for (std::size_t part : {std::size_t{0}, std::size_t{1}, - std::size_t{5}}) { - if (part < selected_parts.size()) { - selected_parts[part] = 1; - } - } - const bool selected_loaded = - selected_player.loadSelectedParts( - selected_parts, &player_error); - const bool selected_compact = - selected_player.memoryUsageBytes() * 2 < - full_player.memoryUsageBytes(); - const bool selected_complete = - selectedAnimationPatternsDecoded( - selected_player, full_player, selected_parts); - if (!check( - selected_loaded && selected_compact && selected_complete, - "Selected player layers were not decoded completely or " - "did not reduce their allocation.")) { - std::cerr - << "full=" << full_player.memoryUsageBytes() - << " selected=" << selected_player.memoryUsageBytes() - << " loaded=" << selected_loaded - << " complete=" << selected_complete - << " error=" << player_error << '\n'; - return 1; - } - const std::uint64_t equipped_bytes = - selected_player.memoryUsageBytes(); - if (selected_parts.size() > 5) { - selected_parts[5] = 0; - } - if (!check( - selected_player.loadSelectedParts( - selected_parts, &player_error) && - selected_player.memoryUsageBytes() < - equipped_bytes && - selectedAnimationPatternsDecoded( - selected_player, - full_player, - selected_parts), - "Changing equipment layers did not release unused player " - "bitmaps.")) { - return 1; - } - - osf::ResourceManager resources(data_root); - if (!check( - resources.loadCommonPattern( - 0, "System\\Common\\Pattern\\Font00.njp"), - "The common resource fixture could not be loaded.")) { - return 1; - } - const std::uint64_t common_bytes = - resources.memoryUsageBytes(); - if (!check( - common_bytes > 0, - "The common resource memory was not accounted.")) { - return 1; - } - - osf::ResourceManager selected_font(data_root); - if (!check( - selected_font.loadCommonPattern( - 0, - "System\\Common\\Pattern\\Font00.njp", - osf::englishRetailFontPatternSelection()) && - selected_font.pattern(0) && - selected_font.pattern(0)->patternDecoded(0) && - !selected_font.pattern(0)->patternDecoded(1) && - selected_font.pattern(0)->patternDecoded(2) && - selected_font.memoryUsageBytes() * 10 < common_bytes, - "The English font sheet was not decoded selectively.")) { - return 1; - } - selected_font.releaseCommonPattern(0); - if (!check( - selected_font.pattern(0) == nullptr && - selected_font.memoryUsageBytes() == 0, - "Releasing a state-scoped common pattern retained memory.")) { - return 1; - } - - osf::ItemInventoryResource inventory_artwork; - std::array< - std::uint8_t, - osf::ItemInventoryResource::group_count> item_groups{}; - item_groups.fill(1); - std::string inventory_error; - if (!check( - inventory_artwork.load(data_root, &inventory_error) && - inventory_artwork.group(0) == nullptr && - inventory_artwork.prepareGroups( - item_groups, &inventory_error), - "The lazy inventory artwork fixture could not load.")) { - return 1; - } - const std::uint64_t all_item_bytes = - inventory_artwork.memoryUsageBytes(); - item_groups.fill(0); - item_groups[3] = 1; - if (!check( - inventory_artwork.prepareGroups( - item_groups, &inventory_error) && - inventory_artwork.group(0) == nullptr && - inventory_artwork.group(3) != nullptr && - inventory_artwork.memoryUsageBytes() * 5 < - all_item_bytes, - "Closing inventory containers retained their artwork sheets.")) { - return 1; - } - - osf::EffectVisualResources effect_visuals; - if (!check( - effect_visuals.load(data_root, 11000040) && - effect_visuals.load(data_root, 11000240), - "The effect-cache fixture could not be loaded.")) { - return 1; - } - const std::uint64_t all_effect_visual_bytes = - effect_visuals.memoryUsageBytes(); - effect_visuals.retainOnly({11000240}); - if (!check( - effect_visuals.find(11000040) == nullptr && - effect_visuals.find(11000240) != nullptr && - effect_visuals.memoryUsageBytes() < - all_effect_visual_bytes, - "The effect animation cache retained an inactive resource.")) { - return 1; - } - - osf::EffectPatternResources effect_patterns; - if (!check( - effect_patterns.load(data_root, 10000020) && - effect_patterns.load(data_root, 11000011), - "The static-effect cache fixture could not be loaded.")) { - return 1; - } - const std::uint64_t all_effect_pattern_bytes = - effect_patterns.memoryUsageBytes(); - effect_patterns.retainOnly({10000020}); - if (!check( - effect_patterns.find(11000011) == nullptr && - effect_patterns.find(10000020) != nullptr && - effect_patterns.memoryUsageBytes() < - all_effect_pattern_bytes, - "The static-effect cache retained an inactive resource.")) { - return 1; - } - - if (!check( - resources.loadTitlePattern( - 4, "System\\Title\\Pattern\\Title.njp") && - resources.loadTitleAnimation( - 0, - "System\\Title\\Pattern\\Smoke00.Caf") && - resources.pattern(4) != nullptr && - !resources.titleAnimation(0)->charts().empty() && - resources.memoryUsageBytes() > common_bytes, - "The title resource scope could not be loaded.")) { - return 1; - } - - resources.releaseTitleResources(); - if (!check( - resources.pattern(4) == nullptr && - resources.titleAnimation(0)->charts().empty() && - resources.pattern(0) != nullptr && - resources.memoryUsageBytes() == common_bytes, - "Releasing the title scope retained title data or removed common data.")) { - return 1; - } - - if (!check( - resources.loadCharacterSelectPattern( - 4, "System\\Select\\Pattern\\Select.njp") && - resources.pattern(4) != nullptr, - "The character-select resource scope could not be loaded.")) { - return 1; - } - resources.loadSavedCharacters(); - resources.releaseCharacterSelectResources(); - if (!check( - resources.pattern(4) == nullptr && - resources.savedGameCount() == 0 && - resources.savedGames().empty() && - resources.savedPreviews().empty() && - resources.pattern(0) != nullptr, - "Releasing character select retained saves, previews, or patterns.")) { - return 1; - } - - if (!check( - resources.loadGameplayPattern( - 5, "System\\Game\\Pattern\\Bar.njp") && - resources.pattern(5) != nullptr && - resources.prepareGameplayPattern( - 6, - "System\\Game\\Pattern\\Status.njp", - true) && - resources.pattern(6) != nullptr && - resources.prepareGameplayPattern( - 6, - "System\\Game\\Pattern\\Status.njp", - false) && - resources.pattern(6) == nullptr, - "The gameplay resource scope could not be loaded.")) { - return 1; - } - std::vector status_selection(121, 0); - status_selection[5] = 1; - if (!check( - resources.prepareGameplayPattern( - 6, - "System\\Game\\Pattern\\Status.njp", - status_selection, - true) && - resources.pattern(6) && - resources.pattern(6)->patternDecoded(5) && - !resources.pattern(6)->patternDecoded(2), - "A gameplay panel did not replace its full sheet with the " - "requested pattern selection.")) { - return 1; - } - resources.releaseGameplayPattern(6); - resources.releaseGameplayResources(); - return check( - resources.pattern(5) == nullptr && - resources.pattern(0) != nullptr, - "Releasing gameplay retained its patterns or removed common data.") - ? 0 - : 1; -#endif -} +#include "resources/resource_manager.hpp" +#include "resources/character_visual_resource.hpp" +#include "resources/font_resource.hpp" +#include "resources/item_inventory_resource.hpp" +#include "resources/effect_pattern_resource.hpp" +#include "resources/effect_visual_resource.hpp" + +#include +#include +#include +#include + +namespace { + +bool check(bool condition, const char* message) { + if (!condition) { + std::cerr << message << '\n'; + } + return condition; +} + +bool patternDecoded( + const osf::gapi::NjpImage& image, + std::int32_t pattern_index) { + if (pattern_index < 0 || + static_cast(pattern_index) >= + image.patterns().size() || + !image.patternDecoded(static_cast( + pattern_index))) { + return false; + } + for (const osf::gapi::NjpPatternPart& part : + image.patterns()[static_cast( + pattern_index)].parts) { + if (part.part_index < 0 || + static_cast(part.part_index) >= + image.parts().size() || + !image.parts()[static_cast( + part.part_index)].hasDecodedPixels()) { + return false; + } + } + return true; +} + +bool selectedAnimationPatternsDecoded( + const osf::CharacterVisualResource& visual, + const osf::CharacterVisualResource& reference, + const std::vector& enabled_parts) { + for (const osf::gapi::CafChart& chart : + visual.animation().charts()) { + for (const osf::gapi::CafDirection& direction : + chart.directions) { + for (std::size_t part_index = 0; + part_index < direction.parts.size(); + ++part_index) { + if (part_index >= enabled_parts.size() || + enabled_parts[part_index] == 0) { + continue; + } + for (const osf::gapi::CafCell& cell : + direction.parts[part_index]) { + if (cell.pattern_index < 0) { + continue; + } + const bool normal_required = patternDecoded( + reference.patterns(), cell.pattern_index); + const bool shadow_required = + (cell.status & 8) != 0 && + patternDecoded( + reference.shadowPatterns(), + cell.pattern_index); + if ((normal_required && + !patternDecoded( + visual.patterns(), + cell.pattern_index)) || + (shadow_required && + !patternDecoded( + visual.shadowPatterns(), + cell.pattern_index))) { + return false; + } + } + } + } + } + return true; +} + +} // namespace + +int main() { +#ifndef OPENSHADOWFLARE_SOURCE_DIR + return 0; +#else + const std::filesystem::path data_root = + std::filesystem::path(OPENSHADOWFLARE_SOURCE_DIR) / + "tmp/ShadowFlare"; + if (!std::filesystem::is_directory(data_root)) { + return 0; + } + + const std::filesystem::path female_root = + data_root / "Player" / "Female"; + osf::CharacterVisualResource full_player; + osf::CharacterVisualResource selected_player; + std::string player_error; + if (!check( + full_player.load( + female_root, "Animation00", &player_error) && + selected_player.loadAnimation( + female_root, "Animation00", &player_error), + player_error.c_str())) { + return 1; + } + std::vector selected_parts( + selected_player.animation().maxPartCount(), 0); + for (std::size_t part : {std::size_t{0}, std::size_t{1}, + std::size_t{5}}) { + if (part < selected_parts.size()) { + selected_parts[part] = 1; + } + } + const bool selected_loaded = + selected_player.loadSelectedParts( + selected_parts, &player_error); + const bool selected_compact = + selected_player.memoryUsageBytes() * 2 < + full_player.memoryUsageBytes(); + const bool selected_complete = + selectedAnimationPatternsDecoded( + selected_player, full_player, selected_parts); + if (!check( + selected_loaded && selected_compact && selected_complete, + "Selected player layers were not decoded completely or " + "did not reduce their allocation.")) { + std::cerr + << "full=" << full_player.memoryUsageBytes() + << " selected=" << selected_player.memoryUsageBytes() + << " loaded=" << selected_loaded + << " complete=" << selected_complete + << " error=" << player_error << '\n'; + return 1; + } + const std::uint64_t equipped_bytes = + selected_player.memoryUsageBytes(); + if (selected_parts.size() > 5) { + selected_parts[5] = 0; + } + if (!check( + selected_player.loadSelectedParts( + selected_parts, &player_error) && + selected_player.memoryUsageBytes() < + equipped_bytes && + selectedAnimationPatternsDecoded( + selected_player, + full_player, + selected_parts), + "Changing equipment layers did not release unused player " + "bitmaps.")) { + return 1; + } + + osf::ResourceManager resources(data_root); + if (!check( + resources.loadCommonPattern( + 0, "System\\Common\\Pattern\\Font00.njp"), + "The common resource fixture could not be loaded.")) { + return 1; + } + const std::uint64_t common_bytes = + resources.memoryUsageBytes(); + if (!check( + common_bytes > 0, + "The common resource memory was not accounted.")) { + return 1; + } + + osf::ResourceManager selected_font(data_root); + if (!check( + selected_font.loadCommonPattern( + 0, + "System\\Common\\Pattern\\Font00.njp", + osf::englishRetailFontPatternSelection()) && + selected_font.pattern(0) && + selected_font.pattern(0)->patternDecoded(0) && + !selected_font.pattern(0)->patternDecoded(1) && + selected_font.pattern(0)->patternDecoded(2) && + selected_font.memoryUsageBytes() * 10 < common_bytes, + "The English font sheet was not decoded selectively.")) { + return 1; + } + selected_font.releaseCommonPattern(0); + if (!check( + selected_font.pattern(0) == nullptr && + selected_font.memoryUsageBytes() == 0, + "Releasing a state-scoped common pattern retained memory.")) { + return 1; + } + + osf::ItemInventoryResource inventory_artwork; + std::array< + std::uint8_t, + osf::ItemInventoryResource::group_count> item_groups{}; + item_groups.fill(1); + std::string inventory_error; + if (!check( + inventory_artwork.load(data_root, &inventory_error) && + inventory_artwork.group(0) == nullptr && + inventory_artwork.prepareGroups( + item_groups, &inventory_error), + "The lazy inventory artwork fixture could not load.")) { + return 1; + } + const std::uint64_t all_item_bytes = + inventory_artwork.memoryUsageBytes(); + item_groups.fill(0); + item_groups[3] = 1; + if (!check( + inventory_artwork.prepareGroups( + item_groups, &inventory_error) && + inventory_artwork.group(0) == nullptr && + inventory_artwork.group(3) != nullptr && + inventory_artwork.memoryUsageBytes() * 5 < + all_item_bytes, + "Closing inventory containers retained their artwork sheets.")) { + return 1; + } + + osf::EffectVisualResources effect_visuals; + if (!check( + effect_visuals.load(data_root, 11000040) && + effect_visuals.load(data_root, 11000240), + "The effect-cache fixture could not be loaded.")) { + return 1; + } + const std::uint64_t all_effect_visual_bytes = + effect_visuals.memoryUsageBytes(); + effect_visuals.retainOnly({11000240}); + if (!check( + effect_visuals.find(11000040) == nullptr && + effect_visuals.find(11000240) != nullptr && + effect_visuals.memoryUsageBytes() < + all_effect_visual_bytes, + "The effect animation cache retained an inactive resource.")) { + return 1; + } + + osf::EffectPatternResources effect_patterns; + if (!check( + effect_patterns.load(data_root, 10000020) && + effect_patterns.load(data_root, 11000011), + "The static-effect cache fixture could not be loaded.")) { + return 1; + } + const std::uint64_t all_effect_pattern_bytes = + effect_patterns.memoryUsageBytes(); + effect_patterns.retainOnly({10000020}); + if (!check( + effect_patterns.find(11000011) == nullptr && + effect_patterns.find(10000020) != nullptr && + effect_patterns.memoryUsageBytes() < + all_effect_pattern_bytes, + "The static-effect cache retained an inactive resource.")) { + return 1; + } + + if (!check( + resources.loadTitlePattern( + 4, "System\\Title\\Pattern\\Title.njp") && + resources.loadTitleAnimation( + 0, + "System\\Title\\Pattern\\Smoke00.Caf") && + resources.pattern(4) != nullptr && + !resources.titleAnimation(0)->charts().empty() && + resources.memoryUsageBytes() > common_bytes, + "The title resource scope could not be loaded.")) { + return 1; + } + + resources.releaseTitleResources(); + if (!check( + resources.pattern(4) == nullptr && + resources.titleAnimation(0)->charts().empty() && + resources.pattern(0) != nullptr && + resources.memoryUsageBytes() == common_bytes, + "Releasing the title scope retained title data or removed common data.")) { + return 1; + } + + if (!check( + resources.loadCharacterSelectPattern( + 4, "System\\Select\\Pattern\\Select.njp") && + resources.pattern(4) != nullptr, + "The character-select resource scope could not be loaded.")) { + return 1; + } + resources.loadSavedCharacters(); + resources.releaseCharacterSelectResources(); + if (!check( + resources.pattern(4) == nullptr && + resources.savedGameCount() == 0 && + resources.savedGames().empty() && + resources.savedPreviews().empty() && + resources.pattern(0) != nullptr, + "Releasing character select retained saves, previews, or patterns.")) { + return 1; + } + + if (!check( + resources.loadGameplayPattern( + 5, "System\\Game\\Pattern\\Bar.njp") && + resources.pattern(5) != nullptr && + resources.prepareGameplayPattern( + 6, + "System\\Game\\Pattern\\Status.njp", + true) && + resources.pattern(6) != nullptr && + resources.prepareGameplayPattern( + 6, + "System\\Game\\Pattern\\Status.njp", + false) && + resources.pattern(6) == nullptr, + "The gameplay resource scope could not be loaded.")) { + return 1; + } + std::vector status_selection(121, 0); + status_selection[5] = 1; + if (!check( + resources.prepareGameplayPattern( + 6, + "System\\Game\\Pattern\\Status.njp", + status_selection, + true) && + resources.pattern(6) && + resources.pattern(6)->patternDecoded(5) && + !resources.pattern(6)->patternDecoded(2), + "A gameplay panel did not replace its full sheet with the " + "requested pattern selection.")) { + return 1; + } + resources.releaseGameplayPattern(6); + resources.releaseGameplayResources(); + return check( + resources.pattern(5) == nullptr && + resources.pattern(0) != nullptr, + "Releasing gameplay retained its patterns or removed common data.") + ? 0 + : 1; +#endif +} diff --git a/thirdparty/lal/CMakeLists.txt b/thirdparty/lal/CMakeLists.txt index 6d19e0ec..e7a0a0d5 100644 --- a/thirdparty/lal/CMakeLists.txt +++ b/thirdparty/lal/CMakeLists.txt @@ -99,6 +99,9 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") ALSA::ALSA Threads::Threads ) +elseif(PLATFORM_PS2) + target_sources(lal PRIVATE lal_ps2.c) + target_link_libraries(lal PRIVATE audsrv kernel) else() message(FATAL_ERROR "LAL has no backend for this platform") endif() diff --git a/thirdparty/lal/lal_ps2.c b/thirdparty/lal/lal_ps2.c new file mode 100644 index 00000000..a2555dee --- /dev/null +++ b/thirdparty/lal/lal_ps2.c @@ -0,0 +1,121 @@ +/* + * LAL backend for the Sony PlayStation 2 (audsrv). + * + * The SPU2 is driven through ps2sdk's audsrv module, which runs a streaming + * thread on the EE side and a ring buffer in the IOP. lal_platform_init + * requests the 44100 Hz / 16-bit / stereo output format and registers a + * fill-buffer callback: whenever audsrv's ring buffer has room for one more + * chunk, the callback pulls that many frames out of the mixer through + * lal_mix_frames and enqueues them for playback. + * + * The callback runs on audsrv's own thread while the game thread mutates + * the mixer through the LAL API, so the platform lock is implemented with a + * binary kernel semaphore. + */ + +#include "lal_internal.h" + +#include +#include +#include + +#include +#include + +#define LAL_PS2_CHUNK_FRAMES 1024 +#define LAL_PS2_CHUNK_BYTES \ + (LAL_PS2_CHUNK_FRAMES * LAL_OUTPUT_CHANNELS * 2) + +static int16_t g_mix_buffer[LAL_PS2_CHUNK_FRAMES * LAL_OUTPUT_CHANNELS]; +static int g_mutex = -1; +static bool g_started; + +static int on_fill(void *arg) { + (void) arg; + lal_platform_lock(); + lal_mix_frames(g_mix_buffer, LAL_PS2_CHUNK_FRAMES); + lal_platform_unlock(); + audsrv_play_audio((const char *) g_mix_buffer, LAL_PS2_CHUNK_BYTES); + return 0; +} + +bool lal_platform_init(void) { + struct audsrv_fmt_t format; + ee_sema_t semaphore; + int result; + + if (g_started) { + return true; + } + + semaphore.attr = 0; + semaphore.option = 0; + semaphore.init_count = 1; + semaphore.max_count = 1; + g_mutex = CreateSema(&semaphore); + if (g_mutex < 0) { + lal_set_error("Could not create the PS2 audio mutex."); + return false; + } + + if (SifLoadModule("cdrom0:\\AUDSRV.IRX;1", 0, NULL) < 0) { + DeleteSema(g_mutex); + g_mutex = -1; + lal_set_error("Could not load the PS2 audio driver module."); + return false; + } + + if (audsrv_init() != 0) { + DeleteSema(g_mutex); + g_mutex = -1; + lal_set_error("Could not initialize the PS2 audio driver."); + return false; + } + + format.freq = LAL_OUTPUT_SAMPLE_RATE; + format.bits = 16; + format.channels = LAL_OUTPUT_CHANNELS; + if (audsrv_set_format(&format) != 0) { + audsrv_quit(); + DeleteSema(g_mutex); + g_mutex = -1; + lal_set_error("The PS2 audio driver does not support the output format."); + return false; + } + + result = audsrv_on_fillbuf(LAL_PS2_CHUNK_BYTES, on_fill, NULL); + if (result != 0) { + audsrv_quit(); + DeleteSema(g_mutex); + g_mutex = -1; + lal_set_error("Could not register the PS2 audio callback."); + return false; + } + + g_started = true; + return true; +} + +void lal_platform_shutdown(void) { + if (!g_started) { + return; + } + g_started = false; + audsrv_quit(); + if (g_mutex >= 0) { + DeleteSema(g_mutex); + g_mutex = -1; + } +} + +void lal_platform_lock(void) { + if (g_mutex >= 0) { + WaitSema(g_mutex); + } +} + +void lal_platform_unlock(void) { + if (g_mutex >= 0) { + SignalSema(g_mutex); + } +} diff --git a/thirdparty/lwl/CMakeLists.txt b/thirdparty/lwl/CMakeLists.txt index f27651fc..9e5e7622 100644 --- a/thirdparty/lwl/CMakeLists.txt +++ b/thirdparty/lwl/CMakeLists.txt @@ -84,6 +84,9 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") X11::Xinerama "${CMAKE_DL_LIBS}" ) +elseif(PLATFORM_PS2) + target_sources(lwl PRIVATE lwl_ps2.c) + target_link_libraries(lwl PRIVATE pad kernel) else() message(FATAL_ERROR "LWL has no backend for this platform") endif() diff --git a/thirdparty/lwl/lwl_ps2.c b/thirdparty/lwl/lwl_ps2.c new file mode 100644 index 00000000..b452f841 --- /dev/null +++ b/thirdparty/lwl/lwl_ps2.c @@ -0,0 +1,492 @@ +/* + * LWL backend for the Sony PlayStation 2. + * + * There is no window system on the PS2. The "window" collapses to a virtual + * 640x480 screen that the presentation backend fills each frame, so the + * game sees a fixed-size surface with no resizing or focus events. + * + * Input comes from up to two controllers (ports 0 and 1). The D-pad and + * face buttons are converted into LWL key events using the same key names + * the other backends use ("up", "down", "return", "escape", and so on). + * The left analog stick drives a virtual pointer, because the game relies + * on mouse-style pointing even in menus: the stick moves an accumulated + * pointer position and the cross button maps to the primary mouse button. + * + * The pads are polled on each lwl_poll_event call; everything runs on the + * game thread, so no locking is required. + */ + +#include "lwl.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#define LWL_PS2_PORT_COUNT 2 +#define LWL_PS2_EVENT_QUEUE_CAPACITY 64 +#define LWL_PS2_POINTER_SPEED 720.0 +#define LWL_PS2_STICK_DEADZONE 32 +#define LWL_PS2_VIRTUAL_WIDTH 640 +#define LWL_PS2_VIRTUAL_HEIGHT 480 + +struct LwlWindow { + int width; + int height; +}; + +struct LwlGlContext { + int unused; +}; + +struct button_key { + unsigned short bit; + const char *key; +}; + +static const struct button_key k_button_keys[] = { + { PAD_UP, "up" }, + { PAD_DOWN, "down" }, + { PAD_LEFT, "left" }, + { PAD_RIGHT, "right" }, + { PAD_CROSS, "return" }, + { PAD_CIRCLE, "escape" }, + { PAD_TRIANGLE, "m" }, + { PAD_SQUARE, "i" }, + { PAD_L1, "n" }, + { PAD_L2, "h" }, + { PAD_R1, "q" }, + { PAD_R2, "r" }, + { PAD_SELECT, "f12" }, + { PAD_START, "escape" }, +}; + +static LwlEvent g_event_queue[LWL_PS2_EVENT_QUEUE_CAPACITY]; +static int g_event_head; +static int g_event_tail; +static LwlWindow *g_window; +static bool g_pad_initialized; +static bool g_iop_modules_loaded; +static unsigned char g_pad_buffer[LWL_PS2_PORT_COUNT][256] + __attribute__((aligned(64))); +static unsigned short g_prev_buttons[LWL_PS2_PORT_COUNT]; +static double g_pointer_x = LWL_PS2_VIRTUAL_WIDTH / 2.0; +static double g_pointer_y = LWL_PS2_VIRTUAL_HEIGHT / 2.0; +static int g_last_emit_x = -1; +static int g_last_emit_y = -1; +static double g_last_poll_time; + +static void push_event(const LwlEvent *event) { + int next = (g_event_tail + 1) % LWL_PS2_EVENT_QUEUE_CAPACITY; + if (next == g_event_head) { + g_event_head = (g_event_head + 1) % LWL_PS2_EVENT_QUEUE_CAPACITY; + } + g_event_queue[g_event_tail] = *event; + g_event_tail = next; +} + +static bool pop_event(LwlEvent *event) { + if (g_event_head == g_event_tail) { + return false; + } + *event = g_event_queue[g_event_head]; + g_event_head = (g_event_head + 1) % LWL_PS2_EVENT_QUEUE_CAPACITY; + return true; +} + +static void emit_key(const char *key, bool pressed) { + LwlEvent event; + memset(&event, 0, sizeof(event)); + event.type = pressed ? LWL_EVENT_KEY_DOWN : LWL_EVENT_KEY_UP; + snprintf(event.key, sizeof(event.key), "%s", key); + push_event(&event); +} + +static void emit_pointer_button(bool pressed) { + LwlEvent event; + memset(&event, 0, sizeof(event)); + event.type = pressed ? LWL_EVENT_MOUSE_DOWN : LWL_EVENT_MOUSE_UP; + event.x = (int) g_pointer_x; + event.y = (int) g_pointer_y; + event.button = 1; + event.clicks = 1; + push_event(&event); +} + +static void emit_pointer_move(void) { + int x = (int) g_pointer_x; + int y = (int) g_pointer_y; + if (x == g_last_emit_x && y == g_last_emit_y) { + return; + } + g_last_emit_x = x; + g_last_emit_y = y; + LwlEvent event; + memset(&event, 0, sizeof(event)); + event.type = LWL_EVENT_MOUSE_MOVE; + event.x = x; + event.y = y; + push_event(&event); +} + +static void open_pad_port(int port) { + padPortOpen(port, 0, g_pad_buffer[port]); + padSetMainMode(port, 0, PAD_MMODE_DUALSHOCK, PAD_MMODE_LOCK); +} + +static void poll_pad(int port) { + struct padButtonStatus status; + int state = padGetState(port, 0); + unsigned short buttons; + unsigned short pressed; + unsigned short released; + int horizontal; + int vertical; + double elapsed; + double speed; + + if (state != PAD_STATE_STABLE) { + if (state == PAD_STATE_DISCONN) { + g_prev_buttons[port] = 0; + } + return; + } + if (!padRead(port, 0, &status)) { + return; + } + + buttons = (unsigned short) (0xffffu ^ status.btns); + pressed = (unsigned short) (buttons & ~g_prev_buttons[port]); + released = (unsigned short) (~buttons & g_prev_buttons[port]); + g_prev_buttons[port] = buttons; + + { + size_t index; + for (index = 0; index < sizeof(k_button_keys) / sizeof(k_button_keys[0]); + ++index) { + if (pressed & k_button_keys[index].bit) { + emit_key(k_button_keys[index].key, true); + } + if (released & k_button_keys[index].bit) { + emit_key(k_button_keys[index].key, false); + } + } + } + + if (pressed & PAD_CROSS) { + emit_pointer_button(true); + } + if (released & PAD_CROSS) { + emit_pointer_button(false); + } + + horizontal = (int) status.ljoy_h - 128; + vertical = (int) status.ljoy_v - 128; + if (horizontal > -LWL_PS2_STICK_DEADZONE && + horizontal < LWL_PS2_STICK_DEADZONE) { + horizontal = 0; + } + if (vertical > -LWL_PS2_STICK_DEADZONE && + vertical < LWL_PS2_STICK_DEADZONE) { + vertical = 0; + } + if (horizontal == 0 && vertical == 0) { + return; + } + + elapsed = lwl_time_seconds() - g_last_poll_time; + if (elapsed <= 0.0) { + return; + } + g_last_poll_time += elapsed; + speed = LWL_PS2_POINTER_SPEED * elapsed; + + g_pointer_x += (double) horizontal / 127.0 * speed; + g_pointer_y += (double) vertical / 127.0 * speed; + if (g_pointer_x < 0.0) { + g_pointer_x = 0.0; + } else if (g_pointer_x > LWL_PS2_VIRTUAL_WIDTH - 1) { + g_pointer_x = LWL_PS2_VIRTUAL_WIDTH - 1; + } + if (g_pointer_y < 0.0) { + g_pointer_y = 0.0; + } else if (g_pointer_y > LWL_PS2_VIRTUAL_HEIGHT - 1) { + g_pointer_y = LWL_PS2_VIRTUAL_HEIGHT - 1; + } + emit_pointer_move(); +} + +bool lwl_init(void) { + int port; + g_event_head = 0; + g_event_tail = 0; + g_last_poll_time = 0.0; + if (!g_pad_initialized) { + if (!g_iop_modules_loaded) { + static const char *const module_paths[] = { + "cdrom0:\\IOMANX.IRX;1", + "cdrom0:\\FILEXIO.IRX;1", + "cdrom0:\\SIO2MAN.IRX;1", + "cdrom0:\\PADMAN.IRX;1", + }; + size_t index; + for (index = 0; index < sizeof(module_paths) / sizeof(module_paths[0]); + ++index) { + SifLoadModule(module_paths[index], 0, NULL); + } + g_iop_modules_loaded = true; + } + if (padInit(0) == 1) { + for (port = 0; port < LWL_PS2_PORT_COUNT; ++port) { + open_pad_port(port); + g_prev_buttons[port] = 0; + } + g_pad_initialized = true; + } + } + return true; +} + +void lwl_shutdown(void) { + g_event_head = 0; + g_event_tail = 0; + if (g_pad_initialized) { + padEnd(); + g_pad_initialized = false; + } +} + +LwlWindow *lwl_window_create(const char *title, int width, int height) { + LwlWindow *window = (LwlWindow *) calloc(1, sizeof(*window)); + if (!window) { + return NULL; + } + window->width = width; + window->height = height; + g_window = window; + lwl_window_set_title(window, title); + return window; +} + +LwlWindow *lwl_window_create_with_native_message_handler( + const char *title, int width, int height, + LwlNativeMessageHandler handler, void *user_data) { + (void) handler; + (void) user_data; + return lwl_window_create(title, width, height); +} + +LwlWindow *lwl_window_attach_native(void *native_window, int width, + int height) { + (void) native_window; + (void) width; + (void) height; + return NULL; +} + +void *lwl_window_get_native_handle(LwlWindow *window) { + (void) window; + return NULL; +} + +void lwl_window_destroy(LwlWindow *window) { + if (window == g_window) { + g_window = NULL; + } + free(window); +} + +void lwl_window_show(LwlWindow *window) { (void) window; } + +void lwl_window_set_title(LwlWindow *window, const char *title) { + (void) window; + (void) title; +} + +void lwl_window_set_mode(LwlWindow *window, LwlWindowMode mode) { + (void) window; + (void) mode; +} + +bool lwl_window_has_focus(LwlWindow *window) { + (void) window; + return true; +} + +void lwl_window_set_cursor(LwlWindow *window, LwlCursor cursor) { + (void) window; + (void) cursor; +} + +bool lwl_window_set_cursor_image(LwlWindow *window, const LwlColor *pixels, + int width, int height, int hotspot_x, + int hotspot_y) { + (void) window; + (void) pixels; + (void) width; + (void) height; + (void) hotspot_x; + (void) hotspot_y; + return false; +} + +void lwl_window_set_cursor_visible(LwlWindow *window, bool visible) { + (void) window; + (void) visible; +} + +bool lwl_window_set_size(LwlWindow *window, int width, int height) { + if (!window) { + return false; + } + window->width = width; + window->height = height; + return true; +} + +void lwl_window_get_size(LwlWindow *window, int *width, int *height) { + if (width) { + *width = window ? window->width : LWL_PS2_VIRTUAL_WIDTH; + } + if (height) { + *height = window ? window->height : LWL_PS2_VIRTUAL_HEIGHT; + } +} + +LwlColor *lwl_window_get_framebuffer(LwlWindow *window, int *width, + int *height) { + (void) window; + (void) width; + (void) height; + return NULL; +} + +bool lwl_window_resize_framebuffer(LwlWindow *window, int width, int height) { + (void) window; + (void) width; + (void) height; + return false; +} + +void lwl_window_update_rects(LwlWindow *window, const LwlRect *rects, + int count) { + (void) window; + (void) rects; + (void) count; +} + +bool lwl_poll_event(LwlWindow *window, LwlEvent *event) { + (void) window; + int port; + if (!event) { + return false; + } + if (g_pad_initialized && g_last_poll_time == 0.0) { + g_last_poll_time = lwl_time_seconds(); + } + for (port = 0; port < LWL_PS2_PORT_COUNT; ++port) { + poll_pad(port); + } + return pop_event(event); +} + +bool lwl_wait_event(LwlWindow *window, double timeout_seconds) { + (void) window; + (void) timeout_seconds; + return g_event_head != g_event_tail; +} + +char *lwl_clipboard_get(LwlWindow *window) { + (void) window; + return NULL; +} + +void lwl_clipboard_set(LwlWindow *window, const char *text) { + (void) window; + (void) text; +} + +char *lwl_select_folder(LwlWindow *window, const char *title) { + (void) window; + (void) title; + return NULL; +} + +void lwl_free(void *ptr) { free(ptr); } + +double lwl_time_seconds(void) { + struct timespec timestamp; + if (clock_gettime(CLOCK_REALTIME, ×tamp) == 0) { + return (double) timestamp.tv_sec + (double) timestamp.tv_nsec * 1e-9; + } + return 0.0; +} + +void lwl_sleep_seconds(double seconds) { + if (seconds <= 0.0) { + return; + } + DelayThread((s32) (seconds * 1000000.0)); +} + +void lwl_sleep_until_seconds(double time_seconds) { + double remaining = time_seconds - lwl_time_seconds(); + if (remaining <= 0.0) { + return; + } + DelayThread((s32) (remaining * 1000000.0)); +} + +const char *lwl_platform_name(void) { return "ps2"; } + +double lwl_display_scale(void) { return 1.0; } + +bool lwl_exe_path(char *buf, int size) { + (void) buf; + (void) size; + return false; +} + +/* --- OpenGL (unavailable on PS2) ----------------------------------------- */ + +LwlGlConfig lwl_gl_config_default(void) { + LwlGlConfig config; + memset(&config, 0, sizeof(config)); + config.api = LWL_GL_API_DESKTOP; + return config; +} + +LwlGlContext *lwl_gl_context_create(LwlWindow *window, + const LwlGlConfig *requested_config) { + (void) window; + (void) requested_config; + return NULL; +} + +void lwl_gl_context_destroy(LwlGlContext *context) { (void) context; } + +bool lwl_gl_context_make_current(LwlGlContext *context) { + (void) context; + return false; +} + +void lwl_gl_context_swap_buffers(LwlGlContext *context) { (void) context; } + +bool lwl_gl_context_set_swap_interval(LwlGlContext *context, int interval) { + (void) context; + (void) interval; + return false; +} + +void *lwl_gl_get_proc_address(const char *name) { + (void) name; + return NULL; +} diff --git a/tools/ps2/build-iso.sh b/tools/ps2/build-iso.sh new file mode 100644 index 00000000..0f5f116c --- /dev/null +++ b/tools/ps2/build-iso.sh @@ -0,0 +1,95 @@ +#!/bin/sh + +set -eu + +usage() { + cat <<'EOF' +Usage: tools/ps2/build-iso.sh [options] + +Options: + --data-dir PATH Include an owned ShadowFlare data directory in a + personal-use ISO. Omit for a data-free distributable ISO. + --out-dir PATH Directory for the ISO and disc files. + Default: build/ps2 + -h, --help Show this help text. +EOF +} + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +repo_root=$(CDPATH= cd -- "$script_dir/../.." && pwd -P) +data_dir= +out_dir="$repo_root/build/ps2" +PS2DEV=${PS2DEV:-/usr/local/ps2dev} +PS2SDK=${PS2SDK:-$PS2DEV/ps2sdk} +GSKIT=${GSKIT:-$PS2DEV/gsKit} + +while [ "$#" -gt 0 ]; do + case "$1" in + --data-dir) + [ "$#" -ge 2 ] || { echo "--data-dir needs a path" >&2; exit 2; } + data_dir=$2 + shift 2 + ;; + --out-dir) + [ "$#" -ge 2 ] || { echo "--out-dir needs a path" >&2; exit 2; } + out_dir=$2 + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [ -n "$data_dir" ]; then + if [ ! -d "$data_dir" ]; then + echo "Data directory not found: $data_dir" >&2 + exit 1 + fi + data_dir=$(CDPATH= cd -- "$data_dir" && pwd -P) +fi + +mkdir -p "$out_dir" +out_dir=$(CDPATH= cd -- "$out_dir" && pwd -P) + +if [ -n "$data_dir" ]; then + missing_paths= + for required_path in SFlare.Cfg System Scenario Save Player Map Character; do + if [ ! -e "$data_dir/$required_path" ]; then + missing_paths="$missing_paths $required_path" + fi + done + if [ -n "$missing_paths" ]; then + echo "Data directory '$data_dir' is missing:$missing_paths" >&2 + exit 1 + fi +fi + +if [ ! -f "$PS2SDK/ps2dev.cmake" ]; then + echo "PS2SDK CMake toolchain not found: $PS2SDK/ps2dev.cmake" >&2 + echo "Set PS2DEV/PS2SDK after installing PS2DEV." >&2 + exit 1 +fi +if ! command -v cmake >/dev/null 2>&1 || + ! command -v genisoimage >/dev/null 2>&1; then + echo "cmake and genisoimage are required to build the PS2 ISO." >&2 + exit 1 +fi +export PS2DEV PS2SDK GSKIT + +echo "== build ISO into $out_dir ==" +if [ -n "$data_dir" ]; then + REPO="$repo_root" DATA="$data_dir" OUT="$out_dir" \ + INCLUDE_GAME_DATA=1 sh "$script_dir/make-iso.sh" + echo "Personal-use ISO ready: $out_dir/openshadowflare.iso" +else + REPO="$repo_root" OUT="$out_dir" sh "$script_dir/make-iso.sh" + echo "Data-free ISO ready: $out_dir/openshadowflare.iso" + echo "Use --data-dir to build a personal-use disc." +fi diff --git a/tools/ps2/make-iso.sh b/tools/ps2/make-iso.sh new file mode 100644 index 00000000..721a23b7 --- /dev/null +++ b/tools/ps2/make-iso.sh @@ -0,0 +1,81 @@ +# Builds the PlayStation 2 disc image for OpenShadowFlare. +# +# Invoked by build-iso.sh on a host with PS2DEV installed. Produces a data-free +# ISO by default; INCLUDE_GAME_DATA=1 makes a local personal-use disc with a +# packed SFGAME.BIN archive. + +set -u +: "${PS2DEV:=/usr/local/ps2dev}" +: "${PS2SDK:=$PS2DEV/ps2sdk}" +: "${GSKIT:=$PS2DEV/gsKit}" +export PS2DEV PS2SDK GSKIT +export PATH=$PATH:$PS2DEV/bin:$PS2DEV/ee/bin:$PS2DEV/iop/bin:$PS2SDK/bin:$PS2SDK/ee/bin:$PS2SDK/iop/bin + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +REPO=${REPO:-$(CDPATH= cd -- "$script_dir/../.." && pwd -P)} +DATA=${DATA:-} +OUT=${OUT:-$REPO/build/ps2} +ELF_NAME=${ELF_NAME:-OPENSHAD.ELF} +INCLUDE_GAME_DATA=${INCLUDE_GAME_DATA:-0} +DISC_ROOT=$(mktemp -d /tmp/openshadowflare-disc.XXXXXX) +trap 'rm -rf "$DISC_ROOT"' EXIT HUP INT TERM + +mkdir -p "$OUT" + +case "$INCLUDE_GAME_DATA" in + 0|1) ;; + *) echo "INCLUDE_GAME_DATA must be 0 or 1" >&2; exit 2 ;; +esac + +if [ "$INCLUDE_GAME_DATA" = 1 ]; then + if [ -z "$DATA" ] || [ ! -d "$DATA" ]; then + echo "Game data is required when INCLUDE_GAME_DATA=1" >&2 + exit 1 + fi + echo "== pack game data for personal-use disc ==" + gcc -O2 -Wall -o /tmp/pack "$REPO/tools/ps2/pack.c" || exit 1 + /tmp/pack "$DATA" "$DISC_ROOT/SFGAME.BIN" || exit 1 +fi + +echo "== build game ==" +rm -rf /tmp/ps2build +cmake -S "$REPO" -B /tmp/ps2build \ + -DCMAKE_TOOLCHAIN_FILE=$PS2SDK/ps2dev.cmake \ + -DBUILD_TESTING=OFF \ + -DOPENSHADOWFLARE_BUILD_EXE=ON || exit 1 +cmake --build /tmp/ps2build -j4 -- -k || exit 1 +ELF=$(find /tmp/ps2build -name 'ShadowFlare_rebuilt*' -type f | head -1) +if [ -z "$ELF" ]; then + echo "make-iso: no ELF produced" >&2 + exit 1 +fi +cp "$ELF" "$DISC_ROOT/$ELF_NAME" + +echo "== copy IOP modules ==" +cp "$PS2SDK/iop/irx/iomanX.irx" "$DISC_ROOT/IOMANX.IRX" || exit 1 +cp "$PS2SDK/iop/irx/fileXio.irx" "$DISC_ROOT/FILEXIO.IRX" || exit 1 +cp "$PS2SDK/iop/irx/sio2man.irx" "$DISC_ROOT/SIO2MAN.IRX" || exit 1 +cp "$PS2SDK/iop/irx/padman.irx" "$DISC_ROOT/PADMAN.IRX" || exit 1 +cp "$PS2SDK/iop/irx/audsrv.irx" "$DISC_ROOT/AUDSRV.IRX" || exit 1 +cp "$PS2SDK/iop/irx/usbd.irx" "$DISC_ROOT/USBD.IRX" || exit 1 +cp "$PS2SDK/iop/irx/usbhdfsd.irx" "$DISC_ROOT/USBHDFSD.IRX" || exit 1 + +echo "== build ISO ==" +printf 'BOOT2 = cdrom0:\\%s;1\nVER = 1.01\nVMODE = NTSC\n' "$ELF_NAME" > "$DISC_ROOT/SYSTEM.CNF" +rm -f "$OUT/openshadowflare.iso" +set -- \ + "$DISC_ROOT/SYSTEM.CNF" \ + "$DISC_ROOT/$ELF_NAME" \ + "$DISC_ROOT/IOMANX.IRX" \ + "$DISC_ROOT/FILEXIO.IRX" \ + "$DISC_ROOT/SIO2MAN.IRX" \ + "$DISC_ROOT/PADMAN.IRX" \ + "$DISC_ROOT/AUDSRV.IRX" \ + "$DISC_ROOT/USBD.IRX" \ + "$DISC_ROOT/USBHDFSD.IRX" +if [ "$INCLUDE_GAME_DATA" = 1 ]; then + set -- "$@" "$DISC_ROOT/SFGAME.BIN" +fi +genisoimage -iso-level 2 -R -J -V OPENSHDOW -o "$OUT/openshadowflare.iso" "$@" \ + || exit 1 +ls -la "$OUT/openshadowflare.iso" diff --git a/tools/ps2/pack.c b/tools/ps2/pack.c new file mode 100644 index 00000000..a787e045 --- /dev/null +++ b/tools/ps2/pack.c @@ -0,0 +1,254 @@ +/* + * Host-side packer for the PlayStation 2 game-data archive. + * + * Produces SFGAME.BIN, a single index-first archive that the PS2 port reads + * through tools/ps2's fileio backend (see ps2_data_backend.cpp). The game's + * disc-access filesystem hook (the BIOS fileio module) can only reach flat, + * uppercase, 8.3 root files, so the whole data tree is packed into one root + * file whose first block is a self-describing index. + * + * Layout (all little-endian u32, offsets from start of file): + * [0] header: magic 'SFB1' | version | entry_count | index_size + * [16] entries[entry_count]: name_offset | name_size | data_offset | data_size + * ... name bytes (name_offset is relative to the start of the index block) + * ... zero padding to the next 2048-byte boundary (index_size ends here) + * ... data blobs, each padded to a 2048-byte boundary + */ + +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include +#include + +#define SFB_MAGIC 0x53464231u +#define SFB_VERSION 1u +#define SECTOR 2048u +#define HEADER_SIZE 16u +#define ENTRY_SIZE 16u + +struct entry { + char *name; + size_t name_len; + size_t file_size; + unsigned long data_offset; +}; + +static struct entry *entries; +static size_t entry_count; +static size_t entry_capacity; + +static int entry_compare(const void *a, const void *b) { + const struct entry *ea = (const struct entry *)a; + const struct entry *eb = (const struct entry *)b; + return strcmp(ea->name, eb->name); +} + +static void add_entry(const char *name, size_t file_size) { + if (entry_count == entry_capacity) { + entry_capacity = entry_capacity ? entry_capacity * 2 : 4096; + entries = (struct entry *)realloc( + entries, entry_capacity * sizeof(struct entry)); + if (!entries) { + fprintf(stderr, "pack: out of memory\n"); + exit(1); + } + } + struct entry *entry = &entries[entry_count++]; + entry->name = strdup(name); + if (!entry->name) { + fprintf(stderr, "pack: out of memory\n"); + exit(1); + } + entry->name_len = strlen(name); + entry->file_size = file_size; + entry->data_offset = 0; +} + +static size_t align_up(size_t value, size_t boundary) { + return (value + boundary - 1) & ~(boundary - 1); +} + +static int walk_directory(const char *directory, const char *prefix) { + DIR *dir = opendir(directory); + if (!dir) { + fprintf(stderr, "pack: cannot open directory %s: %s\n", + directory, strerror(errno)); + return -1; + } + + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(entry->d_name, ".") == 0 || + strcmp(entry->d_name, "..") == 0) { + continue; + } + + char child_path[4096]; + snprintf(child_path, sizeof(child_path), "%s/%s", directory, + entry->d_name); + + struct stat st; + if (stat(child_path, &st) != 0) { + fprintf(stderr, "pack: cannot stat %s: %s\n", child_path, + strerror(errno)); + continue; + } + + char child_name[4096]; + if (prefix[0]) { + snprintf(child_name, sizeof(child_name), "%s/%s", prefix, + entry->d_name); + } else { + snprintf(child_name, sizeof(child_name), "%s", entry->d_name); + } + + if (S_ISDIR(st.st_mode)) { + if (walk_directory(child_path, child_name) != 0) { + closedir(dir); + return -1; + } + } else if (S_ISREG(st.st_mode)) { + add_entry(child_name, (size_t)st.st_size); + } + } + + closedir(dir); + return 0; +} + +static int pack_file(FILE *out, const char *data_dir, const char *name) { + char path[4096]; + snprintf(path, sizeof(path), "%s/%s", data_dir, name); + + FILE *input = fopen(path, "rb"); + if (!input) { + fprintf(stderr, "pack: cannot open %s: %s\n", path, strerror(errno)); + return -1; + } + + char buffer[SECTOR]; + size_t read; + while ((read = fread(buffer, 1, sizeof(buffer), input)) > 0) { + if (fwrite(buffer, 1, read, out) != read) { + fprintf(stderr, "pack: write error\n"); + fclose(input); + return -1; + } + } + fclose(input); + + static char pad[SECTOR] = {0}; + if (ftell(out) % SECTOR != 0) { + const size_t remaining = SECTOR - (ftell(out) % SECTOR); + fwrite(pad, 1, remaining, out); + } + return 0; +} + +int main(int argc, char **argv) { + if (argc != 3) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + const char *data_dir = argv[1]; + const char *out_path = argv[2]; + + if (walk_directory(data_dir, "") != 0) { + return 1; + } + if (entry_count == 0) { + fprintf(stderr, "pack: no files found under %s\n", data_dir); + return 1; + } + qsort(entries, entry_count, sizeof(struct entry), entry_compare); + + size_t index_size = HEADER_SIZE + entry_count * ENTRY_SIZE; + size_t name_offsets[entry_count]; + for (size_t index = 0; index < entry_count; ++index) { + name_offsets[index] = index_size - HEADER_SIZE; + index_size += entries[index].name_len; + } + index_size = align_up(index_size, SECTOR); + + unsigned long data_offset = (unsigned long)index_size; + for (size_t index = 0; index < entry_count; ++index) { + entries[index].data_offset = data_offset; + data_offset += (unsigned long)align_up(entries[index].file_size, SECTOR); + } + + FILE *out = fopen(out_path, "wb"); + if (!out) { + fprintf(stderr, "pack: cannot create %s: %s\n", out_path, + strerror(errno)); + return 1; + } + + unsigned char header[HEADER_SIZE]; + header[0] = (unsigned char)(SFB_MAGIC); + header[1] = (unsigned char)(SFB_MAGIC >> 8); + header[2] = (unsigned char)(SFB_MAGIC >> 16); + header[3] = (unsigned char)(SFB_MAGIC >> 24); + header[4] = (unsigned char)(SFB_VERSION); + header[5] = (unsigned char)(SFB_VERSION >> 8); + header[6] = (unsigned char)(SFB_VERSION >> 16); + header[7] = (unsigned char)(SFB_VERSION >> 24); + header[8] = (unsigned char)(entry_count); + header[9] = (unsigned char)(entry_count >> 8); + header[10] = (unsigned char)(entry_count >> 16); + header[11] = (unsigned char)(entry_count >> 24); + header[12] = (unsigned char)(index_size); + header[13] = (unsigned char)(index_size >> 8); + header[14] = (unsigned char)(index_size >> 16); + header[15] = (unsigned char)(index_size >> 24); + fwrite(header, 1, sizeof(header), out); + + for (size_t index = 0; index < entry_count; ++index) { + unsigned char entry[ENTRY_SIZE]; + const unsigned long name_offset = (unsigned long)name_offsets[index]; + const unsigned long name_len = (unsigned long)entries[index].name_len; + const unsigned long data_off = entries[index].data_offset; + const unsigned long data_len = (unsigned long)entries[index].file_size; + entry[0] = (unsigned char)(name_offset); + entry[1] = (unsigned char)(name_offset >> 8); + entry[2] = (unsigned char)(name_offset >> 16); + entry[3] = (unsigned char)(name_offset >> 24); + entry[4] = (unsigned char)(name_len); + entry[5] = (unsigned char)(name_len >> 8); + entry[6] = (unsigned char)(name_len >> 16); + entry[7] = (unsigned char)(name_len >> 24); + entry[8] = (unsigned char)(data_off); + entry[9] = (unsigned char)(data_off >> 8); + entry[10] = (unsigned char)(data_off >> 16); + entry[11] = (unsigned char)(data_off >> 24); + entry[12] = (unsigned char)(data_len); + entry[13] = (unsigned char)(data_len >> 8); + entry[14] = (unsigned char)(data_len >> 16); + entry[15] = (unsigned char)(data_len >> 24); + fwrite(entry, 1, sizeof(entry), out); + } + + for (size_t index = 0; index < entry_count; ++index) { + fwrite(entries[index].name, 1, entries[index].name_len, out); + } + + static unsigned char pad[SECTOR] = {0}; + const size_t remaining = index_size - ftell(out); + fwrite(pad, 1, remaining, out); + + for (size_t index = 0; index < entry_count; ++index) { + if (pack_file(out, data_dir, entries[index].name) != 0) { + fclose(out); + return 1; + } + } + + fclose(out); + fprintf(stderr, "pack: %zu files, index %zu bytes, total %lu bytes\n", + entry_count, index_size, data_offset); + return 0; +}