From cc71651ee48ef3bc132cb9d271eeb01b4f9928b2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 19 Nov 2023 19:30:55 +0200 Subject: [PATCH] Show proper error message if player attempts to load save with missing identifiers instead of silent crash to main menu --- Mods/vcmi/config/vcmi/english.json | 1 + Mods/vcmi/config/vcmi/ukrainian.json | 1 + lib/constants/EntityIdentifiers.cpp | 84 ++++++++++------------------ lib/constants/IdentifierBase.h | 20 ++++++- server/CGameHandler.cpp | 11 ++++ 5 files changed, 60 insertions(+), 57 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index e6cb860bb..fdf481902 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -75,6 +75,7 @@ "vcmi.server.confirmReconnect" : "Do you want to reconnect to the last session?", "vcmi.server.errors.modNoDependency" : "Failed to load mod {'%s'}!\n It depends on mod {'%s'} which is not active!\n", "vcmi.server.errors.modConflict" : "Failed to load mod {'%s'}!\n Conflicts with active mod {'%s'}!\n", + "vcmi.server.errors.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!", "vcmi.settingsMainWindow.generalTab.hover" : "General", "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior.", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index e7be77050..8fc46d9a9 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -75,6 +75,7 @@ "vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", "vcmi.server.errors.modNoDependency" : "Не вдалося увімкнути мод {'%s'}!\n Модифікація потребує мод {'%s'} який зараз не активний!\n", "vcmi.server.errors.modConflict" : "Не вдалося увімкнути мод {'%s'}!\n Конфліктує з активним модом {'%s'}!\n", + "vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!", "vcmi.settingsMainWindow.generalTab.hover" : "Загальні", "vcmi.settingsMainWindow.generalTab.help" : "Перемикає на вкладку загальних параметрів, яка містить налаштування, пов'язані із загальною поведінкою ігрового клієнта", diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 224bd986a..d119c0f76 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -122,12 +122,21 @@ namespace GameConstants #endif } -si32 HeroClassID::decode(const std::string & identifier) +int32_t IdentifierBase::resolveIdentifier(const std::string & entityType, const std::string identifier) { if (identifier.empty()) return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "heroClass", identifier); - return rawId.value(); + + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType, identifier); + + if (rawId) + return rawId.value(); + throw IdentifierResolutionException(identifier); +} + +si32 HeroClassID::decode(const std::string & identifier) +{ + return resolveIdentifier("heroClass", identifier); } std::string HeroClassID::encode(const si32 index) @@ -171,10 +180,7 @@ std::string MapObjectID::encode(int32_t index) si32 MapObjectID::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "object", identifier); - return rawId.value(); + return resolveIdentifier("object", identifier); } std::string MapObjectSubID::encode(MapObjectID primaryID, int32_t index) @@ -193,17 +199,13 @@ std::string MapObjectSubID::encode(MapObjectID primaryID, int32_t index) si32 MapObjectSubID::decode(MapObjectID primaryID, const std::string & identifier) { - if (identifier.empty()) - return -1; - if(primaryID == Obj::PRISON || primaryID == Obj::HERO) return HeroTypeID::decode(identifier); if (primaryID == Obj::SPELL_SCROLL) return SpellID::decode(identifier); - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), VLC->objtypeh->getJsonKey(primaryID), identifier); - return rawId.value(); + return resolveIdentifier(VLC->objtypeh->getJsonKey(primaryID), identifier); } std::string BoatId::encode(int32_t index) @@ -215,20 +217,14 @@ std::string BoatId::encode(int32_t index) si32 BoatId::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "core:boat", identifier); - return rawId.value(); + return resolveIdentifier("core:boat", identifier); } si32 HeroTypeID::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; if (identifier == "random") return -2; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); - return rawId.value(); + return resolveIdentifier("hero", identifier); } std::string HeroTypeID::encode(const si32 index) @@ -257,10 +253,7 @@ const Artifact * ArtifactIDBase::toEntity(const Services * services) const si32 ArtifactID::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", identifier); - return rawId.value(); + return resolveIdentifier("artifact", identifier); } std::string ArtifactID::encode(const si32 index) @@ -277,10 +270,7 @@ std::string ArtifactID::entityType() si32 SecondarySkill::decode(const std::string& identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "secondarySkill", identifier); - return rawId.value(); + return resolveIdentifier("secondarySkill", identifier); } std::string SecondarySkill::encode(const si32 index) @@ -317,10 +307,7 @@ const Creature * CreatureIDBase::toEntity(const CreatureService * creatures) con si32 CreatureID::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "creature", identifier); - return rawId.value(); + return resolveIdentifier("creature", identifier); } std::string CreatureID::encode(const si32 index) @@ -367,10 +354,7 @@ const HeroType * HeroTypeID::toEntity(const Services * services) const si32 SpellID::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); - return rawId.value(); + return resolveIdentifier("spell", identifier); } std::string SpellID::encode(const si32 index) @@ -382,10 +366,7 @@ std::string SpellID::encode(const si32 index) si32 BattleField::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); - return rawId.value(); + return resolveIdentifier("battlefield", identifier); } std::string BattleField::encode(const si32 index) @@ -439,7 +420,7 @@ std::string PlayerColor::entityType() si32 PrimarySkill::decode(const std::string& identifier) { - return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + return resolveIdentifier(entityType(), identifier); } std::string PrimarySkill::encode(const si32 index) @@ -454,11 +435,7 @@ std::string PrimarySkill::entityType() si32 FactionID::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - return rawId.value(); + return resolveIdentifier(entityType(), identifier); } std::string FactionID::encode(const si32 index) @@ -480,13 +457,10 @@ const Faction * FactionID::toEntity(const Services * service) const si32 TerrainId::decode(const std::string & identifier) { - if (identifier.empty()) - return static_cast(TerrainId::NONE); if (identifier == "native") return TerrainId::NATIVE_TERRAIN; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - return rawId.value(); + return resolveIdentifier(entityType(), identifier); } std::string TerrainId::encode(const si32 index) @@ -508,8 +482,7 @@ si32 RoadId::decode(const std::string & identifier) if (identifier.empty()) return RoadId::NO_ROAD.getNum(); - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - return rawId.value(); + return resolveIdentifier(entityType(), identifier); } std::string RoadId::encode(const si32 index) @@ -529,8 +502,7 @@ si32 RiverId::decode(const std::string & identifier) if (identifier.empty()) return RiverId::NO_RIVER.getNum(); - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - return rawId.value(); + return resolveIdentifier(entityType(), identifier); } std::string RiverId::encode(const si32 index) @@ -574,7 +546,7 @@ const ObstacleInfo * Obstacle::getInfo() const si32 SpellSchool::decode(const std::string & identifier) { - return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + return resolveIdentifier(entityType(), identifier); } std::string SpellSchool::encode(const si32 index) @@ -592,7 +564,7 @@ std::string SpellSchool::entityType() si32 GameResID::decode(const std::string & identifier) { - return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + return resolveIdentifier(entityType(), identifier); } std::string GameResID::encode(const si32 index) diff --git a/lib/constants/IdentifierBase.h b/lib/constants/IdentifierBase.h index c72903ca0..62c2e671b 100644 --- a/lib/constants/IdentifierBase.h +++ b/lib/constants/IdentifierBase.h @@ -9,6 +9,19 @@ */ #pragma once +VCMI_LIB_NAMESPACE_BEGIN + +class IdentifierResolutionException : public std::runtime_error +{ +public: + const std::string identifierName; + + IdentifierResolutionException(const std::string & identifierName ) + : std::runtime_error("Failed to resolve identifier " + identifierName) + , identifierName(identifierName) + {} +}; + class IdentifierBase { protected: @@ -21,6 +34,11 @@ protected: {} ~IdentifierBase() = default; + + /// Attempts to resolve identifier using provided entity type + /// Returns resolved numeric identifier + /// Throws IdentifierResolutionException on failure + static int32_t resolveIdentifier(const std::string & entityType, const std::string identifier); public: int32_t num; @@ -233,4 +251,4 @@ public: } }; -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +VCMI_LIB_NAMESPACE_END diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index b9011b4db..5bdba02b1 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1802,9 +1802,20 @@ bool CGameHandler::load(const std::string & filename) lobby->announceMessage(errorMsg); return false; } + catch(const IdentifierResolutionException & e) + { + logGlobal->error("Failed to load game: %s", e.what()); + MetaString errorMsg; + errorMsg.appendTextID("vcmi.server.errors.unknownEntity"); + errorMsg.replaceRawString(e.identifierName); + lobby->announceMessage(errorMsg.toString());//FIXME: should be localized on client side + return false; + } + catch(const std::exception & e) { logGlobal->error("Failed to load game: %s", e.what()); + lobby->announceMessage(std::string("Failed to load game: ") + e.what()); return false; } gs->preInit(VLC);