diff --git a/source/main/gui/panels/GUI_MainSelector.cpp b/source/main/gui/panels/GUI_MainSelector.cpp index 09050d6f1f..16631b7980 100644 --- a/source/main/gui/panels/GUI_MainSelector.cpp +++ b/source/main/gui/panels/GUI_MainSelector.cpp @@ -285,7 +285,7 @@ void CLASS::EventComboChangePositionTypeComboBox(MyGUI::ComboBoxPtr _sender, siz OnCategorySelected(categoryID); if (!m_searching) { - m_category_index[m_loader_type] = static_cast(_index); + m_category_last_index[m_loader_type] = static_cast(_index); } } @@ -335,230 +335,134 @@ struct sort_entries } }; -struct sort_search_results +bool CLASS::IsFresh(CacheEntry* entry) { - bool operator ()(std::pair const& a, std::pair const& b) const - { - return a.second < b.second; - } -}; + return entry->filetime >= m_cache_file_freshness - 86400; +} void CLASS::UpdateGuiData() { - std::map mCategoryUsage; m_Type->removeAllItems(); m_Model->removeAllItems(); m_entries.clear(); - std::vector timestamps; - std::vector entries = RoR::App::GetCacheSystem()->GetEntries(); - std::sort(entries.begin(), entries.end(), sort_entries()); - for (std::vector::iterator it = entries.begin(); it != entries.end(); it++) - { - bool add = false; - if (it->fext == "terrn2") - add = (m_loader_type == LT_Terrain); - if (it->fext == "skin") - add = (m_loader_type == LT_Skin && it->guid == m_actor_spawn_rq.asr_cache_entry->guid); - else if (it->fext == "truck") - add = (m_loader_type == LT_AllBeam || m_loader_type == LT_Vehicle || m_loader_type == LT_Truck ); - else if (it->fext == "car") - add = (m_loader_type == LT_AllBeam || m_loader_type == LT_Vehicle || m_loader_type == LT_Truck || m_loader_type == LT_Car ); - else if (it->fext == "boat") - add = (m_loader_type == LT_AllBeam || m_loader_type == LT_Boat); - else if (it->fext == "airplane") - add = (m_loader_type == LT_AllBeam || m_loader_type == LT_Airplane ); - else if (it->fext == "trailer") - add = (m_loader_type == LT_AllBeam || m_loader_type == LT_Trailer || m_loader_type == LT_Extension); - else if (it->fext == "train") - add = (m_loader_type == LT_AllBeam || m_loader_type == LT_Train); - else if (it->fext == "load") - add = (m_loader_type == LT_AllBeam || m_loader_type == LT_Load || m_loader_type == LT_Extension); - - if (!add) - continue; - - mCategoryUsage[it->categoryid]++; - - // category all - mCategoryUsage[CacheSystem::CID_All]++; - - timestamps.push_back(it->addtimestamp); - - m_entries.push_back(*it); - } - // Find fresh cache entries - if (timestamps.size() > 0) - { - m_cache_file_freshness = *std::max_element(timestamps.begin(), timestamps.end()); - for (std::vector::iterator it = m_entries.begin(); it != m_entries.end(); it++) + // Find all relevant entries + CacheQuery query; + query.cqy_filter_type = m_loader_type; + App::GetCacheSystem()->Query(query); + m_cache_file_freshness = query.cqy_res_last_update; + for (CacheQueryResult const& res: query.cqy_results) + { + m_entries.push_back(*res.cqr_entry); + + if (this->IsFresh(res.cqr_entry)) { - if (it->addtimestamp >= m_cache_file_freshness || it->filetime >= m_cache_file_freshness - 86400) - mCategoryUsage[CacheSystem::CID_Fresh]++; + query.cqy_res_category_usage[CacheCategoryId::CID_Fresh]++; } } - int tally_categories = 0, current_category = 0; - std::vector> sorted_cats; // Temporary, just for shorter diff + // Count used categories + size_t tally_categories = 0; for (size_t i = 0; i < CacheSystem::NUM_CATEGORIES; ++i) { - sorted_cats.push_back(std::make_pair( - CacheSystem::CATEGORIES[i].ccg_id, CacheSystem::CATEGORIES[i].ccg_name)); - } - for (const auto& cat : sorted_cats) - { - if (mCategoryUsage[cat.first] > 0) + if (query.cqy_res_category_usage[CacheSystem::CATEGORIES[i].ccg_id] > 0) + { tally_categories++; + } } - for (const auto& cat : sorted_cats) + + // Display used categories + size_t display_number = 1; + for (size_t i = 0; i < CacheSystem::NUM_CATEGORIES; ++i) { - int num_elements = mCategoryUsage[cat.first]; - if (num_elements > 0) + size_t num_entries = query.cqy_res_category_usage[CacheSystem::CATEGORIES[i].ccg_id]; + if (query.cqy_res_category_usage[CacheSystem::CATEGORIES[i].ccg_id] > 0) { - Ogre::UTFString title = _L("unknown"); - if (!cat.second.empty()) - { - title = _L(cat.second.c_str()); - } - Ogre::UTFString txt = U("[") + TOUTFSTRING(++current_category) + U("/") + TOUTFSTRING(tally_categories) + U("] (") + TOUTFSTRING(num_elements) + U(") ") + title; - m_Type->addItem(convertToMyGUIString(txt), cat.first); + Str<300> title; + title << "[" << display_number << "/" << tally_categories + << "] (" << num_entries << ") " << CacheSystem::CATEGORIES[i].ccg_name; + m_Type->addItem(title.ToCStr(), CacheSystem::CATEGORIES[i].ccg_id); + display_number++; } } + if (m_Type->getItemCount() > 0) { - int idx = m_category_index[m_loader_type] < m_Type->getItemCount() ? m_category_index[m_loader_type] : 0; + int idx = m_category_last_index[m_loader_type] < m_Type->getItemCount() ? m_category_last_index[m_loader_type] : 0; m_Type->setIndexSelected(idx); m_Type->beginToItemSelected(); + + // FIXME: currently this performs duplicate search. Will be remade soon (using DearIMGUI) OnCategorySelected(*m_Type->getItemDataAt(idx)); } } -size_t CLASS::SearchCompare(Ogre::String searchString, CacheEntry* ce) +void CLASS::UpdateSearchParams() { - if (searchString.find(":") == Ogre::String::npos) - { - // normal search - - // the name - Ogre::String dname_lower = ce->dname; - Ogre::StringUtil::toLowerCase(dname_lower); - if (dname_lower.find(searchString) != Ogre::String::npos) - return dname_lower.find(searchString); - - // the filename - Ogre::String fname_lower = ce->fname; - Ogre::StringUtil::toLowerCase(fname_lower); - if (fname_lower.find(searchString) != Ogre::String::npos) - return 100 + fname_lower.find(searchString); - - // the description - Ogre::String desc = ce->description; - Ogre::StringUtil::toLowerCase(desc); - if (desc.find(searchString) != Ogre::String::npos) - return 200 + desc.find(searchString); - - // the authors - if (!ce->authors.empty()) - { - std::vector::const_iterator it; - for (it = ce->authors.begin(); it != ce->authors.end(); it++) - { - // author name - Ogre::String aname = it->name; - Ogre::StringUtil::toLowerCase(aname); - if (aname.find(searchString) != Ogre::String::npos) - return 300 + aname.find(searchString); - - // author email - Ogre::String aemail = it->email; - Ogre::StringUtil::toLowerCase(aemail); - if (aemail.find(searchString) != Ogre::String::npos) - return 400 + aemail.find(searchString); - } - } - return Ogre::String::npos; + std::string searchString = m_SearchLine->getCaption(); + + if (searchString.find(":") == std::string::npos) + { + m_search_method = CacheSearchMethod::FULLTEXT; + m_search_query = searchString; } else { Ogre::StringVector v = Ogre::StringUtil::split(searchString, ":"); if (v.size() < 2) - return Ogre::String::npos; //invalid syntax - - if (v[0] == "guid") { - Ogre::String guid = ce->guid; - Ogre::StringUtil::toLowerCase(guid); - return guid.find(v[1]); + m_search_method = CacheSearchMethod::NONE; + m_search_query = ""; + } + else if (v[0] == "guid") + { + m_search_method = CacheSearchMethod::GUID; + m_search_query = v[1]; } else if (v[0] == "author") { - // the authors - if (!ce->authors.empty()) - { - std::vector::const_iterator it; - for (it = ce->authors.begin(); it != ce->authors.end(); it++) - { - // author name - Ogre::String aname = it->name; - Ogre::StringUtil::toLowerCase(aname); - if (aname.find(v[1]) != Ogre::String::npos) - return aname.find(v[1]); - - // author email - Ogre::String aemail = it->email; - Ogre::StringUtil::toLowerCase(aemail); - if (aemail.find(v[1]) != Ogre::String::npos) - return aemail.find(v[1]); - } - } - return Ogre::String::npos; + m_search_method = CacheSearchMethod::AUTHORS; + m_search_query = v[1]; } else if (v[0] == "wheels") { - Ogre::String wheelsStr = TOUTFSTRING(ce->wheelcount) + "x" + TOUTFSTRING(ce->propwheelcount); - return wheelsStr.find(v[1]); + m_search_method = CacheSearchMethod::WHEELS; + m_search_query = v[1]; } else if (v[0] == "file") { - Ogre::String fn = ce->fname; - Ogre::StringUtil::toLowerCase(fn); - return fn.find(v[1]); + m_search_method = CacheSearchMethod::FILENAME; + m_search_query = v[1]; + } + else + { + m_search_method = CacheSearchMethod::NONE; + m_search_query = ""; } } - return Ogre::String::npos; } void CLASS::OnCategorySelected(int categoryID) { m_Model->removeAllItems(); + m_entries.clear(); - std::vector> entries; - entries.reserve(m_entries.size()); - - if (categoryID == CacheSystem::CID_SearchResults) + CacheQuery query; + query.cqy_filter_type = m_loader_type; + query.cqy_filter_category_id = categoryID; + query.cqy_search_method = m_search_method; + query.cqy_search_string = m_search_query; + if (m_loader_type == LT_Skin && m_selected_entry) { - Ogre::String search_cmd = m_SearchLine->getCaption(); - Ogre::StringUtil::toLowerCase(search_cmd); - for (auto& entry : m_entries) - { - size_t score = SearchCompare(search_cmd, &entry); - if (score != Ogre::String::npos) - { - entries.push_back(std::make_pair(&entry, score)); - } - } - std::stable_sort(entries.begin(), entries.end(), sort_search_results()); + query.cqy_filter_guid = m_selected_entry->guid; } - else + App::GetCacheSystem()->Query(query); + + for (CacheQueryResult const& res: query.cqy_results) { - for (auto& entry : m_entries) + if (categoryID != CacheCategoryId::CID_Fresh || this->IsFresh(res.cqr_entry)) { - if (entry.categoryid == categoryID || categoryID == CacheSystem::CID_All - || (categoryID == CacheSystem::CID_Fresh && - (entry.addtimestamp >= m_cache_file_freshness || entry.filetime >= m_cache_file_freshness - 86400))) - { - entries.push_back(std::make_pair(&entry, 0)); - } + m_entries.push_back(*res.cqr_entry); } } @@ -567,19 +471,11 @@ void CLASS::OnCategorySelected(int categoryID) m_Model->addItem(_L("Default Skin"), 0); // Virtual entry } - int count = 1; - - for (const auto& entry : entries) + size_t display_num = 1; + for (const auto& entry : m_entries) { - try - { - m_Model->addItem(Ogre::StringUtil::format("%d. %s", count, entry.first->dname.c_str()), entry.first->number); - count++; - } - catch (...) - { - m_Model->addItem("ENCODING ERROR", entry.first->number); - } + m_Model->addItem(Ogre::StringUtil::format("%u. %s", display_num, entry.dname.c_str()), entry.number); + display_num++; } if (m_Model->getItemCount() > 0) @@ -590,7 +486,7 @@ void CLASS::OnCategorySelected(int categoryID) OnEntrySelected(*m_Model->getItemDataAt(idx)); } - m_searching = categoryID == CacheSystem::CID_SearchResults; + m_searching = categoryID == CacheCategoryId::CID_SearchResults; } void CLASS::OnEntrySelected(int entryID) @@ -958,7 +854,9 @@ void CLASS::EventSearchTextChange(MyGUI::EditBox* _sender) { if (!MAIN_WIDGET->getVisible()) return; - OnCategorySelected(CacheSystem::CID_SearchResults); + + this->UpdateSearchParams(); + OnCategorySelected(CacheCategoryId::CID_SearchResults); if (m_SearchLine->getTextLength() > 0) { m_Type->setCaption(_L("Search Results")); diff --git a/source/main/gui/panels/GUI_MainSelector.h b/source/main/gui/panels/GUI_MainSelector.h index ebd7085a4e..b76435de42 100644 --- a/source/main/gui/panels/GUI_MainSelector.h +++ b/source/main/gui/panels/GUI_MainSelector.h @@ -26,9 +26,11 @@ #pragma once #include "BeamData.h" // ActorSpawnRequest +#include "CacheSystem.h" // CacheSearchMethod #include "ForwardDeclarations.h" #include "GUI_MainSelectorLayout.h" + namespace RoR { namespace GUI { @@ -71,20 +73,23 @@ class MainSelector : public MainSelectorLayout void OnCategorySelected(int categoryID); void OnEntrySelected(int entryID); void OnSelectionDone(); - size_t SearchCompare(Ogre::String searchString, CacheEntry* ce); + void UpdateSearchParams(); void UpdateControls(CacheEntry* entry); void SetPreviewImage(Ogre::String texture); void FrameEntered(float dt); + bool IsFresh(CacheEntry* entry); CacheEntry* m_selected_entry; LoaderType m_loader_type; + CacheSearchMethod m_search_method; + std::string m_search_query; Ogre::String m_preview_image_texture; bool m_selection_done; std::vector m_entries; bool m_keys_bound; ActorSpawnRequest m_actor_spawn_rq; //!< Serves as context when spawning an actor std::time_t m_cache_file_freshness; - std::map m_category_index; //!< Stores the last manually selected category index for each loader type + std::map m_category_last_index; //!< Stores the last manually selected category index for each loader type std::map m_entry_index; //!< Stores the last manually selected entry index for each loader type bool m_searching; }; diff --git a/source/main/resources/CacheSystem.cpp b/source/main/resources/CacheSystem.cpp index 07613064ad..7fe1057923 100644 --- a/source/main/resources/CacheSystem.cpp +++ b/source/main/resources/CacheSystem.cpp @@ -1227,5 +1227,118 @@ std::shared_ptr CacheSystem::FetchSkinDef(CacheEntry* cache_entry) } } +size_t CacheSystem::Query(CacheQuery& query) +{ + Ogre::StringUtil::toLowerCase(query.cqy_search_string); + for (CacheEntry& entry: m_entries) + { + // Filter by GUID + if (!query.cqy_filter_guid.empty() && entry.guid != query.cqy_filter_guid) + { + continue; + } + + // Filter by category + if (query.cqy_filter_category_id < CacheCategoryId::CID_Max && + query.cqy_filter_category_id != entry.categoryid) + { + continue; + } + + // Filter by entry type + bool add = false; + if (entry.fext == "terrn2") + add = (query.cqy_filter_type == LT_Terrain); + if (entry.fext == "skin") + add = (query.cqy_filter_type == LT_Skin); + else if (entry.fext == "truck") + add = (query.cqy_filter_type == LT_AllBeam || query.cqy_filter_type == LT_Vehicle || query.cqy_filter_type == LT_Truck); + else if (entry.fext == "car") + add = (query.cqy_filter_type == LT_AllBeam || query.cqy_filter_type == LT_Vehicle || query.cqy_filter_type == LT_Truck || query.cqy_filter_type == LT_Car); + else if (entry.fext == "boat") + add = (query.cqy_filter_type == LT_AllBeam || query.cqy_filter_type == LT_Boat); + else if (entry.fext == "airplane") + add = (query.cqy_filter_type == LT_AllBeam || query.cqy_filter_type == LT_Airplane); + else if (entry.fext == "trailer") + add = (query.cqy_filter_type == LT_AllBeam || query.cqy_filter_type == LT_Trailer || query.cqy_filter_type == LT_Extension); + else if (entry.fext == "train") + add = (query.cqy_filter_type == LT_AllBeam || query.cqy_filter_type == LT_Train); + else if (entry.fext == "load") + add = (query.cqy_filter_type == LT_AllBeam || query.cqy_filter_type == LT_Load || query.cqy_filter_type == LT_Extension); + + if (!add) + { + continue; + } + + // Search + size_t score = 0; + bool match = false; + Str<100> wheels_str; + switch (query.cqy_search_method) + { + case CacheSearchMethod::FULLTEXT: + if (match = this->Match(score, entry.dname, query.cqy_search_string, 0)) { break; } + if (match = this->Match(score, entry.fname, query.cqy_search_string, 100)) { break; } + if (match = this->Match(score, entry.description, query.cqy_search_string, 200)) { break; } + for (AuthorInfo const& author: entry.authors) + { + if (match = this->Match(score, author.name, query.cqy_search_string, 300)) { break; } + if (match = this->Match(score, author.email, query.cqy_search_string, 400)) { break; } + } + break; + + case CacheSearchMethod::GUID: + match = this->Match(score, entry.guid, query.cqy_search_string, 0); + break; + + case CacheSearchMethod::AUTHORS: + for (AuthorInfo const& author: entry.authors) + { + if (match = this->Match(score, author.name, query.cqy_search_string, 0)) { break; } + if (match = this->Match(score, author.email, query.cqy_search_string, 0)) { break; } + } + break; + + case CacheSearchMethod::WHEELS: + wheels_str << entry.wheelcount << "x" << entry.propwheelcount; + match = this->Match(score, wheels_str.ToCStr(), query.cqy_search_string, 0); + break; + + case CacheSearchMethod::FILENAME: + match = this->Match(score, entry.fname, query.cqy_search_string, 100); + break; + + default: // CacheSearchMethod::NONE + match = true; + break; + }; + if (match) + { + query.cqy_res_category_usage[entry.categoryid]++; + query.cqy_res_category_usage[CacheCategoryId::CID_All]++; + query.cqy_results.emplace_back(&entry, score); + query.cqy_res_last_update = std::max(query.cqy_res_last_update, entry.addtimestamp); + } + } + + std::sort(query.cqy_results.begin(), query.cqy_results.end()); + return query.cqy_results.size(); +} + +bool CacheSystem::Match(size_t& out_score, std::string data, std::string const& query, size_t score) +{ + Ogre::StringUtil::toLowerCase(data); + size_t pos = data.find(query); + if (pos != std::string::npos) + { + out_score = score + pos; + return true; + } + else + { + return false; + } +} diff --git a/source/main/resources/CacheSystem.h b/source/main/resources/CacheSystem.h index 5bde88f2bc..ae18e0e347 100644 --- a/source/main/resources/CacheSystem.h +++ b/source/main/resources/CacheSystem.h @@ -119,12 +119,58 @@ class CacheEntry std::vector sectionconfigs; }; +enum CacheCategoryId +{ + CID_Max = 9000, + CID_Unsorted = 9990, + CID_All = 9991, + CID_Fresh = 9992, + CID_Hidden = 9993, + CID_SearchResults = 9994 +}; + struct CacheCategory { const int ccg_id; const char* ccg_name; }; +struct CacheQueryResult +{ + CacheQueryResult(CacheEntry* entry, size_t score): + cqr_entry(entry), cqr_score(score) + {} + + bool operator<(CacheQueryResult const& other) { return cqr_score < other.cqr_score; } + bool operator>(CacheQueryResult const& other) { return cqr_score > other.cqr_score; } + + CacheEntry* cqr_entry; + size_t cqr_score; +}; + +enum class CacheSearchMethod // Always case-insensitive +{ + NONE, // No searching + FULLTEXT, // Fields: name, filename, description, author name/mail (in this order, with descending rank) and returns rank+string pos as score + GUID, // Fields: guid + AUTHORS, // Fields: name, email), 'wheels' (), 'file' (filename) + WHEELS, // Fields: num wheels (string), num propelled wheels (string) + FILENAME // Fields: truckfile name +}; + +struct CacheQuery +{ + LoaderType cqy_filter_type = LoaderType::LT_None; + int cqy_filter_category_id = CacheCategoryId::CID_All; + std::string cqy_filter_guid; //!< Exact match; leave empty to disable + CacheSearchMethod cqy_search_method = CacheSearchMethod::NONE; + std::string cqy_search_string; + + std::vector cqy_results; + std::map cqy_res_category_usage; + std::time_t cqy_res_last_update = std::time_t(); +}; + /// A content database /// MOTIVATION: /// RoR users usually have A LOT of content installed. Traversing it all on every game startup would be a pain. @@ -156,6 +202,7 @@ class CacheSystem : public ZeroedMemoryAllocator CacheEntry* FetchSkinByName(std::string const & skin_name); void UnloadActorFromMemory(std::string filename); CacheValidityState EvaluateCacheValidity(); + size_t Query(CacheQuery& query); void LoadResource(CacheEntry& t); //!< Loads the associated resource bundle if not already done. bool CheckResourceLoaded(Ogre::String &in_out_filename); //!< Finds + loads the associated resource bundle if not already done. @@ -169,8 +216,6 @@ class CacheSystem : public ZeroedMemoryAllocator CacheEntry *GetEntry(int modid); Ogre::String GetPrettyName(Ogre::String fname); - enum CategoryID {CID_Max=9000, CID_Unsorted=9990, CID_All=9991, CID_Fresh=9992, CID_Hidden=9993, CID_SearchResults=9994}; - private: void WriteCacheFileJson(); @@ -200,6 +245,8 @@ class CacheSystem : public ZeroedMemoryAllocator void GenerateFileCache(CacheEntry &entry, Ogre::String group); void RemoveFileCache(CacheEntry &entry); + bool Match(size_t& out_score, std::string data, std::string const& query, size_t ); + std::time_t m_update_time; //!< Ensures that all inserted files share the same timestamp std::string m_filenames_hash; //!< stores hash over the content, for quick update detection std::map m_loaded_resource_bundles; //!< Assosiates resource path with resource group