1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-20 20:23:03 +02:00

Merge pull request #4741 from IvanSavenko/serialize_local_state

Serialize local state of player interface
This commit is contained in:
Ivan Savenko 2024-10-10 15:03:14 +03:00 committed by GitHub
commit 81f0222c68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 250 additions and 39 deletions

View File

@ -25,6 +25,7 @@
#include "lib/UnlockGuard.h"
#include "lib/battle/BattleInfo.h"
#include "lib/networkPacks/PacksForServer.h"
#include "lib/networkPacks/SaveLocalState.h"
bool CCallback::teleportHero(const CGHeroInstance *who, const CGTownInstance *where)
{
@ -323,6 +324,15 @@ void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroIn
sendRequest(pack);
}
void CCallback::saveLocalState(const JsonNode & data)
{
SaveLocalState state;
state.data = data;
state.player = *player;
sendRequest(state);
}
void CCallback::save( const std::string &fname )
{
cl->save(fname);

View File

@ -100,6 +100,7 @@ public:
virtual void assembleArtifacts(const ObjectInstanceID & heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)=0;
virtual void eraseArtifactByClient(const ArtifactLocation & al)=0;
virtual bool dismissCreature(const CArmedInstance *obj, SlotID stackPos)=0;
virtual void saveLocalState(const JsonNode & data)=0;
virtual void endTurn()=0;
virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith)
virtual void setFormation(const CGHeroInstance * hero, EArmyFormation mode)=0;
@ -193,6 +194,7 @@ public:
void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1) override;
bool dismissCreature(const CArmedInstance *obj, SlotID stackPos) override;
bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override;
void saveLocalState(const JsonNode & data) override;
void endTurn() override;
void spellResearch(const CGTownInstance *town, SpellID spellAtSlot, bool accepted) override;
void swapGarrisonHero(const CGTownInstance *town) override;

View File

@ -1333,6 +1333,8 @@ void CPlayerInterface::initializeHeroTownList()
localState->addOwnedTown(town);
}
localState->deserialize(*cb->getPlayerState(playerID)->playerLocalSettings);
if(adventureInt)
adventureInt->onHeroChanged(nullptr);
}

View File

@ -11,6 +11,7 @@
#include "PlayerLocalState.h"
#include "../CCallback.h"
#include "../lib/json/JsonNode.h"
#include "../lib/mapObjects/CGHeroInstance.h"
#include "../lib/mapObjects/CGTownInstance.h"
#include "../lib/pathfinder/CGPathNode.h"
@ -23,34 +24,20 @@ PlayerLocalState::PlayerLocalState(CPlayerInterface & owner)
{
}
void PlayerLocalState::saveHeroPaths(std::map<const CGHeroInstance *, int3> & pathsMap)
const PlayerSpellbookSetting & PlayerLocalState::getSpellbookSettings()
{
for(auto & p : paths)
{
if(p.second.nodes.size())
pathsMap[p.first] = p.second.endPos();
else
logGlobal->debug("%s has assigned an empty path! Ignoring it...", p.first->getNameTranslated());
}
return spellbookSettings;
}
void PlayerLocalState::loadHeroPaths(std::map<const CGHeroInstance *, int3> & pathsMap)
void PlayerLocalState::setSpellbookSettings(const PlayerSpellbookSetting & newSettings)
{
if(owner.cb)
{
for(auto & p : pathsMap)
{
CGPath path;
owner.cb->getPathsInfo(p.first)->getPath(path, p.second);
paths[p.first] = path;
logGlobal->trace("Restored path for hero %s leading to %s with %d nodes", p.first->nodeName(), p.second.toString(), path.nodes.size());
}
}
spellbookSettings = newSettings;
}
void PlayerLocalState::setPath(const CGHeroInstance * h, const CGPath & path)
{
paths[h] = path;
syncronizeState();
}
const CGPath & PlayerLocalState::getPath(const CGHeroInstance * h) const
@ -70,6 +57,7 @@ bool PlayerLocalState::setPath(const CGHeroInstance * h, const int3 & destinatio
if(!owner.cb->getPathsInfo(h)->getPath(path, destination))
{
paths.erase(h); //invalidate previously possible path if selected (before other hero blocked only path / fly spell expired)
syncronizeState();
return false;
}
@ -93,6 +81,7 @@ void PlayerLocalState::erasePath(const CGHeroInstance * h)
{
paths.erase(h);
adventureInt->onHeroChanged(h);
syncronizeState();
}
void PlayerLocalState::verifyPath(const CGHeroInstance * h)
@ -170,6 +159,7 @@ void PlayerLocalState::setSelection(const CArmedInstance * selection)
if (adventureInt && selection)
adventureInt->onSelectionChanged(selection);
syncronizeState();
}
bool PlayerLocalState::isHeroSleeping(const CGHeroInstance * hero) const
@ -184,6 +174,7 @@ void PlayerLocalState::setHeroAsleep(const CGHeroInstance * hero)
assert(!vstd::contains(sleepingHeroes, hero));
sleepingHeroes.push_back(hero);
syncronizeState();
}
void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero)
@ -193,6 +184,7 @@ void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero)
assert(vstd::contains(sleepingHeroes, hero));
vstd::erase(sleepingHeroes, hero);
syncronizeState();
}
const std::vector<const CGHeroInstance *> & PlayerLocalState::getWanderingHeroes()
@ -215,6 +207,8 @@ void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero)
if (currentSelection == nullptr)
setSelection(hero);
syncronizeState();
}
void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero)
@ -236,6 +230,8 @@ void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero)
if (currentSelection == nullptr && !ownedTowns.empty())
setSelection(ownedTowns.front());
syncronizeState();
}
void PlayerLocalState::swapWanderingHero(size_t pos1, size_t pos2)
@ -244,6 +240,8 @@ void PlayerLocalState::swapWanderingHero(size_t pos1, size_t pos2)
std::swap(wanderingHeroes.at(pos1), wanderingHeroes.at(pos2));
adventureInt->onHeroOrderChanged();
syncronizeState();
}
const std::vector<const CGTownInstance *> & PlayerLocalState::getOwnedTowns()
@ -266,6 +264,8 @@ void PlayerLocalState::addOwnedTown(const CGTownInstance * town)
if (currentSelection == nullptr)
setSelection(town);
syncronizeState();
}
void PlayerLocalState::removeOwnedTown(const CGTownInstance * town)
@ -282,6 +282,8 @@ void PlayerLocalState::removeOwnedTown(const CGTownInstance * town)
if (currentSelection == nullptr && !ownedTowns.empty())
setSelection(ownedTowns.front());
syncronizeState();
}
void PlayerLocalState::swapOwnedTowns(size_t pos1, size_t pos2)
@ -289,5 +291,119 @@ void PlayerLocalState::swapOwnedTowns(size_t pos1, size_t pos2)
assert(ownedTowns[pos1] && ownedTowns[pos2]);
std::swap(ownedTowns.at(pos1), ownedTowns.at(pos2));
syncronizeState();
adventureInt->onTownOrderChanged();
}
void PlayerLocalState::syncronizeState()
{
JsonNode data;
serialize(data);
owner.cb->saveLocalState(data);
}
void PlayerLocalState::serialize(JsonNode & dest) const
{
dest.clear();
for (auto const * town : ownedTowns)
{
JsonNode record;
record["id"].Integer() = town->id;
dest["towns"].Vector().push_back(record);
}
for (auto const * hero : wanderingHeroes)
{
JsonNode record;
record["id"].Integer() = hero->id;
if (vstd::contains(sleepingHeroes, hero))
record["sleeping"].Bool() = true;
if (paths.count(hero))
{
record["path"]["x"].Integer() = paths.at(hero).lastNode().coord.x;
record["path"]["y"].Integer() = paths.at(hero).lastNode().coord.y;
record["path"]["z"].Integer() = paths.at(hero).lastNode().coord.z;
}
dest["heroes"].Vector().push_back(record);
}
dest["spellbook"]["pageBattle"].Integer() = spellbookSettings.spellbookLastPageBattle;
dest["spellbook"]["pageAdvmap"].Integer() = spellbookSettings.spellbookLastPageAdvmap;
dest["spellbook"]["tabBattle"].Integer() = spellbookSettings.spellbookLastTabBattle;
dest["spellbook"]["tabAdvmap"].Integer() = spellbookSettings.spellbookLastTabAdvmap;
dest["currentSelection"].Integer() = currentSelection->id;
}
void PlayerLocalState::deserialize(const JsonNode & source)
{
// this method must be called after player state has been initialized
assert(currentSelection != nullptr);
assert(!ownedTowns.empty() || wanderingHeroes.empty());
auto oldHeroes = wanderingHeroes;
auto oldTowns = ownedTowns;
paths.clear();
sleepingHeroes.clear();
wanderingHeroes.clear();
ownedTowns.clear();
for (auto const & town : source["towns"].Vector())
{
ObjectInstanceID objID(town["id"].Integer());
const CGTownInstance * townPtr = owner.cb->getTown(objID);
if (!townPtr)
continue;
if (!vstd::contains(oldTowns, townPtr))
continue;
ownedTowns.push_back(townPtr);
vstd::erase(oldTowns, townPtr);
}
for (auto const & hero : source["heroes"].Vector())
{
ObjectInstanceID objID(hero["id"].Integer());
const CGHeroInstance * heroPtr = owner.cb->getHero(objID);
if (!heroPtr)
continue;
if (!vstd::contains(oldHeroes, heroPtr))
continue;
wanderingHeroes.push_back(heroPtr);
vstd::erase(oldHeroes, heroPtr);
if (hero["sleeping"].Bool())
sleepingHeroes.push_back(heroPtr);
if (hero["path"]["x"].isNumber() && hero["path"]["y"].isNumber() && hero["path"]["z"].isNumber())
{
int3 pathTarget(hero["path"]["x"].Integer(), hero["path"]["y"].Integer(), hero["path"]["z"].Integer());
setPath(heroPtr, pathTarget);
}
}
spellbookSettings.spellbookLastPageBattle = source["spellbook"]["pageBattle"].Integer();
spellbookSettings.spellbookLastPageAdvmap = source["spellbook"]["pageAdvmap"].Integer();
spellbookSettings.spellbookLastTabBattle = source["spellbook"]["tabBattle"].Integer();
spellbookSettings.spellbookLastTabAdvmap = source["spellbook"]["tabAdvmap"].Integer();
// append any owned heroes / towns that were not present in loaded state
wanderingHeroes.insert(wanderingHeroes.end(), oldHeroes.begin(), oldHeroes.end());
ownedTowns.insert(ownedTowns.end(), oldTowns.begin(), oldTowns.end());
//FIXME: broken, anything that is selected in here will be overwritten on NewTurn pack
// ObjectInstanceID selectedObjectID(source["currentSelection"].Integer());
// const CGObjectInstance * objectPtr = owner.cb->getObjInstance(selectedObjectID);
// const CArmedInstance * armyPtr = dynamic_cast<const CArmedInstance*>(objectPtr);
//
// if (armyPtr)
// setSelection(armyPtr);
}

View File

@ -14,6 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN
class CGHeroInstance;
class CGTownInstance;
class CArmedInstance;
class JsonNode;
struct CGPath;
class int3;
@ -21,6 +22,15 @@ VCMI_LIB_NAMESPACE_END
class CPlayerInterface;
struct PlayerSpellbookSetting
{
//on which page we left spellbook
int spellbookLastPageBattle = 0;
int spellbookLastPageAdvmap = 0;
int spellbookLastTabBattle = 4;
int spellbookLastTabAdvmap = 4;
};
/// Class that contains potentially serializeable state of a local player
class PlayerLocalState
{
@ -34,18 +44,10 @@ class PlayerLocalState
std::vector<const CGHeroInstance *> wanderingHeroes; //our heroes on the adventure map (not the garrisoned ones)
std::vector<const CGTownInstance *> ownedTowns; //our towns on the adventure map
void saveHeroPaths(std::map<const CGHeroInstance *, int3> & paths);
void loadHeroPaths(std::map<const CGHeroInstance *, int3> & paths);
PlayerSpellbookSetting spellbookSettings;
void syncronizeState();
public:
struct SpellbookLastSetting
{
//on which page we left spellbook
int spellbookLastPageBattle = 0;
int spellbookLastPageAdvmap = 0;
int spellbookLastTabBattle = 4;
int spellbookLastTabAdvmap = 4;
} spellbookSettings;
explicit PlayerLocalState(CPlayerInterface & owner);
@ -53,6 +55,9 @@ public:
void setHeroAsleep(const CGHeroInstance * hero);
void setHeroAwaken(const CGHeroInstance * hero);
const PlayerSpellbookSetting & getSpellbookSettings();
void setSpellbookSettings(const PlayerSpellbookSetting & newSettings);
const std::vector<const CGTownInstance *> & getOwnedTowns();
const CGTownInstance * getOwnedTown(size_t index);
void addOwnedTown(const CGTownInstance * hero);
@ -81,6 +86,9 @@ public:
const CGTownInstance * getCurrentTown() const;
const CArmedInstance * getCurrentArmy() const;
void serialize(JsonNode & dest) const;
void deserialize(const JsonNode & source);
/// Changes currently selected object
void setSelection(const CArmedInstance *sel);
};

View File

@ -205,9 +205,9 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
}
}
selectedTab = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap;
selectedTab = battleSpellsOnly ? myInt->localState->getSpellbookSettings().spellbookLastTabBattle : myInt->localState->getSpellbookSettings().spellbookLastTabAdvmap;
schoolTab->setFrame(selectedTab, 0);
int cp = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbookLastPageAdvmap;
int cp = battleSpellsOnly ? myInt->localState->getSpellbookSettings().spellbookLastPageBattle : myInt->localState->getSpellbookSettings().spellbookLastPageAdvmap;
// spellbook last page battle index is not reset after battle, so this needs to stay here
vstd::abetween(cp, 0, std::max(0, pagesWithinCurrentTab() - 1));
setCurrentPage(cp);
@ -313,8 +313,18 @@ void CSpellWindow::processSpells()
void CSpellWindow::fexitb()
{
(myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap) = selectedTab;
(myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbookLastPageAdvmap) = currentPage;
auto spellBookState = myInt->localState->getSpellbookSettings();
if(myInt->battleInt)
{
spellBookState.spellbookLastTabBattle = selectedTab;
spellBookState.spellbookLastPageBattle = currentPage;
}
else
{
spellBookState.spellbookLastTabAdvmap = selectedTab;
spellBookState.spellbookLastPageAdvmap = currentPage;
}
myInt->localState->setSpellbookSettings(spellBookState);
if(onSpellSelect)
onSpellSelect(SpellID::NONE);
@ -619,8 +629,10 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
auto guard = vstd::makeScopeGuard([this]()
{
owner->myInt->localState->spellbookSettings.spellbookLastTabAdvmap = owner->selectedTab;
owner->myInt->localState->spellbookSettings.spellbookLastPageAdvmap = owner->currentPage;
auto spellBookState = owner->myInt->localState->getSpellbookSettings();
spellBookState.spellbookLastTabAdvmap = owner->selectedTab;
spellBookState.spellbookLastPageAdvmap = owner->currentPage;
owner->myInt->localState->setSpellbookSettings(spellBookState);
});
spells::detail::ProblemImpl problem;

View File

@ -559,6 +559,7 @@ set(lib_MAIN_HEADERS
networkPacks/PacksForServer.h
networkPacks/SetRewardableConfiguration.h
networkPacks/SetStackEffect.h
networkPacks/SaveLocalState.h
networkPacks/StackLocation.h
networkPacks/TradeItem.h

View File

@ -10,6 +10,7 @@
#include "StdInc.h"
#include "CPlayerState.h"
#include "json/JsonNode.h"
#include "mapObjects/CGDwelling.h"
#include "mapObjects/CGTownInstance.h"
#include "mapObjects/CGHeroInstance.h"
@ -20,8 +21,13 @@
VCMI_LIB_NAMESPACE_BEGIN
PlayerState::PlayerState()
: color(-1), human(false), cheated(false), enteredWinningCheatCode(false),
enteredLosingCheatCode(false), status(EPlayerStatus::INGAME)
: color(-1)
, playerLocalSettings(std::make_unique<JsonNode>())
, human(false)
, cheated(false)
, enteredWinningCheatCode(false)
, enteredLosingCheatCode(false)
, status(EPlayerStatus::INGAME)
{
setNodeType(PLAYER);
}

View File

@ -16,7 +16,6 @@
#include "bonuses/CBonusSystemNode.h"
#include "ResourceSet.h"
#include "TurnTimerInfo.h"
#include "ConstTransitivePtr.h"
VCMI_LIB_NAMESPACE_BEGIN
@ -66,6 +65,7 @@ public:
std::vector<QuestInfo> quests; //store info about all received quests
std::vector<Bonus> battleBonuses; //additional bonuses to be added during battle with neutrals
std::map<uint32_t, std::map<ArtifactPosition, ArtifactID>> costumesArtifacts;
std::unique_ptr<JsonNode> playerLocalSettings; // Json with client-defined data, such as order of heroes or current hero paths. Not used by client/lib
bool cheated;
bool enteredWinningCheatCode, enteredLosingCheatCode; //if true, this player has entered cheat codes for loss / victory
@ -116,6 +116,9 @@ public:
h & status;
h & turnTimer;
if (h.version >= Handler::Version::LOCAL_PLAYER_STATE_DATA)
h & *playerLocalSettings;
if (h.version >= Handler::Version::PLAYER_STATE_OWNED_OBJECTS)
{
h & ownedObjects;

View File

@ -13,6 +13,7 @@
#include "PacksForClientBattle.h"
#include "PacksForServer.h"
#include "PacksForLobby.h"
#include "SaveLocalState.h"
#include "SetRewardableConfiguration.h"
#include "SetStackEffect.h"
@ -177,6 +178,7 @@ public:
virtual void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) {}
virtual void visitLobbyShowMessage(LobbyShowMessage & pack) {}
virtual void visitLobbyPvPAction(LobbyPvPAction & pack) {}
virtual void visitSaveLocalState(SaveLocalState & pack) {}
};
VCMI_LIB_NAMESPACE_END

View File

@ -12,6 +12,7 @@
#include "PacksForClient.h"
#include "PacksForClientBattle.h"
#include "PacksForServer.h"
#include "SaveLocalState.h"
#include "SetRewardableConfiguration.h"
#include "StackLocation.h"
#include "PacksForLobby.h"
@ -92,6 +93,12 @@ bool CLobbyPackToServer::isForServer() const
return true;
}
void SaveLocalState::visitTyped(ICPackVisitor & visitor)
{
visitor.visitSaveLocalState(*this);
}
void PackageApplied::visitTyped(ICPackVisitor & visitor)
{
visitor.visitPackageApplied(*this);

View File

@ -0,0 +1,27 @@
/*
* SaveLocalState.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "NetPacksBase.h"
#include "../json/JsonNode.h"
struct DLL_LINKAGE SaveLocalState : public CPackForServer
{
JsonNode data;
void visitTyped(ICPackVisitor & visitor) override;
template <typename Handler> void serialize(Handler & h)
{
h & static_cast<CPackForServer &>(*this);
h & data;
}
};

View File

@ -62,6 +62,7 @@ enum class ESerializationVersion : int32_t
REWARDABLE_BANKS, // 863 - team state contains list of scouted objects, coast visitable rewardable objects
REGION_LABEL, // 864 - labels for campaign regions
SPELL_RESEARCH, // 865 - spell research
LOCAL_PLAYER_STATE_DATA, // 866 - player state contains arbitrary client-side data
CURRENT = SPELL_RESEARCH
CURRENT = LOCAL_PLAYER_STATE_DATA
};

View File

@ -34,6 +34,7 @@
#include "../networkPacks/PacksForClientBattle.h"
#include "../networkPacks/PacksForLobby.h"
#include "../networkPacks/PacksForServer.h"
#include "../networkPacks/SaveLocalState.h"
#include "../networkPacks/SetRewardableConfiguration.h"
#include "../networkPacks/SetStackEffect.h"
@ -290,6 +291,7 @@ void registerTypes(Serializer &s)
s.template registerType<LobbySetExtraOptions>(240);
s.template registerType<SpellResearch>(241);
s.template registerType<SetResearchedSpells>(242);
s.template registerType<SaveLocalState>(243);
}
VCMI_LIB_NAMESPACE_END

View File

@ -4013,6 +4013,9 @@ bool CGameHandler::isBlockedByQueries(const CPackForServer *pack, PlayerColor pl
if (dynamic_cast<const PlayerMessage *>(pack) != nullptr)
return false;
if (dynamic_cast<const SaveLocalState *>(pack) != nullptr)
return false;
auto query = queries->topQuery(player);
if (query && query->blocksPack(pack))
{

View File

@ -19,6 +19,7 @@
#include "queries/MapQueries.h"
#include "../lib/IGameCallback.h"
#include "../lib/CPlayerState.h"
#include "../lib/mapObjects/CGTownInstance.h"
#include "../lib/mapObjects/CGHeroInstance.h"
#include "../lib/gameState/CGameState.h"
@ -389,6 +390,13 @@ void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack)
result = gh.queryReply(pack.qid, pack.reply, pack.player);
}
void ApplyGhNetPackVisitor::visitSaveLocalState(SaveLocalState & pack)
{
gh.throwIfWrongPlayer(&pack);
*gh.gameState()->getPlayerState(pack.player)->playerLocalSettings = pack.data;
result = true;
}
void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack)
{
gh.throwIfWrongPlayer(&pack);

View File

@ -62,4 +62,5 @@ public:
void visitDigWithHero(DigWithHero & pack) override;
void visitCastAdvSpell(CastAdvSpell & pack) override;
void visitPlayerMessage(PlayerMessage & pack) override;
void visitSaveLocalState(SaveLocalState & pack) override;
};