From c16c66dddd4c991e8b00d31512492896655149fe Mon Sep 17 00:00:00 2001 From: Trulio Date: Sat, 1 Aug 2026 13:25:27 -0300 Subject: [PATCH 1/8] Add ps2 iso build --- CMakeLists.txt | 4 +- documentation/ps2-port.md | 17 + src/SF_EXE/cmake/configure_platform.cmake | 3 + src/SF_EXE/cmake/configure_presentation.cmake | 3 +- src/SF_EXE/cmake/platforms/PS2.cmake | 10 + src/SF_EXE/gapi/gapi.cpp | 4 +- src/SF_EXE/items/item_condition.cpp | 2 +- src/SF_EXE/items/item_information.cpp | 4 +- .../libs/RKC_DBFCONTROL/software_backend.cpp | 14 +- src/SF_EXE/libs/RKC_DIB/bitmap.cpp | 4 +- .../libs/RKC_RPGSCRN/display_hit_test.cpp | 2 +- src/SF_EXE/render/character_renderer.cpp | 6 +- .../render/character_select_renderer.cpp | 6 +- .../render/enemy_nameplate_renderer.cpp | 4 +- src/SF_EXE/render/gameplay_hud_renderer.cpp | 6 +- src/SF_EXE/render/gameplay_magic_renderer.cpp | 4 +- src/SF_EXE/render/gameplay_renderer.cpp | 12 +- .../render/item_information_renderer.cpp | 8 +- src/SF_EXE/render/loading_renderer.cpp | 2 +- src/SF_EXE/runtime/main.cpp | 27 +- .../runtime/platform/ps2/application_loop.cpp | 11 + .../runtime/platform/ps2/ps2_data_backend.cpp | 709 ++++++++++++++++++ .../runtime/platform/ps2/ps2_data_backend.hpp | 23 + .../platform/ps2/surface_presenter.cpp | 177 +++++ .../character_select/new_character_flow.cpp | 2 +- src/SF_EXE/states/gameplay_inventory.cpp | 4 +- src/SF_EXE/states/gameplay_transport.cpp | 2 +- src/SF_EXE/ui/conversation_layout.cpp | 4 +- .../ui/player_level_up_notice_layout.cpp | 3 +- src/SF_EXE/world/combat_effect_actor.cpp | 2 +- src/SF_EXE/world/combat_hit_chance.cpp | 4 +- src/SF_EXE/world/companion_actor.cpp | 4 +- src/SF_EXE/world/companion_attack_action.cpp | 2 +- src/SF_EXE/world/enemy_actor.cpp | 2 +- src/SF_EXE/world/enemy_death_rewards.cpp | 8 +- src/SF_EXE/world/movement_controller.cpp | 6 +- src/SF_EXE/world/npc_actor.cpp | 6 +- src/SF_EXE/world/player_actor.cpp | 8 +- src/SF_EXE/world/player_attack_action.cpp | 10 +- src/SF_EXE/world/player_data.cpp | 18 +- src/SF_EXE/world/player_moon_spell.cpp | 32 +- src/SF_EXE/world/player_ranged_attack.cpp | 6 +- src/SF_EXE/world/player_runtime_profile.cpp | 60 +- src/SF_EXE/world/player_spell_action.cpp | 6 +- src/SF_EXE/world/player_spell_parameters.cpp | 10 +- src/SF_EXE/world/world_pointer.cpp | 2 +- tests/native/enemy_effect_controller_test.cpp | 4 +- tests/native/player_spell_cast_test.cpp | 96 +-- thirdparty/lal/CMakeLists.txt | 3 + thirdparty/lwl/CMakeLists.txt | 3 + 50 files changed, 1179 insertions(+), 190 deletions(-) create mode 100644 documentation/ps2-port.md create mode 100644 src/SF_EXE/cmake/platforms/PS2.cmake create mode 100644 src/SF_EXE/runtime/platform/ps2/application_loop.cpp create mode 100644 src/SF_EXE/runtime/platform/ps2/ps2_data_backend.cpp create mode 100644 src/SF_EXE/runtime/platform/ps2/ps2_data_backend.hpp create mode 100644 src/SF_EXE/runtime/platform/ps2/surface_presenter.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bb0de1e5..3e754b3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,9 @@ set_property( add_subdirectory(thirdparty/lwl) add_subdirectory(thirdparty/lal) -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..cf0ea6ec --- /dev/null +++ b/documentation/ps2-port.md @@ -0,0 +1,17 @@ +# PlayStation 2 port + +The PS2 port is packaged through Docker, so the repository does not need a +local ps2dev toolchain. With the original game +files present under `tmp/ShadowFlare`, run: + +```sh +sh tools/ps2/build-iso.sh +``` + +The first run builds the `openshadowflare-ps2` image from +`tools/ps2/Dockerfile`; later runs reuse it. The generated disc files, +including `openshadowflare.iso`, are written to `build/ps2`. + +Use `--build-image` to force a rebuild of the Docker image after changing the +toolchain setup, and `--data-dir` or `--out-dir` to override the default input +or output locations. diff --git a/src/SF_EXE/cmake/configure_platform.cmake b/src/SF_EXE/cmake/configure_platform.cmake index 7ca33e6c..25e4d7b4 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( WIN32 OR APPLE diff --git a/src/SF_EXE/cmake/configure_presentation.cmake b/src/SF_EXE/cmake/configure_presentation.cmake index 1a77d284..451f20b1 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 9c6b04de..ef4105d2 100644 --- a/src/SF_EXE/gapi/gapi.cpp +++ b/src/SF_EXE/gapi/gapi.cpp @@ -25,8 +25,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 105ac210..49652e84 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; } diff --git a/src/SF_EXE/libs/RKC_DBFCONTROL/software_backend.cpp b/src/SF_EXE/libs/RKC_DBFCONTROL/software_backend.cpp index 6f9f432c..329e9691 100644 --- a/src/SF_EXE/libs/RKC_DBFCONTROL/software_backend.cpp +++ b/src/SF_EXE/libs/RKC_DBFCONTROL/software_backend.cpp @@ -13,7 +13,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); } @@ -22,7 +22,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) + @@ -38,7 +38,7 @@ std::uint8_t blendChannel( std::uint8_t destination, std::uint8_t source, std::int32_t opacity) { - opacity = std::clamp(opacity, 0, 1000); + opacity = std::clamp(opacity, std::int32_t{0}, std::int32_t{1000}); return static_cast( (static_cast(source) * opacity + static_cast(destination) * @@ -74,8 +74,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_)), @@ -170,11 +170,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) { 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 c1cedee9..523cdd2a 100644 --- a/src/SF_EXE/libs/RKC_RPGSCRN/display_hit_test.cpp +++ b/src/SF_EXE/libs/RKC_RPGSCRN/display_hit_test.cpp @@ -142,7 +142,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 5c5285d9..67263d2b 100644 --- a/src/SF_EXE/render/character_renderer.cpp +++ b/src/SF_EXE/render/character_renderer.cpp @@ -33,7 +33,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 = @@ -120,10 +120,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 f23ec466..b074a6db 100644 --- a/src/SF_EXE/render/character_select_renderer.cpp +++ b/src/SF_EXE/render/character_select_renderer.cpp @@ -70,7 +70,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, @@ -319,7 +319,7 @@ void renderSavedGames( const std::int32_t itemBrightness = index == static_cast( - std::max(selected, 0)) + std::max(selected, std::int32_t{0})) ? brightness : brightness / 2; renderer.drawPattern( @@ -335,7 +335,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 8ad865d7..88e98fc4 100644 --- a/src/SF_EXE/render/gameplay_hud_renderer.cpp +++ b/src/SF_EXE/render/gameplay_hud_renderer.cpp @@ -34,7 +34,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> @@ -92,7 +92,7 @@ std::int32_t gameplayHudExperienceBarWidth( static_cast( static_cast(experience) * kRetailExperienceWidth / threshold), - 1); + std::int32_t{1}); } std::int32_t gameplayHudBarWidth( @@ -108,7 +108,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 e72c7a59..1966a4e0 100644 --- a/src/SF_EXE/render/gameplay_magic_renderer.cpp +++ b/src/SF_EXE/render/gameplay_magic_renderer.cpp @@ -168,11 +168,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_renderer.cpp b/src/SF_EXE/render/gameplay_renderer.cpp index 3aa1ee45..735a7059 100644 --- a/src/SF_EXE/render/gameplay_renderer.cpp +++ b/src/SF_EXE/render/gameplay_renderer.cpp @@ -421,9 +421,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() + @@ -550,7 +550,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( @@ -848,7 +848,7 @@ void drawGroundItem( 1000, shadow ? std::clamp( - shadow_opacity, 0, 1000) + shadow_opacity, std::int32_t{0}, std::int32_t{1000}) : 1000, shadow ? 1000 @@ -1113,9 +1113,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/item_information_renderer.cpp b/src/SF_EXE/render/item_information_renderer.cpp index ea940085..889f857a 100644 --- a/src/SF_EXE/render/item_information_renderer.cpp +++ b/src/SF_EXE/render/item_information_renderer.cpp @@ -83,12 +83,12 @@ void renderItemInformation( text_height + kInformationPadding * 2; const std::int32_t x = std::clamp( inventory.pointerX() - 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( inventory.pointerY() + 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}; 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/runtime/main.cpp b/src/SF_EXE/runtime/main.cpp index 947875c5..0649811d 100644 --- a/src/SF_EXE/runtime/main.cpp +++ b/src/SF_EXE/runtime/main.cpp @@ -3,6 +3,11 @@ #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 @@ -18,6 +23,9 @@ bool isSmokeTest(int argc, char** argv) { } std::filesystem::path findDataRoot() { +#if defined(__PS2__) + return std::filesystem::path("cdrom0:\\ShadowFlare"); +#else const auto isDataRoot = [](const std::filesystem::path& candidate) { std::error_code error; @@ -85,17 +93,34 @@ std::filesystem::path findDataRoot() { } } 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. - osf::loadGameConfigFile( + 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); } 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..e5fa1fcf --- /dev/null +++ b/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.cpp @@ -0,0 +1,709 @@ +#define _GNU_SOURCE +#define NEWLIB_PORT_AWARE + +#include "ps2_data_backend.hpp" + +#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 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; +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; +} + +} // namespace + +int initDataBackend() { + if (s_initialized) { + return 0; + } + + fioInit(); + 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; + std::fprintf( + stderr, + "ps2 data: %lu files from %s\n", + static_cast(s_entry_count), + kArchiveName); + return 0; +} + +} // 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..9b42f00f --- /dev/null +++ b/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.hpp @@ -0,0 +1,23 @@ +// 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(); + +} // 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..9b09475f --- /dev/null +++ b/src/SF_EXE/runtime/platform/ps2/surface_presenter.cpp @@ -0,0 +1,177 @@ +#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 present(SurfaceView surface) override; + +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; +}; + +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() { + 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::present(SurfaceView surface) { + 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_); + gsKit_sync_flip(gsGlobal_); +} + +} // namespace + +std::unique_ptr +osf::runtime::createSurfacePresenter() { + return std::make_unique(); +} 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/gameplay_inventory.cpp b/src/SF_EXE/states/gameplay_inventory.cpp index 7f058b78..bc991fd4 100644 --- a/src/SF_EXE/states/gameplay_inventory.cpp +++ b/src/SF_EXE/states/gameplay_inventory.cpp @@ -636,7 +636,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 || @@ -680,7 +680,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/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 4de998f5..4aec99e5 100644 --- a/src/SF_EXE/world/combat_effect_actor.cpp +++ b/src/SF_EXE/world/combat_effect_actor.cpp @@ -139,7 +139,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 15296884..1b896076 100644 --- a/src/SF_EXE/world/companion_actor.cpp +++ b/src/SF_EXE/world/companion_actor.cpp @@ -329,7 +329,7 @@ CompanionActor::updateDamagePresentation( presentation_animation_frame_ = std::clamp( presentation_animation_frame_, - 0, + std::int32_t{0}, count - 1); if (!reaction_displacement_suppressed_ && @@ -545,7 +545,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::valid() const { 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/enemy_actor.cpp b/src/SF_EXE/world/enemy_actor.cpp index c142a770..bfe39203 100644 --- a/src/SF_EXE/world/enemy_actor.cpp +++ b/src/SF_EXE/world/enemy_actor.cpp @@ -331,7 +331,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 f78c83e8..e0dc4b5a 100644 --- a/src/SF_EXE/world/enemy_death_rewards.cpp +++ b/src/SF_EXE/world/enemy_death_rewards.cpp @@ -177,8 +177,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 / @@ -258,7 +258,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; @@ -355,7 +355,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 5e4e4c22..c99315aa 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 0e71d42d..b95a8d85 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; @@ -269,7 +269,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; } @@ -319,7 +319,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 0f7ba249..a275870c 100644 --- a/src/SF_EXE/world/player_attack_action.cpp +++ b/src/SF_EXE/world/player_attack_action.cpp @@ -171,18 +171,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( @@ -203,7 +203,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; @@ -225,7 +225,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 double factor = diff --git a/src/SF_EXE/world/player_data.cpp b/src/SF_EXE/world/player_data.cpp index 61c68365..27b99cb2 100644 --- a/src/SF_EXE/world/player_data.cpp +++ b/src/SF_EXE/world/player_data.cpp @@ -218,7 +218,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; @@ -366,8 +366,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) { @@ -381,8 +381,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() { @@ -407,7 +407,7 @@ bool PlayerData::restoreLife( std::clamp( restored, 0, - std::max(0, baseMaximumLife())))); + std::max(std::int32_t{0}, baseMaximumLife())))); return currentLife() != before; } @@ -426,7 +426,7 @@ bool PlayerData::restoreMana( std::clamp( restored, 0, - std::max(0, baseMaximumMana())))); + std::max(std::int32_t{0}, baseMaximumMana())))); return currentMana() != before; } @@ -593,8 +593,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_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 29e70eb1..8fadb492 100644 --- a/src/SF_EXE/world/player_spell_action.cpp +++ b/src/SF_EXE/world/player_spell_action.cpp @@ -278,7 +278,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; } @@ -508,7 +508,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( @@ -530,7 +530,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/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/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 c0b6552e..1637cb32 100644 --- a/tests/native/player_spell_cast_test.cpp +++ b/tests/native/player_spell_cast_test.cpp @@ -674,7 +674,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 && @@ -773,35 +773,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; } @@ -892,30 +892,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; } @@ -1231,13 +1231,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()) { @@ -1506,11 +1506,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()) { @@ -1724,13 +1724,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()) { @@ -1946,13 +1946,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()) { @@ -2921,7 +2921,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( @@ -3352,11 +3352,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()) { @@ -3388,7 +3388,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/thirdparty/lal/CMakeLists.txt b/thirdparty/lal/CMakeLists.txt index d6556d87..483b3dac 100644 --- a/thirdparty/lal/CMakeLists.txt +++ b/thirdparty/lal/CMakeLists.txt @@ -62,6 +62,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/lwl/CMakeLists.txt b/thirdparty/lwl/CMakeLists.txt index 65026b23..9e0b237a 100644 --- a/thirdparty/lwl/CMakeLists.txt +++ b/thirdparty/lwl/CMakeLists.txt @@ -82,6 +82,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() From 1c7654d4f05b9510d8d14632fafb04e7c20d4558 Mon Sep 17 00:00:00 2001 From: Trulio Date: Sat, 1 Aug 2026 14:08:20 -0300 Subject: [PATCH 2/8] Add Dockerfile --- src/SF_EXE/world/runtime_effect_system.cpp | 4 +- src/SF_EXE/world/runtime_effect_system.hpp | 2 +- src/SF_EXE/world/world_scene.hpp | 2 +- src/SF_EXE/world/world_scene_combat.cpp | 7 +- tests/native/check_source_boundaries.cmake | 8 +- thirdparty/lal/lal_ps2.c | 121 +++++ thirdparty/lwl/lwl_ps2.c | 492 +++++++++++++++++++++ tools/ps2/Dockerfile | 53 +++ tools/ps2/build-iso.sh | 122 +++++ tools/ps2/make-iso.sh | 65 +++ tools/ps2/pack.c | 254 +++++++++++ 11 files changed, 1119 insertions(+), 11 deletions(-) create mode 100644 thirdparty/lal/lal_ps2.c create mode 100644 thirdparty/lwl/lwl_ps2.c create mode 100644 tools/ps2/Dockerfile create mode 100644 tools/ps2/build-iso.sh create mode 100644 tools/ps2/make-iso.sh create mode 100644 tools/ps2/pack.c 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/world_scene.hpp b/src/SF_EXE/world/world_scene.hpp index b38d17a4..43c79672 100644 --- a/src/SF_EXE/world/world_scene.hpp +++ b/src/SF_EXE/world/world_scene.hpp @@ -323,7 +323,7 @@ class WorldScene { std::int32_t main_hand_subtype); 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 82ef3d55..2531fd55 100644 --- a/src/SF_EXE/world/world_scene_combat.cpp +++ b/src/SF_EXE/world/world_scene_combat.cpp @@ -8,7 +8,7 @@ namespace osf { void WorldScene::handleEnemyDeathStart( EnemyActor& enemy, - CombatEffectSpawnRequest effect) { + const CombatEffectSpawnRequest& effect) { constexpr std::int32_t kEpisodeOneMask = 1; const std::vector drops = createRetailEnemyDrops( @@ -50,8 +50,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/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/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/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/Dockerfile b/tools/ps2/Dockerfile new file mode 100644 index 00000000..c50069a6 --- /dev/null +++ b/tools/ps2/Dockerfile @@ -0,0 +1,53 @@ +# Builds on the official ps2dev image, but replaces its stale/legacy ps2sdk +# with a hash-verified ps2sdk revision and adds the build and disc-image tools that +# the base image omits (make, cmake, genisoimage, ...). +# +# The ps2sdk rebuild is serial on purpose: GCC 15's lto1 crashes with +# "resolution sub id not in object file" / "file too short" when the SDK's +# LTO-heavy kernel objects are built in parallel (-j). +# +# Build: +# docker build -t openshadowflare-ps2 -f tools/ps2/Dockerfile . +# +# Run: +# docker run --rm -it -v "$PWD:/work" -w /work openshadowflare-ps2 sh +# +# Inside the container PS2DEV, PS2SDK and GSKIT are pre-set and the EE +# compiler is `mips64r5900el-ps2-elf-gcc` (R5900, GCC 15.2.0). + +FROM ps2dev/ps2dev@sha256:c64ae69c9817865ed98ff054e4ae5360b9e280ed952c97946bca95d9d35be995 + +# The build verifies this is still the intended PS2SDK master revision before +# compiling. If upstream advances, the image fails loudly instead of silently +# changing the SDK used by the port. +ARG PS2SDK_REF=e228ff7b61a12ad1192a49338754534362a26e58 + +ENV PS2DEV=/usr/local/ps2dev +ENV PS2SDK=$PS2DEV/ps2sdk +ENV GSKIT=$PS2DEV/gsKit +ENV PATH=$PATH:$PS2DEV/bin:$PS2DEV/ee/bin:$PS2DEV/iop/bin:$PS2DEV/dvp/bin:$PS2SDK/bin:$PS2SDK/ee/bin:$PS2SDK/iop/bin + +RUN apk add --no-cache \ + build-base \ + cmake \ + git \ + bash \ + perl \ + cdrkit + +RUN rm -rf $PS2SDK/ee $PS2SDK/iop $PS2SDK/common $PS2SDK/bin $PS2SDK/samples \ + && git clone --depth 1 --branch master https://github.com/ps2dev/ps2sdk.git /tmp/ps2sdk \ + && test "$(git -C /tmp/ps2sdk rev-parse HEAD)" = "$PS2SDK_REF" \ + && cd /tmp/ps2sdk \ + && make -j1 \ + && make install \ + && cd / \ + && rm -rf /tmp/ps2sdk \ + && apk del git bash perl \ + && ln -sf ../../../ps2sdk/ee/lib/libcglue.a $PS2DEV/ee/mips64r5900el-ps2-elf/lib/libcglue.a \ + && ln -sf ../../../ps2sdk/ee/lib/libpthreadglue.a $PS2DEV/ee/mips64r5900el-ps2-elf/lib/libpthreadglue.a \ + && ln -sf ../../../ps2sdk/ee/lib/libprofglue.a $PS2DEV/ee/mips64r5900el-ps2-elf/lib/libprofglue.a \ + && ln -sf ../../../ps2sdk/ee/lib/libkernel.a $PS2DEV/ee/mips64r5900el-ps2-elf/lib/libkernel.a \ + && ln -sf ../../../ps2sdk/ee/lib/libcdvd.a $PS2DEV/ee/mips64r5900el-ps2-elf/lib/libcdvd.a + +CMD ["sh"] diff --git a/tools/ps2/build-iso.sh b/tools/ps2/build-iso.sh new file mode 100644 index 00000000..554655a6 --- /dev/null +++ b/tools/ps2/build-iso.sh @@ -0,0 +1,122 @@ +#!/bin/sh + +set -eu + +usage() { + cat <<'EOF' +Usage: tools/ps2/build-iso.sh [options] + +Options: + --data-dir PATH Original ShadowFlare data directory. + Default: tmp/ShadowFlare + --out-dir PATH Directory for the ISO and disc files. + Default: build/ps2 + --image NAME Docker image name. Default: openshadowflare-ps2 + --build-image Rebuild the Docker image before packaging. + -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="$repo_root/tmp/ShadowFlare" +out_dir="$repo_root/build/ps2" +image=openshadowflare-ps2 +build_image=0 + +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 + ;; + --image) + [ "$#" -ge 2 ] || { echo "--image needs a name" >&2; exit 2; } + image=$2 + shift 2 + ;; + --build-image) + build_image=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [ ! -d "$data_dir" ]; then + echo "Data directory not found: $data_dir" >&2 + exit 1 +fi +data_dir=$(CDPATH= cd -- "$data_dir" && pwd -P) + +mkdir -p "$out_dir" +out_dir=$(CDPATH= cd -- "$out_dir" && pwd -P) + +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 + +docker_run() { + case "$(uname -s)" in + MINGW*|MSYS*) + MSYS_NO_PATHCONV=1 docker "$@" + ;; + *) + docker "$@" + ;; + esac +} + +docker_host_path() { + case "$(uname -s)" in + MINGW*|MSYS*) + cygpath -w "$1" + ;; + *) + printf '%s\n' "$1" + ;; + esac +} + +repo_mount=$(docker_host_path "$repo_root") +data_mount=$(docker_host_path "$data_dir") +out_mount=$(docker_host_path "$out_dir") + +if [ "$build_image" -eq 0 ] && ! docker image inspect "$image" >/dev/null 2>&1; then + build_image=1 +fi +if [ "$build_image" -eq 1 ]; then + echo "== build Docker image ==" + docker_run build -t "$image" -f "$repo_mount/tools/ps2/Dockerfile" "$repo_mount" +fi + +echo "== build ISO into $out_dir ==" +docker_run run --rm \ + -v "$repo_mount:/mnt/repo" \ + -v "$data_mount:/hostdata:ro" \ + -v "$out_mount:/out" \ + "$image" \ + sh /mnt/repo/tools/ps2/make-iso.sh + +echo "ISO ready: $out_dir/openshadowflare.iso" diff --git a/tools/ps2/make-iso.sh b/tools/ps2/make-iso.sh new file mode 100644 index 00000000..f64fff5c --- /dev/null +++ b/tools/ps2/make-iso.sh @@ -0,0 +1,65 @@ +# Builds the PlayStation 2 disc image for OpenShadowFlare. +# +# Runs inside the openshadowflare-ps2 Docker image: +# docker run --rm -it \ +# -v :/mnt/repo \ +# -v :/hostdata \ +# -v :/out \ +# openshadowflare-ps2 sh /mnt/repo/tools/ps2/make-iso.sh +# +# Produces /out/openshadowflare.iso: SYSTEM.CNF + boot ELF + SFGAME.BIN (the +# packed game-data archive read through ps2_data_backend.cpp). The data tree +# itself is intentionally NOT on the disc: the BIOS fileio module strips path +# separators and matches ISO9660 names case-sensitively, so it can only reach +# flat uppercase root files anyway. + +set -u +export PS2SDK=/usr/local/ps2dev/ps2sdk +export PS2DEV=/usr/local/ps2dev +export GSKIT=$PS2DEV/gsKit +export PATH=$PATH:$PS2DEV/bin:$PS2DEV/ee/bin:$PS2DEV/iop/bin:$PS2SDK/bin:$PS2SDK/ee/bin:$PS2SDK/iop/bin + +REPO=${REPO:-/mnt/repo} +DATA=${DATA:-/hostdata} +OUT=${OUT:-/out} +ELF_NAME=${ELF_NAME:-OPENSHAD.ELF} + +echo "== pack game data ==" +gcc -O2 -Wall -o /tmp/pack "$REPO/tools/ps2/pack.c" || exit 1 +/tmp/pack "$DATA" "$OUT/SFGAME.BIN" || exit 1 + +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" "$OUT/$ELF_NAME" + +echo "== copy IOP modules ==" +cp "$PS2SDK/iop/irx/iomanX.irx" "$OUT/IOMANX.IRX" || exit 1 +cp "$PS2SDK/iop/irx/fileXio.irx" "$OUT/FILEXIO.IRX" || exit 1 +cp "$PS2SDK/iop/irx/sio2man.irx" "$OUT/SIO2MAN.IRX" || exit 1 +cp "$PS2SDK/iop/irx/padman.irx" "$OUT/PADMAN.IRX" || exit 1 +cp "$PS2SDK/iop/irx/audsrv.irx" "$OUT/AUDSRV.IRX" || exit 1 + +echo "== build ISO ==" +printf 'BOOT2 = cdrom0:\\%s;1\nVER = 1.01\nVMODE = NTSC\n' "$ELF_NAME" > "$OUT/SYSTEM.CNF" +rm -f "$OUT/openshadowflare.iso" +genisoimage -iso-level 2 -R -J -V OPENSHDOW -o "$OUT/openshadowflare.iso" \ + "$OUT/SYSTEM.CNF" \ + "$OUT/$ELF_NAME" \ + "$OUT/SFGAME.BIN" \ + "$OUT/IOMANX.IRX" \ + "$OUT/FILEXIO.IRX" \ + "$OUT/SIO2MAN.IRX" \ + "$OUT/PADMAN.IRX" \ + "$OUT/AUDSRV.IRX" \ + || 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; +} From 36f2860740c8d407d84161c5d4ff73c4fb8bee5d Mon Sep 17 00:00:00 2001 From: Trulio Date: Mon, 3 Aug 2026 08:49:15 -0300 Subject: [PATCH 3/8] Update Ps2 build workflow --- .github/workflows/ps2.yml | 56 +++++++++ documentation/ps2-port.md | 74 ++++++++++-- src/SF_EXE/cmake/platforms/PS2.cmake | 2 +- src/SF_EXE/items/item_information.cpp | 2 +- src/SF_EXE/items/item_repair.cpp | 2 +- .../libs/RKC_DBFCONTROL/software_backend.cpp | 22 +++- .../render/character_select_renderer.cpp | 3 +- src/SF_EXE/render/gameplay_magic_renderer.cpp | 3 +- .../render/gameplay_overlay_renderer.cpp | 8 +- .../render/gameplay_status_renderer.cpp | 4 +- .../render/item_information_renderer.cpp | 10 +- src/SF_EXE/resources/retail_filesystem.cpp | 11 +- src/SF_EXE/resources/save_catalog.cpp | 5 +- src/SF_EXE/runtime/audio_system.cpp | 3 +- src/SF_EXE/runtime/main.cpp | 3 +- .../runtime/platform/ps2/ps2_data_backend.cpp | 56 +++++++++ .../runtime/platform/ps2/ps2_data_backend.hpp | 1 + .../platform/ps2/surface_presenter.cpp | 32 +++++- src/SF_EXE/states/gameplay_vendor.cpp | 4 +- .../world/companion_explosion_action.cpp | 3 +- src/SF_EXE/world/companion_status_message.cpp | 2 +- src/SF_EXE/world/player_attack_action.cpp | 5 +- src/SF_EXE/world/player_increased_power.cpp | 7 +- src/SF_EXE/world/player_item_controller.cpp | 7 +- src/SF_EXE/world/player_land_mine.cpp | 11 +- src/SF_EXE/world/player_transport_spell.cpp | 7 +- src/SF_EXE/world/player_voice.cpp | 5 +- src/SF_EXE/world/retail_save_mines.cpp | 7 +- src/SF_EXE/world/runtime_effect_actor.cpp | 2 +- .../world/scenario_screen_particles.cpp | 3 +- .../world/scenario_visual_presentation.cpp | 7 +- src/SF_EXE/world/scenario_world.cpp | 3 +- src/SF_EXE/world/world_scene_effects.cpp | 2 +- src/SF_EXE/world/world_scene_interaction.cpp | 3 +- .../world/world_scene_player_profile.cpp | 2 +- src/SF_EXE/world/world_scene_presentation.cpp | 3 +- .../world/world_scene_transport_spell.cpp | 6 +- tools/ps2/Dockerfile | 53 --------- tools/ps2/build-iso.sh | 107 +++++++----------- tools/ps2/make-iso.sh | 90 +++++++++------ 40 files changed, 413 insertions(+), 223 deletions(-) create mode 100644 .github/workflows/ps2.yml delete mode 100644 tools/ps2/Dockerfile diff --git a/.github/workflows/ps2.yml b/.github/workflows/ps2.yml new file mode 100644 index 00000000..53ad735f --- /dev/null +++ b/.github/workflows/ps2.yml @@ -0,0 +1,56 @@ +name: Build PlayStation 2 ISO + +on: + push: + 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 + +jobs: + build: + runs-on: ubuntu-latest + env: + PS2DEV: ${{ runner.temp }}/ps2dev + + steps: + - name: Check out the repository + uses: actions/checkout@v6 + + - name: Install PS2DEV + run: | + sudo apt-get update + sudo apt-get install --yes cmake genisoimage + 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/documentation/ps2-port.md b/documentation/ps2-port.md index cf0ea6ec..ce6ff18a 100644 --- a/documentation/ps2-port.md +++ b/documentation/ps2-port.md @@ -1,17 +1,71 @@ -# PlayStation 2 port +# PS2 port -The PS2 port is packaged through Docker, so the repository does not need a -local ps2dev toolchain. With the original game -files present under `tmp/ShadowFlare`, run: +## 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 first run builds the `openshadowflare-ps2` image from -`tools/ps2/Dockerfile`; later runs reuse it. The generated disc files, -including `openshadowflare.iso`, are written to `build/ps2`. +The archive is published on [PS2DEV releases](https://github.com/ps2dev/ps2dev/releases). + +The default output, `build/ps2/openshadowflare.iso`, is data-free and suitable +for CI and distribution. 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 + ... +``` + +PCSX2 maps its ISO folder to the `host0:` 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/ +``` + +Alternatively, create a private all-in-one disc from an owned retail +installation: + +```sh +sh tools/ps2/build-iso.sh --data-dir /path/to/ShadowFlare +``` + +## Type portability -Use `--build-image` to force a rebuild of the Docker image after changing the -toolchain setup, and `--data-dir` or `--out-dir` to override the default input -or output locations. +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/cmake/platforms/PS2.cmake b/src/SF_EXE/cmake/platforms/PS2.cmake index aea68fe9..19ea0a2a 100644 --- a/src/SF_EXE/cmake/platforms/PS2.cmake +++ b/src/SF_EXE/cmake/platforms/PS2.cmake @@ -6,5 +6,5 @@ function(osf_configure_ps2_platform target) runtime/platform/ps2/ps2_data_backend.cpp runtime/platform/ps2/surface_presenter.cpp ) - target_link_libraries(${target} PRIVATE gskit dmakit) + target_link_libraries(${target} PRIVATE gskit dmakit loadfile) endfunction() diff --git a/src/SF_EXE/items/item_information.cpp b/src/SF_EXE/items/item_information.cpp index 14b7d827..a0568927 100644 --- a/src/SF_EXE/items/item_information.cpp +++ b/src/SF_EXE/items/item_information.cpp @@ -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 faa2d75c..75d372b8 100644 --- a/src/SF_EXE/libs/RKC_DBFCONTROL/software_backend.cpp +++ b/src/SF_EXE/libs/RKC_DBFCONTROL/software_backend.cpp @@ -61,7 +61,7 @@ std::uint8_t addChannel( std::min( static_cast(destination) + source_amount, - 255)); + std::int32_t{255})); } std::uint8_t readPixelIndex( @@ -158,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 || @@ -480,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; } @@ -674,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() + @@ -713,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/render/character_select_renderer.cpp b/src/SF_EXE/render/character_select_renderer.cpp index 43d60261..b8e08214 100644 --- a/src/SF_EXE/render/character_select_renderer.cpp +++ b/src/SF_EXE/render/character_select_renderer.cpp @@ -327,7 +327,8 @@ void renderSavedGames( : 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}; diff --git a/src/SF_EXE/render/gameplay_magic_renderer.cpp b/src/SF_EXE/render/gameplay_magic_renderer.cpp index a60cabeb..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; } 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_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/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 1aa1b2fa..9c87484b 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 { @@ -105,7 +106,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/main.cpp b/src/SF_EXE/runtime/main.cpp index 0649811d..988d10b5 100644 --- a/src/SF_EXE/runtime/main.cpp +++ b/src/SF_EXE/runtime/main.cpp @@ -24,7 +24,8 @@ bool isSmokeTest(int argc, char** argv) { std::filesystem::path findDataRoot() { #if defined(__PS2__) - return std::filesystem::path("cdrom0:\\ShadowFlare"); + return std::filesystem::path( + osf::runtime::platform::ps2::dataRoot()); #else const auto isDataRoot = [](const std::filesystem::path& candidate) { diff --git a/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.cpp b/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.cpp index e5fa1fcf..efee18ba 100644 --- a/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.cpp +++ b/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.cpp @@ -3,7 +3,10 @@ #include "ps2_data_backend.hpp" +#include +#include #include +#include #include #include @@ -24,6 +27,15 @@ 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; @@ -57,6 +69,7 @@ struct DirHandle { }; 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; @@ -591,6 +604,23 @@ bool readFull(int fd, void* buffer, int size) { 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() { @@ -599,6 +629,27 @@ int initDataBackend() { } 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); @@ -695,6 +746,7 @@ int initDataBackend() { _libcglue_fdman_path_ops = &s_path_ops; s_initialized = true; + s_data_root = kArchiveDataRoot; std::fprintf( stderr, "ps2 data: %lu files from %s\n", @@ -703,6 +755,10 @@ int initDataBackend() { return 0; } +const char* dataRoot() { + return s_data_root; +} + } // namespace ps2 } // namespace platform } // namespace runtime diff --git a/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.hpp b/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.hpp index 9b42f00f..ab43bbea 100644 --- a/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.hpp +++ b/src/SF_EXE/runtime/platform/ps2/ps2_data_backend.hpp @@ -14,6 +14,7 @@ namespace platform { namespace ps2 { int initDataBackend(); +const char* dataRoot(); } // namespace ps2 } // namespace platform diff --git a/src/SF_EXE/runtime/platform/ps2/surface_presenter.cpp b/src/SF_EXE/runtime/platform/ps2/surface_presenter.cpp index 9b09475f..bfdce38f 100644 --- a/src/SF_EXE/runtime/platform/ps2/surface_presenter.cpp +++ b/src/SF_EXE/runtime/platform/ps2/surface_presenter.cpp @@ -27,7 +27,12 @@ class Ps2SurfacePresenter final bool initialize( LwlWindow* window, std::string* error) override; - void present(SurfaceView surface) override; + void prepareFrame(SurfaceView surface) override; + void displayFrame() override; +#if OSF_ENABLE_DEBUG_TOOLS + std::optional + videoMemoryUsageBytes() const override; +#endif private: void shutdown(); @@ -38,6 +43,7 @@ class Ps2SurfacePresenter final 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) { @@ -104,6 +110,7 @@ bool Ps2SurfacePresenter::initialize( } void Ps2SurfacePresenter::shutdown() { + framePrepared_ = false; textureWidth_ = 0; textureHeight_ = 0; textureMemory_.reset(); @@ -129,7 +136,8 @@ void Ps2SurfacePresenter::uploadSurface(const SurfaceView& surface) { gsKit_texture_upload(gsGlobal_, &texture_); } -void Ps2SurfacePresenter::present(SurfaceView surface) { +void Ps2SurfacePresenter::prepareFrame(SurfaceView surface) { + framePrepared_ = false; if (!gsGlobal_ || !textureMemory_ || !surface.pixels || surface.width <= 0 || surface.height <= 0) { return; @@ -166,8 +174,28 @@ void Ps2SurfacePresenter::present(SurfaceView surface) { 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 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/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/player_attack_action.cpp b/src/SF_EXE/world/player_attack_action.cpp index e24852f0..38f598fd 100644 --- a/src/SF_EXE/world/player_attack_action.cpp +++ b/src/SF_EXE/world/player_attack_action.cpp @@ -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; 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_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/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..553d27a0 100644 --- a/src/SF_EXE/world/scenario_world.cpp +++ b/src/SF_EXE/world/scenario_world.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -77,7 +78,7 @@ std::filesystem::path scenarioDirectory( std::snprintf( directory, sizeof(directory), - "%08d", + "%08" PRId32, scenario_id); return data_root / "Scenario" / directory; } 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/tools/ps2/Dockerfile b/tools/ps2/Dockerfile deleted file mode 100644 index c50069a6..00000000 --- a/tools/ps2/Dockerfile +++ /dev/null @@ -1,53 +0,0 @@ -# Builds on the official ps2dev image, but replaces its stale/legacy ps2sdk -# with a hash-verified ps2sdk revision and adds the build and disc-image tools that -# the base image omits (make, cmake, genisoimage, ...). -# -# The ps2sdk rebuild is serial on purpose: GCC 15's lto1 crashes with -# "resolution sub id not in object file" / "file too short" when the SDK's -# LTO-heavy kernel objects are built in parallel (-j). -# -# Build: -# docker build -t openshadowflare-ps2 -f tools/ps2/Dockerfile . -# -# Run: -# docker run --rm -it -v "$PWD:/work" -w /work openshadowflare-ps2 sh -# -# Inside the container PS2DEV, PS2SDK and GSKIT are pre-set and the EE -# compiler is `mips64r5900el-ps2-elf-gcc` (R5900, GCC 15.2.0). - -FROM ps2dev/ps2dev@sha256:c64ae69c9817865ed98ff054e4ae5360b9e280ed952c97946bca95d9d35be995 - -# The build verifies this is still the intended PS2SDK master revision before -# compiling. If upstream advances, the image fails loudly instead of silently -# changing the SDK used by the port. -ARG PS2SDK_REF=e228ff7b61a12ad1192a49338754534362a26e58 - -ENV PS2DEV=/usr/local/ps2dev -ENV PS2SDK=$PS2DEV/ps2sdk -ENV GSKIT=$PS2DEV/gsKit -ENV PATH=$PATH:$PS2DEV/bin:$PS2DEV/ee/bin:$PS2DEV/iop/bin:$PS2DEV/dvp/bin:$PS2SDK/bin:$PS2SDK/ee/bin:$PS2SDK/iop/bin - -RUN apk add --no-cache \ - build-base \ - cmake \ - git \ - bash \ - perl \ - cdrkit - -RUN rm -rf $PS2SDK/ee $PS2SDK/iop $PS2SDK/common $PS2SDK/bin $PS2SDK/samples \ - && git clone --depth 1 --branch master https://github.com/ps2dev/ps2sdk.git /tmp/ps2sdk \ - && test "$(git -C /tmp/ps2sdk rev-parse HEAD)" = "$PS2SDK_REF" \ - && cd /tmp/ps2sdk \ - && make -j1 \ - && make install \ - && cd / \ - && rm -rf /tmp/ps2sdk \ - && apk del git bash perl \ - && ln -sf ../../../ps2sdk/ee/lib/libcglue.a $PS2DEV/ee/mips64r5900el-ps2-elf/lib/libcglue.a \ - && ln -sf ../../../ps2sdk/ee/lib/libpthreadglue.a $PS2DEV/ee/mips64r5900el-ps2-elf/lib/libpthreadglue.a \ - && ln -sf ../../../ps2sdk/ee/lib/libprofglue.a $PS2DEV/ee/mips64r5900el-ps2-elf/lib/libprofglue.a \ - && ln -sf ../../../ps2sdk/ee/lib/libkernel.a $PS2DEV/ee/mips64r5900el-ps2-elf/lib/libkernel.a \ - && ln -sf ../../../ps2sdk/ee/lib/libcdvd.a $PS2DEV/ee/mips64r5900el-ps2-elf/lib/libcdvd.a - -CMD ["sh"] diff --git a/tools/ps2/build-iso.sh b/tools/ps2/build-iso.sh index 554655a6..0f5f116c 100644 --- a/tools/ps2/build-iso.sh +++ b/tools/ps2/build-iso.sh @@ -7,22 +7,21 @@ usage() { Usage: tools/ps2/build-iso.sh [options] Options: - --data-dir PATH Original ShadowFlare data directory. - Default: tmp/ShadowFlare + --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 - --image NAME Docker image name. Default: openshadowflare-ps2 - --build-image Rebuild the Docker image before packaging. -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="$repo_root/tmp/ShadowFlare" +data_dir= out_dir="$repo_root/build/ps2" -image=openshadowflare-ps2 -build_image=0 +PS2DEV=${PS2DEV:-/usr/local/ps2dev} +PS2SDK=${PS2SDK:-$PS2DEV/ps2sdk} +GSKIT=${GSKIT:-$PS2DEV/gsKit} while [ "$#" -gt 0 ]; do case "$1" in @@ -36,15 +35,6 @@ while [ "$#" -gt 0 ]; do out_dir=$2 shift 2 ;; - --image) - [ "$#" -ge 2 ] || { echo "--image needs a name" >&2; exit 2; } - image=$2 - shift 2 - ;; - --build-image) - build_image=1 - shift - ;; -h|--help) usage exit 0 @@ -57,66 +47,49 @@ while [ "$#" -gt 0 ]; do esac done -if [ ! -d "$data_dir" ]; then - echo "Data directory not found: $data_dir" >&2 - exit 1 +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 -data_dir=$(CDPATH= cd -- "$data_dir" && pwd -P) mkdir -p "$out_dir" out_dir=$(CDPATH= cd -- "$out_dir" && pwd -P) -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" +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 -done -if [ -n "$missing_paths" ]; then - echo "Data directory '$data_dir' is missing:$missing_paths" >&2 - exit 1 fi -docker_run() { - case "$(uname -s)" in - MINGW*|MSYS*) - MSYS_NO_PATHCONV=1 docker "$@" - ;; - *) - docker "$@" - ;; - esac -} - -docker_host_path() { - case "$(uname -s)" in - MINGW*|MSYS*) - cygpath -w "$1" - ;; - *) - printf '%s\n' "$1" - ;; - esac -} - -repo_mount=$(docker_host_path "$repo_root") -data_mount=$(docker_host_path "$data_dir") -out_mount=$(docker_host_path "$out_dir") - -if [ "$build_image" -eq 0 ] && ! docker image inspect "$image" >/dev/null 2>&1; then - build_image=1 +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 [ "$build_image" -eq 1 ]; then - echo "== build Docker image ==" - docker_run build -t "$image" -f "$repo_mount/tools/ps2/Dockerfile" "$repo_mount" +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 ==" -docker_run run --rm \ - -v "$repo_mount:/mnt/repo" \ - -v "$data_mount:/hostdata:ro" \ - -v "$out_mount:/out" \ - "$image" \ - sh /mnt/repo/tools/ps2/make-iso.sh - -echo "ISO ready: $out_dir/openshadowflare.iso" +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 index f64fff5c..721a23b7 100644 --- a/tools/ps2/make-iso.sh +++ b/tools/ps2/make-iso.sh @@ -1,32 +1,41 @@ # Builds the PlayStation 2 disc image for OpenShadowFlare. # -# Runs inside the openshadowflare-ps2 Docker image: -# docker run --rm -it \ -# -v :/mnt/repo \ -# -v :/hostdata \ -# -v :/out \ -# openshadowflare-ps2 sh /mnt/repo/tools/ps2/make-iso.sh -# -# Produces /out/openshadowflare.iso: SYSTEM.CNF + boot ELF + SFGAME.BIN (the -# packed game-data archive read through ps2_data_backend.cpp). The data tree -# itself is intentionally NOT on the disc: the BIOS fileio module strips path -# separators and matches ISO9660 names case-sensitively, so it can only reach -# flat uppercase root files anyway. +# 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 -export PS2SDK=/usr/local/ps2dev/ps2sdk -export PS2DEV=/usr/local/ps2dev -export GSKIT=$PS2DEV/gsKit +: "${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 -REPO=${REPO:-/mnt/repo} -DATA=${DATA:-/hostdata} -OUT=${OUT:-/out} +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" -echo "== pack game data ==" -gcc -O2 -Wall -o /tmp/pack "$REPO/tools/ps2/pack.c" || exit 1 -/tmp/pack "$DATA" "$OUT/SFGAME.BIN" || exit 1 +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 @@ -40,26 +49,33 @@ if [ -z "$ELF" ]; then echo "make-iso: no ELF produced" >&2 exit 1 fi -cp "$ELF" "$OUT/$ELF_NAME" +cp "$ELF" "$DISC_ROOT/$ELF_NAME" echo "== copy IOP modules ==" -cp "$PS2SDK/iop/irx/iomanX.irx" "$OUT/IOMANX.IRX" || exit 1 -cp "$PS2SDK/iop/irx/fileXio.irx" "$OUT/FILEXIO.IRX" || exit 1 -cp "$PS2SDK/iop/irx/sio2man.irx" "$OUT/SIO2MAN.IRX" || exit 1 -cp "$PS2SDK/iop/irx/padman.irx" "$OUT/PADMAN.IRX" || exit 1 -cp "$PS2SDK/iop/irx/audsrv.irx" "$OUT/AUDSRV.IRX" || exit 1 +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" > "$OUT/SYSTEM.CNF" +printf 'BOOT2 = cdrom0:\\%s;1\nVER = 1.01\nVMODE = NTSC\n' "$ELF_NAME" > "$DISC_ROOT/SYSTEM.CNF" rm -f "$OUT/openshadowflare.iso" -genisoimage -iso-level 2 -R -J -V OPENSHDOW -o "$OUT/openshadowflare.iso" \ - "$OUT/SYSTEM.CNF" \ - "$OUT/$ELF_NAME" \ - "$OUT/SFGAME.BIN" \ - "$OUT/IOMANX.IRX" \ - "$OUT/FILEXIO.IRX" \ - "$OUT/SIO2MAN.IRX" \ - "$OUT/PADMAN.IRX" \ - "$OUT/AUDSRV.IRX" \ +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" From 9d81de39ef22ddd63a898aa476d569bf30ab0895 Mon Sep 17 00:00:00 2001 From: Trulio Date: Mon, 3 Aug 2026 08:53:40 -0300 Subject: [PATCH 4/8] fix ps2 cmake --- src/SF_EXE/cmake/platforms/PS2.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SF_EXE/cmake/platforms/PS2.cmake b/src/SF_EXE/cmake/platforms/PS2.cmake index 19ea0a2a..aea68fe9 100644 --- a/src/SF_EXE/cmake/platforms/PS2.cmake +++ b/src/SF_EXE/cmake/platforms/PS2.cmake @@ -6,5 +6,5 @@ function(osf_configure_ps2_platform target) runtime/platform/ps2/ps2_data_backend.cpp runtime/platform/ps2/surface_presenter.cpp ) - target_link_libraries(${target} PRIVATE gskit dmakit loadfile) + target_link_libraries(${target} PRIVATE gskit dmakit) endfunction() From e4451aadb9288a20f71cdd9ae6d5cc979082972b Mon Sep 17 00:00:00 2001 From: Trulio Date: Mon, 3 Aug 2026 09:05:03 -0300 Subject: [PATCH 5/8] update ps2 doc (enable host filesystem) --- documentation/ps2-port.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/documentation/ps2-port.md b/documentation/ps2-port.md index ce6ff18a..f9b6b81e 100644 --- a/documentation/ps2-port.md +++ b/documentation/ps2-port.md @@ -45,8 +45,9 @@ FAT32 USB drive/ ... ``` -PCSX2 maps its ISO folder to the `host0:` device. The PS2 ISO includes the -USB drivers required to use `mass:` on hardware. Copy the owned game data: +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. From 5efe5ef82438eaad62aada879f0c469d180fd2e5 Mon Sep 17 00:00:00 2001 From: Trulio Date: Mon, 3 Aug 2026 09:07:22 -0300 Subject: [PATCH 6/8] update ps2 port doc (full iso build) --- documentation/ps2-port.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/documentation/ps2-port.md b/documentation/ps2-port.md index f9b6b81e..f69ec18e 100644 --- a/documentation/ps2-port.md +++ b/documentation/ps2-port.md @@ -23,9 +23,11 @@ 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 data-free and suitable -for CI and distribution. 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: +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 / @@ -57,11 +59,16 @@ cp -a /path/to/ShadowFlare build/ps2/ cp -a /path/to/ShadowFlare /media/$USER/PS2USB/ ``` -Alternatively, create a private all-in-one disc from an owned retail -installation: +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 From 5fc55b19af194f181210e57c658beac5e854b7d6 Mon Sep 17 00:00:00 2001 From: Trulio Date: Mon, 3 Aug 2026 09:16:04 -0300 Subject: [PATCH 7/8] Fix ps2 gh workflow --- .github/workflows/ps2.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ps2.yml b/.github/workflows/ps2.yml index 53ad735f..6226cb4e 100644 --- a/.github/workflows/ps2.yml +++ b/.github/workflows/ps2.yml @@ -2,6 +2,8 @@ name: Build PlayStation 2 ISO on: push: + branches: + - master paths: - '.github/workflows/ps2.yml' - 'CMakeLists.txt' @@ -20,6 +22,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest @@ -30,10 +36,16 @@ jobs: - name: Check out the repository uses: actions/checkout@v6 - - name: Install PS2DEV + - name: Install build tools run: | sudo apt-get update - sudo apt-get install --yes cmake genisoimage + 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 \ From 4490480674895639512453a6b28524dde1eaf7ca Mon Sep 17 00:00:00 2001 From: Trulio Date: Mon, 3 Aug 2026 09:18:48 -0300 Subject: [PATCH 8/8] Fix ps2 workflow runner --- .github/workflows/ps2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ps2.yml b/.github/workflows/ps2.yml index 6226cb4e..c7a9ad44 100644 --- a/.github/workflows/ps2.yml +++ b/.github/workflows/ps2.yml @@ -30,7 +30,7 @@ jobs: build: runs-on: ubuntu-latest env: - PS2DEV: ${{ runner.temp }}/ps2dev + PS2DEV: /tmp/ps2dev steps: - name: Check out the repository