diff --git a/src/engine/dir.cpp b/src/engine/dir.cpp index 27337dc02..f466bd03d 100644 --- a/src/engine/dir.cpp +++ b/src/engine/dir.cpp @@ -42,6 +42,45 @@ namespace { +#if defined( TARGET_PS_VITA ) + // On PS Vita, getting a list of files using std::filesystem for some reason works much slower than using the native file system API + class SceUIDWrapper final + { + public: + SceUIDWrapper( const std::string & path ) + : uid( sceIoDopen( path.c_str() ) ) + { + // Do nothing. + } + + SceUIDWrapper( const SceUIDWrapper & ) = delete; + + ~SceUIDWrapper() + { + if ( !isValid() ) { + return; + } + + sceIoDclose( uid ); + } + + SceUIDWrapper & operator=( const SceUIDWrapper & ) = delete; + + bool isValid() const + { + return uid >= 0; + } + + SceUID get() const + { + return uid; + } + + private: + const SceUID uid; + }; +#endif + template bool nameFilter( const std::string & filename, const bool needExactMatch, const std::string & filter, const F & strCmp ) { @@ -76,41 +115,6 @@ namespace #endif #if defined( TARGET_PS_VITA ) - // On PS Vita, getting a list of files using std::filesystem for some reason works much slower than using the native file system API - class SceUIDWrapper - { - public: - SceUIDWrapper( const std::string & path ) - : uid( sceIoDopen( path.c_str() ) ) - {} - - SceUIDWrapper( const SceUIDWrapper & ) = delete; - - ~SceUIDWrapper() - { - if ( !isValid() ) { - return; - } - - sceIoDclose( uid ); - } - - SceUIDWrapper & operator=( const SceUIDWrapper & ) = delete; - - bool isValid() const - { - return uid >= 0; - } - - SceUID get() const - { - return uid; - } - - private: - const SceUID uid; - }; - const SceUIDWrapper uid( path ); if ( !uid.isValid() ) { return; @@ -148,6 +152,45 @@ namespace files.emplace_back( System::fsPathToString( entryPath ) ); } +#endif + } + + void getChildDirectories( const std::string & path, ListDirs & dirs ) + { + std::string correctedPath; + if ( !System::GetCaseInsensitivePath( path, correctedPath ) ) { + return; + } + +#if defined( TARGET_PS_VITA ) + const SceUIDWrapper uid( path ); + if ( !uid.isValid() ) { + return; + } + + SceIoDirent entry; + + while ( sceIoDread( uid.get(), &entry ) > 0 ) { + // Ensure that this directory entry is a regular file + if ( !S_ISDIR( entry.d_stat.st_mode ) ) { + continue; + } + + dirs.emplace_back( System::concatPath( path, entry.d_name ) ); + } +#else + std::error_code ec; + + // Using the non-throwing overload + for ( const std::filesystem::directory_entry & entry : std::filesystem::directory_iterator( correctedPath, ec ) ) { + // Using the non-throwing overload + if ( !entry.is_directory( ec ) ) { + continue; + } + + const std::filesystem::path & entryPath = entry.path(); + dirs.emplace_back( System::fsPathToString( entryPath.filename() ) ); + } #endif } } @@ -188,3 +231,8 @@ bool ListFiles::IsEmpty( const std::string & path, const std::string & filter ) list.ReadDir( path, filter ); return list.empty(); } + +void ListDirs::ReadDir( const std::string & path ) +{ + getChildDirectories( path, *this ); +} diff --git a/src/engine/dir.h b/src/engine/dir.h index 2ac88d3c7..7b07e4f47 100644 --- a/src/engine/dir.h +++ b/src/engine/dir.h @@ -40,3 +40,9 @@ struct ListFiles : public std::list // Returns true if there are no files in the 'path' directory with names ending in 'filter', case-insensitive, otherwise returns false. static bool IsEmpty( const std::string & path, const std::string & filter ); }; + +struct ListDirs : public std::list +{ + // Adds internal directories from the 'path' directory, case-insensitive. + void ReadDir( const std::string & path ); +}; diff --git a/src/fheroes2/dialog/dialog_selectfile.cpp b/src/fheroes2/dialog/dialog_selectfile.cpp index 203588778..889d14fdc 100644 --- a/src/fheroes2/dialog/dialog_selectfile.cpp +++ b/src/fheroes2/dialog/dialog_selectfile.cpp @@ -64,6 +64,8 @@ namespace // This constant sets the maximum displayed file name width. This value affects the dialog horizontal size. const int32_t maxFileNameWidth = 260; + std::string currentSaveFileFolder; + void redrawDateTime( fheroes2::Image & output, const time_t timestamp, const int32_t dstx, const int32_t dsty, const fheroes2::FontType font ) { const size_t arraySize = 5; @@ -115,25 +117,88 @@ namespace textInput.drawInRoi( field.x + 4 + ( maxFileNameWidth - textInput.width() ) / 2, field.y + 4, output, field ); } - class FileInfoListBox : public Interface::ListBox + struct OSEntryInfo final + { + std::string folderName; + + std::optional fileInfo; + }; + + class OSEntryListBox final : public Interface::ListBox { public: - using Interface::ListBox::ActionListDoubleClick; - using Interface::ListBox::ActionListSingleClick; - using Interface::ListBox::ActionListPressRight; + using Interface::ListBox::ActionListDoubleClick; + using Interface::ListBox::ActionListSingleClick; + using Interface::ListBox::ActionListPressRight; using ListBox::ListBox; - void RedrawItem( const Maps::FileInfo & info, int32_t dstx, int32_t dsty, bool current ) override; - void RedrawBackground( const fheroes2::Point & dst ) override; - - void ActionCurrentUp() override; - void ActionCurrentDn() override; - void ActionListDoubleClick( Maps::FileInfo & info ) override; - void ActionListSingleClick( Maps::FileInfo & info ) override; - - void ActionListPressRight( Maps::FileInfo & info ) override + void RedrawItem( const OSEntryInfo & entry, int32_t dstx, int32_t dsty, bool current ) override { + fheroes2::Display & display = fheroes2::Display::instance(); + dsty += 2; + + if ( entry.folderName.empty() ) { + // This is a file. + assert( entry.fileInfo.has_value() ); + + std::string fileName = System::GetStem( entry.fileInfo->filename ); + assert( !fileName.empty() ); + + const fheroes2::FontType font = current ? fheroes2::FontType::normalYellow() : fheroes2::FontType::normalWhite(); + + fheroes2::Text text{ std::move( fileName ), font }; + text.keepLineTrailingSpaces(); + text.fitToOneRow( maxFileNameWidth ); + text.draw( dstx + 4 + ( maxFileNameWidth - text.width() ) / 2, dsty, display ); + + redrawDateTime( display, entry.fileInfo->timestamp, dstx + maxFileNameWidth + 9, dsty, font ); + } + else { + const fheroes2::FontType font + = current ? fheroes2::FontType::normalYellow() : fheroes2::FontType{ fheroes2::FontSize::NORMAL, fheroes2::FontColor::GRAY }; + fheroes2::Text text{ entry.folderName, font }; + text.keepLineTrailingSpaces(); + text.fitToOneRow( maxFileNameWidth ); + text.draw( dstx + 4 + ( maxFileNameWidth - text.width() ) / 2, dsty, display ); + } + } + + void RedrawBackground( const fheroes2::Point & /*unused*/ ) override + { + _listBackground->restore(); + } + + void ActionCurrentUp() override + { + // Do nothing. + } + + void ActionCurrentDn() override + { + // Do nothing. + } + + void ActionListDoubleClick( OSEntryInfo & /*unused*/ ) override + { + _isDoubleClicked = true; + } + + void ActionListSingleClick( OSEntryInfo & /*unused*/ ) override + { + // Do nothing. + } + + void ActionListPressRight( OSEntryInfo & entry ) override + { + if ( !entry.folderName.empty() ) { + fheroes2::showStandardTextMessage( _( "Directory" ), entry.folderName, Dialog::ZERO ); + return; + } + + assert( entry.fileInfo.has_value() ); + const auto & info = entry.fileInfo.value(); + const fheroes2::Text header( System::GetStem( info.filename ), fheroes2::FontType::normalYellow() ); fheroes2::MultiFontText body; @@ -171,49 +236,6 @@ namespace std::unique_ptr _listBackground; }; - void FileInfoListBox::RedrawItem( const Maps::FileInfo & info, int32_t dstx, int32_t dsty, bool current ) - { - std::string savname( System::GetStem( info.filename ) ); - assert( !savname.empty() ); - - const fheroes2::FontType font = current ? fheroes2::FontType::normalYellow() : fheroes2::FontType::normalWhite(); - fheroes2::Display & display = fheroes2::Display::instance(); - - dsty += 2; - - fheroes2::Text text{ std::move( savname ), font }; - text.keepLineTrailingSpaces(); - text.fitToOneRow( maxFileNameWidth ); - text.draw( dstx + 4 + ( maxFileNameWidth - text.width() ) / 2, dsty, display ); - - redrawDateTime( display, info.timestamp, dstx + maxFileNameWidth + 9, dsty, font ); - } - - void FileInfoListBox::RedrawBackground( const fheroes2::Point & /* unused */ ) - { - _listBackground->restore(); - } - - void FileInfoListBox::ActionCurrentUp() - { - // Do nothing. - } - - void FileInfoListBox::ActionCurrentDn() - { - // Do nothing. - } - - void FileInfoListBox::ActionListDoubleClick( Maps::FileInfo & /*unused*/ ) - { - _isDoubleClicked = true; - } - - void FileInfoListBox::ActionListSingleClick( Maps::FileInfo & /*unused*/ ) - { - // Do nothing. - } - void sortMapInfos( MapsFileInfoList & mapInfos ) { const SaveFileSortingMethod sortType = Settings::Get().getSaveFileSortingMethod(); @@ -226,10 +248,23 @@ namespace } } - MapsFileInfoList getSortedMapsFileInfoList() + std::vector getOSEntries( const std::string & path ) { + std::vector entries; + + ListDirs dirs; + dirs.ReadDir( path ); + + entries.emplace_back().folderName = "[Parent folder]"; + + entries.reserve( dirs.size() + 1 ); + + for ( auto & dir : dirs ) { + entries.emplace_back().folderName = "[" + dir + ']'; + } + ListFiles files; - files.ReadDir( Game::GetSaveDir(), Game::GetSaveFileExtension() ); + files.ReadDir( path, Game::GetSaveFileExtension() ); MapsFileInfoList mapInfos; mapInfos.reserve( files.size() ); @@ -244,15 +279,28 @@ namespace sortMapInfos( mapInfos ); - return mapInfos; + entries.reserve( entries.size() + mapInfos.size() ); + for ( auto & mapInfo : mapInfos ) { + entries.emplace_back().fileInfo = std::move( mapInfo ); + } + + return entries; } - MapsFileInfoList::const_iterator findInMapInfos( const MapsFileInfoList::const_iterator & begin, const MapsFileInfoList::const_iterator & end, - const std::string & lastChoice ) + std::vector::const_iterator findInMapInfos( const std::vector & entries, const std::string & lastChoice ) { - return std::find_if( begin, end, [&lastChoice]( const Maps::FileInfo & info ) { return info.filename == lastChoice; } ); + return std::find_if( entries.begin(), entries.end(), [&lastChoice]( const auto & entry ) { + if ( !entry.folderName.empty() ) { + return false; + } + + assert( entry.fileInfo.has_value() ); + return entry.fileInfo->filename == lastChoice; + } + ); } + /* MapsFileInfoList::const_iterator findInMapInfos( const MapsFileInfoList::const_iterator & begin, const MapsFileInfoList::const_iterator & end, const std::string & lastChoice, const uint32_t lastChoiceTimestamp, const SaveFileSortingMethod sortingMethod ) { @@ -291,7 +339,9 @@ namespace return end; } + */ + /* MapsFileInfoList::const_iterator findInMapInfos( const MapsFileInfoList::const_iterator & begin, const MapsFileInfoList::const_iterator & end, const std::string & lastChoice, const SaveFileSortingMethod sortingMethod ) { @@ -319,13 +369,18 @@ namespace return end; } + */ std::string selectFileListSimple( const std::string & header, const std::string & lastfile, const bool isEditing ) { // setup cursor const CursorRestorer cursorRestorer( true, Cursor::POINTER ); - MapsFileInfoList lists = getSortedMapsFileInfoList(); + if ( currentSaveFileFolder.empty() ) { + currentSaveFileFolder = Game::GetSaveDir(); + } + + auto lists = getOSEntries( Game::GetSaveDir() ); const int32_t listHeightDeduction = 120; const int32_t listAreaOffsetY = 3; @@ -372,7 +427,7 @@ namespace background.renderOkayCancelButtons( buttonOk, buttonCancel ); - FileInfoListBox listbox( dialogArea.getPosition() ); + OSEntryListBox listbox( dialogArea.getPosition() ); // Initialize list background restorer to use it in list method 'listbox.RedrawBackground()'. listbox.initListBackgroundRestorer( listRoi ); @@ -404,7 +459,7 @@ namespace if ( !lastfile.empty() ) { filename = System::GetStem( lastfile ); charInsertPos = filename.size(); - const MapsFileInfoList::const_iterator it = findInMapInfos( lists.cbegin(), lists.cend(), lastfile, fileSortingMethod ); + const auto it = findInMapInfos( lists, lastfile ); if ( it != lists.cend() ) { listbox.SetCurrent( std::distance( lists.cbegin(), it ) ); @@ -419,7 +474,7 @@ namespace } if ( filename.empty() && listbox.isSelected() ) { - filename = System::GetStem( listbox.GetCurrent().filename ); + // filename = System::GetStem( listbox.GetCurrent().filename ); charInsertPos = filename.size(); } @@ -467,6 +522,7 @@ namespace fheroes2::Blit( mark, display, { markBackgroundDateRoi.x + 3, markBackgroundDateRoi.y + 3, mark.width(), mark.height() } ); } + /* // Redraw sort radio buttons, sort file list in new method, and return whether to redraw the list. auto switchFileSorting = [&markBackgroundNameRoi, &markBackgroundDateRoi, &markBackground, &mark, &display, &listbox, &lists, &settings]( const bool doSortByDate ) { @@ -503,6 +559,7 @@ namespace } return redrawNeeded; }; + */ if ( isEditing ) { // Render a button to open the Virtual Keyboard window. @@ -523,7 +580,7 @@ namespace if ( listbox.isSelected() ) { // Render the saved file name, date and time. redrawTextInputField( filename, textInputRoi, display ); - redrawDateTime( display, listbox.GetCurrent().timestamp, dateTimeOffsetX, textInputRoi.y + 4, fheroes2::FontType::normalYellow() ); + // redrawDateTime( display, listbox.GetCurrent().timestamp, dateTimeOffsetX, textInputRoi.y + 4, fheroes2::FontType::normalYellow() ); } display.render( background.totalArea() ); @@ -558,6 +615,7 @@ namespace bool listUpdated = false; if ( le.isKeyPressed( fheroes2::Key::KEY_DELETE ) && isListboxSelected ) { + /* listbox.SetCurrent( listId ); listbox.Redraw(); @@ -588,11 +646,12 @@ namespace } needRedraw = true; + */ } else if ( ( buttonOk.isEnabled() && le.MouseClickLeft( buttonOk.area() ) ) || Game::HotKeyPressEvent( Game::HotKeyEvent::DEFAULT_OKAY ) || listbox.isDoubleClicked() ) { if ( isListboxSelected ) { - result = listbox.GetCurrent().filename; + // result = listbox.GetCurrent().filename; } else if ( !filename.empty() ) { result = System::concatPath( Game::GetSaveDir(), filename + Game::GetSaveFileExtension() ); @@ -600,11 +659,11 @@ namespace } else if ( le.MouseClickLeft( nameHeaderRoi ) && settings.getSaveFileSortingMethod() != SaveFileSortingMethod::FILENAME ) { listUpdated = true; - needRedraw = switchFileSorting( false ); + // needRedraw = switchFileSorting( false ); } else if ( le.MouseClickLeft( dateHeaderRoi ) && settings.getSaveFileSortingMethod() != SaveFileSortingMethod::TIMESTAMP ) { listUpdated = true; - needRedraw = switchFileSorting( true ); + // needRedraw = switchFileSorting( true ); } else if ( isEditing ) { assert( textInput != nullptr ); @@ -688,20 +747,29 @@ namespace if ( needRedraw ) { if ( isListboxSelected ) { - const std::string selectedFileName = System::GetStem( listbox.GetCurrent().filename ); - if ( lastSelectedSaveFileName != selectedFileName ) { - lastSelectedSaveFileName = selectedFileName; - filename = selectedFileName; - charInsertPos = filename.size(); - - buttonOkDisabler(); + const auto & currentItem = listbox.GetCurrent(); + if ( !currentItem.folderName.empty() ) { + buttonOk.disable(); + buttonOk.draw(); } + else { + assert( currentItem.fileInfo.has_value() ); - textInputBackground.restore(); - dateBackground.restore(); + const std::string selectedFileName = System::GetStem( currentItem.fileInfo->filename ); + if ( lastSelectedSaveFileName != selectedFileName ) { + lastSelectedSaveFileName = selectedFileName; + filename = selectedFileName; + charInsertPos = filename.size(); - redrawTextInputField( filename, textInputRoi, display ); - redrawDateTime( display, listbox.GetCurrent().timestamp, dateTimeOffsetX, textInputRoi.y + 4, fheroes2::FontType::normalYellow() ); + buttonOkDisabler(); + } + + textInputBackground.restore(); + dateBackground.restore(); + + redrawTextInputField( filename, textInputRoi, display ); + redrawDateTime( display, currentItem.fileInfo->timestamp, dateTimeOffsetX, textInputRoi.y + 4, fheroes2::FontType::normalYellow() ); + } } else if ( isEditing ) { // Empty last selected save file name so that we can replace the input field's name if we select the same save file again. diff --git a/src/fheroes2/dialog/dialog_selectscenario.cpp b/src/fheroes2/dialog/dialog_selectscenario.cpp index 13698fe3c..f36cda27e 100644 --- a/src/fheroes2/dialog/dialog_selectscenario.cpp +++ b/src/fheroes2/dialog/dialog_selectscenario.cpp @@ -350,8 +350,11 @@ void ScenarioListBox::_renderSelectedScenarioInfo( fheroes2::Display & display, void ScenarioListBox::_renderMapName( const Maps::FileInfo & info, bool selected, const int32_t baseYOffset, fheroes2::Display & display ) const { + const auto normalFontColor = (!_isPOLSupported && info.version != GameVersion::SUCCESSION_WARS ) ? fheroes2::FontColor::GRAY : fheroes2::FontColor::WHITE; + + fheroes2::Text mapName{ info.name, - { fheroes2::FontSize::NORMAL, ( selected ? fheroes2::FontColor::YELLOW : fheroes2::FontColor::WHITE ) }, + { fheroes2::FontSize::NORMAL, ( selected ? fheroes2::FontColor::YELLOW : normalFontColor ) }, info.getSupportedLanguage() }; mapName.fitToOneRow( SCENARIO_LIST_MAP_NAME_WIDTH ); const int32_t xCoordinate = GetCenteredTextXCoordinate( _offsetX + SCENARIO_LIST_MAP_NAME_OFFSET_X, SCENARIO_LIST_MAP_NAME_WIDTH, mapName.width() ); @@ -478,6 +481,8 @@ const Maps::FileInfo * Dialog::SelectScenario( MapsFileInfoList & all, const boo } } + const bool isPOLSupported = Settings::Get().isPriceOfLoyaltySupported(); + outputMapSelectionInTextSupportMode(); const CursorRestorer cursorRestorer( true, Cursor::POINTER ); @@ -640,7 +645,12 @@ const Maps::FileInfo * Dialog::SelectScenario( MapsFileInfoList & all, const boo return nullptr; } - return &( *it ); + if ( !isPOLSupported && it->version != GameVersion::SUCCESSION_WARS ) { + fheroes2::showStandardTextMessage( "", _( "This map requires the Price of Loyalty game resources." ), Dialog::OK ); + } + else { + return &( *it ); + } } if ( Game::HotKeyPressEvent( Game::HotKeyEvent::DEFAULT_CANCEL ) ) { diff --git a/src/fheroes2/dialog/dialog_selectscenario.h b/src/fheroes2/dialog/dialog_selectscenario.h index 8cba6317f..1ca97f176 100644 --- a/src/fheroes2/dialog/dialog_selectscenario.h +++ b/src/fheroes2/dialog/dialog_selectscenario.h @@ -30,6 +30,7 @@ #include "maps.h" #include "maps_fileinfo.h" #include "math_base.h" +#include "settings.h" namespace fheroes2 { @@ -47,7 +48,10 @@ public: explicit ScenarioListBox( const fheroes2::Point & pt ) : Interface::ListBox( pt ) , _offsetX( pt.x ) - {} + , _isPOLSupported( Settings::Get().isPriceOfLoyaltySupported() ) + { + // Do nothing. + } void RedrawItem( const Maps::FileInfo & info, int32_t /*dstx*/, int32_t dsty, bool current ) override; void RedrawBackground( const fheroes2::Point & dst ) override; @@ -89,6 +93,7 @@ private: const int32_t _offsetX; bool _isDoubleClicked{ false }; bool _isForEditor{ false }; + const bool _isPOLSupported{ false }; void _renderScenarioListItem( const Maps::FileInfo & info, fheroes2::Display & display, const int32_t dsty, const bool current ) const; void _renderSelectedScenarioInfo( fheroes2::Display & display, const fheroes2::Point & dst ); diff --git a/src/fheroes2/maps/maps.h b/src/fheroes2/maps/maps.h index 80f760b1e..c3783bd1a 100644 --- a/src/fheroes2/maps/maps.h +++ b/src/fheroes2/maps/maps.h @@ -51,7 +51,8 @@ namespace Maps SMALL = 36, MEDIUM = 72, LARGE = 108, - XLARGE = 144 + XLARGE = 144, + GIGANTIC = 180, }; using Indexes = MapsIndexes; diff --git a/src/fheroes2/maps/maps_fileinfo.cpp b/src/fheroes2/maps/maps_fileinfo.cpp index 87350dab5..a842e70fe 100644 --- a/src/fheroes2/maps/maps_fileinfo.cpp +++ b/src/fheroes2/maps/maps_fileinfo.cpp @@ -733,13 +733,13 @@ MapsFileInfoList Maps::getAllMapFileInfos( const uint8_t humanPlayerCount ) const bool isPOLSupported = Settings::Get().isPriceOfLoyaltySupported(); - if ( isPOLSupported ) { + // if ( isPOLSupported ) { maps.Append( Settings::FindFiles( "maps", ".mx2", false ) ); - } + // } MapsFileInfoList validMaps = getValidMaps( maps, humanPlayerCount, true ); - if ( isPOLSupported ) { + // if ( isPOLSupported ) { const ListFiles resurrectionMaps = Settings::FindFiles( "maps", ".fh2m", false ); MapsFileInfoList validResurrectionMaps = getValidMaps( resurrectionMaps, humanPlayerCount, false ); @@ -748,7 +748,7 @@ MapsFileInfoList Maps::getAllMapFileInfos( const uint8_t humanPlayerCount ) for ( auto & map : validResurrectionMaps ) { validMaps.emplace_back( std::move( map ) ); } - } + // } std::sort( validMaps.begin(), validMaps.end(), Maps::FileInfo::CompareByMapName{} );