From 25150ead122dd0fb9ea4aa4568f85d61ccc44033 Mon Sep 17 00:00:00 2001 From: Ion Agorria Date: Tue, 26 Sep 2023 19:43:51 +0200 Subject: [PATCH] WIP mods menu --- Source/Game/GameContent.cpp | 29 ++++++++++- Source/Game/GameContent.h | 2 + Source/Game/Texts.cpp | 14 +++++ Source/UserInterface/HistoryScene.cpp | 5 +- Source/UserInterface/Menu/AddonsMenu.cpp | 63 +++++++++++++++++++++++ Source/UserInterface/PerimeterShellUI.cpp | 18 +++---- Source/UserInterface/PerimeterShellUI.h | 3 +- Source/Util/SystemUtil.cpp | 4 +- 8 files changed, 120 insertions(+), 18 deletions(-) diff --git a/Source/Game/GameContent.cpp b/Source/Game/GameContent.cpp index d797b5439..469dc0bdc 100644 --- a/Source/Game/GameContent.cpp +++ b/Source/Game/GameContent.cpp @@ -11,6 +11,7 @@ namespace scripts_export { #include "BelligerentSelect.h" #include "qd_textdb.h" #include "Localization.h" +#include "codepages/codepages.h" #include #include @@ -601,11 +602,15 @@ void detectGameContent() { data.mod_name = mod_ini.get("Mod", "name"); data.mod_version = mod_ini.get("Mod", "version"); if (data.mod_name.empty()) { - fprintf(stderr, "Missing name in Mod section at %s, not loading\n", path_ini.c_str()); data.available = false; + fprintf(stderr, "Missing name in Mod section at %s, not loading\n", path_ini.c_str()); + data.errors += qdTextDB::instance().getText("Interface.Menu.Mods.ErrorMissingAttribute"); + data.errors += " [Mod] name\n"; } else if (data.mod_version.empty()) { - fprintf(stderr, "Missing version in Mod section at %s, not loading\n", path_ini.c_str()); data.available = false; + fprintf(stderr, "Missing version in Mod section at %s, not loading\n", path_ini.c_str()); + data.errors += qdTextDB::instance().getText("Interface.Menu.Mods.ErrorMissingAttribute"); + data.errors += " [Mod] version\n"; } //Load optional fields @@ -617,6 +622,16 @@ void detectGameContent() { if (data.mod_description.empty() && locale != "english") { data.mod_description = mod_ini.get("Mod", "description_english"); } + if (!data.mod_description.empty()) { + data.mod_description = convertToCodepage(data.mod_description.c_str(), "english"); + } + } else { + data.mod_description = convertToCodepage(data.mod_description.c_str(), locale); + } + if (!data.mod_description.empty()) { + string_replace_all(data.mod_description, "\\r", ""); + string_replace_all(data.mod_description, "\\n", "\n"); + string_replace_all(data.mod_description, "\\\\", "\\"); } data.mod_authors = mod_ini.get("Mod", "authors"); data.mod_url = mod_ini.get("Mod", "url"); @@ -628,6 +643,8 @@ void detectGameContent() { if (0 < diff) { fprintf(stderr, "Minimum game version '%s' requirement not satisfied for %s, not loading\n", data.content_game_minimum_version.c_str(), data.path.c_str()); + data.errors += qdTextDB::instance().getText("Interface.Menu.Mods.ErrorGameTooOld"); + data.errors += " " + data.content_game_minimum_version + "\n"; data.available = false; } } @@ -646,6 +663,7 @@ void detectGameContent() { data.mod_url = "https://kdlab.com"; } else { fprintf(stderr, "Mod folder %s has missing info file %s, not loading\n", data.path.c_str(), path_ini.c_str()); + data.errors += qdTextDB::instance().getText("Interface.Menu.Mods.ErrorMissingModInfo"); } //Force disable all mods @@ -656,6 +674,7 @@ void detectGameContent() { //Avoid possible duplicates of ET if (data.available && is_content_ET && (terGameContentAvailable & PERIMETER_ET)) { fprintf(stderr, "ET is already loaded when loading ET content at %s, not loading", data.path.c_str()); + data.errors += qdTextDB::instance().getText("Interface.Menu.Mods.ErrorDuplicateContent"); data.available = false; } @@ -693,9 +712,13 @@ void detectGameContent() { if (!getMissingGameContent(terGameContentAvailable, required).empty()) { fprintf(stderr, "Game content '%s' not installed which is a requirement for %s, not loading\n", mod.content_required_content.c_str(), mod.path.c_str()); + mod.errors += qdTextDB::instance().getText("Interface.Menu.Mods.ErrorRequiredContentMissing"); + mod.errors += " " + mod.content_required_content + "\n"; } else { fprintf(stderr, "Game content '%s' installed but not enabled which is a requirement for %s, not loading\n", mod.content_required_content.c_str(), mod.path.c_str()); + mod.errors += qdTextDB::instance().getText("Interface.Menu.Mods.ErrorRequiredContentDisabled"); + mod.errors += " " + mod.content_required_content + "\n"; } mod.available = mod.enabled = false; } @@ -705,6 +728,8 @@ void detectGameContent() { if (terGameContentSelect == disallowed) { fprintf(stderr, "Game content '%s' is enabled which is not compatible for %s, not loading\n", mod.content_disallowed_content.c_str(), mod.path.c_str()); + mod.errors += qdTextDB::instance().getText("Interface.Menu.Mods.ErrorDisallowedContentEnabled"); + mod.errors += " " + mod.content_disallowed_content + "\n"; mod.available = mod.enabled = false; } } diff --git a/Source/Game/GameContent.h b/Source/Game/GameContent.h index bdff0ff54..02d0708bd 100644 --- a/Source/Game/GameContent.h +++ b/Source/Game/GameContent.h @@ -12,6 +12,8 @@ class ModMetadata { public: /// Path for this mod std::string path = {}; + /// Errors when loading mod if any + std::string errors = {}; /// Has campaign missions? //TODO use this on "Change Campaign" bool campaign = false; diff --git a/Source/Game/Texts.cpp b/Source/Game/Texts.cpp index 425c2e5f1..097c15fc7 100644 --- a/Source/Game/Texts.cpp +++ b/Source/Game/Texts.cpp @@ -705,6 +705,13 @@ void qdTextDB::load_supplementary_texts(const std::string& locale) { load_lines({ "GAME_CONTENT.PERIMETER=Периметр", "GAME_CONTENT.PERIMETER_ET=Периметр: Завет Императора", + "Interface.Menu.Mods.ErrorMissingAttribute=Отсутствует атрибут в mod.ini:", + "Interface.Menu.Mods.ErrorGameTooOld=Игра слишком старая, нужна версия:", + "Interface.Menu.Mods.ErrorMissingModInfo=Отсутствует mod.ini\n", + "Interface.Menu.Mods.ErrorDuplicateContent=Содержимое уже загружено\n", + "Interface.Menu.Mods.ErrorRequiredContentMissing=Мод требует контента, который не установлен", + "Interface.Menu.Mods.ErrorRequiredContentDisabled=Мод требует отключенного контента", + "Interface.Menu.Mods.ErrorDisallowedContentEnabled=Мод несовместим с активным в данный момент контентом", "Interface.Menu.Messages.ReplayGameVersionDifferent=Этот повтор был сохранен в другой версии и может отображаться неправильно. Использованная версия игры:", "Interface.Menu.Messages.GameContentMissing=Содержит ресурсы, которые не представлены или не включены в этой копии игры, убедитесь, что они установлены и включены:\n", "Interface.Menu.Messages.GameContentSwitch=Содержит неактивные ресурсы. Переключите кампанию на следующую:", @@ -760,6 +767,13 @@ void qdTextDB::load_supplementary_texts(const std::string& locale) { load_lines({ "GAME_CONTENT.PERIMETER=Perimeter", "GAME_CONTENT.PERIMETER_ET=Perimeter: Emperor's Testament", + "Interface.Menu.Mods.ErrorMissingAttribute=Missing attribute in mod.ini:", + "Interface.Menu.Mods.ErrorGameTooOld=Game is too old, needs version:", + "Interface.Menu.Mods.ErrorMissingModInfo=Missing mod.ini\n", + "Interface.Menu.Mods.ErrorDuplicateContent=Content is already loaded\n", + "Interface.Menu.Mods.ErrorRequiredContentMissing=Mod requires content that is not installed", + "Interface.Menu.Mods.ErrorRequiredContentDisabled=Mod requires content that is disabled", + "Interface.Menu.Mods.ErrorDisallowedContentEnabled=Mod is not compatible with currently enabled content", "Interface.Menu.Messages.ReplayGameVersionDifferent=This replay was saved with a different version and may not display correctly, used game version:", "Interface.Menu.Messages.GameContentMissing=Contains game content that is not present or enabled in your installation, make sure these are installed and enabled in your game:\n", "Interface.Menu.Messages.GameContentSwitch=Contains game content that is not selected, please change the campaign to the following content:\n", diff --git a/Source/UserInterface/HistoryScene.cpp b/Source/UserInterface/HistoryScene.cpp index fb3b939d9..54de32fd2 100644 --- a/Source/UserInterface/HistoryScene.cpp +++ b/Source/UserInterface/HistoryScene.cpp @@ -434,12 +434,9 @@ void HistoryScene::drawPopup() { Vect2f mousePos = historyCamera->getMousePos(); World* w = traceWorld(mousePos); - sPoint pt = {xm::round((mousePos.x + 0.5f) * terRenderDevice->GetSizeX()), + Vect2i pt = {xm::round((mousePos.x + 0.5f) * terRenderDevice->GetSizeX()), xm::round((mousePos.y + 0.5f) * terRenderDevice->GetSizeY()) }; - //TODO is this needed to port? - //ClientToScreen(hWndVisGeneric, &pt); - if (w) { std::string frameNames; std::map ::iterator it; diff --git a/Source/UserInterface/Menu/AddonsMenu.cpp b/Source/UserInterface/Menu/AddonsMenu.cpp index 932181cf0..fc784e528 100644 --- a/Source/UserInterface/Menu/AddonsMenu.cpp +++ b/Source/UserInterface/Menu/AddonsMenu.cpp @@ -174,6 +174,69 @@ void onMMAddonsList(CShellWindow* pWnd, InterfaceEventCode code, int param) { } } else if ( code == EVENT_DOUBLECLICK && param == VK_LBUTTON) { addonEnableSwitch(list); + } else if ( code == EVENT_DRAWWND ) { + Vect2f mousePos = gameShell->mousePosition(); + mousePos.x += 0.5f; + mousePos.y += 0.5f; + + if (!list->HitTest(mousePos.x, mousePos.y)) { + return; + } + + Vect2i pt = { + xm::round(mousePos.x * terRenderDevice->GetSizeX()), + xm::round(mousePos.y * terRenderDevice->GetSizeY()) + }; + + int pos = list->ItemFromPoint(pt.y); + if (pos >= 0 && pos < gameModInfoList.size()) { + ModMetadata* mod = gameModInfoList[pos].mod; + + std::string popupTxt = mod->mod_name; + popupTxt += " [" + mod->mod_version + "]"; + if (!mod->mod_authors.empty()) { + popupTxt += "\n" + mod->mod_authors; + } + if (!mod->mod_url.empty()) { + popupTxt += "\n" + mod->mod_url; + } + if (!mod->errors.empty()) { + popupTxt += "\n&FF0000" + mod->errors; + } + if (!mod->mod_description.empty()) { + const int MAX_LEN = 1000; + if (mod->mod_description.length() > MAX_LEN) { + mod->mod_description = mod->mod_description.substr(0, MAX_LEN) + "..."; + } + popupTxt += "\n\n&FFFFFF" + BreakLongLines(mod->mod_description.c_str(), 160); + } + + //Draw rect + terRenderDevice->SetFont(pWnd->m_hFont); + + Vect2f v1; + Vect2f v2; + terRenderDevice->OutTextRect(0, 0, popupTxt.c_str(), -1, v1, v2); + + const float pad = 5; + Vect2f rectsize = { + v2.x - v1.x + pad * 2, + v2.y - v1.y + pad * 2 + }; + float x_max = terRenderDevice->GetSizeX() - rectsize.x - pad; + float y_max = terRenderDevice->GetSizeY() - rectsize.y - pad; + float y_move = rectsize.y + pad; + Vect2f rectpos = { + min(max(pad, pt.x - rectsize.x / 2.0f), x_max), + min(max(pad, pt.y + (mousePos.y > 0 ? -y_move : (pad + 40))), y_max) + }; + + terRenderDevice->DrawRectangle(rectpos.x, rectpos.y, rectsize.x, rectsize.y, sColor4c(32, 32, 32, 72), 0); + terRenderDevice->DrawRectangle(rectpos.x, rectpos.y, rectsize.x, rectsize.y, sColor4c(255, 255, 255, 255), 1); + terRenderDevice->OutText(rectpos.x + pad, rectpos.y + pad, popupTxt.c_str(), sColor4f(1, 1, 1, 1), -1); + terRenderDevice->SetFont(nullptr); + terRenderDevice->FlushPrimitive2D(); + } } } diff --git a/Source/UserInterface/PerimeterShellUI.cpp b/Source/UserInterface/PerimeterShellUI.cpp index 5400f34e3..19807e426 100644 --- a/Source/UserInterface/PerimeterShellUI.cpp +++ b/Source/UserInterface/PerimeterShellUI.cpp @@ -3401,18 +3401,18 @@ int CListBoxWindow::CheckClick(float fx,float fy) int CListBoxWindow::ItemFromPoint(float _y) { - if(m_pItem[0].m_data.empty()) - return -1; + if (m_pItem[0].m_data.empty()) { + return -1; + } + if (y > _y) { + return 0; + } int i = int((_y - y)/m_fStringHeight) + m_nTopItem; - if(i >= m_pItem[0].m_data.size()) - i = m_pItem[0].m_data.size()-1; - - //TEMP -// if (i > lastAccessibleMissionNumber) { -// i = lastAccessibleMissionNumber; -// } + if (i >= m_pItem[0].m_data.size()) { + i = m_pItem[0].m_data.size() - 1; + } return i; } diff --git a/Source/UserInterface/PerimeterShellUI.h b/Source/UserInterface/PerimeterShellUI.h index 9ce96520c..6b697614c 100644 --- a/Source/UserInterface/PerimeterShellUI.h +++ b/Source/UserInterface/PerimeterShellUI.h @@ -886,7 +886,6 @@ class CListBoxWindow : public CShellWindow }; int CheckClick(float _x,float _y); - int ItemFromPoint(float _y); public: CListBoxWindow(int id, CShellWindow* pParent, EVENTPROC p = 0); ~CListBoxWindow(); @@ -904,6 +903,8 @@ class CListBoxWindow : public CShellWindow void SetCurSel(int n); void SetCurSelPassive(int n); + + int ItemFromPoint(float _y); const char* GetCurSelString(); diff --git a/Source/Util/SystemUtil.cpp b/Source/Util/SystemUtil.cpp index 116413c95..3e7a55f67 100644 --- a/Source/Util/SystemUtil.cpp +++ b/Source/Util/SystemUtil.cpp @@ -410,10 +410,10 @@ std::string IniManager::getFilePath() { const char* IniManager::get(const char* section, const char* key) { - static char buf[256]; + static char buf[10240]; std::string path = getFilePath(); - if(!ReadIniString(section, key, NULL, buf, 256, path)){ + if(!ReadIniString(section, key, NULL, buf, 10240, path)){ *buf = 0; if (check_existence) { fprintf(stderr, "INI key not found %s %s %s\n", path.c_str(), section, key);