diff --git a/.gitignore b/.gitignore index 21b784dd2..28ba6ab2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,15 @@ /client/vcmiclient /server/vcmiserver +/launcher/.lupdate /launcher/vcmilauncher +/mapeditor/.lupdate /launcher/vcmilauncher_automoc.cpp /conan-* build/ .cache/* out/ +/.qt *.dll *.exe *.depend @@ -42,6 +45,7 @@ VCMI_VS11.opensdf .DS_Store CMakeUserPresets.json compile_commands.json +fuzzylite.pc # Visual Studio *.suo @@ -62,5 +66,8 @@ compile_commands.json /deps .vs/ +# Visual Studio Code +/.vscode/ + # CLion .idea/ diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 9846a6efe..f0a29682b 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -843,7 +843,7 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb } } - if(!reachable && vstd::contains(reachabilityMap[hex], unit)) + if(!reachable && std::count(reachabilityMap[hex].begin(), reachabilityMap[hex].end(), unit) > 1) { blockingScore += ratio * (enemyUnit ? BLOCKING_OWN_ATTACK_PENALTY : BLOCKING_OWN_MOVE_PENALTY); } diff --git a/AI/EmptyAI/CEmptyAI.h b/AI/EmptyAI/CEmptyAI.h index 2954564be..eb2935f83 100644 --- a/AI/EmptyAI/CEmptyAI.h +++ b/AI/EmptyAI/CEmptyAI.h @@ -19,8 +19,8 @@ class CEmptyAI : public CGlobalAI std::shared_ptr cb; public: - virtual void saveGame(BinarySerializer & h) override; - virtual void loadGame(BinaryDeserializer & h) override; + void saveGame(BinarySerializer & h) override; + void loadGame(BinaryDeserializer & h) override; void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; void yourTurn(QueryID queryID) override; diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.h b/AI/Nullkiller/Behaviors/BuildingBehavior.h index 813a37619..46d12dd13 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.h +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.h @@ -27,7 +27,7 @@ namespace Goals virtual Goals::TGoalVec decompose() const override; virtual std::string toString() const override; - virtual bool operator==(const BuildingBehavior & other) const override + bool operator==(const BuildingBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.h b/AI/Nullkiller/Behaviors/BuyArmyBehavior.h index a6bf681c7..2d6ea9f0d 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.h +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.h @@ -26,7 +26,7 @@ namespace Goals virtual Goals::TGoalVec decompose() const override; virtual std::string toString() const override; - virtual bool operator==(const BuyArmyBehavior & other) const override + bool operator==(const BuyArmyBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h index ce075c858..1e29fb03c 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h @@ -65,7 +65,7 @@ namespace Goals return *this; } - virtual bool operator==(const CaptureObjectsBehavior & other) const override; + bool operator==(const CaptureObjectsBehavior & other) const override; static Goals::TGoalVec getVisitGoals(const std::vector & paths, const CGObjectInstance * objToVisit = nullptr); diff --git a/AI/Nullkiller/Behaviors/ClusterBehavior.h b/AI/Nullkiller/Behaviors/ClusterBehavior.h index e5a52f73c..22c88f2ce 100644 --- a/AI/Nullkiller/Behaviors/ClusterBehavior.h +++ b/AI/Nullkiller/Behaviors/ClusterBehavior.h @@ -28,10 +28,10 @@ namespace Goals { } - virtual TGoalVec decompose() const override; + TGoalVec decompose() const override; virtual std::string toString() const override; - virtual bool operator==(const ClusterBehavior & other) const override + bool operator==(const ClusterBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.h b/AI/Nullkiller/Behaviors/DefenceBehavior.h index fab4745f5..1eb4ef682 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.h +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.h @@ -32,7 +32,7 @@ namespace Goals virtual Goals::TGoalVec decompose() const override; virtual std::string toString() const override; - virtual bool operator==(const DefenceBehavior & other) const override + bool operator==(const DefenceBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.h b/AI/Nullkiller/Behaviors/GatherArmyBehavior.h index b933575ba..1ae2f3481 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.h +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.h @@ -25,10 +25,10 @@ namespace Goals { } - virtual TGoalVec decompose() const override; + TGoalVec decompose() const override; virtual std::string toString() const override; - virtual bool operator==(const GatherArmyBehavior & other) const override + bool operator==(const GatherArmyBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.h b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.h index 4d1102b4d..103f25d50 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.h +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.h @@ -25,10 +25,10 @@ namespace Goals { } - virtual TGoalVec decompose() const override; + TGoalVec decompose() const override; virtual std::string toString() const override; - virtual bool operator==(const RecruitHeroBehavior & other) const override + bool operator==(const RecruitHeroBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/StartupBehavior.h b/AI/Nullkiller/Behaviors/StartupBehavior.h index 12cfb33c2..8bfcc5734 100644 --- a/AI/Nullkiller/Behaviors/StartupBehavior.h +++ b/AI/Nullkiller/Behaviors/StartupBehavior.h @@ -25,10 +25,10 @@ namespace Goals { } - virtual TGoalVec decompose() const override; + TGoalVec decompose() const override; virtual std::string toString() const override; - virtual bool operator==(const StartupBehavior & other) const override + bool operator==(const StartupBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/StayAtTownBehavior.h b/AI/Nullkiller/Behaviors/StayAtTownBehavior.h index 260cf136a..dd8363968 100644 --- a/AI/Nullkiller/Behaviors/StayAtTownBehavior.h +++ b/AI/Nullkiller/Behaviors/StayAtTownBehavior.h @@ -25,10 +25,10 @@ namespace Goals { } - virtual TGoalVec decompose() const override; + TGoalVec decompose() const override; virtual std::string toString() const override; - virtual bool operator==(const StayAtTownBehavior & other) const override + bool operator==(const StayAtTownBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Goals/AdventureSpellCast.h b/AI/Nullkiller/Goals/AdventureSpellCast.h index 23cfc4a97..de403905a 100644 --- a/AI/Nullkiller/Goals/AdventureSpellCast.h +++ b/AI/Nullkiller/Goals/AdventureSpellCast.h @@ -35,7 +35,7 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; - virtual bool operator==(const AdventureSpellCast & other) const override; + bool operator==(const AdventureSpellCast & other) const override; }; } diff --git a/AI/Nullkiller/Goals/Build.h b/AI/Nullkiller/Goals/Build.h index 8ff8d3228..cb69dc9b1 100644 --- a/AI/Nullkiller/Goals/Build.h +++ b/AI/Nullkiller/Goals/Build.h @@ -32,7 +32,7 @@ namespace Goals TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const Build & other) const override + bool operator==(const Build & other) const override { return true; } diff --git a/AI/Nullkiller/Goals/BuildBoat.h b/AI/Nullkiller/Goals/BuildBoat.h index 31383d256..1a2432081 100644 --- a/AI/Nullkiller/Goals/BuildBoat.h +++ b/AI/Nullkiller/Goals/BuildBoat.h @@ -29,7 +29,7 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; - virtual bool operator==(const BuildBoat & other) const override; + bool operator==(const BuildBoat & other) const override; }; } diff --git a/AI/Nullkiller/Goals/BuildThis.h b/AI/Nullkiller/Goals/BuildThis.h index f4bacbc5e..0b85108b6 100644 --- a/AI/Nullkiller/Goals/BuildThis.h +++ b/AI/Nullkiller/Goals/BuildThis.h @@ -39,7 +39,7 @@ namespace Goals } BuildThis(BuildingID Bid, const CGTownInstance * tid); - virtual bool operator==(const BuildThis & other) const override; + bool operator==(const BuildThis & other) const override; virtual std::string toString() const override; void accept(AIGateway * ai) override; }; diff --git a/AI/Nullkiller/Goals/BuyArmy.h b/AI/Nullkiller/Goals/BuyArmy.h index d4fdc6166..bd1080fd9 100644 --- a/AI/Nullkiller/Goals/BuyArmy.h +++ b/AI/Nullkiller/Goals/BuyArmy.h @@ -36,11 +36,11 @@ namespace Goals priority = 3;//TODO: evaluate? } - virtual bool operator==(const BuyArmy & other) const override; + bool operator==(const BuyArmy & other) const override; virtual std::string toString() const override; - virtual void accept(AIGateway * ai) override; + void accept(AIGateway * ai) override; }; } diff --git a/AI/Nullkiller/Goals/CGoal.h b/AI/Nullkiller/Goals/CGoal.h index 255535d59..8aa5b90ad 100644 --- a/AI/Nullkiller/Goals/CGoal.h +++ b/AI/Nullkiller/Goals/CGoal.h @@ -44,7 +44,7 @@ namespace Goals //h & value & resID & objid & aid & tile & hero & town & bid; } - virtual bool operator==(const AbstractGoal & g) const override + bool operator==(const AbstractGoal & g) const override { if(goalType != g.goalType) return false; @@ -54,7 +54,7 @@ namespace Goals virtual bool operator==(const T & other) const = 0; - virtual TGoalVec decompose() const override + TGoalVec decompose() const override { TSubgoal single = decomposeSingle(); @@ -90,11 +90,11 @@ namespace Goals return *((T *)this); } - virtual bool isElementar() const override { return true; } + bool isElementar() const override { return true; } virtual HeroPtr getHero() const override { return AbstractGoal::hero; } - virtual int getHeroExchangeCount() const override { return 0; } + int getHeroExchangeCount() const override { return 0; } }; } diff --git a/AI/Nullkiller/Goals/CaptureObject.h b/AI/Nullkiller/Goals/CaptureObject.h index e78993638..5ef3a8d51 100644 --- a/AI/Nullkiller/Goals/CaptureObject.h +++ b/AI/Nullkiller/Goals/CaptureObject.h @@ -34,11 +34,11 @@ namespace Goals name = obj->getObjectName(); } - virtual bool operator==(const CaptureObject & other) const override; + bool operator==(const CaptureObject & other) const override; virtual Goals::TGoalVec decompose() const override; virtual std::string toString() const override; - virtual bool hasHash() const override { return true; } - virtual uint64_t getHash() const override; + bool hasHash() const override { return true; } + uint64_t getHash() const override; }; } diff --git a/AI/Nullkiller/Goals/CompleteQuest.h b/AI/Nullkiller/Goals/CompleteQuest.h index 59ed0dc31..e7e0f3386 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.h +++ b/AI/Nullkiller/Goals/CompleteQuest.h @@ -31,10 +31,10 @@ namespace Goals virtual Goals::TGoalVec decompose() const override; virtual std::string toString() const override; - virtual bool hasHash() const override { return true; } - virtual uint64_t getHash() const override; + bool hasHash() const override { return true; } + uint64_t getHash() const override; - virtual bool operator==(const CompleteQuest & other) const override; + bool operator==(const CompleteQuest & other) const override; private: TGoalVec tryCompleteQuest() const; diff --git a/AI/Nullkiller/Goals/Composition.h b/AI/Nullkiller/Goals/Composition.h index ecb4a1ab9..72bf4e9bb 100644 --- a/AI/Nullkiller/Goals/Composition.h +++ b/AI/Nullkiller/Goals/Composition.h @@ -26,15 +26,15 @@ namespace Goals { } - virtual bool operator==(const Composition & other) const override; + bool operator==(const Composition & other) const override; virtual std::string toString() const override; void accept(AIGateway * ai) override; Composition & addNext(const AbstractGoal & goal); Composition & addNext(TSubgoal goal); Composition & addNextSequence(const TGoalVec & taskSequence); - virtual TGoalVec decompose() const override; - virtual bool isElementar() const override; - virtual int getHeroExchangeCount() const override; + TGoalVec decompose() const override; + bool isElementar() const override; + int getHeroExchangeCount() const override; }; } diff --git a/AI/Nullkiller/Goals/DigAtTile.h b/AI/Nullkiller/Goals/DigAtTile.h index b50708c9b..c99d61d0b 100644 --- a/AI/Nullkiller/Goals/DigAtTile.h +++ b/AI/Nullkiller/Goals/DigAtTile.h @@ -33,7 +33,7 @@ namespace Goals { tile = Tile; } - virtual bool operator==(const DigAtTile & other) const override; + bool operator==(const DigAtTile & other) const override; private: //TSubgoal decomposeSingle() const override; diff --git a/AI/Nullkiller/Goals/DismissHero.h b/AI/Nullkiller/Goals/DismissHero.h index a6d180300..98bfdf1fa 100644 --- a/AI/Nullkiller/Goals/DismissHero.h +++ b/AI/Nullkiller/Goals/DismissHero.h @@ -26,7 +26,7 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; - virtual bool operator==(const DismissHero & other) const override; + bool operator==(const DismissHero & other) const override; }; } diff --git a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h index ca25f280b..8a86c364e 100644 --- a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h +++ b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h @@ -31,7 +31,7 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; - virtual bool operator==(const ExchangeSwapTownHeroes & other) const override; + bool operator==(const ExchangeSwapTownHeroes & other) const override; const CGHeroInstance * getGarrisonHero() const { return garrisonHero; } HeroLockedReason getLockingReason() const { return lockingReason; } diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.h b/AI/Nullkiller/Goals/ExecuteHeroChain.h index b22e18499..a0e0ff39e 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.h +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.h @@ -30,10 +30,10 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; - virtual bool operator==(const ExecuteHeroChain & other) const override; + bool operator==(const ExecuteHeroChain & other) const override; const AIPath & getPath() const { return chainPath; } - virtual int getHeroExchangeCount() const override { return chainPath.exchangeCount; } + int getHeroExchangeCount() const override { return chainPath.exchangeCount; } private: bool moveHeroToTile(const CGHeroInstance * hero, const int3 & tile); diff --git a/AI/Nullkiller/Goals/GatherArmy.h b/AI/Nullkiller/Goals/GatherArmy.h index 9dbb81ec3..804a159c3 100644 --- a/AI/Nullkiller/Goals/GatherArmy.h +++ b/AI/Nullkiller/Goals/GatherArmy.h @@ -36,7 +36,7 @@ namespace Goals TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; - virtual bool operator==(const GatherArmy & other) const override; + bool operator==(const GatherArmy & other) const override; }; } diff --git a/AI/Nullkiller/Goals/Invalid.h b/AI/Nullkiller/Goals/Invalid.h index 6c2d7afa4..9c02e3091 100644 --- a/AI/Nullkiller/Goals/Invalid.h +++ b/AI/Nullkiller/Goals/Invalid.h @@ -32,7 +32,7 @@ namespace Goals return TGoalVec(); } - virtual bool operator==(const Invalid & other) const override + bool operator==(const Invalid & other) const override { return true; } @@ -42,7 +42,7 @@ namespace Goals return "Invalid"; } - virtual void accept(AIGateway * ai) override + void accept(AIGateway * ai) override { throw cannotFulfillGoalException("Can not fulfill Invalid goal!"); } diff --git a/AI/Nullkiller/Goals/RecruitHero.h b/AI/Nullkiller/Goals/RecruitHero.h index 78c6b0867..243f1f6d2 100644 --- a/AI/Nullkiller/Goals/RecruitHero.h +++ b/AI/Nullkiller/Goals/RecruitHero.h @@ -38,7 +38,7 @@ namespace Goals { } - virtual bool operator==(const RecruitHero & other) const override + bool operator==(const RecruitHero & other) const override { return true; } diff --git a/AI/Nullkiller/Goals/SaveResources.h b/AI/Nullkiller/Goals/SaveResources.h index eb0fe3b5b..09438b488 100644 --- a/AI/Nullkiller/Goals/SaveResources.h +++ b/AI/Nullkiller/Goals/SaveResources.h @@ -28,7 +28,7 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; - virtual bool operator==(const SaveResources & other) const override; + bool operator==(const SaveResources & other) const override; }; } diff --git a/AI/Nullkiller/Goals/StayAtTown.h b/AI/Nullkiller/Goals/StayAtTown.h index 880881386..28aa607a2 100644 --- a/AI/Nullkiller/Goals/StayAtTown.h +++ b/AI/Nullkiller/Goals/StayAtTown.h @@ -26,7 +26,7 @@ namespace Goals public: StayAtTown(const CGTownInstance * town, AIPath & path); - virtual bool operator==(const StayAtTown & other) const override; + bool operator==(const StayAtTown & other) const override; virtual std::string toString() const override; void accept(AIGateway * ai) override; float getMovementWasted() const { return movementWasted; } diff --git a/AI/Nullkiller/Goals/Trade.h b/AI/Nullkiller/Goals/Trade.h index 4360c6700..f29ed8c8f 100644 --- a/AI/Nullkiller/Goals/Trade.h +++ b/AI/Nullkiller/Goals/Trade.h @@ -34,7 +34,7 @@ namespace Goals value = val; objid = Objid; } - virtual bool operator==(const Trade & other) const override; + bool operator==(const Trade & other) const override; }; } diff --git a/AI/Nullkiller/Markers/ArmyUpgrade.h b/AI/Nullkiller/Markers/ArmyUpgrade.h index 1af41a5bf..6f67a3ba4 100644 --- a/AI/Nullkiller/Markers/ArmyUpgrade.h +++ b/AI/Nullkiller/Markers/ArmyUpgrade.h @@ -29,7 +29,7 @@ namespace Goals ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade); ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade); - virtual bool operator==(const ArmyUpgrade & other) const override; + bool operator==(const ArmyUpgrade & other) const override; virtual std::string toString() const override; uint64_t getUpgradeValue() const { return upgradeValue; } diff --git a/AI/Nullkiller/Markers/DefendTown.h b/AI/Nullkiller/Markers/DefendTown.h index 34b8b3427..30ec60d9d 100644 --- a/AI/Nullkiller/Markers/DefendTown.h +++ b/AI/Nullkiller/Markers/DefendTown.h @@ -30,7 +30,7 @@ namespace Goals DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack = false); DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender); - virtual bool operator==(const DefendTown & other) const override; + bool operator==(const DefendTown & other) const override; virtual std::string toString() const override; const HitMapInfo & getTreat() const { return treat; } diff --git a/AI/Nullkiller/Markers/HeroExchange.h b/AI/Nullkiller/Markers/HeroExchange.h index 532d62666..fb6be8af9 100644 --- a/AI/Nullkiller/Markers/HeroExchange.h +++ b/AI/Nullkiller/Markers/HeroExchange.h @@ -28,7 +28,7 @@ namespace Goals sethero(targetHero); } - virtual bool operator==(const HeroExchange & other) const override; + bool operator==(const HeroExchange & other) const override; virtual std::string toString() const override; uint64_t getReinforcementArmyStrength() const; diff --git a/AI/Nullkiller/Markers/UnlockCluster.h b/AI/Nullkiller/Markers/UnlockCluster.h index 3318fc7bc..db0ce7658 100644 --- a/AI/Nullkiller/Markers/UnlockCluster.h +++ b/AI/Nullkiller/Markers/UnlockCluster.h @@ -36,7 +36,7 @@ namespace Goals sethero(pathToCenter.targetHero); } - virtual bool operator==(const UnlockCluster & other) const override; + bool operator==(const UnlockCluster & other) const override; virtual std::string toString() const override; std::shared_ptr getCluster() const { return cluster; } const AIPath & getPathToCenter() { return pathToCenter; } diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 912ac3fe2..a8458939c 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -200,7 +200,7 @@ public: const PathfinderConfig * pathfinderConfig, const CPathfinderHelper * pathfinderHelper) override; - virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; + void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; void commit( AIPathNode * destination, diff --git a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.h b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.h index 3d229a812..2f242e767 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.h +++ b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.h @@ -34,7 +34,7 @@ namespace AIPathfinding ~AIPathfinderConfig(); - virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override; + CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override; }; } diff --git a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h index b7d596d26..64f48aa0e 100644 --- a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h +++ b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h @@ -29,7 +29,7 @@ namespace AIPathfinding public: AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero, DayFlags flagsToAdd = DayFlags::NONE); - virtual void execute(const CGHeroInstance * hero) const override; + void execute(const CGHeroInstance * hero) const override; virtual void applyOnDestination( const CGHeroInstance * hero, @@ -38,7 +38,7 @@ namespace AIPathfinding AIPathNode * dstMode, const AIPathNode * srcNode) const override; - virtual bool canAct(const AIPathNode * source) const override; + bool canAct(const AIPathNode * source) const override; virtual std::string toString() const override; }; diff --git a/AI/Nullkiller/Pathfinding/Actions/BattleAction.h b/AI/Nullkiller/Pathfinding/Actions/BattleAction.h index 595ba23cf..d5acb9209 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BattleAction.h +++ b/AI/Nullkiller/Pathfinding/Actions/BattleAction.h @@ -28,7 +28,7 @@ namespace AIPathfinding { } - virtual void execute(const CGHeroInstance * hero) const override; + void execute(const CGHeroInstance * hero) const override; virtual std::string toString() const override; }; diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h index 5e6ca50d4..cb8d32506 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h +++ b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h @@ -25,7 +25,7 @@ namespace AIPathfinding class SummonBoatAction : public VirtualBoatAction { public: - virtual void execute(const CGHeroInstance * hero) const override; + void execute(const CGHeroInstance * hero) const override; virtual void applyOnDestination( const CGHeroInstance * hero, @@ -34,9 +34,9 @@ namespace AIPathfinding AIPathNode * dstMode, const AIPathNode * srcNode) const override; - virtual bool canAct(const AIPathNode * source) const override; + bool canAct(const AIPathNode * source) const override; - virtual const ChainActor * getActor(const ChainActor * sourceActor) const override; + const ChainActor * getActor(const ChainActor * sourceActor) const override; virtual std::string toString() const override; @@ -56,17 +56,17 @@ namespace AIPathfinding { } - virtual bool canAct(const AIPathNode * source) const override; + bool canAct(const AIPathNode * source) const override; - virtual void execute(const CGHeroInstance * hero) const override; + void execute(const CGHeroInstance * hero) const override; virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const override; - virtual const ChainActor * getActor(const ChainActor * sourceActor) const override; + const ChainActor * getActor(const ChainActor * sourceActor) const override; virtual std::string toString() const override; - virtual const CGObjectInstance * targetObject() const override; + const CGObjectInstance * targetObject() const override; }; } diff --git a/AI/Nullkiller/Pathfinding/Actions/QuestAction.h b/AI/Nullkiller/Pathfinding/Actions/QuestAction.h index 15f16a860..467932adc 100644 --- a/AI/Nullkiller/Pathfinding/Actions/QuestAction.h +++ b/AI/Nullkiller/Pathfinding/Actions/QuestAction.h @@ -28,11 +28,11 @@ namespace AIPathfinding { } - virtual bool canAct(const AIPathNode * node) const override; + bool canAct(const AIPathNode * node) const override; virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const override; - virtual void execute(const CGHeroInstance * hero) const override; + void execute(const CGHeroInstance * hero) const override; virtual std::string toString() const override; }; diff --git a/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h b/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h index cfb43cc70..32795972f 100644 --- a/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h +++ b/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h @@ -29,7 +29,7 @@ namespace AIPathfinding { } - virtual void execute(const CGHeroInstance * hero) const override; + void execute(const CGHeroInstance * hero) const override; virtual std::string toString() const override; }; diff --git a/AI/Nullkiller/Pathfinding/Actors.h b/AI/Nullkiller/Pathfinding/Actors.h index 505f7995b..c723c867f 100644 --- a/AI/Nullkiller/Pathfinding/Actors.h +++ b/AI/Nullkiller/Pathfinding/Actors.h @@ -28,7 +28,7 @@ class HeroExchangeArmy : public CArmedInstance public: TResources armyCost; bool requireBuyArmy; - virtual bool needsLastStack() const override; + bool needsLastStack() const override; std::shared_ptr getActorAction() const; HeroExchangeArmy(): CArmedInstance(nullptr, true), requireBuyArmy(false) {} @@ -126,7 +126,7 @@ public: HeroActor(const ChainActor * carrier, const ChainActor * other, const HeroExchangeArmy * army, const Nullkiller * ai); protected: - virtual ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const override; + ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const override; }; class ObjectActor : public ChainActor diff --git a/AI/VCAI/Goals/AdventureSpellCast.h b/AI/VCAI/Goals/AdventureSpellCast.h index 28ded9dd1..c8e38004e 100644 --- a/AI/VCAI/Goals/AdventureSpellCast.h +++ b/AI/VCAI/Goals/AdventureSpellCast.h @@ -39,6 +39,6 @@ namespace Goals void accept(VCAI * ai) override; std::string name() const override; std::string completeMessage() const override; - virtual bool operator==(const AdventureSpellCast & other) const override; + bool operator==(const AdventureSpellCast & other) const override; }; } diff --git a/AI/VCAI/Goals/Build.h b/AI/VCAI/Goals/Build.h index c6d972d9f..dc552e146 100644 --- a/AI/VCAI/Goals/Build.h +++ b/AI/VCAI/Goals/Build.h @@ -29,7 +29,7 @@ namespace Goals TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const Build & other) const override + bool operator==(const Build & other) const override { return true; } diff --git a/AI/VCAI/Goals/BuildBoat.h b/AI/VCAI/Goals/BuildBoat.h index 367fa6ea9..8a707d7e9 100644 --- a/AI/VCAI/Goals/BuildBoat.h +++ b/AI/VCAI/Goals/BuildBoat.h @@ -32,6 +32,6 @@ namespace Goals void accept(VCAI * ai) override; std::string name() const override; std::string completeMessage() const override; - virtual bool operator==(const BuildBoat & other) const override; + bool operator==(const BuildBoat & other) const override; }; } diff --git a/AI/VCAI/Goals/BuildThis.h b/AI/VCAI/Goals/BuildThis.h index 9cc86abb8..8abdac811 100644 --- a/AI/VCAI/Goals/BuildThis.h +++ b/AI/VCAI/Goals/BuildThis.h @@ -43,6 +43,6 @@ namespace Goals } TSubgoal whatToDoToAchieve() override; //bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const BuildThis & other) const override; + bool operator==(const BuildThis & other) const override; }; } diff --git a/AI/VCAI/Goals/BuyArmy.h b/AI/VCAI/Goals/BuyArmy.h index 55dc97ec4..fbd8126a9 100644 --- a/AI/VCAI/Goals/BuyArmy.h +++ b/AI/VCAI/Goals/BuyArmy.h @@ -36,6 +36,6 @@ namespace Goals TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; - virtual bool operator==(const BuyArmy & other) const override; + bool operator==(const BuyArmy & other) const override; }; } diff --git a/AI/VCAI/Goals/CGoal.h b/AI/VCAI/Goals/CGoal.h index bef480c55..f2ff5749c 100644 --- a/AI/VCAI/Goals/CGoal.h +++ b/AI/VCAI/Goals/CGoal.h @@ -76,7 +76,7 @@ namespace Goals //h & value & resID & objid & aid & tile & hero & town & bid; } - virtual bool operator==(const AbstractGoal & g) const override + bool operator==(const AbstractGoal & g) const override { if(goalType != g.goalType) return false; diff --git a/AI/VCAI/Goals/ClearWayTo.h b/AI/VCAI/Goals/ClearWayTo.h index 432f1ad79..da812db44 100644 --- a/AI/VCAI/Goals/ClearWayTo.h +++ b/AI/VCAI/Goals/ClearWayTo.h @@ -40,6 +40,6 @@ namespace Goals TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const ClearWayTo & other) const override; + bool operator==(const ClearWayTo & other) const override; }; } diff --git a/AI/VCAI/Goals/CollectRes.h b/AI/VCAI/Goals/CollectRes.h index 70d76d8cc..8a55cae89 100644 --- a/AI/VCAI/Goals/CollectRes.h +++ b/AI/VCAI/Goals/CollectRes.h @@ -35,6 +35,6 @@ namespace Goals TSubgoal whatToDoToAchieve() override; TSubgoal whatToDoToTrade(); bool fulfillsMe(TSubgoal goal) override; //TODO: Trade - virtual bool operator==(const CollectRes & other) const override; + bool operator==(const CollectRes & other) const override; }; } diff --git a/AI/VCAI/Goals/CompleteQuest.h b/AI/VCAI/Goals/CompleteQuest.h index 16335d278..f50cbfe8f 100644 --- a/AI/VCAI/Goals/CompleteQuest.h +++ b/AI/VCAI/Goals/CompleteQuest.h @@ -30,7 +30,7 @@ namespace Goals TSubgoal whatToDoToAchieve() override; std::string name() const override; std::string completeMessage() const override; - virtual bool operator==(const CompleteQuest & other) const override; + bool operator==(const CompleteQuest & other) const override; private: TGoalVec tryCompleteQuest() const; diff --git a/AI/VCAI/Goals/Conquer.h b/AI/VCAI/Goals/Conquer.h index 295930347..ec274d15b 100644 --- a/AI/VCAI/Goals/Conquer.h +++ b/AI/VCAI/Goals/Conquer.h @@ -27,6 +27,6 @@ namespace Goals } TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; - virtual bool operator==(const Conquer & other) const override; + bool operator==(const Conquer & other) const override; }; } diff --git a/AI/VCAI/Goals/DigAtTile.h b/AI/VCAI/Goals/DigAtTile.h index 963306539..9e426d241 100644 --- a/AI/VCAI/Goals/DigAtTile.h +++ b/AI/VCAI/Goals/DigAtTile.h @@ -36,6 +36,6 @@ namespace Goals return TGoalVec(); } TSubgoal whatToDoToAchieve() override; - virtual bool operator==(const DigAtTile & other) const override; + bool operator==(const DigAtTile & other) const override; }; } diff --git a/AI/VCAI/Goals/Explore.h b/AI/VCAI/Goals/Explore.h index 54e55dcae..a96de7f29 100644 --- a/AI/VCAI/Goals/Explore.h +++ b/AI/VCAI/Goals/Explore.h @@ -46,7 +46,7 @@ namespace Goals TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const Explore & other) const override; + bool operator==(const Explore & other) const override; private: TSubgoal exploreNearestNeighbour(HeroPtr h) const; diff --git a/AI/VCAI/Goals/FindObj.h b/AI/VCAI/Goals/FindObj.h index fad944dec..c1cc74fe7 100644 --- a/AI/VCAI/Goals/FindObj.h +++ b/AI/VCAI/Goals/FindObj.h @@ -42,6 +42,6 @@ namespace Goals } TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const FindObj & other) const override; + bool operator==(const FindObj & other) const override; }; } \ No newline at end of file diff --git a/AI/VCAI/Goals/GatherArmy.h b/AI/VCAI/Goals/GatherArmy.h index 97108df76..213f50dc5 100644 --- a/AI/VCAI/Goals/GatherArmy.h +++ b/AI/VCAI/Goals/GatherArmy.h @@ -33,6 +33,6 @@ namespace Goals TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; - virtual bool operator==(const GatherArmy & other) const override; + bool operator==(const GatherArmy & other) const override; }; } diff --git a/AI/VCAI/Goals/GatherTroops.h b/AI/VCAI/Goals/GatherTroops.h index ff93ca186..5d5553f93 100644 --- a/AI/VCAI/Goals/GatherTroops.h +++ b/AI/VCAI/Goals/GatherTroops.h @@ -35,7 +35,7 @@ namespace Goals TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const GatherTroops & other) const override; + bool operator==(const GatherTroops & other) const override; private: int getCreaturesCount(const CArmedInstance * army); diff --git a/AI/VCAI/Goals/GetArtOfType.h b/AI/VCAI/Goals/GetArtOfType.h index ba31c2a56..4a69cdb82 100644 --- a/AI/VCAI/Goals/GetArtOfType.h +++ b/AI/VCAI/Goals/GetArtOfType.h @@ -35,6 +35,6 @@ namespace Goals return TGoalVec(); } TSubgoal whatToDoToAchieve() override; - virtual bool operator==(const GetArtOfType & other) const override; + bool operator==(const GetArtOfType & other) const override; }; } diff --git a/AI/VCAI/Goals/Invalid.h b/AI/VCAI/Goals/Invalid.h index cd77ac929..044acfd6c 100644 --- a/AI/VCAI/Goals/Invalid.h +++ b/AI/VCAI/Goals/Invalid.h @@ -33,7 +33,7 @@ namespace Goals return iAmElementar(); } - virtual bool operator==(const Invalid & other) const override + bool operator==(const Invalid & other) const override { return true; } diff --git a/AI/VCAI/Goals/RecruitHero.h b/AI/VCAI/Goals/RecruitHero.h index c381eb2ad..253102a2f 100644 --- a/AI/VCAI/Goals/RecruitHero.h +++ b/AI/VCAI/Goals/RecruitHero.h @@ -33,7 +33,7 @@ namespace Goals TSubgoal whatToDoToAchieve() override; - virtual bool operator==(const RecruitHero & other) const override + bool operator==(const RecruitHero & other) const override { return true; } diff --git a/AI/VCAI/Goals/Trade.h b/AI/VCAI/Goals/Trade.h index c8f64416f..f5bced17f 100644 --- a/AI/VCAI/Goals/Trade.h +++ b/AI/VCAI/Goals/Trade.h @@ -33,6 +33,6 @@ namespace Goals priority = 3; //trading is instant, but picking resources is free } TSubgoal whatToDoToAchieve() override; - virtual bool operator==(const Trade & other) const override; + bool operator==(const Trade & other) const override; }; } diff --git a/AI/VCAI/Goals/VisitHero.h b/AI/VCAI/Goals/VisitHero.h index c209a0abf..bfaa6ffd7 100644 --- a/AI/VCAI/Goals/VisitHero.h +++ b/AI/VCAI/Goals/VisitHero.h @@ -37,6 +37,6 @@ namespace Goals TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; std::string completeMessage() const override; - virtual bool operator==(const VisitHero & other) const override; + bool operator==(const VisitHero & other) const override; }; } diff --git a/AI/VCAI/Goals/VisitObj.h b/AI/VCAI/Goals/VisitObj.h index 666a9acf0..b92bb4378 100644 --- a/AI/VCAI/Goals/VisitObj.h +++ b/AI/VCAI/Goals/VisitObj.h @@ -27,6 +27,6 @@ namespace Goals TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; std::string completeMessage() const override; - virtual bool operator==(const VisitObj & other) const override; + bool operator==(const VisitObj & other) const override; }; } diff --git a/AI/VCAI/Goals/VisitTile.h b/AI/VCAI/Goals/VisitTile.h index 334b44c32..50faebd4e 100644 --- a/AI/VCAI/Goals/VisitTile.h +++ b/AI/VCAI/Goals/VisitTile.h @@ -32,6 +32,6 @@ namespace Goals TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; - virtual bool operator==(const VisitTile & other) const override; + bool operator==(const VisitTile & other) const override; }; } diff --git a/AI/VCAI/Goals/Win.h b/AI/VCAI/Goals/Win.h index 07dfcdee5..e4edaac7b 100644 --- a/AI/VCAI/Goals/Win.h +++ b/AI/VCAI/Goals/Win.h @@ -31,7 +31,7 @@ namespace Goals } TSubgoal whatToDoToAchieve() override; - virtual bool operator==(const Win & other) const override + bool operator==(const Win & other) const override { return true; } diff --git a/AI/VCAI/Pathfinding/AINodeStorage.h b/AI/VCAI/Pathfinding/AINodeStorage.h index 51e32fdcc..4b8118191 100644 --- a/AI/VCAI/Pathfinding/AINodeStorage.h +++ b/AI/VCAI/Pathfinding/AINodeStorage.h @@ -99,7 +99,7 @@ public: const PathfinderConfig * pathfinderConfig, const CPathfinderHelper * pathfinderHelper) override; - virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; + void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; const AIPathNode * getAINode(const CGPathNode * node) const; void updateAINode(CGPathNode * node, std::function updater); diff --git a/AI/VCAI/Pathfinding/AIPathfinderConfig.h b/AI/VCAI/Pathfinding/AIPathfinderConfig.h index 81ec9722c..076579e12 100644 --- a/AI/VCAI/Pathfinding/AIPathfinderConfig.h +++ b/AI/VCAI/Pathfinding/AIPathfinderConfig.h @@ -30,6 +30,6 @@ namespace AIPathfinding ~AIPathfinderConfig(); - virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override; + CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override; }; } diff --git a/CI/linux-qt6/before_install.sh b/CI/linux-qt6/before_install.sh index 689101138..df82f65ac 100644 --- a/CI/linux-qt6/before_install.sh +++ b/CI/linux-qt6/before_install.sh @@ -7,4 +7,4 @@ sudo apt-get install libboost-all-dev \ libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \ ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \ -libminizip-dev libfuzzylite-dev # Optional dependencies +libminizip-dev libfuzzylite-dev libsqlite3-dev # Optional dependencies diff --git a/CI/linux/before_install.sh b/CI/linux/before_install.sh index e08075d7d..345634134 100644 --- a/CI/linux/before_install.sh +++ b/CI/linux/before_install.sh @@ -7,4 +7,4 @@ sudo apt-get install libboost-all-dev \ libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ qtbase5-dev \ ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \ -libminizip-dev libfuzzylite-dev qttools5-dev # Optional dependencies +libminizip-dev libfuzzylite-dev qttools5-dev libsqlite3-dev # Optional dependencies diff --git a/CMakeLists.txt b/CMakeLists.txt index e39a446fd..eb27ea4f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ if(NOT CMAKE_BUILD_TYPE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release RelWithDebInfo) endif() +set(buildLobby OFF) set(singleProcess OFF) set(staticAI OFF) if(ANDROID) @@ -89,6 +90,14 @@ if(NOT APPLE_IOS AND NOT ANDROID) option(ENABLE_MONOLITHIC_INSTALL "Install everything in single directory on Linux and Mac" OFF) endif() +if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + set(buildLobby ON) +endif() + +if(NOT APPLE_IOS AND NOT ANDROID) + option(ENABLE_LOBBY "Enable compilation of lobby server" ${buildLobby}) +endif() + option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" OFF) if(ENABLE_CCACHE) find_program(CCACHE ccache REQUIRED) @@ -475,6 +484,10 @@ if(TARGET SDL2_ttf::SDL2_ttf) add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf) endif() +if(ENABLE_LOBBY) + find_package(SQLite3 REQUIRED) +endif() + if(ENABLE_LAUNCHER OR ENABLE_EDITOR) # Widgets finds its own dependencies (QtGui and QtCore). find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Network) @@ -622,6 +635,9 @@ endif() if(ENABLE_EDITOR) add_subdirectory(mapeditor) endif() +if(ENABLE_LOBBY) + add_subdirectory(lobby) +endif() add_subdirectory(client) add_subdirectory(server) if(ENABLE_TEST) diff --git a/Mods/vcmi/config/vcmi/chinese.json b/Mods/vcmi/config/vcmi/chinese.json index dca5f2bdd..d84af049f 100644 --- a/Mods/vcmi/config/vcmi/chinese.json +++ b/Mods/vcmi/config/vcmi/chinese.json @@ -20,6 +20,7 @@ "vcmi.adventureMap.playerAttacked" : "玩家遭受攻击: %s", "vcmi.adventureMap.moveCostDetails" : "移动点数 - 花费: %TURNS 轮 + %POINTS 点移动力, 剩余移动力: %REMAINING", "vcmi.adventureMap.moveCostDetailsNoTurns" : "移动点数 - 花费: %POINTS 点移动力, 剩余移动力: %REMAINING", + "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "抱歉,重放对手行动功能目前暂未实现!", "vcmi.capitalColors.0" : "红色", "vcmi.capitalColors.1" : "蓝色", @@ -70,7 +71,9 @@ "vcmi.lobby.mapPreview" : "地图预览", "vcmi.lobby.noPreview" : "无地上部分", "vcmi.lobby.noUnderground" : "无地下部分", + "vcmi.lobby.sortDate" : "以修改时间排序地图", + "vcmi.client.errors.invalidMap" : "{非法地图或战役}\n\n启动游戏失败,选择的地图或者战役,无效或被污染。原因:\n%s", "vcmi.client.errors.missingCampaigns" : "{找不到数据文件}\n\n没有找到战役数据文件!你可能使用了不完整或损坏的英雄无敌3数据文件,请重新安装数据文件。", "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", "vcmi.server.errors.modsToEnable" : "{需要启用的mod列表}", @@ -81,9 +84,9 @@ "vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!", "vcmi.settingsMainWindow.generalTab.hover" : "常规", - "vcmi.settingsMainWindow.generalTab.help" : "切换到“常规”选项卡 - 设置游戏客户端呈现", + "vcmi.settingsMainWindow.generalTab.help" : "切换到“常规”选项卡 - 配置客户端常规内容", "vcmi.settingsMainWindow.battleTab.hover" : "战斗", - "vcmi.settingsMainWindow.battleTab.help" : "切换到“战斗”选项卡 - 这些设置允许配置战斗界面和相关内容", + "vcmi.settingsMainWindow.battleTab.help" : "切换到“战斗”选项卡 - 配置游戏战斗界面内容", "vcmi.settingsMainWindow.adventureTab.hover" : "冒险地图", "vcmi.settingsMainWindow.adventureTab.help" : "切换到“冒险地图”选项卡 - 冒险地图即玩家能操作英雄移动的界面", @@ -93,43 +96,43 @@ "vcmi.systemOptions.townsGroup" : "城镇画面", "vcmi.systemOptions.fullscreenBorderless.hover" : "全屏 (无边框)", - "vcmi.systemOptions.fullscreenBorderless.help" : "{全屏}\n\n选中时,VCMI将以无边框全屏模式运行。在这种模式下,游戏会使用和桌面一致的分辨率而非设置的分辨率。 ", + "vcmi.systemOptions.fullscreenBorderless.help" : "{全屏}\n\n选中时,VCMI将以无边框全屏模式运行。该模式下,游戏会始终和桌面分辨率保持一致,无视设置的分辨率。 ", "vcmi.systemOptions.fullscreenExclusive.hover" : "全屏 (独占)", - "vcmi.systemOptions.fullscreenExclusive.help" : "{全屏}\n\n选中时,VCMI将以独占全屏模式运行。在这种模式下,游戏会将显示器分辨率改变为设置值。", - "vcmi.systemOptions.resolutionButton.hover" : "分辨率", - "vcmi.systemOptions.resolutionButton.help" : "{分辨率选择}\n\n改变游戏内的分辨率,更改后需要重启游戏使其生效。", - "vcmi.systemOptions.resolutionMenu.hover" : "分辨率选择", + "vcmi.systemOptions.fullscreenExclusive.help" : "{全屏}\n\n选中时,VCMI将以独占全屏模式运行。该模式下,游戏会将显示器分辨率改变为设置值。", + "vcmi.systemOptions.resolutionButton.hover" : "分辨率: %wx%h", + "vcmi.systemOptions.resolutionButton.help" : "{分辨率选择}\n\n改变游戏内的分辨率。", + "vcmi.systemOptions.resolutionMenu.hover" : "选择分辨率", "vcmi.systemOptions.resolutionMenu.help" : "修改游戏运行时的分辨率。", - "vcmi.systemOptions.scalingButton.hover" : "界面大小: %p%", - "vcmi.systemOptions.scalingButton.help" : "{界面大小}\n\n改变界面的大小", - "vcmi.systemOptions.scalingMenu.hover" : "选择界面大小", - "vcmi.systemOptions.scalingMenu.help" : "改变游戏界面大小。", - "vcmi.systemOptions.longTouchButton.hover" : "触控间距: %d 毫秒", // Translation note: "ms" = "milliseconds" - "vcmi.systemOptions.longTouchButton.help" : "{触控间距}\n\n使用触摸屏时,触摸屏幕指定持续时间(以毫秒为单位)后将出现弹出窗口。", - "vcmi.systemOptions.longTouchMenu.hover" : "选择触控间距", - "vcmi.systemOptions.longTouchMenu.help" : "改变触控间距。", + "vcmi.systemOptions.scalingButton.hover" : "界面缩放: %p%", + "vcmi.systemOptions.scalingButton.help" : "{界面缩放}\n\n改变用户界面的缩放比例。", + "vcmi.systemOptions.scalingMenu.hover" : "选择界面缩放", + "vcmi.systemOptions.scalingMenu.help" : "改变游戏内界面缩放。", + "vcmi.systemOptions.longTouchButton.hover" : "长触延迟: %d 毫秒", // Translation note: "ms" = "milliseconds" + "vcmi.systemOptions.longTouchButton.help" : "{长触延迟}\n\n使用触摸屏时,长触屏幕一定时间后出现弹出窗口(单位:毫秒)。", + "vcmi.systemOptions.longTouchMenu.hover" : "选择长触延迟", + "vcmi.systemOptions.longTouchMenu.help" : "改变长触延迟。", "vcmi.systemOptions.longTouchMenu.entry" : "%d 毫秒", "vcmi.systemOptions.framerateButton.hover" : "显示FPS", - "vcmi.systemOptions.framerateButton.help" : "{显示FPS}\n\n打开/关闭在游戏窗口角落的FPS指示器。", + "vcmi.systemOptions.framerateButton.help" : "{显示FPS}\n\n切换在游戏窗口角落显FPS指示器。", "vcmi.systemOptions.hapticFeedbackButton.hover" : "触觉反馈", "vcmi.systemOptions.hapticFeedbackButton.help" : "{触觉反馈}\n\n切换触摸输入的触觉反馈。", "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "界面增强", - "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{界面增强}\n\n显示所有界面增强内容,如大背包和魔法书等。", - "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "增大魔法书界面", - "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{增大魔法书界面}\n\n可以在魔法书单页中显示更多的魔法,从而获得更好的视觉效果。", - "vcmi.systemOptions.audioMuteFocus.hover" : "切换窗口时静音", - "vcmi.systemOptions.audioMuteFocus.help" : "{切换窗口时静音}\n\n快速切换窗口时将静音,在工作时,切换游戏窗口不会有声音。", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{界面增强}\n\n显示所有界面优化增强,如背包按钮等。关闭该项以贴近经典模式。", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "扩展魔法书", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{扩展魔法书}\n\n启用更大的魔法书界面,每页展示更多魔法,但魔法书翻页特效在该模式下无法呈现。", + "vcmi.systemOptions.audioMuteFocus.hover" : "失去焦点时静音", + "vcmi.systemOptions.audioMuteFocus.help" : "{失去焦点时静音}\n\n当窗口失去焦点时静音,游戏内消息提示和新回合提示除外。", "vcmi.adventureOptions.infoBarPick.hover" : "在信息面板显示消息", - "vcmi.adventureOptions.infoBarPick.help" : "{在信息面板显示消息}\n\n来自访问地图物件的信息将显示在信息面板,而不是弹出窗口。", + "vcmi.adventureOptions.infoBarPick.help" : "{在信息面板显示消息}\n\n尽可能将来自访问地图物件的信息将显示在信息面板,而不是弹出窗口。", "vcmi.adventureOptions.numericQuantities.hover" : "生物数量显示", "vcmi.adventureOptions.numericQuantities.help" : "{生物数量显示}\n\n以数字 A-B 格式显示不准确的敌方生物数量。", - "vcmi.adventureOptions.forceMovementInfo.hover" : "在状态栏中显示移动力", - "vcmi.adventureOptions.forceMovementInfo.help" : "{在状态栏中显示移动力}\n\n不需要按ALT就可以显示移动力。", + "vcmi.adventureOptions.forceMovementInfo.hover" : "总是显示移动花费", + "vcmi.adventureOptions.forceMovementInfo.help" : "{总是显示移动花费}\n\n总是在状态栏中显示行动力花费(否则仅在按下ALT时显示)。", "vcmi.adventureOptions.showGrid.hover" : "显示网格", "vcmi.adventureOptions.showGrid.help" : "{显示网格}\n\n显示网格覆盖层,高亮冒险地图物件的边沿。", - "vcmi.adventureOptions.borderScroll.hover" : "滚动边界", - "vcmi.adventureOptions.borderScroll.help" : "{滚动边界}\n\n当光标靠近窗口边缘时滚动冒险地图。 可以通过按住 CTRL 键来禁用。", + "vcmi.adventureOptions.borderScroll.hover" : "边缘滚动", + "vcmi.adventureOptions.borderScroll.help" : "{边缘滚动}\n\n当光标靠近窗口边缘时滚动冒险地图。 可以通过按住 CTRL 键来禁用。", "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "信息面板生物管理", "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{信息面板生物管理}\n\n允许在信息面板中重新排列生物,而不是在默认组件之间循环。", "vcmi.adventureOptions.leftButtonDrag.hover" : "左键拖动地图", @@ -137,13 +140,15 @@ "vcmi.adventureOptions.smoothDragging.hover" : "平滑地图拖动", "vcmi.adventureOptions.smoothDragging.help" : "{平滑地图拖动}\n\n启用后,地图拖动会产生柔和的羽化效果。", "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "关闭淡入淡出特效", - "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{关闭淡入淡出特效}\n\n启用后,跳过物体淡出或类似特效(资源收集,登船等)。设置此项能在渲染开销重时能够加快UI的响应,尤其是在PvP对战中。当移动速度被设置为最大时忽略此项设置。", + "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{关闭淡入淡出特效}\n\n启用后,跳过物体淡出或类似特效(资源收集,登船等)。设置此项能在渲染开销重时能够加快UI的响应,尤其是在PvP对战中。当移动速度被设置为最大时忽略此项设置。", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", "vcmi.adventureOptions.mapScrollSpeed1.help": "将地图卷动速度设置为非常慢", "vcmi.adventureOptions.mapScrollSpeed5.help": "将地图卷动速度设置为非常快", "vcmi.adventureOptions.mapScrollSpeed6.help": "将地图卷动速度设置为即刻。", + "vcmi.adventureOptions.hideBackground.hover" : "隐藏背景", + "vcmi.adventureOptions.hideBackground.help" : "{隐藏背景}\n\n隐藏冒险地图背景,以显示贴图代替。", "vcmi.battleOptions.queueSizeLabel.hover": "回合顺序指示器", "vcmi.battleOptions.queueSizeNoneButton.hover": "关闭", @@ -168,6 +173,8 @@ "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{显示英雄统计数据窗口}\n\n永久切换并显示主要统计数据和法术点的英雄统计数据窗口。", "vcmi.battleOptions.skipBattleIntroMusic.hover": "跳过战斗开始音乐", "vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过战斗开始音乐}\n\n战斗开始音乐播放期间,你也能够进行操作。", + "vcmi.battleOptions.endWithAutocombat.hover": "结束战斗", + "vcmi.battleOptions.endWithAutocombat.help": "{结束战斗}\n\n以自动战斗立即结束剩余战斗过程", "vcmi.adventureMap.revisitObject.hover" : "重新访问", "vcmi.adventureMap.revisitObject.help" : "{重新访问}\n\n让当前英雄重新访问地图建筑或城镇。", @@ -183,17 +190,22 @@ "vcmi.battleWindow.damageEstimation.damage.1" : "%d 伤害", "vcmi.battleWindow.damageEstimation.kills" : "%d 将被消灭", "vcmi.battleWindow.damageEstimation.kills.1" : "%d 将被消灭", + "vcmi.battleWindow.killed" : "已消灭", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s 死于精准射击", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s 死于精准射击", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s 死于精准射击", + "vcmi.battleWindow.endWithAutocombat" : "您确定想以自动战斗立即结束吗?", "vcmi.battleResultsWindow.applyResultsLabel" : "接受战斗结果", "vcmi.tutorialWindow.title" : "触摸屏介绍", - "vcmi.tutorialWindow.decription.RightClick" : "触摸并按住要右键单击的元素。 触摸可用区域以关闭。", - "vcmi.tutorialWindow.decription.MapPanning" : "用一根手指触摸并拖动来移动地图。", - "vcmi.tutorialWindow.decription.MapZooming" : "用两根手指捏合可更改地图缩放比例。", + "vcmi.tutorialWindow.decription.RightClick" : "长按要右键单击的元素。 触摸其他区域以关闭。", + "vcmi.tutorialWindow.decription.MapPanning" : "单指拖拽以移动地图。", + "vcmi.tutorialWindow.decription.MapZooming" : "两指开合更改地图缩放比例。", "vcmi.tutorialWindow.decription.RadialWheel" : "滑动可打开径向轮以执行各种操作,例如生物/英雄管理和城镇排序。", "vcmi.tutorialWindow.decription.BattleDirection" : "要从特定方向攻击,请向要进行攻击的方向滑动。", - "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "如果手指距离足够远,可以取消攻击方向手势。", - "vcmi.tutorialWindow.decription.AbortSpell" : "触摸并按住可取消魔法。", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "将长触状态的手指拉远足够距离,可以取消攻击方向手势。", + "vcmi.tutorialWindow.decription.AbortSpell" : "长触以取消魔法。", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "显示可招募生物", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{显示可招募生物}\n\n在城镇摘要(城镇屏幕的左下角)中显示可招募的生物数量,而不是增长。", @@ -212,7 +224,7 @@ "vcmi.townHall.greetingDefence" : "在%s中稍待片刻,富有战斗经验的战士会教你防御技巧(防御力+1)。", "vcmi.townHall.hasNotProduced" : "本周%s并没有产生什么资源。", "vcmi.townHall.hasProduced" : "本周%s产生了%d个%s。", - "vcmi.townHall.greetingCustomBonus" : "当你的英雄访问%s 时,这个神奇的建筑使你的英雄 +%d %s%s。", + "vcmi.townHall.greetingCustomBonus" : "%s 给予英雄 +%d %s%s。", "vcmi.townHall.greetingCustomUntil" : "直到下一场战斗。", "vcmi.townHall.greetingInTownMagicWell" : "%s使你的魔法值恢复到最大值。", @@ -225,13 +237,15 @@ "vcmi.heroWindow.openBackpack.hover" : "开启宝物背包界面", "vcmi.heroWindow.openBackpack.help" : "用更大的界面显示所有获得的宝物", + "vcmi.tavernWindow.inviteHero" : "邀请英雄", + "vcmi.commanderWindow.artifactMessage" : "你要把这个宝物还给英雄吗?", "vcmi.creatureWindow.showBonuses.hover" : "属性视图", "vcmi.creatureWindow.showBonuses.help" : "显示指挥官的所有属性增益", "vcmi.creatureWindow.showSkills.hover" : "技能视图", "vcmi.creatureWindow.showSkills.help" : "显示指挥官的所有学习的技能", - "vcmi.creatureWindow.returnArtifact.hover" : "交换宝物", + "vcmi.creatureWindow.returnArtifact.hover" : "返还宝物", "vcmi.creatureWindow.returnArtifact.help" : "点击这个按钮将宝物反还到英雄的背包里", "vcmi.questLog.hideComplete.hover" : "隐藏完成任务", @@ -321,6 +335,7 @@ "vcmi.map.victoryCondition.collectArtifacts.message" : "获得所有三件宝物", "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "祝贺你!你取得了天使联盟且消灭了所有敌人,取得了胜利!", "vcmi.map.victoryCondition.angelicAlliance.message" : "击败所有敌人并取得天使联盟", + "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "功亏一篑,你已失去了天使联盟的一个组件。彻底的失败。", // few strings from WoG used by vcmi "vcmi.stackExperience.description" : "» 经 验 获 得 明 细 «\n\n生物类型 ................... : %s\n经验等级 ................. : %s (%i)\n经验点数 ............... : %i\n下一个等级所需经验 .. : %i\n每次战斗最大获得经验 ... : %i%% (%i)\n获得经验的生物数量 .... : %i\n最大招募数量\n不会丢失经验升级 .... : %i\n经验倍数 ........... : %.2f\n升级倍数 .............. : %.2f\n10级后经验值 ........ : %i\n最大招募数量下\n 升级到10级所需经验数量: %i", @@ -374,8 +389,10 @@ "core.bonus.ENCHANTER.description": "每回合群体施放${subtype.spell}", "core.bonus.ENCHANTED.name": "法术加持", "core.bonus.ENCHANTED.description": "永久处于${subtype.spell}影响", + "core.bonus.ENEMY_ATTACK_REDUCTION.name": "忽略攻击 (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description": "被攻击时,进攻方${val}%的攻击力将被无视。", "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "忽略防御 (${val}%)", - "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "当攻击时,目标生物${val}%的防御力将被无视。", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "发动攻击时,防御方${val}%的防御力将被无视。", "core.bonus.FIRE_IMMUNITY.name": "火系免疫", "core.bonus.FIRE_IMMUNITY.description": "免疫所有火系魔法。", "core.bonus.FIRE_SHIELD.name": "烈火神盾 (${val}%)", @@ -386,7 +403,9 @@ "core.bonus.FEAR.description": "使得敌方一只部队恐惧", "core.bonus.FEARLESS.name": "无惧", "core.bonus.FEARLESS.description": "免疫恐惧特质", - "core.bonus.FLYING.name": "飞行兵种", + "core.bonus.FEROCITY.name": "凶猛追击", + "core.bonus.FEROCITY.description": "杀死任意生物后额外攻击${val}次", + "core.bonus.FLYING.name": "飞行能力", "core.bonus.FLYING.description": "以飞行的方式移动(无视障碍)", "core.bonus.FREE_SHOOTING.name": "近身射击", "core.bonus.FREE_SHOOTING.description": "能在近战范围内进行射击", @@ -396,50 +415,52 @@ "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "减少从远程和近战中遭受的物理伤害", "core.bonus.HATE.name": "${subtype.creature}的死敌", "core.bonus.HATE.description": "对${subtype.creature}造成额外${val}%伤害", - "core.bonus.HEALER.name": "治疗", + "core.bonus.HEALER.name": "治疗者", "core.bonus.HEALER.description": "可以治疗友军单位", "core.bonus.HP_REGENERATION.name": "再生", "core.bonus.HP_REGENERATION.description": "每回合恢复${val}点生命值", - "core.bonus.JOUSTING.name": "冲锋", + "core.bonus.JOUSTING.name": "勇士冲锋", "core.bonus.JOUSTING.description": "每移动一格 +${val}%伤害", - "core.bonus.KING.name": "顶级怪物", + "core.bonus.KING.name": "王牌", "core.bonus.KING.description": "受${val}级或更高级屠戮成性影响", "core.bonus.LEVEL_SPELL_IMMUNITY.name": "免疫1-${val}级魔法", "core.bonus.LEVEL_SPELL_IMMUNITY.description": "免疫1-${val}级的魔法", - "core.bonus.LIMITED_SHOOTING_RANGE.name" : "受限射击距离", - "core.bonus.LIMITED_SHOOTING_RANGE.description" : "无法以${val}格外的单位为射击目标", + "core.bonus.LIMITED_SHOOTING_RANGE.name": "射程限制", + "core.bonus.LIMITED_SHOOTING_RANGE.description": "无法瞄准${val}格以外的单位", "core.bonus.LIFE_DRAIN.name": "吸取生命 (${val}%)", "core.bonus.LIFE_DRAIN.description": "吸取${val}%伤害回复自身", - "core.bonus.MANA_CHANNELING.name": "魔法虹吸${val}%", + "core.bonus.MANA_CHANNELING.name": "法力虹吸${val}%", "core.bonus.MANA_CHANNELING.description": "使你的英雄有${val}%几率获得敌人施法的魔法值", - "core.bonus.MANA_DRAIN.name": "吸取魔力", + "core.bonus.MANA_DRAIN.name": "吸取法力", "core.bonus.MANA_DRAIN.description": "每回合吸取${val}魔法值", "core.bonus.MAGIC_MIRROR.name": "魔法神镜 (${val}%)", "core.bonus.MAGIC_MIRROR.description": "${val}%几率将进攻性魔法导向一个敌人单位", "core.bonus.MAGIC_RESISTANCE.name": "魔法抵抗 (${val}%)", "core.bonus.MAGIC_RESISTANCE.description": "${val}%几率抵抗敌人的魔法", - "core.bonus.MIND_IMMUNITY.name": "免疫心智", - "core.bonus.MIND_IMMUNITY.description": "不受心智魔法的影响", - "core.bonus.NO_DISTANCE_PENALTY.name": "无视距离惩罚", - "core.bonus.NO_DISTANCE_PENALTY.description": "任意距离均造成全额伤害", + "core.bonus.MIND_IMMUNITY.name": "免疫心智魔法", + "core.bonus.MIND_IMMUNITY.description": "不受心智相关的魔法影响", + "core.bonus.NO_DISTANCE_PENALTY.name": "无视射程惩罚", + "core.bonus.NO_DISTANCE_PENALTY.description": "任意射程造成全额伤害", "core.bonus.NO_MELEE_PENALTY.name": "无近战惩罚", "core.bonus.NO_MELEE_PENALTY.description": "该生物没有近战伤害惩罚", "core.bonus.NO_MORALE.name": "无士气", "core.bonus.NO_MORALE.description": "生物不受士气影响", - "core.bonus.NO_WALL_PENALTY.name": "无城墙影响", - "core.bonus.NO_WALL_PENALTY.description": "攻城战中不被城墙阻挡造成全额伤害", + "core.bonus.NO_WALL_PENALTY.name": "无城墙惩罚", + "core.bonus.NO_WALL_PENALTY.description": "攻城战中无视城墙阻挡,造成全额伤害", "core.bonus.NON_LIVING.name": "无生命", "core.bonus.NON_LIVING.description": "免疫大多数的效果", "core.bonus.RANDOM_SPELLCASTER.name": "随机施法", "core.bonus.RANDOM_SPELLCASTER.description": "可以施放随机魔法", "core.bonus.RANGED_RETALIATION.name": "远程反击", "core.bonus.RANGED_RETALIATION.description": "可以对远程攻击进行反击", - "core.bonus.RECEPTIVE.name": "接受", + "core.bonus.RECEPTIVE.name": "接纳", "core.bonus.RECEPTIVE.description": "不会免疫有益魔法", "core.bonus.REBIRTH.name": "复生 (${val}%)", "core.bonus.REBIRTH.description": "当整支部队死亡后${val}%会复活", "core.bonus.RETURN_AFTER_STRIKE.name": "攻击后返回", "core.bonus.RETURN_AFTER_STRIKE.description": "近战攻击后回到初始位置", + "core.bonus.REVENGE.name": "复仇", + "core.bonus.REVENGE.description": "根据攻击者在战斗中失去的生命值造成额外伤害", "core.bonus.SHOOTER.name": "远程攻击", "core.bonus.SHOOTER.description": "生物可以射击", "core.bonus.SHOOTS_ALL_ADJACENT.name": "范围远程攻击", @@ -471,7 +492,7 @@ "core.bonus.TRANSMUTATION.name": "变形术", "core.bonus.TRANSMUTATION.description": "${val}%机会将被攻击单位变成其他生物", "core.bonus.UNDEAD.name": "不死生物", - "core.bonus.UNDEAD.description": "该生物属于丧尸", + "core.bonus.UNDEAD.description": "该生物属于不死生物", "core.bonus.UNLIMITED_RETALIATIONS.name": "无限反击", "core.bonus.UNLIMITED_RETALIATIONS.description": "每回合可以无限反击敌人", "core.bonus.WATER_IMMUNITY.name": "水系免疫", diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index c5035299d..699290e5c 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -63,7 +63,6 @@ "vcmi.mainMenu.serverClosing" : "Closing...", "vcmi.mainMenu.hostTCP" : "Host TCP/IP game", "vcmi.mainMenu.joinTCP" : "Join TCP/IP game", - "vcmi.mainMenu.playerName" : "Player", "vcmi.lobby.filepath" : "File path", "vcmi.lobby.creationDate" : "Creation date", @@ -72,13 +71,34 @@ "vcmi.lobby.noPreview" : "no preview", "vcmi.lobby.noUnderground" : "no underground", "vcmi.lobby.sortDate" : "Sorts maps by change date", + + "vcmi.lobby.login.title" : "VCMI Lobby", + "vcmi.lobby.login.username" : "Username:", + "vcmi.lobby.login.connecting" : "Connecting...", + "vcmi.lobby.login.error" : "Connection error: %s", + "vcmi.lobby.login.create" : "New Account", + "vcmi.lobby.login.login" : "Login", + + "vcmi.lobby.room.create" : "Create Room", + "vcmi.lobby.room.players.limit" : "Players Limit", + "vcmi.lobby.room.public" : "Public", + "vcmi.lobby.room.private" : "Private", + "vcmi.lobby.room.description.public" : "Any player can join public room.", + "vcmi.lobby.room.description.private" : "Only invited players can join private room.", + "vcmi.lobby.room.description.new" : "To start the game, select a scenario or set up a random map.", + "vcmi.lobby.room.description.load" : "To start the game, use one of your saved games.", + "vcmi.lobby.room.description.limit" : "Up to %d players can enter your room, including you.", + "vcmi.lobby.room.new" : "New Game", + "vcmi.lobby.room.load" : "Load Game", + "vcmi.lobby.room.type" : "Room Type", + "vcmi.lobby.room.mode" : "Game Mode", "vcmi.client.errors.invalidMap" : "{Invalid map or campaign}\n\nFailed to start game! Selected map or campaign might be invalid or corrupted. Reason:\n%s", "vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.", + "vcmi.server.errors.disconnected" : "{Network Error}\n\nConnection to game server has been lost!", "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", "vcmi.server.errors.modsToEnable" : "{Following mods are required}", "vcmi.server.errors.modsToDisable" : "{Following mods must be disabled}", - "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!", diff --git a/Mods/vcmi/config/vcmi/spanish.json b/Mods/vcmi/config/vcmi/spanish.json index ab1f02eb7..12291a2cc 100644 --- a/Mods/vcmi/config/vcmi/spanish.json +++ b/Mods/vcmi/config/vcmi/spanish.json @@ -20,6 +20,7 @@ "vcmi.adventureMap.playerAttacked" : "El jugador ha sido atacado: %s", "vcmi.adventureMap.moveCostDetails" : "Puntos de movimiento - Coste: %TURNS turnos + %POINTS puntos, Puntos restantes: %REMAINING", "vcmi.adventureMap.moveCostDetailsNoTurns" : "Puntos de movimiento - Coste: %POINTS puntos, Puntos restantes: %REMAINING", + "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Disculpe, la repetición del turno del oponente aún no está implementada.", "vcmi.capitalColors.0" : "Rojo", "vcmi.capitalColors.1" : "Azul", @@ -70,7 +71,9 @@ "vcmi.lobby.mapPreview" : "Vista previa del mapa", "vcmi.lobby.noPreview" : "sin vista previa", "vcmi.lobby.noUnderground" : "sin subterráneo", + "vcmi.lobby.sortDate" : "Ordena los mapas por la fecha de modificación", + "vcmi.client.errors.invalidMap" : "{Mapa o Campaña invalido}\n\n¡No se pudo iniciar el juego! El mapa o la campaña seleccionados pueden no ser válidos o estar dañados. Motivo:\n%s", "vcmi.client.errors.missingCampaigns" : "{Archivos de datos faltantes}\n\n¡No se encontraron los archivos de datos de las campañas! Quizás estés utilizando archivos de datos incompletos o dañados de Heroes 3. Por favor, reinstala los datos del juego.", "vcmi.server.errors.existingProcess" : "Otro servidor VCMI está en ejecución. Por favor, termínalo antes de comenzar un nuevo juego.", "vcmi.server.errors.modsToEnable" : "{Se requieren los siguientes mods}", @@ -144,6 +147,8 @@ "vcmi.adventureOptions.mapScrollSpeed1.help": "Establece la velocidad de desplazamiento del mapa como muy lenta", "vcmi.adventureOptions.mapScrollSpeed5.help": "Establece la velocidad de desplazamiento del mapa como muy rápida", "vcmi.adventureOptions.mapScrollSpeed6.help": "Establece la velocidad de desplazamiento del mapa como instantánea.", + "vcmi.adventureOptions.hideBackground.hover" : "Ocultar fondo", + "vcmi.adventureOptions.hideBackground.help" : "{Ocultar fondo}\n\nOculta el mapa de aventuras en el fondo y muestra una textura en su lugar..", "vcmi.battleOptions.queueSizeLabel.hover": "Mostrar orden de turno de criaturas", "vcmi.battleOptions.queueSizeNoneButton.hover": "APAGADO", @@ -168,6 +173,8 @@ "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Mostrar ventanas de estadísticas de héroes}\n\nAlternar permanentemente las ventanas de estadísticas de héroes que muestran estadísticas primarias y puntos de hechizo.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Omitir música de introducción", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Omitir música de introducción}\n\nPermitir acciones durante la música de introducción que se reproduce al comienzo de cada batalla.", + "vcmi.battleOptions.endWithAutocombat.hover": "Finaliza la batalla", + "vcmi.battleOptions.endWithAutocombat.help": "{Finaliza la batalla}\n\nAutomatiza la batalla y la finaliza al instante", "vcmi.adventureMap.revisitObject.hover" : "Revisitar objeto", "vcmi.adventureMap.revisitObject.help" : "{Revisitar objeto}\n\nSi un héroe se encuentra actualmente en un objeto del mapa, puede volver a visitar la ubicación.", @@ -183,6 +190,11 @@ "vcmi.battleWindow.damageEstimation.damage.1" : "%d daño", "vcmi.battleWindow.damageEstimation.kills" : "%d perecerán", "vcmi.battleWindow.damageEstimation.kills.1" : "%d perecerá", + "vcmi.battleWindow.killed" : "Eliminados", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s han sido eliminados por disparos certeros", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s ha sido eliminado por un disparo certero", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s han sido eliminados por disparos certeros", + "vcmi.battleWindow.endWithAutocombat" : "¿Quieres finalizar la batalla con combate automatizado?", "vcmi.battleResultsWindow.applyResultsLabel" : "Aplicar resultado de la batalla", @@ -225,6 +237,8 @@ "vcmi.heroWindow.openBackpack.hover" : "Abrir ventana de mochila de artefactos", "vcmi.heroWindow.openBackpack.help" : "Abre la ventana que facilita la gestión de la mochila de artefactos.", + "vcmi.tavernWindow.inviteHero" : "Invitar heroe", + "vcmi.commanderWindow.artifactMessage" : "¿Quieres devolver este artefacto al héroe?", "vcmi.creatureWindow.showBonuses.hover" : "Cambiar a vista de bonificaciones", @@ -321,6 +335,7 @@ "vcmi.map.victoryCondition.collectArtifacts.message" : "Adquirir tres artefactos", "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "¡Felicidades! Todos tus enemigos han sido derrotados y tienes la Alianza Angelical. ¡La victoria es tuya!", "vcmi.map.victoryCondition.angelicAlliance.message" : "Derrota a todos los enemigos y crea la Alianza Angelical", + "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Por desgracia, has perdido parte de la Alianza Angélica. Todo se ha perdido.", // few strings from WoG used by vcmi "vcmi.stackExperience.description" : "» D e t a l l e s d e E x p e r i e n c i a d e l G r u p o «\n\nTipo de Criatura ................ : %s\nRango de Experiencia ............ : %s (%i)\nPuntos de Experiencia ............ : %i\nPuntos de Experiencia para el\nSiguiente Rango ............... : %i\nExperiencia Máxima por Batalla .. : %i%% (%i)\nNúmero de Criaturas en el grupo .. : %i\nMáximo de Nuevos Reclutas sin\nPerder el Rango Actual ......... : %i\nMultiplicador de Experiencia .... : %.2f\nMultiplicador de Actualización .. : %.2f\nExperiencia después del Rango 10 : %i\nMáximo de Nuevos Reclutas para\nMantener el Rango 10 si\nEstá en la Experiencia Máxima : %i", @@ -374,6 +389,8 @@ "core.bonus.ENCHANTER.description": "Puede lanzar ${subtype.spell} masivo cada turno", "core.bonus.ENCHANTED.name": "Encantado", "core.bonus.ENCHANTED.description": "Afectado por el hechizo permanente ${subtype.spell}", + "core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignorar ataque (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description": "Al ser atacado, ${val}% del daño del atacante es ignorado", "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignorar Defensa (${val}%)", "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignora una parte de la defensa al atacar", "core.bonus.FIRE_IMMUNITY.name": "Inmunidad al Fuego", @@ -386,6 +403,8 @@ "core.bonus.FEAR.description": "Causa miedo a un grupo enemigo", "core.bonus.FEARLESS.name": "Inmune al miedo", "core.bonus.FEARLESS.description": "Inmune a la habilidad de miedo", + "core.bonus.FEROCITY.name": "Ferocidad", + "core.bonus.FEROCITY.description": "Ataca ${val} veces adicionales en caso de eliminar a alguien", "core.bonus.FLYING.name": "Volar", "core.bonus.FLYING.description": "Puede volar (ignora obstáculos)", "core.bonus.FREE_SHOOTING.name": "Disparo cercano", @@ -416,8 +435,8 @@ "core.bonus.MANA_DRAIN.description": "Drena ${val} de maná cada turno", "core.bonus.MAGIC_MIRROR.name": "Espejo mágico (${val}%)", "core.bonus.MAGIC_MIRROR.description": "Tiene una probabilidad del ${val}% de redirigir un hechizo ofensivo al enemigo", - "core.bonus.MAGIC_RESISTANCE.name": "Resistencia mágica (${MR}%)", - "core.bonus.MAGIC_RESISTANCE.description": "Tiene una probabilidad del ${MR}% de resistir el hechizo del enemigo", + "core.bonus.MAGIC_RESISTANCE.name": "Resistencia mágica (${val}%)", + "core.bonus.MAGIC_RESISTANCE.description": "Tiene una probabilidad del ${val}% de resistir el hechizo del enemigo", "core.bonus.MIND_IMMUNITY.name": "Inmunidad a hechizos mentales", "core.bonus.MIND_IMMUNITY.description": "Inmune a hechizos de tipo mental", "core.bonus.NO_DISTANCE_PENALTY.name": "Sin penalización por distancia", @@ -440,10 +459,12 @@ "core.bonus.REBIRTH.description": "El ${val}% del grupo resucitará después de la muerte", "core.bonus.RETURN_AFTER_STRIKE.name": "Atacar y volver", "core.bonus.RETURN_AFTER_STRIKE.description": "Regresa después de un ataque cuerpo a cuerpo", + "core.bonus.REVENGE.name": "Venganza", + "core.bonus.REVENGE.description": "Inflige daño adicional según la salud perdida del atacante en la batalla.", "core.bonus.SHOOTER.name": "A distancia", "core.bonus.SHOOTER.description": "La criatura puede disparar", "core.bonus.SHOOTS_ALL_ADJACENT.name": "Dispara en todas direcciones", - "core.bonus.SHOOTS_ALL_ADJACENT.description": "Los ataques a distancia de esta criatura impactan a todos los objetivos en un área pequeña", + "core.bonus.SHOOTS_ALL_ADJACENT.description": "Los ataques a distancia de esta criatura impactan a todos los objetivos en un área reducida", "core.bonus.SOUL_STEAL.name": "Roba almas", "core.bonus.SOUL_STEAL.description": "Gana ${val} nuevas criaturas por cada enemigo eliminado", "core.bonus.SPELLCASTER.name": "Lanzador de hechizos", diff --git a/client/CMT.cpp b/client/CMT.cpp index 64b85c69c..5305a273e 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -138,16 +138,7 @@ int main(int argc, char * argv[]) ("nointro,i", "skips intro movies") ("donotstartserver,d","do not attempt to start server and just connect to it instead server") ("serverport", po::value(), "override port specified in config file") - ("savefrequency", po::value(), "limit auto save creation to each N days") - ("lobby", "parameters address, port, uuid to connect ro remote lobby session") - ("lobby-address", po::value(), "address to remote lobby") - ("lobby-port", po::value(), "port to remote lobby") - ("lobby-host", "if this client hosts session") - ("lobby-uuid", po::value(), "uuid to the server") - ("lobby-connections", po::value(), "connections of server") - ("lobby-username", po::value(), "player name") - ("lobby-gamemode", po::value(), "use 0 for new game and 1 for load game") - ("uuid", po::value(), "uuid for the client"); + ("savefrequency", po::value(), "limit auto save creation to each N days"); if(argc > 1) { @@ -371,46 +362,6 @@ int main(int argc, char * argv[]) } std::vector names; - session["lobby"].Bool() = false; - if(vm.count("lobby")) - { - session["lobby"].Bool() = true; - session["host"].Bool() = false; - session["address"].String() = vm["lobby-address"].as(); - if(vm.count("lobby-username")) - session["username"].String() = vm["lobby-username"].as(); - else - session["username"].String() = settings["launcher"]["lobbyUsername"].String(); - if(vm.count("lobby-gamemode")) - session["gamemode"].Integer() = vm["lobby-gamemode"].as(); - else - session["gamemode"].Integer() = 0; - CSH->uuid = vm["uuid"].as(); - session["port"].Integer() = vm["lobby-port"].as(); - logGlobal->info("Remote lobby mode at %s:%d, uuid is %s", session["address"].String(), session["port"].Integer(), CSH->uuid); - if(vm.count("lobby-host")) - { - session["host"].Bool() = true; - session["hostConnections"].String() = std::to_string(vm["lobby-connections"].as()); - session["hostUuid"].String() = vm["lobby-uuid"].as(); - logGlobal->info("This client will host session, server uuid is %s", session["hostUuid"].String()); - } - - //we should not reconnect to previous game in online mode - Settings saveSession = settings.write["server"]["reconnect"]; - saveSession->Bool() = false; - - //start lobby immediately - names.push_back(session["username"].String()); - ESelectionScreen sscreen = session["gamemode"].Integer() == 0 ? ESelectionScreen::newGame : ESelectionScreen::loadGame; - CMM->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI); - } - - // Restore remote session - start game immediately - if(settings["server"]["reconnect"].Bool()) - { - CSH->restoreLastSession(); - } if(!settings["session"]["headless"].Bool()) { @@ -451,7 +402,6 @@ static void mainLoop() while(1) //main SDL events loop { GH.input().fetchEvents(); - CSH->applyPacksOnLobbyScreen(); GH.renderFrame(); } } diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 20697bc2b..bd5791476 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -95,6 +95,12 @@ set(client_SRCS renderSDL/ScreenHandler.cpp renderSDL/SDL_Extensions.cpp + globalLobby/GlobalLobbyClient.cpp + globalLobby/GlobalLobbyLoginWindow.cpp + globalLobby/GlobalLobbyServerSetup.cpp + globalLobby/GlobalLobbyWidget.cpp + globalLobby/GlobalLobbyWindow.cpp + widgets/Buttons.cpp widgets/CArtifactHolder.cpp widgets/CComponent.cpp @@ -270,6 +276,13 @@ set(client_HEADERS renderSDL/SDL_Extensions.h renderSDL/SDL_PixelAccess.h + globalLobby/GlobalLobbyClient.h + globalLobby/GlobalLobbyDefines.h + globalLobby/GlobalLobbyLoginWindow.h + globalLobby/GlobalLobbyServerSetup.h + globalLobby/GlobalLobbyWidget.h + globalLobby/GlobalLobbyWindow.h + widgets/Buttons.h widgets/CArtifactHolder.h widgets/CComponent.h diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 5b0a37966..3d08ad7d9 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1870,14 +1870,9 @@ void CPlayerInterface::proposeLoadingGame() CGI->generaltexth->allTexts[68], []() { - GH.dispatchMainThread( - []() - { - CSH->endGameplay(); - GH.defActionsDef = 63; - CMM->menu->switchToTab("load"); - } - ); + CSH->endGameplay(); + GH.defActionsDef = 63; + CMM->menu->switchToTab("load"); }, nullptr ); diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index ee3f47164..2899906a9 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -16,6 +16,7 @@ #include "gui/CGuiHandler.h" #include "gui/WindowHandler.h" +#include "globalLobby/GlobalLobbyClient.h" #include "lobby/CSelectionBase.h" #include "lobby/CLobbyScreen.h" #include "windows/InfoWindows.h" @@ -46,16 +47,15 @@ #include "../lib/mapObjects/MiscObjects.h" #include "../lib/modding/ModIncompatibility.h" #include "../lib/rmg/CMapGenOptions.h" +#include "../lib/serializer/Connection.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/registerTypes/RegisterTypesLobbyPacks.h" -#include "../lib/serializer/Connection.h" #include "../lib/serializer/CMemorySerializer.h" #include "../lib/UnlockGuard.h" #include #include #include -#include #include "../lib/serializer/Cast.h" #include "LobbyClientNetPackVisitors.h" @@ -67,8 +67,6 @@ template class CApplyOnLobby; -const std::string CServerHandler::localhostAddress{"127.0.0.1"}; - #if defined(VCMI_ANDROID) && !defined(SINGLE_PROCESS_APP) extern std::atomic_bool androidTestServerReadyFlag; #endif @@ -76,8 +74,8 @@ extern std::atomic_bool androidTestServerReadyFlag; class CBaseForLobbyApply { public: - virtual bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const = 0; - virtual void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const = 0; + virtual bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const = 0; + virtual void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const = 0; virtual ~CBaseForLobbyApply(){}; template static CBaseForLobbyApply * getApplier(const U * t = nullptr) { @@ -88,124 +86,137 @@ public: template class CApplyOnLobby : public CBaseForLobbyApply { public: - bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override + bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const override { - boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - - T * ptr = static_cast(pack); + auto & ref = static_cast(pack); ApplyOnLobbyHandlerNetPackVisitor visitor(*handler); - logNetwork->trace("\tImmediately apply on lobby: %s", typeid(ptr).name()); - ptr->visit(visitor); + logNetwork->trace("\tImmediately apply on lobby: %s", typeid(ref).name()); + ref.visit(visitor); return visitor.getResult(); } - void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const override + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const override { - T * ptr = static_cast(pack); + auto & ref = static_cast(pack); ApplyOnLobbyScreenNetPackVisitor visitor(*handler, lobby); - logNetwork->trace("\tApply on lobby from queue: %s", typeid(ptr).name()); - ptr->visit(visitor); + logNetwork->trace("\tApply on lobby from queue: %s", typeid(ref).name()); + ref.visit(visitor); } }; template<> class CApplyOnLobby: public CBaseForLobbyApply { public: - bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override + bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const override { logGlobal->error("Cannot apply plain CPack!"); assert(0); return false; } - void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const override + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const override { logGlobal->error("Cannot apply plain CPack!"); assert(0); } }; -static const std::string NAME_AFFIX = "client"; -static const std::string NAME = GameConstants::VCMI_VERSION + std::string(" (") + NAME_AFFIX + ')'; //application name +CServerHandler::~CServerHandler() +{ + networkHandler->stop(); + try + { + threadNetwork.join(); + } + catch (const std::runtime_error & e) + { + logGlobal->error("Failed to shut down network thread! Reason: %s", e.what()); + assert(0); + } +} CServerHandler::CServerHandler() - : state(EClientState::NONE), mx(std::make_shared()), client(nullptr), loadMode(0), campaignStateToSend(nullptr), campaignServerRestartLock(false) + : networkHandler(INetworkHandler::createHandler()) + , lobbyClient(std::make_unique()) + , applier(std::make_unique>()) + , threadNetwork(&CServerHandler::threadRunNetwork, this) + , state(EClientState::NONE) + , serverPort(0) + , campaignStateToSend(nullptr) + , screenType(ESelectionScreen::unknown) + , serverMode(EServerMode::NONE) + , loadMode(ELoadMode::NONE) + , client(nullptr) { uuid = boost::uuids::to_string(boost::uuids::random_generator()()); - //read from file to restore last session - if(!settings["server"]["uuid"].isNull() && !settings["server"]["uuid"].String().empty()) - uuid = settings["server"]["uuid"].String(); - applier = std::make_shared>(); registerTypesLobbyPacks(*applier); } -CServerHandler::~CServerHandler() = default; +void CServerHandler::threadRunNetwork() +{ + logGlobal->info("Starting network thread"); + setThreadName("runNetwork"); + networkHandler->run(); + logGlobal->info("Ending network thread"); +} -void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std::vector * names) +void CServerHandler::resetStateForLobby(EStartMode mode, ESelectionScreen screen, EServerMode newServerMode, const std::vector & names) { hostClientId = -1; - state = EClientState::NONE; + setState(EClientState::NONE); + serverMode = newServerMode; mapToStart = nullptr; th = std::make_unique(); - packsForLobbyScreen.clear(); - c.reset(); + logicConnection.reset(); si = std::make_shared(); playerNames.clear(); si->difficulty = 1; si->mode = mode; + screenType = screen; myNames.clear(); - if(names && !names->empty()) //if have custom set of player names - use it - myNames = *names; + if(!names.empty()) //if have custom set of player names - use it + myNames = names; else myNames.push_back(settings["general"]["playerName"].String()); } -void CServerHandler::startLocalServerAndConnect() +GlobalLobbyClient & CServerHandler::getGlobalLobby() { - if(threadRunLocalServer) - threadRunLocalServer->join(); + return *lobbyClient; +} + +INetworkHandler & CServerHandler::getNetworkHandler() +{ + return *networkHandler; +} + +void CServerHandler::startLocalServerAndConnect(bool connectToLobby) +{ + if(threadRunLocalServer.joinable()) + threadRunLocalServer.join(); th->update(); - - auto errorMsg = CGI->generaltexth->translate("vcmi.server.errors.existingProcess"); - try - { - CConnection testConnection(localhostAddress, getDefaultPort(), NAME, uuid); - logNetwork->error("Port is busy, check if another instance of vcmiserver is working"); - CInfoWindow::showInfoDialog(errorMsg, {}); - return; - } - catch(std::runtime_error & error) - { - //no connection means that port is not busy and we can start local server - } - + #if defined(SINGLE_PROCESS_APP) boost::condition_variable cond; - std::vector args{"--uuid=" + uuid, "--port=" + std::to_string(getHostPort())}; - if(settings["session"]["lobby"].Bool() && settings["session"]["host"].Bool()) - { - args.push_back("--lobby=" + settings["session"]["address"].String()); - args.push_back("--connections=" + settings["session"]["hostConnections"].String()); - args.push_back("--lobby-port=" + std::to_string(settings["session"]["port"].Integer())); - args.push_back("--lobby-uuid=" + settings["session"]["hostUuid"].String()); - } - threadRunLocalServer = std::make_shared([&cond, args, this] { + std::vector args{"--port=" + std::to_string(getLocalPort())}; + if(connectToLobby) + args.push_back("--lobby"); + + threadRunLocalServer = boost::thread([&cond, args] { setThreadName("CVCMIServer"); CVCMIServer::create(&cond, args); - onServerFinished(); }); - threadRunLocalServer->detach(); #elif defined(VCMI_ANDROID) { CAndroidVMHelper envHelper; envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "startServer", true); } #else - threadRunLocalServer = std::make_shared(&CServerHandler::threadRunServer, this); //runs server executable; + threadRunLocalServer = boost::thread(&CServerHandler::threadRunServer, this, connectToLobby); //runs server executable; #endif logNetwork->trace("Setting up thread calling server: %d ms", th->getDiff()); @@ -236,7 +247,7 @@ void CServerHandler::startLocalServerAndConnect() while(!androidTestServerReadyFlag.load()) { logNetwork->info("still waiting..."); - boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); } logNetwork->info("waiting for server finished..."); androidTestServerReadyFlag = false; @@ -245,151 +256,165 @@ void CServerHandler::startLocalServerAndConnect() th->update(); //put breakpoint here to attach to server before it does something stupid - justConnectToServer(localhostAddress, 0); + connectToServer(getLocalHostname(), getLocalPort()); logNetwork->trace("\tConnecting to the server: %d ms", th->getDiff()); } -void CServerHandler::justConnectToServer(const std::string & addr, const ui16 port) +void CServerHandler::connectToServer(const std::string & addr, const ui16 port) { - state = EClientState::CONNECTING; - while(!c && state != EClientState::CONNECTION_CANCELLED) - { - try - { - logNetwork->info("Establishing connection..."); - c = std::make_shared( - addr.size() ? addr : getHostAddress(), - port ? port : getHostPort(), - NAME, uuid); + logNetwork->info("Establishing connection to %s:%d...", addr, port); + setState(EClientState::CONNECTING); + serverHostname = addr; + serverPort = port; - nextClient = std::make_unique(); - c->iser.cb = nextClient.get(); - } - catch(std::runtime_error & error) - { - logNetwork->warn("\nCannot establish connection. %s Retrying in 1 second", error.what()); - boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); - } + if (!isServerLocal()) + { + Settings remoteAddress = settings.write["server"]["remoteHostname"]; + remoteAddress->String() = addr; + + Settings remotePort = settings.write["server"]["remotePort"]; + remotePort->Integer() = port; } - if(state == EClientState::CONNECTION_CANCELLED) + networkHandler->connectToRemote(*this, addr, port); +} + +void CServerHandler::onConnectionFailed(const std::string & errorMessage) +{ + assert(getState() == EClientState::CONNECTING); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + if (isServerLocal()) + { + // retry - local server might be still starting up + logNetwork->debug("\nCannot establish connection. %s. Retrying...", errorMessage); + networkHandler->createTimer(*this, std::chrono::milliseconds(100)); + } + else + { + // remote server refused connection - show error message + setState(EClientState::NONE); + CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.mainMenu.serverConnectionFailed"), {}); + } +} + +void CServerHandler::onTimer() +{ + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + if(getState() == EClientState::CONNECTION_CANCELLED) { logNetwork->info("Connection aborted by player!"); return; } - c->handler = std::make_shared(&CServerHandler::threadHandleConnection, this); - - if(!addr.empty() && addr != getHostAddress()) - { - Settings serverAddress = settings.write["server"]["server"]; - serverAddress->String() = addr; - } - if(port && port != getHostPort()) - { - Settings serverPort = settings.write["server"]["port"]; - serverPort->Integer() = port; - } + assert(isServerLocal()); + networkHandler->connectToRemote(*this, getLocalHostname(), getLocalPort()); } -void CServerHandler::applyPacksOnLobbyScreen() +void CServerHandler::onConnectionEstablished(const NetworkConnectionPtr & netConnection) { - if(!c || !c->handler) - return; + assert(getState() == EClientState::CONNECTING); - boost::unique_lock lock(*mx); - while(!packsForLobbyScreen.empty()) + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + networkConnection = netConnection; + + logNetwork->info("Connection established"); + + if (serverMode == EServerMode::LOBBY_GUEST) { - boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - CPackForLobby * pack = packsForLobbyScreen.front(); - packsForLobbyScreen.pop_front(); - CBaseForLobbyApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //find the applier - apply->applyOnLobbyScreen(dynamic_cast(SEL), this, pack); - GH.windows().totalRedraw(); - delete pack; + // say hello to lobby to switch connection to proxy mode + getGlobalLobby().sendProxyConnectionLogin(netConnection); } + + logicConnection = std::make_shared(netConnection); + logicConnection->uuid = uuid; + logicConnection->enterLobbyConnectionMode(); + sendClientConnecting(); } -void CServerHandler::stopServerConnection() +void CServerHandler::applyPackOnLobbyScreen(CPackForLobby & pack) { - if(c->handler) - { - while(!c->handler->timed_join(boost::chrono::milliseconds(50))) - applyPacksOnLobbyScreen(); - c->handler->join(); - } + const CBaseForLobbyApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(&pack)); //find the applier + apply->applyOnLobbyScreen(dynamic_cast(SEL), this, pack); + GH.windows().totalRedraw(); } std::set CServerHandler::getHumanColors() { - return clientHumanColors(c->connectionID); + return clientHumanColors(logicConnection->connectionID); } - PlayerColor CServerHandler::myFirstColor() const { - return clientFirstColor(c->connectionID); + return clientFirstColor(logicConnection->connectionID); } bool CServerHandler::isMyColor(PlayerColor color) const { - return isClientColor(c->connectionID, color); + return isClientColor(logicConnection->connectionID, color); } ui8 CServerHandler::myFirstId() const { - return clientFirstId(c->connectionID); + return clientFirstId(logicConnection->connectionID); +} + +EClientState CServerHandler::getState() const +{ + return state; +} + +void CServerHandler::setState(EClientState newState) +{ + state = newState; } bool CServerHandler::isServerLocal() const { - if(threadRunLocalServer) - return true; - - return false; + return threadRunLocalServer.joinable(); } bool CServerHandler::isHost() const { - return c && hostClientId == c->connectionID; + return logicConnection && hostClientId == logicConnection->connectionID; } bool CServerHandler::isGuest() const { - return !c || hostClientId != c->connectionID; + return !logicConnection || hostClientId != logicConnection->connectionID; } -ui16 CServerHandler::getDefaultPort() +const std::string & CServerHandler::getLocalHostname() const { - return static_cast(settings["server"]["port"].Integer()); + return settings["server"]["localHostname"].String(); } -std::string CServerHandler::getDefaultPortStr() +ui16 CServerHandler::getLocalPort() const { - return std::to_string(getDefaultPort()); + return settings["server"]["localPort"].Integer(); } -std::string CServerHandler::getHostAddress() const +const std::string & CServerHandler::getRemoteHostname() const { - if(settings["session"]["lobby"].isNull() || !settings["session"]["lobby"].Bool()) - return settings["server"]["server"].String(); - - if(settings["session"]["host"].Bool()) - return localhostAddress; - - return settings["session"]["address"].String(); + return settings["server"]["remoteHostname"].String(); } -ui16 CServerHandler::getHostPort() const +ui16 CServerHandler::getRemotePort() const { - if(settings["session"]["lobby"].isNull() || !settings["session"]["lobby"].Bool()) - return getDefaultPort(); - - if(settings["session"]["host"].Bool()) - return getDefaultPort(); - - return settings["session"]["port"].Integer(); + return settings["server"]["remotePort"].Integer(); +} + +const std::string & CServerHandler::getCurrentHostname() const +{ + return serverHostname; +} + +ui16 CServerHandler::getCurrentPort() const +{ + return serverPort; } void CServerHandler::sendClientConnecting() const @@ -404,13 +429,16 @@ void CServerHandler::sendClientConnecting() const void CServerHandler::sendClientDisconnecting() { // FIXME: This is workaround needed to make sure client not trying to sent anything to non existed server - if(state == EClientState::DISCONNECTING) + if(getState() == EClientState::DISCONNECTING) + { + assert(0); return; + } - state = EClientState::DISCONNECTING; + setState(EClientState::DISCONNECTING); mapToStart = nullptr; LobbyClientDisconnected lcd; - lcd.clientId = c->connectionID; + lcd.clientId = logicConnection->connectionID; logNetwork->info("Connection has been requested to be closed."); if(isServerLocal()) { @@ -422,18 +450,14 @@ void CServerHandler::sendClientDisconnecting() logNetwork->info("Sent leaving signal to the server"); } sendLobbyPack(lcd); - - { - // Network thread might be applying network pack at this moment - auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); - c->close(); - c.reset(); - } + networkConnection->close(); + networkConnection.reset(); + logicConnection.reset(); } void CServerHandler::setCampaignState(std::shared_ptr newCampaign) { - state = EClientState::LOBBY_CAMPAIGN; + setState(EClientState::LOBBY_CAMPAIGN); LobbySetCampaign lsc; lsc.ourCampaign = newCampaign; sendLobbyPack(lsc); @@ -441,7 +465,7 @@ void CServerHandler::setCampaignState(std::shared_ptr newCampaign void CServerHandler::setCampaignMap(CampaignScenarioID mapId) const { - if(state == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place + if(getState() == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place return; LobbySetCampaignMap lscm; @@ -451,7 +475,7 @@ void CServerHandler::setCampaignMap(CampaignScenarioID mapId) const void CServerHandler::setCampaignBonus(int bonusId) const { - if(state == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place + if(getState() == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place return; LobbySetCampaignBonus lscb; @@ -575,9 +599,7 @@ void CServerHandler::sendRestartGame() const { GH.windows().createAndPushWindow(); - LobbyEndGame endGame; - endGame.closeConnection = false; - endGame.restart = true; + LobbyRestartGame endGame; sendLobbyPack(endGame); } @@ -621,17 +643,18 @@ void CServerHandler::sendStartGame(bool allowOnlyAI) const if(!settings["session"]["headless"].Bool()) GH.windows().createAndPushWindow(); + LobbyPrepareStartGame lpsg; + sendLobbyPack(lpsg); + LobbyStartGame lsg; if(client) { lsg.initializedStartInfo = std::make_shared(* const_cast(client->getStartInfo(true))); - lsg.initializedStartInfo->mode = StartInfo::NEW_GAME; + lsg.initializedStartInfo->mode = EStartMode::NEW_GAME; lsg.initializedStartInfo->seedToBeUsed = lsg.initializedStartInfo->seedPostInit = 0; * si = * lsg.initializedStartInfo; } sendLobbyPack(lsg); - c->enterLobbyConnectionMode(); - c->disableStackSendingByID(); } void CServerHandler::startMapAfterConnection(std::shared_ptr to) @@ -644,83 +667,54 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta if(CMM) CMM->disable(); - std::swap(client, nextClient); - highScoreCalc = nullptr; switch(si->mode) { - case StartInfo::NEW_GAME: + case EStartMode::NEW_GAME: client->newGame(gameState); break; - case StartInfo::CAMPAIGN: + case EStartMode::CAMPAIGN: client->newGame(gameState); break; - case StartInfo::LOAD_GAME: + case EStartMode::LOAD_GAME: client->loadGame(gameState); break; default: throw std::runtime_error("Invalid mode"); } // After everything initialized we can accept CPackToClient netpacks - c->enterGameplayConnectionMode(client->gameState()); - state = EClientState::GAMEPLAY; - - //store settings to continue game - if(!isServerLocal() && isGuest()) - { - Settings saveSession = settings.write["server"]["reconnect"]; - saveSession->Bool() = true; - Settings saveUuid = settings.write["server"]["uuid"]; - saveUuid->String() = uuid; - Settings saveNames = settings.write["server"]["names"]; - saveNames->Vector().clear(); - for(auto & name : myNames) - { - JsonNode jsonName; - jsonName.String() = name; - saveNames->Vector().push_back(jsonName); - } - } + logicConnection->enterGameplayConnectionMode(client->gameState()); + setState(EClientState::GAMEPLAY); } -void CServerHandler::endGameplay(bool closeConnection, bool restart) +void CServerHandler::endGameplay() { - if(closeConnection) - { - // Game is ending - // Tell the network thread to reach a stable state - CSH->sendClientDisconnecting(); - logNetwork->info("Closed connection."); - } + // Game is ending + // Tell the network thread to reach a stable state + CSH->sendClientDisconnecting(); + logNetwork->info("Closed connection."); client->endGame(); client.reset(); - if(!restart) + if(CMM) { - if(CMM) - { - GH.curInt = CMM.get(); - CMM->enable(); - } - else - { - GH.curInt = CMainMenu::create().get(); - } + GH.curInt = CMM.get(); + CMM->enable(); } - - if(c) + else { - nextClient = std::make_unique(); - c->iser.cb = nextClient.get(); - c->enterLobbyConnectionMode(); - c->disableStackSendingByID(); + GH.curInt = CMainMenu::create().get(); } - - //reset settings - Settings saveSession = settings.write["server"]["reconnect"]; - saveSession->Bool() = false; +} + +void CServerHandler::restartGameplay() +{ + client->endGame(); + client.reset(); + + logicConnection->enterLobbyConnectionMode(); } void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr cs) @@ -741,7 +735,6 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared GH.dispatchMainThread([ourCampaign, this]() { - CSH->campaignServerRestartLock.set(true); CSH->endGameplay(); auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog; @@ -764,13 +757,14 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared GH.windows().createAndPushWindow(true, *highScoreCalc); } }; + + threadRunLocalServer.join(); if(epilogue.hasPrologEpilog) { GH.windows().createAndPushWindow(epilogue, finisher); } else { - CSH->campaignServerRestartLock.waitUntil(false); finisher(); } }); @@ -796,15 +790,15 @@ int CServerHandler::howManyPlayerInterfaces() return playerInts; } -ui8 CServerHandler::getLoadMode() +ELoadMode CServerHandler::getLoadMode() { - if(loadMode != ELoadMode::TUTORIAL && state == EClientState::GAMEPLAY) + if(loadMode != ELoadMode::TUTORIAL && getState() == EClientState::GAMEPLAY) { if(si->campState) return ELoadMode::CAMPAIGN; for(auto pn : playerNames) { - if(pn.second.connection != c->connectionID) + if(pn.second.connection != logicConnection->connectionID) return ELoadMode::MULTI; } if(howManyPlayerInterfaces() > 1) //this condition will work for hotseat mode OR multiplayer with allowed more than 1 color per player to control @@ -815,48 +809,24 @@ ui8 CServerHandler::getLoadMode() return loadMode; } -void CServerHandler::restoreLastSession() -{ - auto loadSession = [this]() - { - uuid = settings["server"]["uuid"].String(); - for(auto & name : settings["server"]["names"].Vector()) - myNames.push_back(name.String()); - resetStateForLobby(StartInfo::LOAD_GAME, &myNames); - screenType = ESelectionScreen::loadGame; - justConnectToServer(settings["server"]["server"].String(), settings["server"]["port"].Integer()); - }; - - auto cleanUpSession = []() - { - //reset settings - Settings saveSession = settings.write["server"]["reconnect"]; - saveSession->Bool() = false; - }; - - CInfoWindow::showYesNoDialog(VLC->generaltexth->translate("vcmi.server.confirmReconnect"), {}, loadSession, cleanUpSession); -} - void CServerHandler::debugStartTest(std::string filename, bool save) { logGlobal->info("Starting debug test with file: %s", filename); auto mapInfo = std::make_shared(); if(save) { - resetStateForLobby(StartInfo::LOAD_GAME); + resetStateForLobby(EStartMode::LOAD_GAME, ESelectionScreen::loadGame, EServerMode::LOCAL, {}); mapInfo->saveInit(ResourcePath(filename, EResType::SAVEGAME)); - screenType = ESelectionScreen::loadGame; } else { - resetStateForLobby(StartInfo::NEW_GAME); + resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOCAL, {}); mapInfo->mapInit(filename); - screenType = ESelectionScreen::newGame; } if(settings["session"]["donotstartserver"].Bool()) - justConnectToServer(localhostAddress, 3030); + connectToServer(getLocalHostname(), getLocalPort()); else - startLocalServerAndConnect(); + startLocalServerAndConnect(false); boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); @@ -899,91 +869,71 @@ public: { } - virtual bool callTyped() override { return false; } + bool callTyped() override { return false; } - virtual void visitForLobby(CPackForLobby & lobbyPack) override + void visitForLobby(CPackForLobby & lobbyPack) override { handler.visitForLobby(lobbyPack); } - virtual void visitForClient(CPackForClient & clientPack) override + void visitForClient(CPackForClient & clientPack) override { handler.visitForClient(clientPack); } }; -void CServerHandler::threadHandleConnection() +void CServerHandler::onPacketReceived(const std::shared_ptr &, const std::vector & message) { - setThreadName("handleConnection"); - c->enterLobbyConnectionMode(); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - try + if(getState() == EClientState::DISCONNECTING) { - sendClientConnecting(); - while(c && c->connected) - { - while(state == EClientState::STARTING) - boost::this_thread::sleep_for(boost::chrono::milliseconds(10)); - - CPack * pack = c->retrievePack(); - if(state == EClientState::DISCONNECTING) - { - // FIXME: server shouldn't really send netpacks after it's tells client to disconnect - // Though currently they'll be delivered and might cause crash. - vstd::clear_pointer(pack); - } - else - { - ServerHandlerCPackVisitor visitor(*this); - pack->visit(visitor); - } - } + assert(0); //Should not be possible - socket must be closed at this point + return; } - //catch only asio exceptions - catch(const boost::system::system_error & e) + + CPack * pack = logicConnection->retrievePack(message); + ServerHandlerCPackVisitor visitor(*this); + pack->visit(visitor); +} + +void CServerHandler::onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) +{ + if(getState() == EClientState::DISCONNECTING) { - if(state == EClientState::DISCONNECTING) - { - logNetwork->info("Successfully closed connection to server, ending listening thread!"); - } - else - { - if (e.code() == boost::asio::error::eof) - logNetwork->error("Lost connection to server, ending listening thread! Connection has been closed"); - else - logNetwork->error("Lost connection to server, ending listening thread! Reason: %s", e.what()); - - if(client) - { - state = EClientState::DISCONNECTING; - - GH.dispatchMainThread([]() - { - CSH->endGameplay(); - GH.defActionsDef = 63; - CMM->menu->switchToTab("main"); - }); - } - else - { - auto lcd = new LobbyClientDisconnected(); - lcd->clientId = c->connectionID; - boost::unique_lock lock(*mx); - packsForLobbyScreen.push_back(lcd); - } - } + assert(networkConnection == nullptr); + // Note: this branch can be reached on app shutdown, when main thread holds mutex till destruction + logNetwork->info("Successfully closed connection to server!"); + return; } + + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + logNetwork->error("Lost connection to server! Connection has been closed"); + + if(client) + { + CSH->endGameplay(); + GH.defActionsDef = 63; + CMM->menu->switchToTab("main"); + CSH->showServerError(CGI->generaltexth->translate("vcmi.server.errors.disconnected")); + } + else + { + LobbyClientDisconnected lcd; + lcd.clientId = logicConnection->connectionID; + applyPackOnLobbyScreen(lcd); + } + + networkConnection.reset(); } void CServerHandler::visitForLobby(CPackForLobby & lobbyPack) { - if(applier->getApplier(CTypeList::getInstance().getTypeID(&lobbyPack))->applyOnLobbyHandler(this, &lobbyPack)) + if(applier->getApplier(CTypeList::getInstance().getTypeID(&lobbyPack))->applyOnLobbyHandler(this, lobbyPack)) { if(!settings["session"]["headless"].Bool()) - { - boost::unique_lock lock(*mx); - packsForLobbyScreen.push_back(&lobbyPack); - } + applyPackOnLobbyScreen(lobbyPack); } } @@ -992,22 +942,16 @@ void CServerHandler::visitForClient(CPackForClient & clientPack) client->handlePack(&clientPack); } -void CServerHandler::threadRunServer() +void CServerHandler::threadRunServer(bool connectToLobby) { #if !defined(VCMI_MOBILE) setThreadName("runServer"); const std::string logName = (VCMIDirs::get().userLogsPath() / "server_log.txt").string(); std::string comm = VCMIDirs::get().serverPath().string() - + " --port=" + std::to_string(getHostPort()) - + " --run-by-client" - + " --uuid=" + uuid; - if(settings["session"]["lobby"].Bool() && settings["session"]["host"].Bool()) - { - comm += " --lobby=" + settings["session"]["address"].String(); - comm += " --connections=" + settings["session"]["hostConnections"].String(); - comm += " --lobby-port=" + std::to_string(settings["session"]["port"].Integer()); - comm += " --lobby-uuid=" + settings["session"]["hostUuid"].String(); - } + + " --port=" + std::to_string(getLocalPort()) + + " --run-by-client"; + if(connectToLobby) + comm += " --lobby"; comm += " > \"" + logName + '\"'; logGlobal->info("Server command line: %s", comm); @@ -1035,22 +979,31 @@ void CServerHandler::threadRunServer() } else { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + if (getState() == EClientState::CONNECTING) + { + showServerError(CGI->generaltexth->translate("vcmi.server.errors.existingProcess")); + setState(EClientState::CONNECTION_CANCELLED); // stop attempts to reconnect + } logNetwork->error("Error: server failed to close correctly or crashed!"); logNetwork->error("Check %s for more info", logName); } - onServerFinished(); #endif } -void CServerHandler::onServerFinished() -{ - threadRunLocalServer.reset(); - if (CSH) - CSH->campaignServerRestartLock.setn(false); -} - void CServerHandler::sendLobbyPack(const CPackForLobby & pack) const { - if(state != EClientState::STARTING) - c->sendPack(&pack); + if(getState() != EClientState::STARTING) + logicConnection->sendPack(&pack); +} + +bool CServerHandler::inLobbyRoom() const +{ + return CSH->serverMode == EServerMode::LOBBY_HOST || CSH->serverMode == EServerMode::LOBBY_GUEST; +} + +bool CServerHandler::inGame() const +{ + return logicConnection != nullptr; } diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 4db794ede..59573676d 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -11,6 +11,7 @@ #include "../lib/CStopWatch.h" +#include "../lib/network/NetworkInterface.h" #include "../lib/StartInfo.h" #include "../lib/CondSh.h" @@ -34,10 +35,14 @@ VCMI_LIB_NAMESPACE_END class CClient; class CBaseForLobbyApply; +class GlobalLobbyClient; class HighScoreCalculation; class HighScoreParameter; +enum class ESelectionScreen : ui8; +enum class ELoadMode : ui8; + // TODO: Add mutex so we can't set CONNECTION_CANCELLED if client already connected, but thread not setup yet enum class EClientState : ui8 { @@ -49,7 +54,14 @@ enum class EClientState : ui8 STARTING, // Gameplay interfaces being created, we pause netpacks retrieving GAMEPLAY, // In-game, used by some UI DISCONNECTING, // We disconnecting, drop all netpacks - CONNECTION_FAILED // We could not connect to server +}; + +enum class EServerMode : uint8_t +{ + NONE = 0, + LOCAL, // no global lobby + LOBBY_HOST, // We are hosting global server available via global lobby + LOBBY_GUEST // Connecting to a remote server via proxy provided by global lobby }; class IServerAPI @@ -80,63 +92,68 @@ public: }; /// structure to handle running server and connecting to it -class CServerHandler : public IServerAPI, public LobbyInfo +class CServerHandler final : public IServerAPI, public LobbyInfo, public INetworkClientListener, public INetworkTimerListener, boost::noncopyable { friend class ApplyOnLobbyHandlerNetPackVisitor; - - std::shared_ptr> applier; - std::shared_ptr mx; - std::list packsForLobbyScreen; //protected by mx - + std::unique_ptr networkHandler; + std::shared_ptr networkConnection; + std::unique_ptr lobbyClient; + std::unique_ptr> applier; std::shared_ptr mapToStart; - std::vector myNames; - std::shared_ptr highScoreCalc; - /// temporary helper member that exists while game in lobby mode - /// required to correctly deserialize gamestate using client-side game callback - std::unique_ptr nextClient; + boost::thread threadRunLocalServer; + boost::thread threadNetwork; + + std::atomic state; + + void threadRunNetwork(); + void threadRunServer(bool connectToLobby); - void threadHandleConnection(); - void threadRunServer(); - void onServerFinished(); void sendLobbyPack(const CPackForLobby & pack) const override; + void onPacketReceived(const NetworkConnectionPtr &, const std::vector & message) override; + void onConnectionFailed(const std::string & errorMessage) override; + void onConnectionEstablished(const NetworkConnectionPtr &) override; + void onDisconnected(const NetworkConnectionPtr &, const std::string & errorMessage) override; + void onTimer() override; + + void applyPackOnLobbyScreen(CPackForLobby & pack); + + std::string serverHostname; + ui16 serverPort; + + bool isServerLocal() const; + public: - std::atomic state; + /// High-level connection overlay that is capable of (de)serializing network data + std::shared_ptr logicConnection; + //////////////////// // FIXME: Bunch of crutches to glue it all together // For starting non-custom campaign and continue to next mission std::shared_ptr campaignStateToSend; - ui8 screenType; // To create lobby UI only after server is setup - ui8 loadMode; // For saves filtering in SelectionTab + ESelectionScreen screenType; // To create lobby UI only after server is setup + EServerMode serverMode; + ELoadMode loadMode; // For saves filtering in SelectionTab //////////////////// std::unique_ptr th; - std::shared_ptr threadRunLocalServer; - - std::shared_ptr c; std::unique_ptr client; - CondSh campaignServerRestartLock; - - static const std::string localhostAddress; - CServerHandler(); ~CServerHandler(); - std::string getHostAddress() const; - ui16 getHostPort() const; + void resetStateForLobby(EStartMode mode, ESelectionScreen screen, EServerMode serverMode, const std::vector & names); + void startLocalServerAndConnect(bool connectToLobby); + void connectToServer(const std::string & addr, const ui16 port); - void resetStateForLobby(const StartInfo::EMode mode, const std::vector * names = nullptr); - void startLocalServerAndConnect(); - void justConnectToServer(const std::string & addr, const ui16 port); - void applyPacksOnLobbyScreen(); - void stopServerConnection(); + GlobalLobbyClient & getGlobalLobby(); + INetworkHandler & getNetworkHandler(); // Helpers for lobby state access std::set getHumanColors(); @@ -144,12 +161,21 @@ public: bool isMyColor(PlayerColor color) const; ui8 myFirstId() const; // Used by chat only! - bool isServerLocal() const; + EClientState getState() const; + void setState(EClientState newState); + bool isHost() const; bool isGuest() const; + bool inLobbyRoom() const; + bool inGame() const; - static ui16 getDefaultPort(); - static std::string getDefaultPortStr(); + const std::string & getCurrentHostname() const; + const std::string & getLocalHostname() const; + const std::string & getRemoteHostname() const; + + ui16 getCurrentPort() const; + ui16 getLocalPort() const; + ui16 getRemotePort() const; // Lobby server API for UI void sendClientConnecting() const override; @@ -175,15 +201,14 @@ public: void debugStartTest(std::string filename, bool save = false); void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr); - void endGameplay(bool closeConnection = true, bool restart = false); + void endGameplay(); + void restartGameplay(); void startCampaignScenario(HighScoreParameter param, std::shared_ptr cs = {}); void showServerError(const std::string & txt) const; // TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle int howManyPlayerInterfaces(); - ui8 getLoadMode(); - - void restoreLastSession(); + ELoadMode getLoadMode(); void visitForLobby(CPackForLobby & lobbyPack); void visitForClient(CPackForClient & clientPack); diff --git a/client/Client.cpp b/client/Client.cpp index 8febc6bae..9ff148e9e 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -30,11 +30,12 @@ #include "../lib/UnlockGuard.h" #include "../lib/battle/BattleInfo.h" #include "../lib/serializer/BinaryDeserializer.h" +#include "../lib/serializer/BinarySerializer.h" +#include "../lib/serializer/Connection.h" #include "../lib/mapping/CMapService.h" #include "../lib/pathfinder/CGPathNode.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/registerTypes/RegisterTypesClientPacks.h" -#include "../lib/serializer/Connection.h" #include #include @@ -297,7 +298,7 @@ void CClient::serialize(BinaryDeserializer & h) bool shouldResetInterface = true; // Client no longer handle this player at all - if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), pid)) + if(!vstd::contains(CSH->getAllClientPlayers(CSH->logicConnection->connectionID), pid)) { logGlobal->trace("Player %s is not belong to this client. Destroying interface", pid); } @@ -397,7 +398,7 @@ void CClient::initPlayerEnvironments() { playerEnvironments.clear(); - auto allPlayers = CSH->getAllClientPlayers(CSH->c->connectionID); + auto allPlayers = CSH->getAllClientPlayers(CSH->logicConnection->connectionID); bool hasHumanPlayer = false; for(auto & color : allPlayers) { @@ -427,7 +428,7 @@ void CClient::initPlayerInterfaces() for(auto & playerInfo : gs->scenarioOps->playerInfos) { PlayerColor color = playerInfo.first; - if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), color)) + if(!vstd::contains(CSH->getAllClientPlayers(CSH->logicConnection->connectionID), color)) continue; if(!vstd::contains(playerint, color)) @@ -457,7 +458,7 @@ void CClient::initPlayerInterfaces() installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); } - if(CSH->getAllClientPlayers(CSH->c->connectionID).count(PlayerColor::NEUTRAL)) + if(CSH->getAllClientPlayers(CSH->logicConnection->connectionID).count(PlayerColor::NEUTRAL)) installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); @@ -520,7 +521,6 @@ void CClient::handlePack(CPack * pack) CBaseForCLApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //find the applier if(apply) { - boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); apply->applyOnClBefore(this, pack); logNetwork->trace("\tMade first apply on cl: %s", typeid(pack).name()); gs->apply(pack); @@ -545,7 +545,7 @@ int CClient::sendRequest(const CPackForServer * request, PlayerColor player) waitingRequest.pushBack(requestID); request->requestID = requestID; request->player = player; - CSH->c->sendPack(request); + CSH->logicConnection->sendPack(request); if(vstd::contains(playerint, player)) playerint[player]->requestSent(request, requestID); diff --git a/client/Client.h b/client/Client.h index 85893b5b8..5fe64b6b1 100644 --- a/client/Client.h +++ b/client/Client.h @@ -173,7 +173,7 @@ public: void showTeleportDialog(TeleportDialog * iw) override {}; void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {}; void giveResource(PlayerColor player, GameResID which, int val) override {}; - virtual void giveResources(PlayerColor player, TResources resources) override {}; + void giveResources(PlayerColor player, TResources resources) override {}; void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet & creatures, bool remove) override {}; void takeCreatures(ObjectInstanceID objid, const std::vector & creatures) override {}; diff --git a/client/ClientNetPackVisitors.h b/client/ClientNetPackVisitors.h index 08c0908e2..51812dedc 100644 --- a/client/ClientNetPackVisitors.h +++ b/client/ClientNetPackVisitors.h @@ -118,16 +118,16 @@ public: { } - virtual void visitChangeObjPos(ChangeObjPos & pack) override; - virtual void visitRemoveObject(RemoveObject & pack) override; - virtual void visitTryMoveHero(TryMoveHero & pack) override; - virtual void visitGiveHero(GiveHero & pack) override; - virtual void visitBattleStart(BattleStart & pack) override; - virtual void visitBattleNextRound(BattleNextRound & pack) override; - virtual void visitBattleUpdateGateState(BattleUpdateGateState & pack) override; - virtual void visitBattleResult(BattleResult & pack) override; - virtual void visitBattleStackMoved(BattleStackMoved & pack) override; - virtual void visitBattleAttack(BattleAttack & pack) override; - virtual void visitStartAction(StartAction & pack) override; - virtual void visitSetObjectProperty(SetObjectProperty & pack) override; + void visitChangeObjPos(ChangeObjPos & pack) override; + void visitRemoveObject(RemoveObject & pack) override; + void visitTryMoveHero(TryMoveHero & pack) override; + void visitGiveHero(GiveHero & pack) override; + void visitBattleStart(BattleStart & pack) override; + void visitBattleNextRound(BattleNextRound & pack) override; + void visitBattleUpdateGateState(BattleUpdateGateState & pack) override; + void visitBattleResult(BattleResult & pack) override; + void visitBattleStackMoved(BattleStackMoved & pack) override; + void visitBattleAttack(BattleAttack & pack) override; + void visitStartAction(StartAction & pack) override; + void visitSetObjectProperty(SetObjectProperty & pack) override; }; diff --git a/client/LobbyClientNetPackVisitors.h b/client/LobbyClientNetPackVisitors.h index 7e19e614b..b3996cce7 100644 --- a/client/LobbyClientNetPackVisitors.h +++ b/client/LobbyClientNetPackVisitors.h @@ -32,11 +32,12 @@ public: bool getResult() const { return result; } - virtual void visitLobbyClientConnected(LobbyClientConnected & pack) override; - virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; - virtual void visitLobbyEndGame(LobbyEndGame & pack) override; - virtual void visitLobbyStartGame(LobbyStartGame & pack) override; - virtual void visitLobbyUpdateState(LobbyUpdateState & pack) override; + void visitLobbyClientConnected(LobbyClientConnected & pack) override; + void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; + void visitLobbyRestartGame(LobbyRestartGame & pack) override; + void visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack) override; + void visitLobbyStartGame(LobbyStartGame & pack) override; + void visitLobbyUpdateState(LobbyUpdateState & pack) override; }; class ApplyOnLobbyScreenNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) @@ -51,11 +52,11 @@ public: { } - virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; - virtual void visitLobbyChatMessage(LobbyChatMessage & pack) override; - virtual void visitLobbyGuiAction(LobbyGuiAction & pack) override; - virtual void visitLobbyStartGame(LobbyStartGame & pack) override; - virtual void visitLobbyLoadProgress(LobbyLoadProgress & pack) override; - virtual void visitLobbyUpdateState(LobbyUpdateState & pack) override; - virtual void visitLobbyShowMessage(LobbyShowMessage & pack) override; + void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; + void visitLobbyChatMessage(LobbyChatMessage & pack) override; + void visitLobbyGuiAction(LobbyGuiAction & pack) override; + void visitLobbyStartGame(LobbyStartGame & pack) override; + void visitLobbyLoadProgress(LobbyLoadProgress & pack) override; + void visitLobbyUpdateState(LobbyUpdateState & pack) override; + void visitLobbyShowMessage(LobbyShowMessage & pack) override; }; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 2d9e89eaf..d01890f37 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -27,8 +27,8 @@ #include "../CCallback.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/FileInfo.h" -#include "../lib/serializer/Connection.h" #include "../lib/serializer/BinarySerializer.h" +#include "../lib/serializer/Connection.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/VCMI_Lib.h" @@ -424,7 +424,7 @@ void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface cl.initPlayerEnvironments(); initInterfaces(); } - else if(pack.playerConnectionId == CSH->c->connectionID) + else if(pack.playerConnectionId == CSH->logicConnection->connectionID) { plSettings.connectedPlayerIDs.insert(pack.playerConnectionId); cl.playerint.clear(); diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index aee45c228..6b7108527 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -19,9 +19,13 @@ #include "lobby/ExtraOptionsTab.h" #include "lobby/SelectionTab.h" #include "lobby/CBonusSelection.h" +#include "globalLobby/GlobalLobbyWindow.h" +#include "globalLobby/GlobalLobbyServerSetup.h" +#include "globalLobby/GlobalLobbyClient.h" #include "CServerHandler.h" #include "CGameInfo.h" +#include "Client.h" #include "gui/CGuiHandler.h" #include "gui/WindowHandler.h" #include "widgets/Buttons.h" @@ -36,26 +40,50 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientCon result = false; // Check if it's LobbyClientConnected for our client - if(pack.uuid == handler.c->uuid) + if(pack.uuid == handler.logicConnection->uuid) { - handler.c->connectionID = pack.clientId; + handler.logicConnection->connectionID = pack.clientId; if(handler.mapToStart) + { handler.setMapInfo(handler.mapToStart); + } else if(!settings["session"]["headless"].Bool()) - GH.windows().createAndPushWindow(static_cast(handler.screenType)); - handler.state = EClientState::LOBBY; + { + if (GH.windows().topWindow()) + GH.windows().popWindows(1); + + if (!GH.windows().findWindows().empty()) + { + assert(handler.serverMode == EServerMode::LOBBY_HOST); + // announce opened game room + // TODO: find better approach? + int roomType = settings["lobby"]["roomType"].Integer(); + + if (roomType != 0) + handler.getGlobalLobby().sendOpenPrivateRoom(); + else + handler.getGlobalLobby().sendOpenPublicRoom(); + } + + while (!GH.windows().findWindows().empty()) + { + // if global lobby is open, pop all dialogs on top of it as well as lobby itself + GH.windows().popWindows(1); + } + + GH.windows().createAndPushWindow(handler.screenType); + } + handler.setState(EClientState::LOBBY); } } void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) { - if(pack.clientId != handler.c->connectionID) + if(pack.clientId != handler.logicConnection->connectionID) { result = false; return; } - - handler.stopServerConnection(); } void ApplyOnLobbyScreenNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) @@ -106,30 +134,31 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack } } -void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyEndGame(LobbyEndGame & pack) +void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyRestartGame(LobbyRestartGame & pack) { - if(handler.state == EClientState::GAMEPLAY) - { - handler.endGameplay(pack.closeConnection, pack.restart); - } - - if(pack.restart) - { - if (handler.validateGameStart()) - handler.sendStartGame(); - } + assert(handler.getState() == EClientState::GAMEPLAY); + + handler.restartGameplay(); + handler.sendStartGame(); +} + +void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack) +{ + handler.client = std::make_unique(); + handler.logicConnection->enterLobbyConnectionMode(); + handler.logicConnection->setCallback(handler.client.get()); } void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) { - if(pack.clientId != -1 && pack.clientId != handler.c->connectionID) + if(pack.clientId != -1 && pack.clientId != handler.logicConnection->connectionID) { result = false; return; } - handler.state = EClientState::STARTING; - if(handler.si->mode != StartInfo::LOAD_GAME || pack.clientId == handler.c->connectionID) + handler.setState(EClientState::STARTING); + if(handler.si->mode != EStartMode::LOAD_GAME || pack.clientId == handler.logicConnection->connectionID) { auto modeBackup = handler.si->mode; handler.si = pack.initializedStartInfo; @@ -174,7 +203,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & if(!lobby) //stub: ignore message for game mode return; - if(!lobby->bonusSel && handler.si->campState && handler.state == EClientState::LOBBY_CAMPAIGN) + if(!lobby->bonusSel && handler.si->campState && handler.getState() == EClientState::LOBBY_CAMPAIGN) { lobby->bonusSel = std::make_shared(); GH.windows().pushWindow(lobby->bonusSel); diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 739231f83..e2cd006c1 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -852,12 +852,19 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) owner(owner) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + uint32_t queueSize = QUEUE_SIZE_BIG; + if(embedded) { - pos.w = QUEUE_SIZE * 41; + int32_t queueSmallOutsideYOffset = 65; + bool queueSmallOutside = settings["battle"]["queueSmallOutside"].Bool() && (pos.y - queueSmallOutsideYOffset) >= 0; + queueSize = std::clamp(static_cast(settings["battle"]["queueSmallSlots"].Float()), 1, queueSmallOutside ? GH.screenDimensions().x / 41 : 19); + + pos.w = queueSize * 41; pos.h = 49; pos.x += parent->pos.w/2 - pos.w/2; - pos.y += 10; + pos.y += queueSmallOutside ? -queueSmallOutsideYOffset : 10; icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL")); stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); @@ -878,7 +885,7 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) } stateIcons->preload(); - stackBoxes.resize(QUEUE_SIZE); + stackBoxes.resize(queueSize); for (int i = 0; i < stackBoxes.size(); i++) { stackBoxes[i] = std::make_shared(this); diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index cdd7f0f66..1a3d69ce1 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -239,7 +239,7 @@ class StackQueue : public CIntObject std::optional getBoundUnitID() const; }; - static const int QUEUE_SIZE = 10; + static const int QUEUE_SIZE_BIG = 10; std::shared_ptr background; std::vector> stackBoxes; BattleInterface & owner; diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index adfbbde07..ae53dcf31 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -145,7 +145,7 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) Settings full = settings.write["video"]["fullscreen"]; full->Bool() = !full->Bool(); - GH.onScreenResize(); + GH.onScreenResize(false); return; } } @@ -163,7 +163,7 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) #ifndef VCMI_IOS { boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - GH.onScreenResize(); + GH.onScreenResize(false); } #endif break; diff --git a/client/eventsSDL/InputSourceKeyboard.cpp b/client/eventsSDL/InputSourceKeyboard.cpp index 01b7c990e..0901dc420 100644 --- a/client/eventsSDL/InputSourceKeyboard.cpp +++ b/client/eventsSDL/InputSourceKeyboard.cpp @@ -16,6 +16,8 @@ #include "../gui/CGuiHandler.h" #include "../gui/EventDispatcher.h" #include "../gui/ShortcutHandler.h" +#include "../CServerHandler.h" +#include "../globalLobby/GlobalLobbyClient.h" #include #include @@ -31,6 +33,8 @@ InputSourceKeyboard::InputSourceKeyboard() void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key) { + assert(key.state == SDL_PRESSED); + if (SDL_IsTextInputActive() == SDL_TRUE) { if(key.keysym.sym == SDLK_v && isKeyboardCtrlDown()) @@ -51,7 +55,11 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key) return; // ignore periodic event resends } - assert(key.state == SDL_PRESSED); + + if(key.keysym.sym == SDLK_TAB && isKeyboardCtrlDown()) + { + CSH->getGlobalLobby().activateInterface(); + } if(key.keysym.sym >= SDLK_F1 && key.keysym.sym <= SDLK_F15 && settings["session"]["spectate"].Bool()) { diff --git a/client/globalLobby/GlobalLobbyClient.cpp b/client/globalLobby/GlobalLobbyClient.cpp new file mode 100644 index 000000000..f529602ee --- /dev/null +++ b/client/globalLobby/GlobalLobbyClient.cpp @@ -0,0 +1,366 @@ +/* + * GlobalLobbyClient.cpp, 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 + * + */ + +#include "StdInc.h" +#include "GlobalLobbyClient.h" + +#include "GlobalLobbyLoginWindow.h" +#include "GlobalLobbyWindow.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../windows/InfoWindows.h" +#include "../CServerHandler.h" +#include "../mainmenu/CMainMenu.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/MetaString.h" +#include "../../lib/TextOperations.h" + +GlobalLobbyClient::GlobalLobbyClient() = default; +GlobalLobbyClient::~GlobalLobbyClient() = default; + +static std::string getCurrentTimeFormatted(int timeOffsetSeconds = 0) +{ + // FIXME: better/unified way to format date + auto timeNowChrono = std::chrono::system_clock::now(); + timeNowChrono += std::chrono::seconds(timeOffsetSeconds); + + return TextOperations::getFormattedTimeLocal(std::chrono::system_clock::to_time_t(timeNowChrono)); +} + +void GlobalLobbyClient::onPacketReceived(const std::shared_ptr &, const std::vector & message) +{ + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + JsonNode json(message.data(), message.size()); + + if(json["type"].String() == "accountCreated") + return receiveAccountCreated(json); + + if(json["type"].String() == "operationFailed") + return receiveOperationFailed(json); + + if(json["type"].String() == "loginSuccess") + return receiveLoginSuccess(json); + + if(json["type"].String() == "chatHistory") + return receiveChatHistory(json); + + if(json["type"].String() == "chatMessage") + return receiveChatMessage(json); + + if(json["type"].String() == "activeAccounts") + return receiveActiveAccounts(json); + + if(json["type"].String() == "activeGameRooms") + return receiveActiveGameRooms(json); + + if(json["type"].String() == "joinRoomSuccess") + return receiveJoinRoomSuccess(json); + + if(json["type"].String() == "inviteReceived") + return receiveInviteReceived(json); + + logGlobal->error("Received unexpected message from lobby server: %s", json["type"].String()); +} + +void GlobalLobbyClient::receiveAccountCreated(const JsonNode & json) +{ + auto loginWindowPtr = loginWindow.lock(); + + if(!loginWindowPtr || !GH.windows().topWindow()) + throw std::runtime_error("lobby connection finished without active login window!"); + + { + Settings configID = settings.write["lobby"]["accountID"]; + configID->String() = json["accountID"].String(); + + Settings configName = settings.write["lobby"]["displayName"]; + configName->String() = json["displayName"].String(); + + Settings configCookie = settings.write["lobby"]["accountCookie"]; + configCookie->String() = json["accountCookie"].String(); + } + + sendClientLogin(); +} + +void GlobalLobbyClient::receiveOperationFailed(const JsonNode & json) +{ + auto loginWindowPtr = loginWindow.lock(); + + if(loginWindowPtr) + loginWindowPtr->onConnectionFailed(json["reason"].String()); + + // TODO: handle errors in lobby menu +} + +void GlobalLobbyClient::receiveLoginSuccess(const JsonNode & json) +{ + { + Settings configCookie = settings.write["lobby"]["accountCookie"]; + configCookie->String() = json["accountCookie"].String(); + + Settings configName = settings.write["lobby"]["displayName"]; + configName->String() = json["displayName"].String(); + } + + auto loginWindowPtr = loginWindow.lock(); + + if(!loginWindowPtr || !GH.windows().topWindow()) + throw std::runtime_error("lobby connection finished without active login window!"); + + loginWindowPtr->onConnectionSuccess(); +} + +void GlobalLobbyClient::receiveChatHistory(const JsonNode & json) +{ + for(const auto & entry : json["messages"].Vector()) + { + std::string accountID = entry["accountID"].String(); + std::string displayName = entry["displayName"].String(); + std::string messageText = entry["messageText"].String(); + int ageSeconds = entry["ageSeconds"].Integer(); + std::string timeFormatted = getCurrentTimeFormatted(-ageSeconds); + + auto lobbyWindowPtr = lobbyWindow.lock(); + if(lobbyWindowPtr) + lobbyWindowPtr->onGameChatMessage(displayName, messageText, timeFormatted); + } +} + +void GlobalLobbyClient::receiveChatMessage(const JsonNode & json) +{ + std::string accountID = json["accountID"].String(); + std::string displayName = json["displayName"].String(); + std::string messageText = json["messageText"].String(); + std::string timeFormatted = getCurrentTimeFormatted(); + auto lobbyWindowPtr = lobbyWindow.lock(); + if(lobbyWindowPtr) + lobbyWindowPtr->onGameChatMessage(displayName, messageText, timeFormatted); +} + +void GlobalLobbyClient::receiveActiveAccounts(const JsonNode & json) +{ + activeAccounts.clear(); + + for (auto const & jsonEntry : json["accounts"].Vector()) + { + GlobalLobbyAccount account; + + account.accountID = jsonEntry["accountID"].String(); + account.displayName = jsonEntry["displayName"].String(); + account.status = jsonEntry["status"].String(); + + activeAccounts.push_back(account); + } + + auto lobbyWindowPtr = lobbyWindow.lock(); + if(lobbyWindowPtr) + lobbyWindowPtr->onActiveAccounts(activeAccounts); +} + +void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json) +{ + activeRooms.clear(); + + for (auto const & jsonEntry : json["gameRooms"].Vector()) + { + GlobalLobbyRoom room; + + room.gameRoomID = jsonEntry["gameRoomID"].String(); + room.hostAccountID = jsonEntry["hostAccountID"].String(); + room.hostAccountDisplayName = jsonEntry["hostAccountDisplayName"].String(); + room.description = jsonEntry["description"].String(); + room.playersCount = jsonEntry["playersCount"].Integer(); + room.playersLimit = jsonEntry["playersLimit"].Integer(); + + activeRooms.push_back(room); + } + + auto lobbyWindowPtr = lobbyWindow.lock(); + if(lobbyWindowPtr) + lobbyWindowPtr->onActiveRooms(activeRooms); +} + +void GlobalLobbyClient::receiveInviteReceived(const JsonNode & json) +{ + assert(0); //TODO +} + +void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json) +{ + Settings configRoom = settings.write["lobby"]["roomID"]; + configRoom->String() = json["gameRoomID"].String(); + + if (json["proxyMode"].Bool()) + { + CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_GUEST, {}); + CSH->loadMode = ELoadMode::MULTI; + + std::string hostname = settings["lobby"]["hostname"].String(); + int16_t port = settings["lobby"]["port"].Integer(); + CSH->connectToServer(hostname, port); + } +} + +void GlobalLobbyClient::onConnectionEstablished(const std::shared_ptr & connection) +{ + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + networkConnection = connection; + + auto loginWindowPtr = loginWindow.lock(); + + if(!loginWindowPtr || !GH.windows().topWindow()) + throw std::runtime_error("lobby connection established without active login window!"); + + loginWindowPtr->onConnectionSuccess(); +} + +void GlobalLobbyClient::sendClientRegister(const std::string & accountName) +{ + JsonNode toSend; + toSend["type"].String() = "clientRegister"; + toSend["displayName"].String() = accountName; + sendMessage(toSend); +} + +void GlobalLobbyClient::sendClientLogin() +{ + JsonNode toSend; + toSend["type"].String() = "clientLogin"; + toSend["accountID"] = settings["lobby"]["accountID"]; + toSend["accountCookie"] = settings["lobby"]["accountCookie"]; + sendMessage(toSend); +} + +void GlobalLobbyClient::onConnectionFailed(const std::string & errorMessage) +{ + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + auto loginWindowPtr = loginWindow.lock(); + + if(!loginWindowPtr || !GH.windows().topWindow()) + throw std::runtime_error("lobby connection failed without active login window!"); + + logGlobal->warn("Connection to game lobby failed! Reason: %s", errorMessage); + loginWindowPtr->onConnectionFailed(errorMessage); +} + +void GlobalLobbyClient::onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) +{ + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + assert(connection == networkConnection); + networkConnection.reset(); + + while (!GH.windows().findWindows().empty()) + { + // if global lobby is open, pop all dialogs on top of it as well as lobby itself + GH.windows().popWindows(1); + } + + CInfoWindow::showInfoDialog("Connection to game lobby was lost!", {}); +} + +void GlobalLobbyClient::sendMessage(const JsonNode & data) +{ + networkConnection->sendPacket(data.toBytes(true)); +} + +void GlobalLobbyClient::sendOpenPublicRoom() +{ + JsonNode toSend; + toSend["type"].String() = "openGameRoom"; + toSend["hostAccountID"] = settings["lobby"]["accountID"]; + toSend["roomType"].String() = "public"; + sendMessage(toSend); +} + +void GlobalLobbyClient::sendOpenPrivateRoom() +{ + JsonNode toSend; + toSend["type"].String() = "openGameRoom"; + toSend["hostAccountID"] = settings["lobby"]["accountID"]; + toSend["roomType"].String() = "private"; + sendMessage(toSend); +} + +void GlobalLobbyClient::connect() +{ + std::string hostname = settings["lobby"]["hostname"].String(); + int16_t port = settings["lobby"]["port"].Integer(); + CSH->getNetworkHandler().connectToRemote(*this, hostname, port); +} + +bool GlobalLobbyClient::isConnected() const +{ + return networkConnection != nullptr; +} + +std::shared_ptr GlobalLobbyClient::createLoginWindow() +{ + auto loginWindowPtr = loginWindow.lock(); + if(loginWindowPtr) + return loginWindowPtr; + + auto loginWindowNew = std::make_shared(); + loginWindow = loginWindowNew; + + return loginWindowNew; +} + +std::shared_ptr GlobalLobbyClient::createLobbyWindow() +{ + auto lobbyWindowPtr = lobbyWindow.lock(); + if(lobbyWindowPtr) + return lobbyWindowPtr; + + lobbyWindowPtr = std::make_shared(); + lobbyWindow = lobbyWindowPtr; + lobbyWindowLock = lobbyWindowPtr; + return lobbyWindowPtr; +} + +const std::vector & GlobalLobbyClient::getActiveAccounts() const +{ + return activeAccounts; +} + +const std::vector & GlobalLobbyClient::getActiveRooms() const +{ + return activeRooms; +} + +void GlobalLobbyClient::activateInterface() +{ + if (!GH.windows().findWindows().empty()) + return; + + if (!GH.windows().findWindows().empty()) + return; + + if (isConnected()) + GH.windows().pushWindow(createLobbyWindow()); + else + GH.windows().pushWindow(createLoginWindow()); +} + +void GlobalLobbyClient::sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection) +{ + JsonNode toSend; + toSend["type"].String() = "clientProxyLogin"; + toSend["accountID"] = settings["lobby"]["accountID"]; + toSend["accountCookie"] = settings["lobby"]["accountCookie"]; + toSend["gameRoomID"] = settings["lobby"]["roomID"]; + + netConnection->sendPacket(toSend.toBytes(true)); +} diff --git a/client/globalLobby/GlobalLobbyClient.h b/client/globalLobby/GlobalLobbyClient.h new file mode 100644 index 000000000..b269c7018 --- /dev/null +++ b/client/globalLobby/GlobalLobbyClient.h @@ -0,0 +1,70 @@ +/* + * GlobalLobbyClient.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 "GlobalLobbyDefines.h" +#include "../../lib/network/NetworkInterface.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class GlobalLobbyLoginWindow; +class GlobalLobbyWindow; + +class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyable +{ + std::vector activeAccounts; + std::vector activeRooms; + + std::shared_ptr networkConnection; + + std::weak_ptr loginWindow; + std::weak_ptr lobbyWindow; + std::shared_ptr lobbyWindowLock; // helper strong reference to prevent window destruction on closing + + void onPacketReceived(const std::shared_ptr &, const std::vector & message) override; + void onConnectionFailed(const std::string & errorMessage) override; + void onConnectionEstablished(const std::shared_ptr &) override; + void onDisconnected(const std::shared_ptr &, const std::string & errorMessage) override; + + void receiveAccountCreated(const JsonNode & json); + void receiveOperationFailed(const JsonNode & json); + void receiveLoginSuccess(const JsonNode & json); + void receiveChatHistory(const JsonNode & json); + void receiveChatMessage(const JsonNode & json); + void receiveActiveAccounts(const JsonNode & json); + void receiveActiveGameRooms(const JsonNode & json); + void receiveJoinRoomSuccess(const JsonNode & json); + void receiveInviteReceived(const JsonNode & json); + + std::shared_ptr createLoginWindow(); + std::shared_ptr createLobbyWindow(); + +public: + explicit GlobalLobbyClient(); + ~GlobalLobbyClient(); + + const std::vector & getActiveAccounts() const; + const std::vector & getActiveRooms() const; + + /// Activate interface and pushes lobby UI as top window + void activateInterface(); + void sendMessage(const JsonNode & data); + void sendClientRegister(const std::string & accountName); + void sendClientLogin(); + void sendOpenPublicRoom(); + void sendOpenPrivateRoom(); + + void sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection); + + void connect(); + bool isConnected() const; +}; diff --git a/client/globalLobby/GlobalLobbyDefines.h b/client/globalLobby/GlobalLobbyDefines.h new file mode 100644 index 000000000..ae61d3516 --- /dev/null +++ b/client/globalLobby/GlobalLobbyDefines.h @@ -0,0 +1,27 @@ +/* + * GlobalLobbyClient.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 + +struct GlobalLobbyAccount +{ + std::string accountID; + std::string displayName; + std::string status; +}; + +struct GlobalLobbyRoom +{ + std::string gameRoomID; + std::string hostAccountID; + std::string hostAccountDisplayName; + std::string description; + int playersCount; + int playersLimit; +}; diff --git a/client/globalLobby/GlobalLobbyLoginWindow.cpp b/client/globalLobby/GlobalLobbyLoginWindow.cpp new file mode 100644 index 000000000..673941c9f --- /dev/null +++ b/client/globalLobby/GlobalLobbyLoginWindow.cpp @@ -0,0 +1,128 @@ +/* + * GlobalLobbyLoginWindow.cpp, 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 + * + */ + +#include "StdInc.h" +#include "GlobalLobbyLoginWindow.h" + +#include "GlobalLobbyClient.h" +#include "GlobalLobbyWindow.h" + +#include "../CGameInfo.h" +#include "../CServerHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../widgets/Buttons.h" +#include "../widgets/Images.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/TextControls.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/MetaString.h" + +GlobalLobbyLoginWindow::GlobalLobbyLoginWindow() + : CWindowObject(BORDERED) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos.w = 284; + pos.h = 220; + + filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); + labelTitle = std::make_shared( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title")); + labelUsername = std::make_shared( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username")); + backgroundUsername = std::make_shared(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64)); + inputUsername = std::make_shared(Rect(15, 93, 260, 16), FONT_SMALL, nullptr, ETextAlignment::TOPLEFT, true); + buttonLogin = std::make_shared(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); }); + buttonClose = std::make_shared(Point(210, 180), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }); + labelStatus = std::make_shared( "", Rect(15, 115, 255, 60), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + + auto buttonRegister = std::make_shared(Point(10, 40), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0); + auto buttonLogin = std::make_shared(Point(146, 40), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0); + buttonRegister->addTextOverlay(CGI->generaltexth->translate("vcmi.lobby.login.create"), EFonts::FONT_SMALL, Colors::YELLOW); + buttonLogin->addTextOverlay(CGI->generaltexth->translate("vcmi.lobby.login.login"), EFonts::FONT_SMALL, Colors::YELLOW); + + toggleMode = std::make_shared(nullptr); + toggleMode->addToggle(0, buttonRegister); + toggleMode->addToggle(1, buttonLogin); + toggleMode->setSelected(settings["lobby"]["roomType"].Integer()); + toggleMode->addCallback([this](int index){onLoginModeChanged(index);}); + + if (settings["lobby"]["accountID"].String().empty()) + { + buttonLogin->block(true); + toggleMode->setSelected(0); + } + else + toggleMode->setSelected(1); + + filledBackground->playerColored(PlayerColor(1)); + inputUsername->cb += [this](const std::string & text) + { + this->buttonLogin->block(text.empty()); + }; + + center(); +} + +void GlobalLobbyLoginWindow::onLoginModeChanged(int value) +{ + if (value == 0) + { + inputUsername->setText(""); + } + else + { + inputUsername->setText(settings["lobby"]["displayName"].String()); + } +} + +void GlobalLobbyLoginWindow::onClose() +{ + close(); + // TODO: abort ongoing connection attempt, if any +} + +void GlobalLobbyLoginWindow::onLogin() +{ + labelStatus->setText(CGI->generaltexth->translate("vcmi.lobby.login.connecting")); + if(!CSH->getGlobalLobby().isConnected()) + CSH->getGlobalLobby().connect(); + else + onConnectionSuccess(); + + buttonClose->block(true); +} + +void GlobalLobbyLoginWindow::onConnectionSuccess() +{ + std::string accountID = settings["lobby"]["accountID"].String(); + + if(toggleMode->getSelected() == 0) + CSH->getGlobalLobby().sendClientRegister(inputUsername->getText()); + else + CSH->getGlobalLobby().sendClientLogin(); +} + +void GlobalLobbyLoginWindow::onLoginSuccess() +{ + close(); + CSH->getGlobalLobby().activateInterface(); +} + +void GlobalLobbyLoginWindow::onConnectionFailed(const std::string & reason) +{ + MetaString formatter; + formatter.appendTextID("vcmi.lobby.login.error"); + formatter.replaceRawString(reason); + + labelStatus->setText(formatter.toString()); + buttonClose->block(false); +} diff --git a/client/globalLobby/GlobalLobbyLoginWindow.h b/client/globalLobby/GlobalLobbyLoginWindow.h new file mode 100644 index 000000000..a589ac650 --- /dev/null +++ b/client/globalLobby/GlobalLobbyLoginWindow.h @@ -0,0 +1,45 @@ +/* + * GlobalLobbyLoginWindow.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 "../windows/CWindowObject.h" + +class CLabel; +class CTextBox; +class CTextInput; +class CToggleGroup; +class FilledTexturePlayerColored; +class TransparentFilledRectangle; +class CButton; + +class GlobalLobbyLoginWindow : public CWindowObject +{ + std::shared_ptr filledBackground; + std::shared_ptr labelTitle; + std::shared_ptr labelUsername; + std::shared_ptr labelStatus; + std::shared_ptr backgroundUsername; + std::shared_ptr inputUsername; + + std::shared_ptr buttonLogin; + std::shared_ptr buttonClose; + std::shared_ptr toggleMode; // create account or use existing + + void onLoginModeChanged(int value); + void onClose(); + void onLogin(); + +public: + GlobalLobbyLoginWindow(); + + void onConnectionSuccess(); + void onLoginSuccess(); + void onConnectionFailed(const std::string & reason); +}; diff --git a/client/globalLobby/GlobalLobbyServerSetup.cpp b/client/globalLobby/GlobalLobbyServerSetup.cpp new file mode 100644 index 000000000..eb566390f --- /dev/null +++ b/client/globalLobby/GlobalLobbyServerSetup.cpp @@ -0,0 +1,142 @@ +/* + * GlobalLobbyServerSetup.cpp, 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 + * + */ + +#include "StdInc.h" +#include "GlobalLobbyServerSetup.h" + +#include "../CGameInfo.h" +#include "../CServerHandler.h" +#include "../gui/CGuiHandler.h" +#include "../mainmenu/CMainMenu.h" +#include "../widgets/Buttons.h" +#include "../widgets/Images.h" +#include "../widgets/TextControls.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/MetaString.h" + +GlobalLobbyServerSetup::GlobalLobbyServerSetup() + : CWindowObject(BORDERED) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos.w = 284; + pos.h = 340; + + filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); + labelTitle = std::make_shared( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.create")); + labelPlayerLimit = std::make_shared( pos.w / 2, 48, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.players.limit")); + labelRoomType = std::make_shared( pos.w / 2, 108, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.type")); + labelGameMode = std::make_shared( pos.w / 2, 158, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.mode")); + + togglePlayerLimit = std::make_shared(nullptr); + togglePlayerLimit->addToggle(2, std::make_shared(Point(10 + 39*0, 60), AnimationPath::builtin("RanNum2"), CButton::tooltip(), 0)); + togglePlayerLimit->addToggle(3, std::make_shared(Point(10 + 39*1, 60), AnimationPath::builtin("RanNum3"), CButton::tooltip(), 0)); + togglePlayerLimit->addToggle(4, std::make_shared(Point(10 + 39*2, 60), AnimationPath::builtin("RanNum4"), CButton::tooltip(), 0)); + togglePlayerLimit->addToggle(5, std::make_shared(Point(10 + 39*3, 60), AnimationPath::builtin("RanNum5"), CButton::tooltip(), 0)); + togglePlayerLimit->addToggle(6, std::make_shared(Point(10 + 39*4, 60), AnimationPath::builtin("RanNum6"), CButton::tooltip(), 0)); + togglePlayerLimit->addToggle(7, std::make_shared(Point(10 + 39*5, 60), AnimationPath::builtin("RanNum7"), CButton::tooltip(), 0)); + togglePlayerLimit->addToggle(8, std::make_shared(Point(10 + 39*6, 60), AnimationPath::builtin("RanNum8"), CButton::tooltip(), 0)); + togglePlayerLimit->setSelected(settings["lobby"]["roomPlayerLimit"].Integer()); + togglePlayerLimit->addCallback([this](int index){onPlayerLimitChanged(index);}); + + auto buttonPublic = std::make_shared(Point(10, 120), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0); + auto buttonPrivate = std::make_shared(Point(146, 120), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0); + buttonPublic->addTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.public"), EFonts::FONT_SMALL, Colors::YELLOW); + buttonPrivate->addTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.private"), EFonts::FONT_SMALL, Colors::YELLOW); + + toggleRoomType = std::make_shared(nullptr); + toggleRoomType->addToggle(0, buttonPublic); + toggleRoomType->addToggle(1, buttonPrivate); + toggleRoomType->setSelected(settings["lobby"]["roomType"].Integer()); + toggleRoomType->addCallback([this](int index){onRoomTypeChanged(index);}); + + auto buttonNewGame = std::make_shared(Point(10, 170), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0); + auto buttonLoadGame = std::make_shared(Point(146, 170), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0); + buttonNewGame->addTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.new"), EFonts::FONT_SMALL, Colors::YELLOW); + buttonLoadGame->addTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.load"), EFonts::FONT_SMALL, Colors::YELLOW); + + toggleGameMode = std::make_shared(nullptr); + toggleGameMode->addToggle(0, buttonNewGame); + toggleGameMode->addToggle(1, buttonLoadGame); + toggleGameMode->setSelected(settings["lobby"]["roomMode"].Integer()); + toggleGameMode->addCallback([this](int index){onGameModeChanged(index);}); + + labelDescription = std::make_shared("", Rect(10, 195, pos.w - 20, 80), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + + buttonCreate = std::make_shared(Point(10, 300), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onCreate(); }); + buttonClose = std::make_shared(Point(210, 300), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }); + + filledBackground->playerColored(PlayerColor(1)); + + updateDescription(); + center(); +} + +void GlobalLobbyServerSetup::updateDescription() +{ + MetaString description; + description.appendRawString("%s %s %s"); + if(toggleRoomType->getSelected() == 0) + description.replaceTextID("vcmi.lobby.room.description.public"); + else + description.replaceTextID("vcmi.lobby.room.description.private"); + + if(toggleGameMode->getSelected() == 0) + description.replaceTextID("vcmi.lobby.room.description.new"); + else + description.replaceTextID("vcmi.lobby.room.description.load"); + + description.replaceTextID("vcmi.lobby.room.description.limit"); + description.replaceNumber(togglePlayerLimit->getSelected()); + + labelDescription->setText(description.toString()); +} + +void GlobalLobbyServerSetup::onPlayerLimitChanged(int value) +{ + Settings config = settings.write["lobby"]["roomPlayerLimit"]; + config->Integer() = value; + updateDescription(); +} + +void GlobalLobbyServerSetup::onRoomTypeChanged(int value) +{ + Settings config = settings.write["lobby"]["roomType"]; + config->Integer() = value; + updateDescription(); +} + +void GlobalLobbyServerSetup::onGameModeChanged(int value) +{ + Settings config = settings.write["lobby"]["roomMode"]; + config->Integer() = value; + updateDescription(); +} + +void GlobalLobbyServerSetup::onCreate() +{ + if(toggleGameMode->getSelected() == 0) + CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_HOST, {}); + else + CSH->resetStateForLobby(EStartMode::LOAD_GAME, ESelectionScreen::loadGame, EServerMode::LOBBY_HOST, {}); + + CSH->loadMode = ELoadMode::MULTI; + CSH->startLocalServerAndConnect(true); + + buttonCreate->block(true); + buttonClose->block(true); +} + +void GlobalLobbyServerSetup::onClose() +{ + close(); +} diff --git a/client/globalLobby/GlobalLobbyServerSetup.h b/client/globalLobby/GlobalLobbyServerSetup.h new file mode 100644 index 000000000..f8fa09d34 --- /dev/null +++ b/client/globalLobby/GlobalLobbyServerSetup.h @@ -0,0 +1,49 @@ +/* + * GlobalLobbyServerSetup.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 "../windows/CWindowObject.h" + +class CLabel; +class CTextBox; +class FilledTexturePlayerColored; +class CButton; +class CToggleGroup; + +class GlobalLobbyServerSetup : public CWindowObject +{ + std::shared_ptr filledBackground; + std::shared_ptr labelTitle; + + std::shared_ptr labelPlayerLimit; + std::shared_ptr labelRoomType; + std::shared_ptr labelGameMode; + + std::shared_ptr togglePlayerLimit; // 2-8 + std::shared_ptr toggleRoomType; // public or private + std::shared_ptr toggleGameMode; // new game or load game + + std::shared_ptr labelDescription; + std::shared_ptr labelStatus; + + std::shared_ptr buttonCreate; + std::shared_ptr buttonClose; + + void updateDescription(); + void onPlayerLimitChanged(int value); + void onRoomTypeChanged(int value); + void onGameModeChanged(int value); + + void onCreate(); + void onClose(); + +public: + GlobalLobbyServerSetup(); +}; diff --git a/client/globalLobby/GlobalLobbyWidget.cpp b/client/globalLobby/GlobalLobbyWidget.cpp new file mode 100644 index 000000000..2be6b21e8 --- /dev/null +++ b/client/globalLobby/GlobalLobbyWidget.cpp @@ -0,0 +1,154 @@ +/* + * GlobalLobbyWidget.cpp, 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 + * + */ + +#include "StdInc.h" +#include "GlobalLobbyWidget.h" + +#include "GlobalLobbyClient.h" +#include "GlobalLobbyWindow.h" + +#include "../CServerHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../widgets/Buttons.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/ObjectLists.h" +#include "../widgets/TextControls.h" + +#include "../../lib/MetaString.h" +GlobalLobbyWidget::GlobalLobbyWidget(GlobalLobbyWindow * window) + : window(window) +{ + addCallback("closeWindow", [](int) { GH.windows().popWindows(1); }); + addCallback("sendMessage", [this](int) { this->window->doSendChatMessage(); }); + addCallback("createGameRoom", [this](int) { this->window->doCreateGameRoom(); }); + + REGISTER_BUILDER("accountList", &GlobalLobbyWidget::buildAccountList); + REGISTER_BUILDER("roomList", &GlobalLobbyWidget::buildRoomList); + + const JsonNode config(JsonPath::builtin("config/widgets/lobbyWindow.json")); + build(config); +} + +std::shared_ptr GlobalLobbyWidget::buildAccountList(const JsonNode & config) const +{ + const auto & createCallback = [this](size_t index) -> std::shared_ptr + { + const auto & accounts = CSH->getGlobalLobby().getActiveAccounts(); + + if(index < accounts.size()) + return std::make_shared(this->window, accounts[index]); + return std::make_shared(); + }; + + auto position = readPosition(config["position"]); + auto itemOffset = readPosition(config["itemOffset"]); + auto sliderPosition = readPosition(config["sliderPosition"]); + auto sliderSize = readPosition(config["sliderSize"]); + size_t visibleSize = 4; // FIXME: how many items can fit into UI? + size_t totalSize = 4; //FIXME: how many items are there in total + int sliderMode = 1 | 4; // present, vertical, blue + int initialPos = 0; + + return std::make_shared(createCallback, position, itemOffset, visibleSize, totalSize, initialPos, sliderMode, Rect(sliderPosition, sliderSize) ); +} + +std::shared_ptr GlobalLobbyWidget::buildRoomList(const JsonNode & config) const +{ + const auto & createCallback = [this](size_t index) -> std::shared_ptr + { + const auto & rooms = CSH->getGlobalLobby().getActiveRooms(); + + if(index < rooms.size()) + return std::make_shared(this->window, rooms[index]); + return std::make_shared(); + }; + + auto position = readPosition(config["position"]); + auto itemOffset = readPosition(config["itemOffset"]); + auto sliderPosition = readPosition(config["sliderPosition"]); + auto sliderSize = readPosition(config["sliderSize"]); + size_t visibleSize = 4; // FIXME: how many items can fit into UI? + size_t totalSize = 4; //FIXME: how many items are there in total + int sliderMode = 1 | 4; // present, vertical, blue + int initialPos = 0; + + return std::make_shared(createCallback, position, itemOffset, visibleSize, totalSize, initialPos, sliderMode, Rect(sliderPosition, sliderSize) ); +} + +std::shared_ptr GlobalLobbyWidget::getAccountNameLabel() +{ + return widget("accountNameLabel"); +} + +std::shared_ptr GlobalLobbyWidget::getMessageInput() +{ + return widget("messageInput"); +} + +std::shared_ptr GlobalLobbyWidget::getGameChat() +{ + return widget("gameChat"); +} + +std::shared_ptr GlobalLobbyWidget::getAccountList() +{ + return widget("accountList"); +} + +std::shared_ptr GlobalLobbyWidget::getRoomList() +{ + return widget("roomList"); +} + +GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + const auto & onInviteClicked = [window, accountID=accountDescription.accountID]() + { + window->doInviteAccount(accountID); + }; + + pos.w = 130; + pos.h = 40; + + backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64)); + labelName = std::make_shared( 5, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, accountDescription.displayName); + labelStatus = std::make_shared( 5, 20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, accountDescription.status); + + if (CSH->inLobbyRoom()) + buttonInvite = std::make_shared(Point(95, 8), AnimationPath::builtin("settingsWindow/button32"), CButton::tooltip(), onInviteClicked); +} + +GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + const auto & onJoinClicked = [window, roomID=roomDescription.gameRoomID]() + { + window->doJoinRoom(roomID); + }; + + auto roomSizeText = MetaString::createFromRawString("%d/%d"); + roomSizeText.replaceNumber(roomDescription.playersCount); + roomSizeText.replaceNumber(roomDescription.playersLimit); + + pos.w = 230; + pos.h = 40; + + backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64)); + labelName = std::make_shared( 5, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName); + labelStatus = std::make_shared( 5, 20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, roomDescription.description); + labelRoomSize = std::make_shared( 160, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, roomSizeText.toString()); + + if (!CSH->inGame()) + buttonJoin = std::make_shared(Point(195, 8), AnimationPath::builtin("settingsWindow/button32"), CButton::tooltip(), onJoinClicked); +} diff --git a/client/globalLobby/GlobalLobbyWidget.h b/client/globalLobby/GlobalLobbyWidget.h new file mode 100644 index 000000000..c4a52e688 --- /dev/null +++ b/client/globalLobby/GlobalLobbyWidget.h @@ -0,0 +1,57 @@ +/* + * GlobalLobbyWidget.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 "../gui/InterfaceObjectConfigurable.h" + +class GlobalLobbyWindow; +struct GlobalLobbyAccount; +struct GlobalLobbyRoom; +class CListBox; + +class GlobalLobbyWidget : public InterfaceObjectConfigurable +{ + GlobalLobbyWindow * window; + + std::shared_ptr buildAccountList(const JsonNode &) const; + std::shared_ptr buildRoomList(const JsonNode &) const; + +public: + explicit GlobalLobbyWidget(GlobalLobbyWindow * window); + + std::shared_ptr getAccountNameLabel(); + std::shared_ptr getMessageInput(); + std::shared_ptr getGameChat(); + std::shared_ptr getAccountList(); + std::shared_ptr getRoomList(); +}; + +class GlobalLobbyAccountCard : public CIntObject +{ +public: + GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription); + + std::shared_ptr backgroundOverlay; + std::shared_ptr labelName; + std::shared_ptr labelStatus; + std::shared_ptr buttonInvite; +}; + +class GlobalLobbyRoomCard : public CIntObject +{ +public: + GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription); + + std::shared_ptr backgroundOverlay; + std::shared_ptr labelName; + std::shared_ptr labelRoomSize; + std::shared_ptr labelStatus; + std::shared_ptr buttonJoin; +}; diff --git a/client/globalLobby/GlobalLobbyWindow.cpp b/client/globalLobby/GlobalLobbyWindow.cpp new file mode 100644 index 000000000..5ef963cfc --- /dev/null +++ b/client/globalLobby/GlobalLobbyWindow.cpp @@ -0,0 +1,105 @@ +/* + * GlobalLobbyWindow.cpp, 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 + * + */ + +#include "StdInc.h" +#include "GlobalLobbyWindow.h" + +#include "GlobalLobbyClient.h" +#include "GlobalLobbyServerSetup.h" +#include "GlobalLobbyWidget.h" + +#include "../CServerHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../widgets/TextControls.h" +#include "../widgets/ObjectLists.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/MetaString.h" + +GlobalLobbyWindow::GlobalLobbyWindow() + : CWindowObject(BORDERED) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + widget = std::make_shared(this); + pos = widget->pos; + center(); + + widget->getAccountNameLabel()->setText(settings["lobby"]["displayName"].String()); +} + +void GlobalLobbyWindow::doSendChatMessage() +{ + std::string messageText = widget->getMessageInput()->getText(); + + JsonNode toSend; + toSend["type"].String() = "sendChatMessage"; + toSend["messageText"].String() = messageText; + + CSH->getGlobalLobby().sendMessage(toSend); + + widget->getMessageInput()->setText(""); +} + +void GlobalLobbyWindow::doCreateGameRoom() +{ + GH.windows().createAndPushWindow(); +} + +void GlobalLobbyWindow::doInviteAccount(const std::string & accountID) +{ + JsonNode toSend; + toSend["type"].String() = "sendInvite"; + toSend["accountID"].String() = accountID; + + CSH->getGlobalLobby().sendMessage(toSend); +} + +void GlobalLobbyWindow::doJoinRoom(const std::string & roomID) +{ + JsonNode toSend; + toSend["type"].String() = "joinGameRoom"; + toSend["gameRoomID"].String() = roomID; + + CSH->getGlobalLobby().sendMessage(toSend); +} + +void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when) +{ + MetaString chatMessageFormatted; + chatMessageFormatted.appendRawString("[%s] {%s}: %s\n"); + chatMessageFormatted.replaceRawString(when); + chatMessageFormatted.replaceRawString(sender); + chatMessageFormatted.replaceRawString(message); + + chatHistory += chatMessageFormatted.toString(); + + widget->getGameChat()->setText(chatHistory); +} + +void GlobalLobbyWindow::onActiveAccounts(const std::vector & accounts) +{ + widget->getAccountList()->reset(); +} + +void GlobalLobbyWindow::onActiveRooms(const std::vector & rooms) +{ + widget->getRoomList()->reset(); +} + +void GlobalLobbyWindow::onJoinedRoom() +{ + widget->getAccountList()->reset(); +} + +void GlobalLobbyWindow::onLeftRoom() +{ + widget->getAccountList()->reset(); +} diff --git a/client/globalLobby/GlobalLobbyWindow.h b/client/globalLobby/GlobalLobbyWindow.h new file mode 100644 index 000000000..d6d868653 --- /dev/null +++ b/client/globalLobby/GlobalLobbyWindow.h @@ -0,0 +1,38 @@ +/* + * GlobalLobbyWindow.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 "../windows/CWindowObject.h" + +class GlobalLobbyWidget; +struct GlobalLobbyAccount; +struct GlobalLobbyRoom; + +class GlobalLobbyWindow : public CWindowObject +{ + std::string chatHistory; + + std::shared_ptr widget; + +public: + GlobalLobbyWindow(); + + void doSendChatMessage(); + void doCreateGameRoom(); + + void doInviteAccount(const std::string & accountID); + void doJoinRoom(const std::string & roomID); + + void onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when); + void onActiveAccounts(const std::vector & accounts); + void onActiveRooms(const std::vector & rooms); + void onJoinedRoom(); + void onLeftRoom(); +}; diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index 72bc72c37..2bffc662c 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -251,8 +251,11 @@ void CGuiHandler::setStatusbar(std::shared_ptr newStatusBar) currentStatusBar = newStatusBar; } -void CGuiHandler::onScreenResize() +void CGuiHandler::onScreenResize(bool resolutionChanged) { - screenHandler().onScreenResize(); + if(resolutionChanged) + { + screenHandler().onScreenResize(); + } windows().onScreenResize(); } diff --git a/client/gui/CGuiHandler.h b/client/gui/CGuiHandler.h index 4f651c159..a8a555264 100644 --- a/client/gui/CGuiHandler.h +++ b/client/gui/CGuiHandler.h @@ -92,8 +92,8 @@ public: void init(); void renderFrame(); - /// called whenever user selects different resolution, requiring to center/resize all windows - void onScreenResize(); + /// called whenever SDL_WINDOWEVENT_RESTORED is reported or the user selects a different resolution, requiring to center/resize all windows + void onScreenResize(bool resolutionChanged); void handleEvents(); //takes events from queue and calls interested objects void fakeMouseMove(); diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 27383365d..b12bb75b1 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -129,6 +129,12 @@ void InterfaceObjectConfigurable::build(const JsonNode &config) for(const auto & item : items->Vector()) addWidget(item["name"].String(), buildWidget(item)); + + // load only if set + if (!config["width"].isNull()) + pos.w = config["width"].Integer(); + if (!config["height"].isNull()) + pos.h = config["height"].Integer(); } void InterfaceObjectConfigurable::addConditional(const std::string & name, bool active) diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 0939e776b..558b97a15 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -15,6 +15,7 @@ #include #include "CSelectionBase.h" +#include "ExtraOptionsTab.h" #include "../CGameInfo.h" #include "../CMusicHandler.h" @@ -86,7 +87,7 @@ CBonusSelection::CBonusSelection() labelMapDescription = std::make_shared(481, 253, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]); mapDescription = std::make_shared("", Rect(480, 278, 292, 108), 1); - labelChooseBonus = std::make_shared(511, 432, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[71]); + labelChooseBonus = std::make_shared(475, 432, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[71]); groupBonuses = std::make_shared(std::bind(&IServerAPI::setCampaignBonus, CSH, _1)); flagbox = std::make_shared(Rect(486, 407, 335, 23)); @@ -94,17 +95,17 @@ CBonusSelection::CBonusSelection() std::vector difficulty; std::string difficultyString = CGI->generaltexth->allTexts[492]; boost::split(difficulty, difficultyString, boost::is_any_of(" ")); - labelDifficulty = std::make_shared(689, 432, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, difficulty.back()); + labelDifficulty = std::make_shared(724, settings["general"]["enableUiEnhancements"].Bool() ? 457 : 432, FONT_MEDIUM, ETextAlignment::TOPCENTER, Colors::WHITE, difficulty.back()); for(size_t b = 0; b < difficultyIcons.size(); ++b) { - difficultyIcons[b] = std::make_shared(AnimationPath::builtinTODO("GSPBUT" + std::to_string(b + 3) + ".DEF"), 0, 0, 709, 455); + difficultyIcons[b] = std::make_shared(AnimationPath::builtinTODO("GSPBUT" + std::to_string(b + 3) + ".DEF"), 0, 0, 709, settings["general"]["enableUiEnhancements"].Bool() ? 480 : 455); } if(getCampaign()->playerSelectedDifficulty()) { - buttonDifficultyLeft = std::make_shared(Point(694, 508), AnimationPath::builtin("SCNRBLF.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this)); - buttonDifficultyRight = std::make_shared(Point(738, 508), AnimationPath::builtin("SCNRBRT.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this)); + buttonDifficultyLeft = std::make_shared(settings["general"]["enableUiEnhancements"].Bool() ? Point(693, 495) : Point(694, 508), AnimationPath::builtin("SCNRBLF.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this)); + buttonDifficultyRight = std::make_shared(settings["general"]["enableUiEnhancements"].Bool() ? Point(739, 495) : Point(738, 508), AnimationPath::builtin("SCNRBRT.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this)); } for(auto scenarioID : getCampaign()->allScenarios()) @@ -117,6 +118,16 @@ CBonusSelection::CBonusSelection() if (!getCampaign()->getMusic().empty()) CCS->musich->playMusic( getCampaign()->getMusic(), true, false); + + if(settings["general"]["enableUiEnhancements"].Bool()) + { + tabExtraOptions = std::make_shared(); + tabExtraOptions->recActions = UPDATE | SHOWALL | LCLICK | RCLICK_POPUP; + tabExtraOptions->recreate(true); + tabExtraOptions->setEnabled(false); + buttonExtraOptions = std::make_shared(Point(643, 431), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], [this]{ tabExtraOptions->setEnabled(!tabExtraOptions->isActive()); GH.windows().totalRedraw(); }, EShortcut::NONE); + buttonExtraOptions->addTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.extraOptions.hover"), FONT_SMALL, Colors::WHITE); + } } void CBonusSelection::createBonusesIcons() @@ -307,15 +318,12 @@ void CBonusSelection::createBonusesIcons() void CBonusSelection::updateAfterStateChange() { - if(CSH->state != EClientState::GAMEPLAY) + if(CSH->getState() != EClientState::GAMEPLAY) { buttonRestart->disable(); buttonVideo->disable(); buttonStart->enable(); - if(!getCampaign()->conqueredScenarios().empty()) - buttonBack->block(true); - else - buttonBack->block(false); + buttonBack->block(!getCampaign()->conqueredScenarios().empty()); } else { @@ -358,7 +366,7 @@ void CBonusSelection::updateAfterStateChange() void CBonusSelection::goBack() { - if(CSH->state != EClientState::GAMEPLAY) + if(CSH->getState() != EClientState::GAMEPLAY) { GH.windows().popWindows(2); } diff --git a/client/lobby/CBonusSelection.h b/client/lobby/CBonusSelection.h index 394c3b81b..989e459b3 100644 --- a/client/lobby/CBonusSelection.h +++ b/client/lobby/CBonusSelection.h @@ -27,6 +27,7 @@ class CAnimImage; class CLabel; class CFlagBox; class ISelectionScreenInfo; +class ExtraOptionsTab; /// Campaign screen where you can choose one out of three starting bonuses class CBonusSelection : public CWindowObject @@ -82,4 +83,7 @@ public: std::shared_ptr buttonDifficultyLeft; std::shared_ptr buttonDifficultyRight; std::shared_ptr iconsMapSizes; + + std::shared_ptr tabExtraOptions; + std::shared_ptr buttonExtraOptions; }; diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index 15a2cd7f3..63cda2fd1 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -112,7 +112,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) CLobbyScreen::~CLobbyScreen() { // TODO: For now we always destroy whole lobby when leaving bonus selection screen - if(CSH->state == EClientState::LOBBY_CAMPAIGN) + if(CSH->getState() == EClientState::LOBBY_CAMPAIGN) CSH->sendClientDisconnecting(); } diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index f35ce9e18..9e20faaa8 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -47,7 +47,7 @@ #include "../../lib/filesystem/Filesystem.h" #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" -#include "../../lib/serializer/Connection.h" +#include "../../lib/CRandomGenerator.h" ISelectionScreenInfo::ISelectionScreenInfo(ESelectionScreen ScreenType) : screenType(ScreenType) @@ -229,7 +229,7 @@ void InfoCard::changeSelection() iconsLossCondition->setFrame(header->defeatIconIndex); labelLossConditionText->setText(header->defeatMessage.toString()); flagbox->recreate(); - labelDifficulty->setText(CGI->generaltexth->arraytxt[142 + mapInfo->mapHeader->difficulty]); + labelDifficulty->setText(CGI->generaltexth->arraytxt[142 + vstd::to_underlying(mapInfo->mapHeader->difficulty)]); iconDifficulty->setSelected(SEL->getCurrentDifficulty()); if(SEL->screenType == ESelectionScreen::loadGame || SEL->screenType == ESelectionScreen::saveGame) for(auto & button : iconDifficulty->buttons) @@ -335,7 +335,7 @@ CChatBox::CChatBox(const Rect & rect) Rect textInputArea(1, rect.h - height, rect.w - 1, height); Rect chatHistoryArea(3, 1, rect.w - 3, rect.h - height - 1); inputBackground = std::make_shared(textInputArea, ColorRGBA(0,0,0,192)); - inputBox = std::make_shared(textInputArea, EFonts::FONT_SMALL, 0); + inputBox = std::make_shared(textInputArea, EFonts::FONT_SMALL, nullptr, ETextAlignment::TOPLEFT, true); inputBox->removeUsedEvents(KEYBOARD); chatHistory = std::make_shared("", chatHistoryArea, 1); diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index f67d3d447..99f6851f7 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -892,7 +892,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, name, 95); else { - labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, nullptr, false); + labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, nullptr, ETextAlignment::CENTER, false); labelPlayerNameEdit->setText(name); } labelWhoCanPlay = std::make_shared(Rect(6, 23, 45, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]); diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index a89943dc8..bb150efe7 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -12,6 +12,7 @@ #include "CSelectionBase.h" #include "../widgets/ComboBox.h" +#include "../widgets/Images.h" #include "../widgets/Slider.h" #include "../widgets/TextControls.h" #include "../CServerHandler.h" @@ -295,7 +296,7 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) } } -void OptionsTabBase::recreate() +void OptionsTabBase::recreate(bool campaign) { auto const & generateSimturnsDurationText = [](int days) -> std::string { @@ -417,4 +418,10 @@ void OptionsTabBase::recreate() buttonUnlimitedReplay->setSelectedSilent(SEL->getStartInfo()->extraOptionsInfo.unlimitedReplay); buttonUnlimitedReplay->block(SEL->screenType == ESelectionScreen::loadGame); } + + if(auto textureCampaignOverdraw = widget("textureCampaignOverdraw")) + { + if(!campaign) + textureCampaignOverdraw->disable(); + } } diff --git a/client/lobby/OptionsTabBase.h b/client/lobby/OptionsTabBase.h index dc7bf63f2..15c6372c9 100644 --- a/client/lobby/OptionsTabBase.h +++ b/client/lobby/OptionsTabBase.h @@ -28,5 +28,5 @@ class OptionsTabBase : public InterfaceObjectConfigurable public: OptionsTabBase(const JsonPath & configPath); - void recreate(); + void recreate(bool campaign = false); }; diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index c0a49f1b0..2d389095a 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -189,7 +189,7 @@ void RandomMapTab::updateMapInfoByHost() } } - mapInfo->mapHeader->difficulty = 1; // Normal + mapInfo->mapHeader->difficulty = EMapDifficulty::NORMAL; mapInfo->mapHeader->height = mapGenOptions->getHeight(); mapInfo->mapHeader->width = mapGenOptions->getWidth(); mapInfo->mapHeader->twoLevel = mapGenOptions->getHasTwoLevels(); diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index a44c90e5a..85e014c88 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -43,7 +43,6 @@ #include "../../lib/mapping/CMapHeader.h" #include "../../lib/mapping/MapFormat.h" #include "../../lib/TerrainHandler.h" -#include "../../lib/serializer/Connection.h" bool mapSorter::operator()(const std::shared_ptr aaa, const std::shared_ptr bbb) { @@ -769,7 +768,7 @@ void SelectionTab::parseSaves(const std::unordered_set & files) mapInfo->saveInit(file); // Filter out other game modes - bool isCampaign = mapInfo->scenarioOptionsOfSave->mode == StartInfo::CAMPAIGN; + bool isCampaign = mapInfo->scenarioOptionsOfSave->mode == EStartMode::CAMPAIGN; bool isMultiplayer = mapInfo->amountOfHumanPlayersInSave > 1; bool isTutorial = boost::to_upper_copy(mapInfo->scenarioOptionsOfSave->mapname) == "MAPS/TUTORIAL"; switch(CSH->getLoadMode()) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 07b119950..4abcc712b 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -372,7 +372,7 @@ CHighScoreInput::CHighScoreInput(std::string playerName, std::function(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT); buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CHighScoreInput::abort, this), EShortcut::GLOBAL_CANCEL); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); - textInput = std::make_shared(Rect(18, 104, 200, 25), FONT_SMALL, 0); + textInput = std::make_shared(Rect(18, 104, 200, 25), FONT_SMALL, nullptr, ETextAlignment::CENTER, true); textInput->setText(playerName); } @@ -384,4 +384,4 @@ void CHighScoreInput::okay() void CHighScoreInput::abort() { ready(""); -} \ No newline at end of file +} diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 1d867d308..4ff244b9b 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -24,6 +24,9 @@ #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../render/Canvas.h" +#include "../globalLobby/GlobalLobbyLoginWindow.h" +#include "../globalLobby/GlobalLobbyClient.h" +#include "../globalLobby/GlobalLobbyWindow.h" #include "../widgets/CComponent.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" @@ -44,7 +47,6 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/JsonNode.h" #include "../../lib/campaign/CampaignHandler.h" -#include "../../lib/serializer/Connection.h" #include "../../lib/serializer/CTypeList.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/CCompressedStream.h" @@ -185,11 +187,11 @@ static std::function genCommand(CMenuScreen * menu, std::vector(ESelectionScreen::newGame); }; case 2: - return std::bind(CMainMenu::openLobby, ESelectionScreen::campaignList, true, nullptr, ELoadMode::NONE); + return []() { CMainMenu::openLobby(ESelectionScreen::campaignList, true, {}, ELoadMode::NONE);}; case 3: return std::bind(CMainMenu::startTutorial); } @@ -200,13 +202,14 @@ static std::function genCommand(CMenuScreen * menu, std::vector(ESelectionScreen::loadGame); }; case 2: - return std::bind(CMainMenu::openLobby, ESelectionScreen::loadGame, true, nullptr, ELoadMode::CAMPAIGN); + return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::CAMPAIGN);}; case 3: - return std::bind(CMainMenu::openLobby, ESelectionScreen::loadGame, true, nullptr, ELoadMode::TUTORIAL); + return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::TUTORIAL);}; + } } break; @@ -356,10 +359,9 @@ void CMainMenu::update() GH.windows().simpleRedraw(); } -void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vector * names, ELoadMode loadMode) +void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vector & names, ELoadMode loadMode) { - CSH->resetStateForLobby(screenType == ESelectionScreen::newGame ? StartInfo::NEW_GAME : StartInfo::LOAD_GAME, names); - CSH->screenType = screenType; + CSH->resetStateForLobby(screenType == ESelectionScreen::newGame ? EStartMode::NEW_GAME : EStartMode::LOAD_GAME, screenType, EServerMode::LOCAL, names); CSH->loadMode = loadMode; GH.windows().createAndPushWindow(host); @@ -374,8 +376,7 @@ void CMainMenu::openCampaignLobby(const std::string & campaignFileName, std::str void CMainMenu::openCampaignLobby(std::shared_ptr campaign) { - CSH->resetStateForLobby(StartInfo::CAMPAIGN); - CSH->screenType = ESelectionScreen::campaignList; + CSH->resetStateForLobby(EStartMode::CAMPAIGN, ESelectionScreen::campaignList, EServerMode::LOCAL, {}); CSH->campaignStateToSend = campaign; GH.windows().createAndPushWindow(); } @@ -418,7 +419,7 @@ void CMainMenu::startTutorial() auto mapInfo = std::make_shared(); mapInfo->mapInit(tutorialMap.getName()); - CMainMenu::openLobby(ESelectionScreen::newGame, true, nullptr, ELoadMode::NONE); + CMainMenu::openLobby(ESelectionScreen::newGame, true, {}, ELoadMode::NONE); CSH->startMapAfterConnection(mapInfo); } @@ -456,12 +457,21 @@ CMultiMode::CMultiMode(ESelectionScreen ScreenType) playerName->setText(getPlayerName()); playerName->cb += std::bind(&CMultiMode::onNameChange, this, _1); - buttonHotseat = std::make_shared(Point(373, 78), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this)); - buttonHost = std::make_shared(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this)); - buttonJoin = std::make_shared(Point(373, 78 + 57 * 2), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this)); + buttonHotseat = std::make_shared(Point(373, 78 + 57 * 0), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this)); + buttonLobby = std::make_shared(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBONL.DEF"), CGI->generaltexth->zelp[265], std::bind(&CMultiMode::openLobby, this)); + + buttonHost = std::make_shared(Point(373, 78 + 57 * 3), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this)); + buttonJoin = std::make_shared(Point(373, 78 + 57 * 4), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this)); + buttonCancel = std::make_shared(Point(373, 424), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[288], [=](){ close();}, EShortcut::GLOBAL_CANCEL); } +void CMultiMode::openLobby() +{ + close(); + CSH->getGlobalLobby().activateInterface(); +} + void CMultiMode::hostTCP() { auto savedScreenType = screenType; @@ -480,7 +490,7 @@ std::string CMultiMode::getPlayerName() { std::string name = settings["general"]["playerName"].String(); if(name == "Player") - name = CGI->generaltexth->translate("vcmi.mainMenu.playerName"); + name = CGI->generaltexth->translate("core.genrltxt.434"); return name; } @@ -533,7 +543,7 @@ void CMultiPlayers::enterSelectionScreen() Settings name = settings.write["general"]["playerName"]; name->String() = names[0]; - CMainMenu::openLobby(screenType, host, &names, loadMode); + CMainMenu::openLobby(screenType, host, names, loadMode); } CSimpleJoinScreen::CSimpleJoinScreen(bool host) @@ -550,7 +560,7 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host) { textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverConnecting")); buttonOk->block(true); - startConnectThread(); + startConnection(); } else { @@ -560,8 +570,8 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host) inputPort->filters += std::bind(&CTextInput::numberFilter, _1, _2, 0, 65535); inputAddress->giveFocus(); } - inputAddress->setText(host ? CServerHandler::localhostAddress : CSH->getHostAddress(), true); - inputPort->setText(std::to_string(CSH->getHostPort()), true); + inputAddress->setText(host ? CSH->getLocalHostname() : CSH->getRemoteHostname(), true); + inputPort->setText(std::to_string(host ? CSH->getLocalPort() : CSH->getRemotePort()), true); buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CSimpleJoinScreen::leaveScreen, this), EShortcut::GLOBAL_CANCEL); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); @@ -573,20 +583,13 @@ void CSimpleJoinScreen::connectToServer() buttonOk->block(true); GH.stopTextInput(); - startConnectThread(inputAddress->getText(), boost::lexical_cast(inputPort->getText())); + startConnection(inputAddress->getText(), boost::lexical_cast(inputPort->getText())); } void CSimpleJoinScreen::leaveScreen() { - if(CSH->state == EClientState::CONNECTING) - { - textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverClosing")); - CSH->state = EClientState::CONNECTION_CANCELLED; - } - else if(GH.windows().isTopWindow(this)) - { - close(); - } + textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverClosing")); + CSH->setState(EClientState::CONNECTION_CANCELLED); } void CSimpleJoinScreen::onChange(const std::string & newText) @@ -594,7 +597,7 @@ void CSimpleJoinScreen::onChange(const std::string & newText) buttonOk->block(inputAddress->getText().empty() || inputPort->getText().empty()); } -void CSimpleJoinScreen::startConnectThread(const std::string & addr, ui16 port) +void CSimpleJoinScreen::startConnection(const std::string & addr, ui16 port) { #if defined(SINGLE_PROCESS_APP) && defined(VCMI_ANDROID) // in single process build server must use same JNIEnv as client @@ -602,35 +605,11 @@ void CSimpleJoinScreen::startConnectThread(const std::string & addr, ui16 port) // https://github.com/libsdl-org/SDL/blob/main/docs/README-android.md#threads-and-the-java-vm CVCMIServer::reuseClientJNIEnv(SDL_AndroidGetJNIEnv()); #endif - boost::thread connector(&CSimpleJoinScreen::connectThread, this, addr, port); - connector.detach(); -} - -void CSimpleJoinScreen::connectThread(const std::string & addr, ui16 port) -{ - setThreadName("connectThread"); - if(!addr.length()) - CSH->startLocalServerAndConnect(); + if(addr.empty()) + CSH->startLocalServerAndConnect(false); else - CSH->justConnectToServer(addr, port); - - // async call to prevent thread race - GH.dispatchMainThread([this](){ - if(CSH->state == EClientState::CONNECTION_FAILED) - { - CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.mainMenu.serverConnectionFailed"), {}); - - textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverAddressEnter")); - GH.startTextInput(inputAddress->pos); - buttonOk->block(false); - } - - if(GH.windows().isTopWindow(this)) - { - close(); - } - }); + CSH->connectToServer(addr, port); } CLoadingScreen::CLoadingScreen() diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index ea9010797..7ef4fef8d 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -31,11 +31,11 @@ class CLabel; // TODO: Find new location for these enums -enum ESelectionScreen : ui8 { +enum class ESelectionScreen : ui8 { unknown = 0, newGame, loadGame, saveGame, scenarioInfo, campaignList }; -enum ELoadMode : ui8 +enum class ELoadMode : ui8 { NONE = 0, SINGLE, MULTI, CAMPAIGN, TUTORIAL }; @@ -86,12 +86,14 @@ public: std::shared_ptr picture; std::shared_ptr playerName; std::shared_ptr buttonHotseat; + std::shared_ptr buttonLobby; std::shared_ptr buttonHost; std::shared_ptr buttonJoin; std::shared_ptr buttonCancel; std::shared_ptr statusBar; CMultiMode(ESelectionScreen ScreenType); + void openLobby(); void hostTCP(); void joinTCP(); std::string getPlayerName(); @@ -148,7 +150,7 @@ public: void activate() override; void onScreenResize() override; void update() override; - static void openLobby(ESelectionScreen screenType, bool host, const std::vector * names, ELoadMode loadMode); + static void openLobby(ESelectionScreen screenType, bool host, const std::vector & names, ELoadMode loadMode); static void openCampaignLobby(const std::string & campaignFileName, std::string campaignSet = ""); static void openCampaignLobby(std::shared_ptr campaign); static void startTutorial(); @@ -175,8 +177,7 @@ class CSimpleJoinScreen : public WindowBase void connectToServer(); void leaveScreen(); void onChange(const std::string & newText); - void startConnectThread(const std::string & addr = {}, ui16 port = 0); - void connectThread(const std::string & addr, ui16 port); + void startConnection(const std::string & addr = {}, ui16 port = 0); public: CSimpleJoinScreen(bool host = true); diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index 1eebe87ea..acdfcd2dd 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -40,7 +40,7 @@ class CHoverableArea: public virtual CIntObject public: std::string hoverText; - virtual void hover (bool on) override; + void hover (bool on) override; CHoverableArea(); virtual ~CHoverableArea(); diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 5338f34eb..1e4e1efd0 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -551,8 +551,8 @@ Point CGStatusBar::getBorderSize() return Point(); } -CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB, bool giveFocusToInput) - : CLabel(Pos.x, Pos.y, font, ETextAlignment::CENTER), +CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB, ETextAlignment alignment, bool giveFocusToInput) + : CLabel(Pos.x, Pos.y, font, alignment), cb(CB) { setRedrawParent(true); diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index d922d0f87..dce169a93 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -204,7 +204,7 @@ public: void setText(const std::string & nText, bool callCb); void setHelpText(const std::string &); - CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB, bool giveFocusToInput = true); + CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB, ETextAlignment alignment, bool giveFocusToInput); CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName, const CFunctionList & CB); CTextInput(const Rect & Pos, std::shared_ptr srf); diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h index 1602e80c3..bbd0b0e79 100644 --- a/client/windows/CMapOverview.h +++ b/client/windows/CMapOverview.h @@ -24,7 +24,7 @@ class CTextBox; class IImage; class Canvas; class TransparentFilledRectangle; -enum ESelectionScreen : ui8; +enum class ESelectionScreen : ui8; class CMapOverview; diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index a09670c5e..9c41f673e 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -136,7 +136,7 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m searchBoxRectangle = std::make_shared(r.resize(1), rectangleColor, borderColor); searchBoxDescription = std::make_shared(r.center().x, r.center().y, FONT_SMALL, ETextAlignment::CENTER, grayedColor, CGI->generaltexth->translate("vcmi.spellBook.search")); - searchBox = std::make_shared(r, FONT_SMALL, std::bind(&CSpellWindow::searchInput, this)); + searchBox = std::make_shared(r, FONT_SMALL, std::bind(&CSpellWindow::searchInput, this), ETextAlignment::CENTER, true); } processSpells(); diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 03b4c7f86..bf1489567 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -328,8 +328,8 @@ CSplitWindow::CSplitWindow(const CCreature * creature, std::function(Rect(20, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, true)); - rightInput = std::make_shared(Rect(176, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, false)); + leftInput = std::make_shared(Rect(20, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, true), ETextAlignment::CENTER, true); + rightInput = std::make_shared(Rect(176, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, false), ETextAlignment::CENTER, true); //add filters to allow only number input leftInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, leftMin, leftMax); diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index eea665326..d79ed40f3 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -79,7 +79,7 @@ public: class CRClickPopup : public WindowBase { public: - virtual void close() override; + void close() override; bool isPopupWindow() const override; static std::shared_ptr createCustomInfoWindow(Point position, const CGObjectInstance * specific); diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index 69ee10fef..2058b664c 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -317,7 +317,7 @@ void GeneralOptionsTab::setGameResolution(int index) widget("resolutionLabel")->setText(resolutionToLabelString(resolution.x, resolution.y)); GH.dispatchMainThread([](){ - GH.onScreenResize(); + GH.onScreenResize(true); }); } @@ -341,7 +341,7 @@ void GeneralOptionsTab::setFullscreenMode(bool on, bool exclusive) updateResolutionSelector(); GH.dispatchMainThread([](){ - GH.onScreenResize(); + GH.onScreenResize(true); }); } @@ -400,7 +400,7 @@ void GeneralOptionsTab::setGameScaling(int index) widget("scalingLabel")->setText(scalingToLabelString(scaling)); GH.dispatchMainThread([](){ - GH.onScreenResize(); + GH.onScreenResize(true); }); } diff --git a/client/windows/settings/SettingsMainWindow.cpp b/client/windows/settings/SettingsMainWindow.cpp index d122c8a02..b9ccb0815 100644 --- a/client/windows/settings/SettingsMainWindow.cpp +++ b/client/windows/settings/SettingsMainWindow.cpp @@ -143,12 +143,9 @@ void SettingsMainWindow::mainMenuButtonCallback() [this]() { close(); - GH.dispatchMainThread( []() - { - CSH->endGameplay(); - GH.defActionsDef = 63; - CMM->menu->switchToTab("main"); - }); + CSH->endGameplay(); + GH.defActionsDef = 63; + CMM->menu->switchToTab("main"); }, 0 ); diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 39b8044f5..63599b2cc 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -124,6 +124,10 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/modding/IdentifierStorage.cpp ${MAIN_LIB_DIR}/modding/ModUtility.cpp + ${MAIN_LIB_DIR}/network/NetworkConnection.cpp + ${MAIN_LIB_DIR}/network/NetworkHandler.cpp + ${MAIN_LIB_DIR}/network/NetworkServer.cpp + ${MAIN_LIB_DIR}/networkPacks/NetPacksLib.cpp ${MAIN_LIB_DIR}/pathfinder/CGPathNode.cpp @@ -151,6 +155,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/rmg/Zone.cpp ${MAIN_LIB_DIR}/rmg/Functions.cpp ${MAIN_LIB_DIR}/rmg/RmgMap.cpp + ${MAIN_LIB_DIR}/rmg/PenroseTiling.cpp ${MAIN_LIB_DIR}/rmg/modificators/Modificator.cpp ${MAIN_LIB_DIR}/rmg/modificators/ObjectManager.cpp ${MAIN_LIB_DIR}/rmg/modificators/ObjectDistributor.cpp @@ -471,6 +476,12 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/modding/ModUtility.h ${MAIN_LIB_DIR}/modding/ModVerificationInfo.h + ${MAIN_LIB_DIR}/network/NetworkConnection.h + ${MAIN_LIB_DIR}/network/NetworkDefines.h + ${MAIN_LIB_DIR}/network/NetworkHandler.h + ${MAIN_LIB_DIR}/network/NetworkInterface.h + ${MAIN_LIB_DIR}/network/NetworkServer.h + ${MAIN_LIB_DIR}/networkPacks/ArtifactLocation.h ${MAIN_LIB_DIR}/networkPacks/BattleChanges.h ${MAIN_LIB_DIR}/networkPacks/Component.h @@ -522,6 +533,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/rmg/RmgMap.h ${MAIN_LIB_DIR}/rmg/float3.h ${MAIN_LIB_DIR}/rmg/Functions.h + ${MAIN_LIB_DIR}/rmg/PenroseTiling.h ${MAIN_LIB_DIR}/rmg/modificators/Modificator.h ${MAIN_LIB_DIR}/rmg/modificators/ObjectManager.h ${MAIN_LIB_DIR}/rmg/modificators/ObjectDistributor.h diff --git a/config/gameConfig.json b/config/gameConfig.json index b7d7bcd91..5c13bb56b 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -384,7 +384,9 @@ // if enabled, pathfinder will take use of one-way monoliths with multiple exits. "useMonolithOneWayRandom" : false, // if enabled and hero has whirlpool protection effect, pathfinder will take use of whirpools - "useWhirlpool" : true + "useWhirlpool" : true, + // if enabled flying will work like in original game, otherwise nerf similar to HotA flying is applied + "originalFlyRules" : false }, "bonuses" : diff --git a/config/heroes/tower.json b/config/heroes/tower.json index 0fecfc54b..4ddc15e86 100644 --- a/config/heroes/tower.json +++ b/config/heroes/tower.json @@ -70,6 +70,7 @@ "torosar": { "index": 36, + "compatibilityIdentifiers" : [ "torosar " ], "class" : "alchemist", "female": false, "spellbook": [ "magicArrow" ], diff --git a/config/randomMap.json b/config/randomMap.json index 800a2f893..70a4d0dd6 100644 --- a/config/randomMap.json +++ b/config/randomMap.json @@ -3,8 +3,9 @@ { "treasure" : [ - { "min" : 2000, "max" : 6000, "density" : 1 }, - { "min" : 100, "max" : 1000, "density" : 5 } + { "min" : 4000, "max" : 12000, "density" : 1 }, + { "min" : 1000, "max" : 4000, "density" : 3 }, + { "min" : 100, "max" : 1000, "density" : 6 } ], "shipyard" : { diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 022cf10ab..613eb765b 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -304,7 +304,7 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "rangeLimitHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize", "stickyHeroInfoWindows", "enableAutocombatSpells", "endWithAutocombat" ], + "required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "rangeLimitHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize", "stickyHeroInfoWindows", "enableAutocombatSpells", "endWithAutocombat", "queueSmallSlots", "queueSmallOutside" ], "properties" : { "speedFactor" : { "type" : "number", @@ -354,6 +354,14 @@ "endWithAutocombat" : { "type": "boolean", "default": false + }, + "queueSmallSlots" : { + "type": "number", + "default": 10 + }, + "queueSmallOutside" : { + "type": "boolean", + "default": false } } }, @@ -361,19 +369,23 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "server", "port", "localInformation", "playerAI", "alliedAI", "friendlyAI", "neutralAI", "enemyAI", "reconnect", "uuid", "names" ], + "required" : [ "localHostname", "localPort", "remoteHostname", "remotePort", "playerAI", "alliedAI", "friendlyAI", "neutralAI", "enemyAI" ], "properties" : { - "server" : { + "localHostname" : { "type" : "string", "default" : "127.0.0.1" }, - "port" : { + "localPort" : { "type" : "number", "default" : 3030 }, - "localInformation" : { + "remoteHostname" : { + "type" : "string", + "default" : "" + }, + "remotePort" : { "type" : "number", - "default" : 2 + "default" : 3030 }, "playerAI" : { "type" : "string", @@ -394,23 +406,6 @@ "enemyAI" : { "type" : "string", "default" : "BattleAI" - }, - "reconnect" : { - "type" : "boolean", - "default" : false - }, - "uuid" : { - "type" : "string", - "default" : "" - }, - "names" : { - "type" : "array", - "default" : [], - "items" : - { - "type" : "string", - "default" : "" - } } } }, @@ -570,11 +565,47 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "mapPreview" ], + "required" : [ "mapPreview", "accountID", "accountCookie", "displayName", "hostname", "port", "roomPlayerLimit", "roomType", "roomMode", "roomID" ], "properties" : { "mapPreview" : { "type" : "boolean", "default" : true + }, + "accountID" : { + "type" : "string", + "default" : "" + }, + "accountCookie" : { + "type" : "string", + "default" : "" + }, + "displayName" : { + "type" : "string", + "default" : "" + }, + "hostname" : { + "type" : "string", + "default" : "127.0.0.1" + }, + "port" : { + "type" : "number", + "default" : 30303 + }, + "roomPlayerLimit" : { + "type" : "number", + "default" : 2 + }, + "roomType" : { + "type" : "number", + "default" : 0 + }, + "roomMode" : { + "type" : "number", + "default" : 0 + }, + "roomID" : { + "type" : "string", + "default" : "" } } }, diff --git a/config/widgets/extraOptionsTab.json b/config/widgets/extraOptionsTab.json index d8a333b9c..91b41bced 100644 --- a/config/widgets/extraOptionsTab.json +++ b/config/widgets/extraOptionsTab.json @@ -9,6 +9,13 @@ "image": "ADVOPTBK", "position": {"x": 0, "y": 6} }, + { + "name": "textureCampaignOverdraw", + "type": "texture", + "color" : "blue", + "image": "DIBOXBCK", + "rect": {"x": 391, "y": 14, "w": 82, "h": 569} + }, { "name": "labelTitle", "type": "label", diff --git a/config/widgets/lobbyWindow.json b/config/widgets/lobbyWindow.json new file mode 100644 index 000000000..e8bdc8889 --- /dev/null +++ b/config/widgets/lobbyWindow.json @@ -0,0 +1,199 @@ +{ + "customTypes" : { + "labelTitleMain" : { + "type": "label", + "font": "big", + "alignment": "left", + "color": "yellow" + }, + "labelTitle" : { + "type": "label", + "font": "small", + "alignment": "left", + "color": "yellow" + }, + "backgroundTexture" : { + "type": "texture", + "font": "tiny", + "color" : "blue", + "image": "DIBOXBCK" + }, + "areaFilled":{ + "type": "transparentFilledRectangle", + "color": [0, 0, 0, 75], + "colorLine": [64, 80, 128, 255] + } + }, + + + "width": 1024, + "height": 600, + + "items": + [ + { + "type": "backgroundTexture", + "rect": {"w": 1024, "h": 600} + }, + + { + "type": "areaFilled", + "rect": {"x": 5, "y": 5, "w": 250, "h": 40} + }, + { + "name" : "accountNameLabel", + "type": "labelTitleMain", + "position": {"x": 15, "y": 10} + }, + + { + "type": "areaFilled", + "rect": {"x": 5, "y": 50, "w": 250, "h": 500} + }, + { + "type": "labelTitle", + "position": {"x": 15, "y": 53}, + "text" : "Room List" + }, + { + "type" : "roomList", + "name" : "roomList", + "position" : { "x" : 7, "y" : 69 }, + "itemOffset" : { "x" : 0, "y" : 40 }, + "sliderPosition" : { "x" : 230, "y" : 0 }, + "sliderSize" : { "x" : 450, "y" : 450 } + }, + + { + "type": "areaFilled", + "rect": {"x": 270, "y": 50, "w": 150, "h": 540} + }, + { + "type": "labelTitle", + "position": {"x": 280, "y": 53}, + "text" : "Channel List" + }, + + { + "type": "areaFilled", + "rect": {"x": 430, "y": 50, "w": 430, "h": 515} + }, + { + "type": "labelTitle", + "position": {"x": 440, "y": 53}, + "text" : "Game Chat" + }, + { + "type": "textBox", + "name": "gameChat", + "font": "small", + "alignment": "left", + "color": "white", + "rect": {"x": 440, "y": 70, "w": 430, "h": 495} + }, + + { + "type": "areaFilled", + "rect": {"x": 430, "y": 565, "w": 395, "h": 25} + }, + { + "name" : "messageInput", + "type": "textInput", + "alignment" : "left", + "rect": {"x": 440, "y": 568, "w": 375, "h": 20} + }, + + { + "type": "areaFilled", + "rect": {"x": 870, "y": 50, "w": 150, "h": 540} + }, + { + "type": "labelTitle", + "position": {"x": 880, "y": 53}, + "text" : "Account List" + }, + { + "type" : "accountList", + "name" : "accountList", + "position" : { "x" : 872, "y" : 69 }, + "itemOffset" : { "x" : 0, "y" : 40 }, + "sliderPosition" : { "x" : 130, "y" : 0 }, + "sliderSize" : { "x" : 520, "y" : 520 } + }, + + { + "type": "button", + "position": {"x": 840, "y": 10}, + "image": "settingsWindow/button80", + "help": "core.help.288", + "callback": "closeWindow", + "items": + [ + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "Leave" + } + ] + }, + + { + "type": "button", + "position": {"x": 940, "y": 10}, + "image": "settingsWindow/button80", + "help": "core.help.288", + "callback": "closeWindow", + "hotkey": "globalCancel", + "items": + [ + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "Close" + } + ] + }, + + { + "type": "button", + "position": {"x": 828, "y": 565}, + "image": "settingsWindow/button32", + "help": "core.help.288", + "callback": "sendMessage", + "hotkey": "globalAccept", + "items": + [ + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": ">" + } + ] + }, + + { + "type": "button", + "position": {"x": 10, "y": 555}, + "image": "settingsWindow/button190", + "help": "core.help.288", + "callback": "createGameRoom", + "items": + [ + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "Create Room" + } + ] + }, + + ] +} diff --git a/docs/developers/Building_Linux.md b/docs/developers/Building_Linux.md index 4190ad49b..559527f1b 100644 --- a/docs/developers/Building_Linux.md +++ b/docs/developers/Building_Linux.md @@ -49,12 +49,14 @@ Information about building packages from the Arch User Repository (AUR) can be f # Getting the sources -VCMI is still in development. We recommend the following initial directory structure: +We recommend the following directory structure: . ├── vcmi -> contains sources and is under git control └── build -> contains build output, makefiles, object files,... +Out-of-source builds keep the local repository clean so one doesn't have to manually exclude files generated during the build from commits. + You can get latest sources with: `git clone -b develop --recursive https://github.com/vcmi/vcmi.git` @@ -65,25 +67,25 @@ You can get latest sources with: ```sh mkdir build && cd build -cmake ../vcmi +cmake -S ../vcmi ``` # Additional options that you may want to use: ## To enable debugging: -`cmake ../vcmi -D CMAKE_BUILD_TYPE=Debug` +`cmake -S ../vcmi -D CMAKE_BUILD_TYPE=Debug` **Notice**: The ../vcmi/ is not a typo, it will place makefile scripts into the build dir as the build dir is your working dir when calling CMake. ## To use ccache: -`cmake ../vcmi -D ENABLE_CCACHE:BOOL=ON` +`cmake -S ../vcmi -D ENABLE_CCACHE:BOOL=ON` ## Trigger build `cmake --build . -- -j2` (-j2 = compile with 2 threads, you can specify any value) -That will generate vcmiclient, vcmiserver, vcmilauncher as well as .so libraries in **build/bin/** directory. +That will generate vcmiclient, vcmiserver, vcmilauncher as well as .so libraries in the **build/bin/** directory. # Package building diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 613ebecfe..f55f8d32c 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -15,10 +15,6 @@ set(launcher_SRCS launcherdirs.cpp jsonutils.cpp updatedialog_moc.cpp - lobby/lobby.cpp - lobby/lobby_moc.cpp - lobby/lobbyroomrequest_moc.cpp - lobby/chat_moc.cpp ) set(launcher_HEADERS @@ -37,10 +33,6 @@ set(launcher_HEADERS launcherdirs.h jsonutils.h updatedialog_moc.h - lobby/lobby.h - lobby/lobby_moc.h - lobby/lobbyroomrequest_moc.h - lobby/chat_moc.h main.h ) @@ -52,9 +44,6 @@ set(launcher_FORMS firstLaunch/firstlaunch_moc.ui mainwindow_moc.ui updatedialog_moc.ui - lobby/lobby_moc.ui - lobby/lobbyroomrequest_moc.ui - lobby/chat_moc.ui ) set(launcher_TS diff --git a/launcher/icons/menu-lobby.png b/launcher/icons/menu-lobby.png deleted file mode 100644 index 6385fc3b5..000000000 Binary files a/launcher/icons/menu-lobby.png and /dev/null differ diff --git a/launcher/icons/room-private.png b/launcher/icons/room-private.png deleted file mode 100644 index 33896d9b9..000000000 Binary files a/launcher/icons/room-private.png and /dev/null differ diff --git a/launcher/lobby/chat_moc.cpp b/launcher/lobby/chat_moc.cpp deleted file mode 100644 index a260ba312..000000000 --- a/launcher/lobby/chat_moc.cpp +++ /dev/null @@ -1,173 +0,0 @@ -/* - * chat_moc.cpp, 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 - * - */ -#include "StdInc.h" -#include "chat_moc.h" -#include "ui_chat_moc.h" - -Chat::Chat(QWidget *parent) : - QWidget(parent), - ui(new Ui::Chat) -{ - ui->setupUi(this); - - namesCompleter.setModel(ui->listUsers->model()); - namesCompleter.setCompletionMode(QCompleter::InlineCompletion); - - ui->messageEdit->setCompleter(&namesCompleter); - - for([[maybe_unused]] auto i : {GLOBAL, ROOM}) - chatDocuments.push_back(new QTextDocument(this)); - - setChatId(GLOBAL); -} - -Chat::~Chat() -{ - delete ui; -} - -void Chat::setUsername(const QString & user) -{ - username = user; -} - -void Chat::setSession(const QString & s) -{ - session = s; - - on_chatSwitch_clicked(); -} - -void Chat::setChannel(const QString & channel) -{ - static const QMap chatNames{{"global", GLOBAL}, {"room", ROOM}}; - - setChatId(chatNames.value(channel)); -} - -void Chat::addUser(const QString & user) -{ - ui->listUsers->addItem(new QListWidgetItem("@" + user)); -} - -void Chat::clearUsers() -{ - ui->listUsers->clear(); -} - -void Chat::chatMessage(const QString & title, const QString & channel, QString body, bool isSystem) -{ - const QTextCharFormat regularFormat; - const QString boldHtml = "%1"; - const QString colorHtml = "%2"; - bool meMentioned = false; - bool isScrollBarBottom = (ui->chat->verticalScrollBar()->maximum() - ui->chat->verticalScrollBar()->value() < 24); - - static const QMap chatNames{{"global", GLOBAL}, {"room", ROOM}}; - QTextDocument * doc = ui->chat->document(); - if(chatNames.contains(channel)) - doc = chatDocuments[chatNames.value(channel)]; - - QTextCursor curs(doc); - curs.movePosition(QTextCursor::End); - - QString titleColor = "Olive"; - if(isSystem || title == "System") - titleColor = "ForestGreen"; - if(title == username) - titleColor = "Gold"; - - curs.insertHtml(boldHtml.arg(colorHtml.arg(titleColor, title + ": "))); - - QRegularExpression mentionRe("@[\\w\\d]+"); - auto subBody = body; - int mem = 0; - for(auto match = mentionRe.match(subBody); match.hasMatch(); match = mentionRe.match(subBody)) - { - body.insert(mem + match.capturedEnd(), QChar(-1)); - body.insert(mem + match.capturedStart(), QChar(-1)); - mem += match.capturedEnd() + 2; - subBody = body.right(body.size() - mem); - } - auto pieces = body.split(QChar(-1)); - for(auto & block : pieces) - { - if(block.startsWith("@")) - { - if(block == "@" + username) - { - meMentioned = true; - curs.insertHtml(boldHtml.arg(colorHtml.arg("IndianRed", block))); - } - else - curs.insertHtml(colorHtml.arg("DeepSkyBlue", block)); - } - else - { - if(isSystem) - curs.insertHtml(colorHtml.arg("ForestGreen", block)); - else - curs.insertText(block, regularFormat); - } - } - curs.insertText("\n", regularFormat); - - if(doc == ui->chat->document() && (meMentioned || isScrollBarBottom)) - { - ui->chat->ensureCursorVisible(); - ui->chat->verticalScrollBar()->setValue(ui->chat->verticalScrollBar()->maximum()); - } -} - -void Chat::chatMessage(const QString & title, QString body, bool isSystem) -{ - chatMessage(title, "", body, isSystem); -} - -void Chat::sysMessage(QString body) -{ - chatMessage("System", body, true); -} - -void Chat::sendMessage() -{ - QString msg(ui->messageEdit->text()); - ui->messageEdit->clear(); - emit messageSent(msg); -} - -void Chat::on_messageEdit_returnPressed() -{ - sendMessage(); -} - -void Chat::on_sendButton_clicked() -{ - sendMessage(); -} - -void Chat::on_chatSwitch_clicked() -{ - static const QMap chatNames{{GLOBAL, "global"}, {ROOM, "room"}}; - - if(chatId == GLOBAL && !session.isEmpty()) - emit channelSwitch(chatNames[ROOM]); - else - emit channelSwitch(chatNames[GLOBAL]); -} - -void Chat::setChatId(ChatId _chatId) -{ - static const QMap chatNames{{GLOBAL, "Global"}, {ROOM, "Room"}}; - - chatId = _chatId; - ui->chatSwitch->setText(chatNames[chatId] + " chat"); - ui->chat->setDocument(chatDocuments[chatId]); -} diff --git a/launcher/lobby/chat_moc.h b/launcher/lobby/chat_moc.h deleted file mode 100644 index a796dc7a9..000000000 --- a/launcher/lobby/chat_moc.h +++ /dev/null @@ -1,70 +0,0 @@ -/* - * chat_moc.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 -#include - -namespace Ui { -class Chat; -} - -class Chat : public QWidget -{ - Q_OBJECT - - enum ChatId - { - GLOBAL = 0, - ROOM - }; - - QCompleter namesCompleter; - QString username; - QString session; - ChatId chatId = GLOBAL; - - QVector chatDocuments; - -private: - void setChatId(ChatId); - void sendMessage(); - -public: - explicit Chat(QWidget *parent = nullptr); - ~Chat(); - - void setUsername(const QString &); - void setSession(const QString &); - void setChannel(const QString &); - - void clearUsers(); - void addUser(const QString & user); - - void chatMessage(const QString & title, const QString & channel, QString body, bool isSystem = false); - void chatMessage(const QString & title, QString body, bool isSystem = false); - -signals: - void messageSent(QString); - void channelSwitch(QString); - -public slots: - void sysMessage(QString body); - -private slots: - void on_messageEdit_returnPressed(); - - void on_sendButton_clicked(); - - void on_chatSwitch_clicked(); - -private: - Ui::Chat *ui; -}; diff --git a/launcher/lobby/chat_moc.ui b/launcher/lobby/chat_moc.ui deleted file mode 100644 index a3208d982..000000000 --- a/launcher/lobby/chat_moc.ui +++ /dev/null @@ -1,121 +0,0 @@ - - - Chat - - - - 0 - 0 - 465 - 413 - - - - Form - - - - 2 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - - - Users in lobby - - - -1 - - - - - - - Global chat - - - - - - - - - - 0 - 0 - - - - - 16777215 - 96 - - - - 0 - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::NoSelection - - - true - - - QListView::SinglePass - - - - - - - - - - -1 - - - 0 - - - - - - - - type you message - - - - - - - send - - - - - - - - - - diff --git a/launcher/lobby/lobby.cpp b/launcher/lobby/lobby.cpp deleted file mode 100644 index 3e8c9648e..000000000 --- a/launcher/lobby/lobby.cpp +++ /dev/null @@ -1,123 +0,0 @@ -/* - * lobby.cpp, 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 - * - */ -#include "StdInc.h" -#include "lobby.h" -#include "../lib/GameConstants.h" - -SocketLobby::SocketLobby(QObject *parent) : - QObject(parent) -{ - socket = new QTcpSocket(this); - connect(socket, SIGNAL(connected()), this, SLOT(connected())); - connect(socket, SIGNAL(disconnected()), this, SLOT(disconnected())); - connect(socket, SIGNAL(readyRead()), this, SLOT(readyRead())); - connect(socket, SIGNAL(bytesWritten(qint64)), this, SLOT(bytesWritten(qint64))); -} - -void SocketLobby::connectServer(const QString & host, int port, const QString & usr, int timeout) -{ - username = usr; - - socket->connectToHost(host, port); - - if(!socket->waitForDisconnected(timeout) && !isConnected) - { - emit text("Error: " + socket->errorString()); - emit disconnect(); - } -} - -void SocketLobby::disconnectServer() -{ - socket->disconnectFromHost(); -} - -void SocketLobby::requestNewSession(const QString & session, int totalPlayers, const QString & pswd, const QMap & mods) -{ - const QString sessionMessage = ProtocolStrings[CREATE].arg(session, pswd, QString::number(totalPlayers), prepareModsClientString(mods)); - send(sessionMessage); -} - -void SocketLobby::requestJoinSession(const QString & session, const QString & pswd, const QMap & mods) -{ - const QString sessionMessage = ProtocolStrings[JOIN].arg(session, pswd, prepareModsClientString(mods)); - send(sessionMessage); -} - -void SocketLobby::requestLeaveSession(const QString & session) -{ - const QString sessionMessage = ProtocolStrings[LEAVE].arg(session); - send(sessionMessage); -} - -void SocketLobby::requestReadySession(const QString & session) -{ - const QString sessionMessage = ProtocolStrings[READY].arg(session); - send(sessionMessage); -} - -void SocketLobby::send(const QString & msg) -{ - QByteArray str = msg.toUtf8(); - int sz = str.size(); - QByteArray pack((const char *)&sz, sizeof(sz)); - pack.append(str); - socket->write(pack); -} - -void SocketLobby::connected() -{ - isConnected = true; - emit text("Connected!"); - - QByteArray greetingBytes; - greetingBytes.append(ProtocolVersion); - greetingBytes.append(ProtocolEncoding.size()); - const QString greetingConst = QString(greetingBytes) - + ProtocolStrings[GREETING].arg(QString::fromStdString(ProtocolEncoding), - username, - QString::fromStdString(GameConstants::VCMI_VERSION)); - send(greetingConst); -} - -void SocketLobby::disconnected() -{ - isConnected = false; - emit disconnect(); - emit text("Disconnected!"); -} - -void SocketLobby::bytesWritten(qint64 bytes) -{ - qDebug() << "We wrote: " << bytes; -} - -void SocketLobby::readyRead() -{ - qDebug() << "Reading..."; - emit receive(socket->readAll()); -} - - -ServerCommand::ServerCommand(ProtocolConsts cmd, const QStringList & args): - command(cmd), - arguments(args) -{ -} - -QString prepareModsClientString(const QMap & mods) -{ - QStringList result; - for(auto & mod : mods.keys()) - { - result << mod + "&" + mods[mod]; - } - return result.join(";"); -} diff --git a/launcher/lobby/lobby.h b/launcher/lobby/lobby.h deleted file mode 100644 index 84fe67e22..000000000 --- a/launcher/lobby/lobby.h +++ /dev/null @@ -1,226 +0,0 @@ -/* - * lobby.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 -#include - -const unsigned int ProtocolVersion = 5; -const std::string ProtocolEncoding = "utf8"; - -class ProtocolError: public std::runtime_error -{ -public: - ProtocolError(const char * w): std::runtime_error(w) {} -}; - -enum ProtocolConsts -{ - //client consts - GREETING, USERNAME, MESSAGE, VERSION, CREATE, JOIN, LEAVE, KICK, READY, FORCESTART, HERE, ALIVE, HOSTMODE, SETCHANNEL, - - //server consts - SESSIONS, CREATED, JOINED, KICKED, SRVERROR, CHAT, CHATCHANNEL, START, STATUS, HOST, MODS, CLIENTMODS, USERS, HEALTH, GAMEMODE, CHANNEL -}; - -const QMap ProtocolStrings -{ - //=== client commands === - - //handshaking with server - //%1: first byte is protocol_version, then size of encoding string in bytes, then encoding string - //%2: client name - //%3: VCMI version - {GREETING, "%1%2%3"}, - - //[unsupported] autorization with username - //%1: username - {USERNAME, "%1"}, - - //sending message to the chat - //%1: message text - {MESSAGE, "%1"}, - - //create new room - //%1: room name - //%2: password for the room - //%3: max number of players - //%4: mods used by host - // each mod has a format modname&modversion, mods should be separated by ; symbol - {CREATE, "%1%2%3%4"}, - - //join to the room - //%1: room name - //%2: password for the room - //%3: list of mods used by player - // each mod has a format modname&modversion, mods should be separated by ; symbol - {JOIN, "%1%2%3"}, - - //leave the room - //%1: room name - {LEAVE, "%1"}, - - //kick user from the current room - //%1: player username - {KICK, "%1"}, - - //signal that player is ready for game - //%1: room name - {READY, "%1"}, - - //[unsupported] start session immediately - //%1: room name - {FORCESTART, "%1"}, - - //request user list - {HERE, ""}, - - //used as reponse to healcheck - {ALIVE, ""}, - - //host sets game mode (new game or load game) - //%1: game mode - 0 for new game, 1 for load game - {HOSTMODE, "%1"}, - - //set new chat channel - //%1: channel name - {SETCHANNEL, "%1"}, - - //=== server commands === - //server commands are started from :>>, arguments are enumerated by : symbol - - //new session was created - //arg[0]: room name - {CREATED, "CREATED"}, - - //list of existing sessions - //arg[0]: amount of sessions, following arguments depending on it - //arg[x]: session name - //arg[x+1]: amount of players in the session - //arg[x+2]: total amount of players allowed - //arg[x+3]: True if session is protected by password - {SESSIONS, "SESSIONS"}, - - //user has joined to the session - //arg[0]: session name - //arg[1]: username (who was joined) - {JOINED, "JOIN"}, - - //user has left the session - //arg[0]: session name - //arg[1]: username (who has left) - {KICKED, "KICK"}, - - //session has been started - //arg[0]: session name - //arg[1]: uuid to be used for connection - {START, "START"}, - - //host ownership for the game session - //arg[0]: uuid to be used by vcmiserver - //arg[1]: amount of players (clients) to be connected - {HOST, "HOST"}, - - //room status - //arg[0]: amount of players, following arguments depending on it - //arg[x]: player username - //arg[x+1]: True if player is ready - {STATUS, "STATUS"}, //joined_players:player_name:is_ready - - //server error - //arg[0]: error message - {SRVERROR, "ERROR"}, - - //mods used in the session by host player - //arg[0]: amount of mods, following arguments depending on it - //arg[x]: mod name - //arg[x+1]: mod version - {MODS, "MODS"}, - - //mods used by user - //arg[0]: username - //arg[1]: amount of mods, following arguments depending on it - //arg[x]: mod name - //arg[x+1]: mod version - {CLIENTMODS, "MODSOTHER"}, - - //received chat message - //arg[0]: sender username - //arg[1]: channel - //arg[2]: message text - {CHAT, "MSG"}, - - //received chat message to specific channel - //arg[0]: sender username - //arg[1]: channel - //arg[2]: message text - {CHATCHANNEL, "MSGCH"}, - - //list of users currently in lobby - //arg[0]: amount of players, following arguments depend on it - //arg[x]: username - //arg[x+1]: room (empty if not in the room) - {USERS, "USERS"}, - - //healthcheck from server - {HEALTH, "HEALTH"}, - - //game mode (new game or load game) set by host - //arg[0]: game mode - {GAMEMODE, "GAMEMODE"}, - - //chat channel changed - //arg[0]: channel name - {CHANNEL, "CHANNEL"}, -}; - -class ServerCommand -{ -public: - ServerCommand(ProtocolConsts, const QStringList & arguments); - - const ProtocolConsts command; - const QStringList arguments; -}; - -class SocketLobby : public QObject -{ - Q_OBJECT -public: - explicit SocketLobby(QObject *parent = nullptr); - void connectServer(const QString & host, int port, const QString & username, int timeout); - void disconnectServer(); - void requestNewSession(const QString & session, int totalPlayers, const QString & pswd, const QMap & mods); - void requestJoinSession(const QString & session, const QString & pswd, const QMap & mods); - void requestLeaveSession(const QString & session); - void requestReadySession(const QString & session); - - void send(const QString &); - -signals: - - void text(QString); - void receive(QString); - void disconnect(); - -public slots: - - void connected(); - void disconnected(); - void bytesWritten(qint64 bytes); - void readyRead(); - -private: - QTcpSocket *socket; - bool isConnected = false; - QString username; -}; - -QString prepareModsClientString(const QMap & mods); diff --git a/launcher/lobby/lobby_moc.cpp b/launcher/lobby/lobby_moc.cpp deleted file mode 100644 index 7c50fd912..000000000 --- a/launcher/lobby/lobby_moc.cpp +++ /dev/null @@ -1,585 +0,0 @@ -/* - * lobby_moc.cpp, 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 - * - */ -#include "StdInc.h" -#include "main.h" -#include "lobby_moc.h" -#include "ui_lobby_moc.h" -#include "lobbyroomrequest_moc.h" -#include "../mainwindow_moc.h" -#include "../modManager/cmodlist.h" -#include "../../lib/CConfigHandler.h" - -enum GameMode -{ - NEW_GAME = 0, LOAD_GAME = 1 -}; - -enum ModResolutionRoles -{ - ModNameRole = Qt::UserRole + 1, - ModEnableRole, - ModResolvableRole -}; - -Lobby::Lobby(QWidget *parent) : - QWidget(parent), - ui(new Ui::Lobby) -{ - ui->setupUi(this); - - connect(&socketLobby, SIGNAL(text(QString)), ui->chatWidget, SLOT(sysMessage(QString))); - connect(&socketLobby, SIGNAL(receive(QString)), this, SLOT(dispatchMessage(QString))); - connect(&socketLobby, SIGNAL(disconnect()), this, SLOT(onDisconnected())); - connect(ui->chatWidget, SIGNAL(messageSent(QString)), this, SLOT(onMessageSent(QString))); - connect(ui->chatWidget, SIGNAL(channelSwitch(QString)), this, SLOT(onChannelSwitch(QString))); - - QString hostString("%1:%2"); - hostString = hostString.arg(QString::fromStdString(settings["launcher"]["lobbyUrl"].String())); - hostString = hostString.arg(settings["launcher"]["lobbyPort"].Integer()); - - ui->serverEdit->setText(hostString); - ui->userEdit->setText(QString::fromStdString(settings["launcher"]["lobbyUsername"].String())); - ui->kickButton->setVisible(false); -} - -void Lobby::changeEvent(QEvent *event) -{ - if(event->type() == QEvent::LanguageChange) - { - ui->retranslateUi(this); - } - QWidget::changeEvent(event); -} - -Lobby::~Lobby() -{ - delete ui; -} - -QMap Lobby::buildModsMap() const -{ - QMap result; - QObject * mainWindow = qApp->activeWindow(); - if(!mainWindow) - mainWindow = parent(); - if(!mainWindow) - return result; //probably something is really wrong here - - while(mainWindow->parent()) - mainWindow = mainWindow->parent(); - const auto & modlist = qobject_cast(mainWindow)->getModList(); - - for(auto & modname : modlist.getModList()) - { - auto mod = modlist.getMod(modname); - if(mod.isEnabled()) - { - result[modname] = mod.getValue("version").toString(); - } - } - return result; -} - -bool Lobby::isModAvailable(const QString & modName, const QString & modVersion) const -{ - QObject * mainWindow = qApp->activeWindow(); - while(mainWindow->parent()) - mainWindow = mainWindow->parent(); - const auto & modlist = qobject_cast(mainWindow)->getModList(); - - if(!modlist.hasMod(modName)) - return false; - - auto mod = modlist.getMod(modName); - return (mod.isInstalled () || mod.isAvailable()) && (mod.getValue("version") == modVersion); -} - -void Lobby::serverCommand(const ServerCommand & command) try -{ - //initialize variables outside of switch block - const QString statusPlaceholder("%1 %2\n"); - const auto & args = command.arguments; - int amount; - int tagPoint; - QString joinStr; - switch(command.command) - { - case SRVERROR: - protocolAssert(args.size()); - ui->chatWidget->chatMessage("System error", args[0], true); - if(authentificationStatus == AuthStatus::AUTH_NONE) - authentificationStatus = AuthStatus::AUTH_ERROR; - break; - - case CREATED: - protocolAssert(args.size()); - hostSession = args[0]; - session = args[0]; - ui->chatWidget->setSession(session); - break; - - case SESSIONS: - protocolAssert(args.size()); - amount = args[0].toInt(); - protocolAssert(amount * 4 == (args.size() - 1)); - ui->sessionsTable->setRowCount(amount); - - tagPoint = 1; - for(int i = 0; i < amount; ++i) - { - QTableWidgetItem * sessionNameItem = new QTableWidgetItem(args[tagPoint++]); - ui->sessionsTable->setItem(i, 0, sessionNameItem); - - int playersJoined = args[tagPoint++].toInt(); - int playersTotal = args[tagPoint++].toInt(); - auto * sessionPlayerItem = new QTableWidgetItem(QString("%1/%2").arg(playersJoined).arg(playersTotal)); - ui->sessionsTable->setItem(i, 1, sessionPlayerItem); - - auto * sessionProtectedItem = new QTableWidgetItem(); - bool isPrivate = (args[tagPoint++] == "True"); - sessionProtectedItem->setData(Qt::UserRole, isPrivate); - if(isPrivate) - sessionProtectedItem->setIcon(QIcon("icons:room-private.png")); - ui->sessionsTable->setItem(i, 2, sessionProtectedItem); - } - break; - - case JOINED: - case KICKED: - protocolAssert(args.size() == 2); - if(args[1] == username) - { - hostModsMap.clear(); - session = ""; - ui->chatWidget->setSession(session); - ui->buttonReady->setText("Ready"); - ui->optNewGame->setChecked(true); - session = args[0]; - ui->chatWidget->setSession(session); - bool isHost = command.command == JOINED && hostSession == session; - ui->optNewGame->setEnabled(isHost); - ui->optLoadGame->setEnabled(isHost); - ui->stackedWidget->setCurrentWidget(command.command == JOINED ? ui->roomPage : ui->sessionsPage); - } - else - { - joinStr = (command.command == JOINED ? "%1 joined to the session %2" : "%1 left session %2"); - ui->chatWidget->sysMessage(joinStr.arg(args[1], args[0])); - } - break; - - case MODS: { - protocolAssert(args.size() > 0); - amount = args[0].toInt(); - protocolAssert(amount * 2 == (args.size() - 1)); - - tagPoint = 1; - for(int i = 0; i < amount; ++i, tagPoint += 2) - hostModsMap[args[tagPoint]] = args[tagPoint + 1]; - - updateMods(); - break; - } - - case CLIENTMODS: { - protocolAssert(args.size() >= 1); - auto & clientModsMap = clientsModsMap[args[0]]; - amount = args[1].toInt(); - protocolAssert(amount * 2 == (args.size() - 2)); - - tagPoint = 2; - for(int i = 0; i < amount; ++i, tagPoint += 2) - clientModsMap[args[tagPoint]] = args[tagPoint + 1]; - - break; - } - - - case STATUS: - protocolAssert(args.size() > 0); - amount = args[0].toInt(); - protocolAssert(amount * 2 == (args.size() - 1)); - - tagPoint = 1; - ui->playersList->clear(); - for(int i = 0; i < amount; ++i, tagPoint += 2) - { - if(args[tagPoint + 1] == "True") - ui->playersList->addItem(new QListWidgetItem(QIcon("icons:mod-enabled.png"), args[tagPoint])); - else - ui->playersList->addItem(new QListWidgetItem(QIcon("icons:mod-disabled.png"), args[tagPoint])); - - if(args[tagPoint] == username) - { - if(args[tagPoint + 1] == "True") - ui->buttonReady->setText("Not ready"); - else - ui->buttonReady->setText("Ready"); - } - } - break; - - case START: { - protocolAssert(args.size() == 1); - //actually start game - gameArgs << "--lobby"; - gameArgs << "--lobby-address" << serverUrl; - gameArgs << "--lobby-port" << QString::number(serverPort); - gameArgs << "--lobby-username" << username; - gameArgs << "--lobby-gamemode" << QString::number(isLoadGameMode); - gameArgs << "--uuid" << args[0]; - startGame(gameArgs); - break; - } - - case HOST: { - protocolAssert(args.size() == 2); - gameArgs << "--lobby-host"; - gameArgs << "--lobby-uuid" << args[0]; - gameArgs << "--lobby-connections" << args[1]; - break; - } - - case CHAT: { - protocolAssert(args.size() > 1); - QString msg; - for(int i = 1; i < args.size(); ++i) - msg += args[i]; - ui->chatWidget->chatMessage(args[0], msg); - break; - } - - case CHATCHANNEL: { - protocolAssert(args.size() > 2); - QString msg; - for(int i = 2; i < args.size(); ++i) - msg += args[i]; - ui->chatWidget->chatMessage(args[0], args[1], msg); - break; - } - - case CHANNEL: { - protocolAssert(args.size() == 1); - ui->chatWidget->setChannel(args[0]); - break; - } - - case HEALTH: { - socketLobby.send(ProtocolStrings[ALIVE]); - break; - } - - case USERS: { - protocolAssert(args.size() > 0); - amount = args[0].toInt(); - - protocolAssert(amount == (args.size() - 1)); - ui->chatWidget->clearUsers(); - for(int i = 0; i < amount; ++i) - { - ui->chatWidget->addUser(args[i + 1]); - } - break; - } - - case GAMEMODE: { - protocolAssert(args.size() == 1); - isLoadGameMode = args[0].toInt(); - if(isLoadGameMode) - ui->optLoadGame->setChecked(true); - else - ui->optNewGame->setChecked(true); - break; - } - - default: - ui->chatWidget->sysMessage("Unknown server command"); - } - - if(authentificationStatus == AuthStatus::AUTH_ERROR) - { - socketLobby.disconnectServer(); - } - else - { - authentificationStatus = AuthStatus::AUTH_OK; - ui->newButton->setEnabled(true); - } -} -catch(const ProtocolError & e) -{ - ui->chatWidget->chatMessage("System error", e.what(), true); -} - -void Lobby::dispatchMessage(QString txt) try -{ - if(txt.isEmpty()) - return; - - QStringList parseTags = txt.split(":>>"); - protocolAssert(parseTags.size() > 1 && parseTags[0].isEmpty() && !parseTags[1].isEmpty()); - - for(int c = 1; c < parseTags.size(); ++c) - { - QStringList parseArgs = parseTags[c].split(":"); - protocolAssert(parseArgs.size() > 1); - - auto ctype = ProtocolStrings.key(parseArgs[0]); - parseArgs.pop_front(); - ServerCommand cmd(ctype, parseArgs); - serverCommand(cmd); - } -} -catch(const ProtocolError & e) -{ - ui->chatWidget->chatMessage("System error", e.what(), true); -} - -void Lobby::onDisconnected() -{ - authentificationStatus = AuthStatus::AUTH_NONE; - session = ""; - ui->chatWidget->setSession(session); - ui->chatWidget->setChannel("global"); - ui->stackedWidget->setCurrentWidget(ui->sessionsPage); - ui->connectButton->setChecked(false); - ui->serverEdit->setEnabled(true); - ui->userEdit->setEnabled(true); - ui->newButton->setEnabled(false); - ui->joinButton->setEnabled(false); - ui->sessionsTable->setRowCount(0); -} - -void Lobby::protocolAssert(bool expr) -{ - if(!expr) - throw ProtocolError("Protocol error"); -} - -void Lobby::on_connectButton_toggled(bool checked) -{ - if(checked) - { - ui->connectButton->setText(tr("Disconnect")); - authentificationStatus = AuthStatus::AUTH_NONE; - username = ui->userEdit->text(); - ui->chatWidget->setUsername(username); - const int connectionTimeout = settings["launcher"]["connectionTimeout"].Integer(); - - auto serverStrings = ui->serverEdit->text().split(":"); - if(serverStrings.size() != 2) - { - QMessageBox::critical(this, "Connection error", "Server address must have the format URL:port"); - return; - } - - serverUrl = serverStrings[0]; - serverPort = serverStrings[1].toInt(); - - Settings node = settings.write["launcher"]; - node["lobbyUrl"].String() = serverUrl.toStdString(); - node["lobbyPort"].Integer() = serverPort; - node["lobbyUsername"].String() = username.toStdString(); - - ui->serverEdit->setEnabled(false); - ui->userEdit->setEnabled(false); - - ui->chatWidget->sysMessage("Connecting to " + serverUrl + ":" + QString::number(serverPort)); - //show text immediately - ui->chatWidget->repaint(); - qApp->processEvents(); - - socketLobby.connectServer(serverUrl, serverPort, username, connectionTimeout); - } - else - { - ui->connectButton->setText(tr("Connect")); - ui->serverEdit->setEnabled(true); - ui->userEdit->setEnabled(true); - ui->chatWidget->clearUsers(); - hostModsMap.clear(); - updateMods(); - socketLobby.disconnectServer(); - } -} - -void Lobby::updateMods() -{ - ui->modsList->clear(); - if(hostModsMap.empty()) - return; - - auto createModListWidget = [](const QIcon & icon, const QString & label, const QString & name, bool enableFlag, bool resolveFlag) - { - auto * lw = new QListWidgetItem(icon, label); - lw->setData(ModResolutionRoles::ModNameRole, name); - lw->setData(ModResolutionRoles::ModEnableRole, enableFlag); - lw->setData(ModResolutionRoles::ModResolvableRole, resolveFlag); - return lw; - }; - - auto enabledMods = buildModsMap(); - for(const auto & mod : hostModsMap.keys()) - { - auto & modValue = hostModsMap[mod]; - auto modName = QString("%1 (v%2)").arg(mod, modValue); - if(enabledMods.contains(mod)) - { - if(enabledMods[mod] == modValue) - enabledMods.remove(mod); //mod fully matches, remove from list - else - { - //mod version mismatch - ui->modsList->addItem(createModListWidget(QIcon("icons:mod-update.png"), modName, mod, true, false)); - } - } - else if(isModAvailable(mod, modValue)) - { - //mod is available and needs to be enabled - ui->modsList->addItem(createModListWidget(QIcon("icons:mod-enabled.png"), modName, mod, true, true)); - } - else - { - //mod is not available and needs to be installed - ui->modsList->addItem(createModListWidget(QIcon("icons:mod-delete.png"), modName, mod, true, false)); - } - } - for(const auto & remainMod : enabledMods.keys()) - { - auto modName = QString("%1 (v%2)").arg(remainMod, enabledMods[remainMod]); - //mod needs to be disabled - ui->modsList->addItem(createModListWidget(QIcon("icons:mod-disabled.png"), modName, remainMod, false, true)); - } - if(!ui->modsList->count()) - { - ui->buttonResolve->setEnabled(false); - ui->modsList->addItem(tr("No issues detected")); - } - else - { - ui->buttonResolve->setEnabled(true); - } -} - -void Lobby::on_newButton_clicked() -{ - new LobbyRoomRequest(socketLobby, "", buildModsMap(), this); -} - -void Lobby::on_joinButton_clicked() -{ - auto * item = ui->sessionsTable->item(ui->sessionsTable->currentRow(), 0); - if(item) - { - auto isPrivate = ui->sessionsTable->item(ui->sessionsTable->currentRow(), 2)->data(Qt::UserRole).toBool(); - if(isPrivate) - new LobbyRoomRequest(socketLobby, item->text(), buildModsMap(), this); - else - socketLobby.requestJoinSession(item->text(), "", buildModsMap()); - } -} - -void Lobby::on_buttonLeave_clicked() -{ - socketLobby.requestLeaveSession(session); -} - -void Lobby::on_buttonReady_clicked() -{ - if(ui->buttonReady->text() == "Ready") - ui->buttonReady->setText("Not ready"); - else - ui->buttonReady->setText("Ready"); - socketLobby.requestReadySession(session); -} - -void Lobby::on_sessionsTable_itemSelectionChanged() -{ - auto selection = ui->sessionsTable->selectedItems(); - ui->joinButton->setEnabled(!selection.empty()); -} - -void Lobby::on_playersList_currentRowChanged(int currentRow) -{ - ui->kickButton->setVisible(ui->playersList->currentItem() - && currentRow > 0 - && ui->playersList->currentItem()->text() != username); -} - -void Lobby::on_kickButton_clicked() -{ - if(ui->playersList->currentItem() && ui->playersList->currentItem()->text() != username) - socketLobby.send(ProtocolStrings[KICK].arg(ui->playersList->currentItem()->text())); -} - - -void Lobby::on_buttonResolve_clicked() -{ - QStringList toEnableList; - QStringList toDisableList; - auto items = ui->modsList->selectedItems(); - if(items.empty()) - { - for(int i = 0; i < ui->modsList->count(); ++i) - items.push_back(ui->modsList->item(i)); - } - - for(auto * item : items) - { - auto modName = item->data(ModResolutionRoles::ModNameRole); - if(modName.isNull()) - continue; - - bool modToEnable = item->data(ModResolutionRoles::ModEnableRole).toBool(); - bool modToResolve = item->data(ModResolutionRoles::ModResolvableRole).toBool(); - - if(!modToResolve) - continue; - - if(modToEnable) - toEnableList << modName.toString(); - else - toDisableList << modName.toString(); - } - - //disabling first, then enabling - for(auto & mod : toDisableList) - emit disableMod(mod); - for(auto & mod : toEnableList) - emit enableMod(mod); -} - -void Lobby::on_optNewGame_toggled(bool checked) -{ - if(checked) - { - if(isLoadGameMode) - socketLobby.send(ProtocolStrings[HOSTMODE].arg(GameMode::NEW_GAME)); - } -} - -void Lobby::on_optLoadGame_toggled(bool checked) -{ - if(checked) - { - if(!isLoadGameMode) - socketLobby.send(ProtocolStrings[HOSTMODE].arg(GameMode::LOAD_GAME)); - } -} - -void Lobby::onMessageSent(QString message) -{ - socketLobby.send(ProtocolStrings[MESSAGE].arg(message)); -} - -void Lobby::onChannelSwitch(QString channel) -{ - socketLobby.send(ProtocolStrings[SETCHANNEL].arg(channel)); -} diff --git a/launcher/lobby/lobby_moc.h b/launcher/lobby/lobby_moc.h deleted file mode 100644 index 23a8ddac1..000000000 --- a/launcher/lobby/lobby_moc.h +++ /dev/null @@ -1,92 +0,0 @@ -/* - * lobby_moc.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 -#include "lobby.h" - -namespace Ui { -class Lobby; -} - -class Lobby : public QWidget -{ - Q_OBJECT - - void changeEvent(QEvent *event) override; -public: - explicit Lobby(QWidget *parent = nullptr); - ~Lobby(); - -signals: - - void enableMod(QString mod); - void disableMod(QString mod); - -public slots: - void updateMods(); - -private slots: - void dispatchMessage(QString); - void serverCommand(const ServerCommand &); - void onMessageSent(QString message); - void onChannelSwitch(QString channel); - - void on_connectButton_toggled(bool checked); - - void on_newButton_clicked(); - - void on_joinButton_clicked(); - - void on_buttonLeave_clicked(); - - void on_buttonReady_clicked(); - - void onDisconnected(); - - void on_sessionsTable_itemSelectionChanged(); - - void on_playersList_currentRowChanged(int currentRow); - - void on_kickButton_clicked(); - - void on_buttonResolve_clicked(); - - void on_optNewGame_toggled(bool checked); - - void on_optLoadGame_toggled(bool checked); - -private: - QString serverUrl; - int serverPort; - bool isLoadGameMode = false; - - Ui::Lobby *ui; - SocketLobby socketLobby; - QString hostSession; - QString session; - QString username; - QStringList gameArgs; - QMap hostModsMap; - QMap> clientsModsMap; - - enum AuthStatus - { - AUTH_NONE, AUTH_OK, AUTH_ERROR - }; - - AuthStatus authentificationStatus = AUTH_NONE; - -private: - QMap buildModsMap() const; - bool isModAvailable(const QString & modName, const QString & modVersion) const; - - - void protocolAssert(bool); -}; diff --git a/launcher/lobby/lobby_moc.ui b/launcher/lobby/lobby_moc.ui deleted file mode 100644 index 07406b877..000000000 --- a/launcher/lobby/lobby_moc.ui +++ /dev/null @@ -1,311 +0,0 @@ - - - Lobby - - - - 0 - 0 - 652 - 383 - - - - - - - - 0 - - - - - - 0 - 0 - - - - Username - - - - - - - - 0 - 0 - - - - Connect - - - true - - - - - - - 127.0.0.1:5002 - - - - - - - Server - - - - - - - - - - -1 - - - 0 - - - - - 0 - - - 10 - - - 0 - - - - - - 0 - 0 - - - - - - - - - - - 0 - 0 - - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - false - - - New room - - - - - - - false - - - Join room - - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - false - - - 80 - - - false - - - true - - - 20 - - - 20 - - - - Session - - - - - Players - - - - - - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - -1 - - - - - Kick player - - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - - - - - Players in the room - - - - - - - Leave - - - - - - - Mods mismatch - - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::MultiSelection - - - - - - - Ready - - - - - - - Resolve - - - - - - - 0 - - - - - New game - - - - - - - Load game - - - - - - - - - - - - - - - - Chat - QWidget -
lobby/chat_moc.h
- 1 -
-
- - -
diff --git a/launcher/lobby/lobbyroomrequest_moc.cpp b/launcher/lobby/lobbyroomrequest_moc.cpp deleted file mode 100644 index 62c2496db..000000000 --- a/launcher/lobby/lobbyroomrequest_moc.cpp +++ /dev/null @@ -1,59 +0,0 @@ -/* - * lobbyroomrequest_moc.cpp, 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 - * - */ -#include "lobbyroomrequest_moc.h" -#include "ui_lobbyroomrequest_moc.h" - -LobbyRoomRequest::LobbyRoomRequest(SocketLobby & socket, const QString & room, const QMap & mods, QWidget *parent) : - QDialog(parent), - ui(new Ui::LobbyRoomRequest), - socketLobby(socket), - mods(mods) -{ - ui->setupUi(this); - ui->nameEdit->setText(room); - if(!room.isEmpty()) - { - ui->nameEdit->setReadOnly(true); - ui->totalPlayers->setEnabled(false); - } - - show(); -} - -void LobbyRoomRequest::changeEvent(QEvent *event) -{ - if(event->type() == QEvent::LanguageChange) - { - ui->retranslateUi(this); - } - QDialog::changeEvent(event); -} - -LobbyRoomRequest::~LobbyRoomRequest() -{ - delete ui; -} - -void LobbyRoomRequest::on_buttonBox_accepted() -{ - if(ui->nameEdit->isReadOnly()) - { - socketLobby.requestJoinSession(ui->nameEdit->text(), ui->passwordEdit->text(), mods); - } - else - { - if(!ui->nameEdit->text().isEmpty()) - { - int totalPlayers = ui->totalPlayers->currentIndex() + 2; //where 2 is a minimum amount of players - socketLobby.requestNewSession(ui->nameEdit->text(), totalPlayers, ui->passwordEdit->text(), mods); - } - } -} - diff --git a/launcher/lobby/lobbyroomrequest_moc.h b/launcher/lobby/lobbyroomrequest_moc.h deleted file mode 100644 index eff7161e4..000000000 --- a/launcher/lobby/lobbyroomrequest_moc.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * lobbyroomrequest_moc.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 - * - */ -#ifndef LOBBYROOMREQUEST_MOC_H -#define LOBBYROOMREQUEST_MOC_H - -#include -#include "lobby.h" - -namespace Ui { -class LobbyRoomRequest; -} - -class LobbyRoomRequest : public QDialog -{ - Q_OBJECT - - void changeEvent(QEvent *event) override; -public: - explicit LobbyRoomRequest(SocketLobby & socket, const QString & room, const QMap & mods, QWidget *parent = nullptr); - ~LobbyRoomRequest(); - -private slots: - void on_buttonBox_accepted(); - -private: - Ui::LobbyRoomRequest *ui; - SocketLobby & socketLobby; - QMap mods; -}; - -#endif // LOBBYROOMREQUEST_MOC_H diff --git a/launcher/lobby/lobbyroomrequest_moc.ui b/launcher/lobby/lobbyroomrequest_moc.ui deleted file mode 100644 index 5b8db9b7b..000000000 --- a/launcher/lobby/lobbyroomrequest_moc.ui +++ /dev/null @@ -1,151 +0,0 @@ - - - LobbyRoomRequest - - - Qt::WindowModal - - - - 0 - 0 - 227 - 188 - - - - Room settings - - - - - - false - - - true - - - - - - Room name - - - - - - - - - - Maximum players - - - - - - - - 0 - 0 - - - - 2 - - - - 2 - - - - - 3 - - - - - 4 - - - - - 5 - - - - - 6 - - - - - 7 - - - - - 8 - - - - - - - - Password (optional) - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - rejected() - LobbyRoomRequest - reject() - - - 316 - 260 - - - 286 - 274 - - - - - buttonBox - accepted() - LobbyRoomRequest - accept() - - - 248 - 254 - - - 157 - 274 - - - - - diff --git a/launcher/mainwindow_moc.cpp b/launcher/mainwindow_moc.cpp index e9ae6d957..882925e34 100644 --- a/launcher/mainwindow_moc.cpp +++ b/launcher/mainwindow_moc.cpp @@ -53,7 +53,6 @@ void MainWindow::computeSidePanelSizes() QVector widgets = { ui->modslistButton, ui->settingsButton, - ui->lobbyButton, ui->aboutButton, ui->startEditorButton, ui->startGameButton @@ -86,10 +85,6 @@ MainWindow::MainWindow(QWidget * parent) ui->setupUi(this); - connect(ui->lobbyView, &Lobby::enableMod, ui->modlistView, &CModListView::enableModByName); - connect(ui->lobbyView, &Lobby::disableMod, ui->modlistView, &CModListView::disableModByName); - connect(ui->modlistView, &CModListView::modsChanged, ui->lobbyView, &Lobby::updateMods); - //load window settings QSettings s(Ui::teamName, Ui::appName); @@ -151,7 +146,6 @@ void MainWindow::enterSetup() { ui->startGameButton->setEnabled(false); ui->startEditorButton->setEnabled(false); - ui->lobbyButton->setEnabled(false); ui->settingsButton->setEnabled(false); ui->aboutButton->setEnabled(false); ui->modslistButton->setEnabled(false); @@ -165,7 +159,6 @@ void MainWindow::exitSetup() ui->startGameButton->setEnabled(true); ui->startEditorButton->setEnabled(true); - ui->lobbyButton->setEnabled(true); ui->settingsButton->setEnabled(true); ui->aboutButton->setEnabled(true); ui->modslistButton->setEnabled(true); @@ -228,12 +221,6 @@ void MainWindow::on_settingsButton_clicked() ui->tabListWidget->setCurrentIndex(TabRows::SETTINGS); } -void MainWindow::on_lobbyButton_clicked() -{ - ui->startGameButton->setEnabled(false); - ui->tabListWidget->setCurrentIndex(TabRows::LOBBY); -} - void MainWindow::on_aboutButton_clicked() { ui->startGameButton->setEnabled(true); diff --git a/launcher/mainwindow_moc.h b/launcher/mainwindow_moc.h index e9ff8d075..14d26c712 100644 --- a/launcher/mainwindow_moc.h +++ b/launcher/mainwindow_moc.h @@ -38,9 +38,8 @@ private: { MODS = 0, SETTINGS = 1, - LOBBY = 2, - SETUP = 3, - ABOUT = 4, + SETUP = 2, + ABOUT = 3, }; void changeEvent(QEvent *event) override; @@ -65,7 +64,6 @@ public slots: private slots: void on_modslistButton_clicked(); void on_settingsButton_clicked(); - void on_lobbyButton_clicked(); void on_startEditorButton_clicked(); void on_aboutButton_clicked(); }; diff --git a/launcher/mainwindow_moc.ui b/launcher/mainwindow_moc.ui index 1208bbe04..2f7a56934 100644 --- a/launcher/mainwindow_moc.ui +++ b/launcher/mainwindow_moc.ui @@ -133,56 +133,6 @@ - - - - - 1 - 10 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Lobby - - - - icons:menu-lobby.pngicons:menu-lobby.png - - - - 64 - 64 - - - - true - - - false - - - true - - - Qt::ToolButtonTextUnderIcon - - - true - - - @@ -370,7 +320,6 @@ - @@ -392,12 +341,6 @@
settingsView/csettingsview_moc.h
1 - - Lobby - QWidget -
lobby/lobby_moc.h
- 1 -
FirstLaunchView QWidget diff --git a/launcher/translation/chinese.ts b/launcher/translation/chinese.ts index 3eec1439d..0582a05c1 100644 --- a/launcher/translation/chinese.ts +++ b/launcher/translation/chinese.ts @@ -16,7 +16,7 @@ VCMI on Github - 访问VCMI的GUTHUB + 访问VCMI的Github @@ -574,7 +574,7 @@ Install successfully downloaded? Renderer - + 渲染器 @@ -687,59 +687,31 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use 显示开场动画 - + Active 激活 - + Disabled 禁用 - + Enable 启用 - + Not Installed 未安装 - + Install 安装 - - Chat - - - Form - 窗体 - - - - Users in lobby - 大厅内用户 - - - - Global chat - 全局聊天 - - - - type you message - 输入你的消息 - - - - send - 发送 - - FirstLaunchView @@ -852,7 +824,7 @@ Heroes® of Might and Magic® III HD is currently not supported! VCMI on Github - 访问VCMI的GUTHUB + 访问VCMI的Github @@ -930,12 +902,12 @@ Heroes® of Might and Magic® III HD is currently not supported! Heroes III installation found! - + 英雄无敌3安装目录已找到! Copy data to VCMI folder? - + 复制数据到VCMI文件夹吗? @@ -1054,119 +1026,6 @@ Heroes® of Might and Magic® III HD is currently not supported! 自动 (%1) - - Lobby - - - - Connect - 连接 - - - - Username - 用户名 - - - - Server - 服务器 - - - - Session - 会话 - - - - Players - 玩家 - - - - Resolve - 解析 - - - - New game - 新游戏 - - - - Load game - 加载游戏 - - - - New room - 新房间 - - - - Join room - 加入房间 - - - - Ready - 准备 - - - - Mods mismatch - Mod统一翻译为模组 - 模组不匹配 - - - - Leave - 离开 - - - - Kick player - 踢出玩家 - - - - Players in the room - 房间内的玩家 - - - - Disconnect - 断开 - - - - No issues detected - 没有发现问题 - - - - LobbyRoomRequest - - - Room settings - 房间设置 - - - - Room name - 房间名称 - - - - Maximum players - 最大玩家数 - - - - Password (optional) - 密码(可选) - - MainWindow @@ -1180,25 +1039,20 @@ Heroes® of Might and Magic® III HD is currently not supported! 设置 - + Help 帮助 - + Map Editor 地图编辑器 - + Start game 开始游戏 - - - Lobby - 大厅 - Mods diff --git a/launcher/translation/czech.ts b/launcher/translation/czech.ts index d0fb7a62f..38b38a4e9 100644 --- a/launcher/translation/czech.ts +++ b/launcher/translation/czech.ts @@ -681,59 +681,31 @@ Exkluzivní celá obrazovka - hra zakryje vaši celou obrazovku a použije vybra Zobrazit intro - + Active Aktivní - + Disabled - + Enable Povolit - + Not Installed - + Install Instalovat - - Chat - - - Form - Formulář - - - - Users in lobby - Uživatelé v předsíni - - - - Global chat - Obecná konverzace - - - - type you message - zadejte vaši zprávu - - - - send - poslat - - FirstLaunchView @@ -1047,118 +1019,6 @@ Heroes® of Might and Magic® III HD není v současnosti podporovaný!Automaticky (%1) - - Lobby - - - - Connect - Připojit - - - - Username - Uživatelské jméno - - - - Server - Server - - - - Session - Relace - - - - Players - Hráči - - - - Resolve - Vyřešit - - - - New game - Nová hra - - - - Load game - Načíst hru - - - - New room - Nová místnost - - - - Join room - Připojit se do místnosti - - - - Ready - Připraven - - - - Mods mismatch - Nesoulad modifikací - - - - Leave - Odejít - - - - Kick player - Vyhodit hráče - - - - Players in the room - Hráči v místnosti - - - - Disconnect - Odpojit - - - - No issues detected - Bez problémů - - - - LobbyRoomRequest - - - Room settings - Nastavení místnosti - - - - Room name - Název místnosti - - - - Maximum players - Maximum hráčů - - - - Password (optional) - Heslo (volitelné) - - MainWindow @@ -1172,25 +1032,20 @@ Heroes® of Might and Magic® III HD není v současnosti podporovaný!Nastavení - + Help Nápověda - + Map Editor Editor map - + Start game Spustit hru - - - Lobby - Předsíň - Mods diff --git a/launcher/translation/english.ts b/launcher/translation/english.ts index 5ed7e0e24..593169a2a 100644 --- a/launcher/translation/english.ts +++ b/launcher/translation/english.ts @@ -668,59 +668,31 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Active - + Disabled - + Enable - + Not Installed - + Install - - Chat - - - Form - - - - - Users in lobby - - - - - Global chat - - - - - type you message - - - - - send - - - FirstLaunchView @@ -1028,118 +1000,6 @@ Heroes® of Might and Magic® III HD is currently not supported! - - Lobby - - - - Connect - - - - - Username - - - - - Server - - - - - Session - - - - - Players - - - - - Resolve - - - - - New game - - - - - Load game - - - - - New room - - - - - Join room - - - - - Ready - - - - - Mods mismatch - - - - - Leave - - - - - Kick player - - - - - Players in the room - - - - - Disconnect - - - - - No issues detected - - - - - LobbyRoomRequest - - - Room settings - - - - - Room name - - - - - Maximum players - - - - - Password (optional) - - - MainWindow @@ -1153,25 +1013,20 @@ Heroes® of Might and Magic® III HD is currently not supported! - + Help - + Map Editor - + Start game - - - Lobby - - Mods diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index b812bee3d..d73d50dd2 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -679,59 +679,31 @@ Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écra Montrer l'intro - + Active Actif - + Disabled Désactivé - + Enable Activé - + Not Installed Pas Installé - + Install Installer - - Chat - - - Form - - - - - Users in lobby - - - - - Global chat - - - - - type you message - - - - - send - - - FirstLaunchView @@ -1045,118 +1017,6 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge Auto (%1) - - Lobby - - - Username - Nom d'utilisateur - - - - - Connect - Connecter - - - - Server - Serveur - - - - New room - Nouveau salon - - - - Join room - Rejoindre le salon - - - - Session - Session - - - - Players - Joueurs - - - - Kick player - Jeter le joueur - - - - Players in the room - Joueurs dans le salon - - - - Leave - Quitter - - - - Mods mismatch - Incohérence de mods - - - - Ready - Prêt - - - - Resolve - Résoudre - - - - New game - Nouvelle partie - - - - Load game - Charger une partie - - - - Disconnect - Déconnecter - - - - No issues detected - Pas de problème détecté - - - - LobbyRoomRequest - - - Room settings - Paramètres de salon - - - - Room name - Nom de salon - - - - Maximum players - Maximum de joueurs - - - - Password (optional) - Mot de passe (optionnel) - - MainWindow @@ -1176,21 +1036,16 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge - Lobby - Salle d'attente - - - Help Aide - + Map Editor Éditeur de carte - + Start game Démarrer une partie diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index 65a6f03c9..36eab6711 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -681,59 +681,31 @@ Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwend Intro anzeigen - + Active Aktiv - + Disabled Deaktiviert - + Enable Aktivieren - + Not Installed Nicht installiert - + Install Installieren - - Chat - - - Form - Formular - - - - Users in lobby - Benutzer in der Lobby - - - - Global chat - Globaler Chat - - - - type you message - Nachricht eingeben - - - - send - senden - - FirstLaunchView @@ -1047,118 +1019,6 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! Auto (%1) - - Lobby - - - - Connect - Verbinden - - - - Username - Benutzername - - - - Server - Server - - - - Session - Sitzung - - - - Players - Spieler - - - - Resolve - Auflösen - - - - New game - Neues Spiel - - - - Load game - Spiel laden - - - - New room - Neuer Raum - - - - Join room - Raum beitreten - - - - Ready - Bereit - - - - Mods mismatch - Mods stimmen nicht überein - - - - Leave - Verlassen - - - - Kick player - Spieler kicken - - - - Players in the room - Spieler im Raum - - - - Disconnect - Verbindung trennen - - - - No issues detected - Keine Probleme festgestellt - - - - LobbyRoomRequest - - - Room settings - Raumeinstellungen - - - - Room name - Raumname - - - - Maximum players - Maximale Spieler - - - - Password (optional) - Passwort (optional) - - MainWindow @@ -1178,21 +1038,16 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! - Lobby - Lobby - - - Help Hilfe - + Map Editor Karteneditor - + Start game Spiel starten diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index 30f1a7a69..d4748758f 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -681,59 +681,31 @@ Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybrane Pokaż intro - + Active Aktywny - + Disabled Wyłączone - + Enable Włącz - + Not Installed Nie zainstalowano - + Install Zainstaluj - - Chat - - - Form - Okno - - - - Users in lobby - Gracze w lobby - - - - Global chat - Globalny czat - - - - type you message - napisz wiadomość - - - - send - wyślij - - FirstLaunchView @@ -1047,118 +1019,6 @@ Heroes III: HD Edition nie jest obecnie wspierane! - - Lobby - - - - Connect - Połącz - - - - Username - Nazwa użytkownika - - - - Server - Serwer - - - - Session - Sesja - - - - Players - Gracze - - - - Resolve - Rozwiąż - - - - New game - Nowa gra - - - - Load game - Wczytaj grę - - - - New room - Nowy pokój - - - - Join room - Dołącz - - - - Ready - Zgłoś gotowość - - - - Mods mismatch - Niezgodność modów - - - - Leave - Wyjdź - - - - Kick player - Wyrzuć gracza - - - - Players in the room - Gracze w pokoju - - - - Disconnect - Rozłącz - - - - No issues detected - Nie znaleziono problemów - - - - LobbyRoomRequest - - - Room settings - Ustawienia pokoju - - - - Room name - Nazwa pokoju - - - - Maximum players - Maks. ilość graczy - - - - Password (optional) - Hasło (opcjonalnie) - - MainWindow @@ -1172,25 +1032,20 @@ Heroes III: HD Edition nie jest obecnie wspierane! Ustawienia - + Help Pomoc - + Map Editor Edytor map - + Start game Uruchom grę - - - Lobby - Lobby - Mods diff --git a/launcher/translation/russian.ts b/launcher/translation/russian.ts index 498a014d3..2ee2ae217 100644 --- a/launcher/translation/russian.ts +++ b/launcher/translation/russian.ts @@ -668,59 +668,31 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Вступление - + Active Активен - + Disabled Отключен - + Enable Включить - + Not Installed Не установлен - + Install Установить - - Chat - - - Form - - - - - Users in lobby - - - - - Global chat - - - - - type you message - - - - - send - - - FirstLaunchView @@ -1034,118 +1006,6 @@ Heroes® of Might and Magic® III HD is currently not supported! Авто (%1) - - Lobby - - - - Connect - Подключиться - - - - Username - Имя пользователя - - - - Server - Сервер - - - - Session - Сессия - - - - Players - Игроки - - - - Resolve - Скорректировать - - - - New game - Новая игра - - - - Load game - Загрузить игру - - - - New room - Создать комнату - - - - Join room - Присоединиться к комнате - - - - Ready - Готово - - - - Mods mismatch - Моды не совпадают - - - - Leave - Выйти - - - - Kick player - Выгнать игрока - - - - Players in the room - Игроки в комнате - - - - Disconnect - Отключиться - - - - No issues detected - Проблем не обнаружено - - - - LobbyRoomRequest - - - Room settings - Настройки комнаты - - - - Room name - Название - - - - Maximum players - Максимум игроков - - - - Password (optional) - Пароль (не обязательно) - - MainWindow @@ -1159,25 +1019,20 @@ Heroes® of Might and Magic® III HD is currently not supported! Параметры - + Help - + Map Editor Редактор карт - + Start game Играть - - - Lobby - Лобби - Mods diff --git a/launcher/translation/spanish.ts b/launcher/translation/spanish.ts index 2dd41b3a0..fab6c2e93 100644 --- a/launcher/translation/spanish.ts +++ b/launcher/translation/spanish.ts @@ -681,59 +681,31 @@ Pantalla completa - el juego cubrirá la totalidad de la pantalla y utilizará l Idioma de los datos de Heroes III. - + Active Activado - + Disabled Desactivado - + Enable Activar - + Not Installed No Instalado - + Install Instalar - - Chat - - - Form - Formulario - - - - Users in lobby - Usuarios en la sala - - - - Global chat - Chat general - - - - type you message - Escribe tu mensaje - - - - send - Enviar - - FirstLaunchView @@ -1047,118 +1019,6 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Automático (%1) - - Lobby - - - - Connect - Conectar - - - - Username - Nombre de usuario - - - - Server - Servidor - - - - Session - Sesión - - - - Players - Jugadores - - - - Resolve - Resolver - - - - New game - Nueva partida - - - - Load game - Cargar partida - - - - New room - Nueva sala - - - - Join room - Unirse a la sala - - - - Ready - Listo - - - - Mods mismatch - No coinciden los mods - - - - Leave - Salir - - - - Kick player - Expulsar jugador - - - - Players in the room - Jugadores en la sala - - - - Disconnect - Desconectar - - - - No issues detected - No se han detectado problemas - - - - LobbyRoomRequest - - - Room settings - Configuración de la sala - - - - Room name - Nombre de la sala - - - - Maximum players - Jugadores máximos - - - - Password (optional) - Contraseña (opcional) - - MainWindow @@ -1172,25 +1032,20 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Configuración - + Help Ayuda - + Map Editor Editor de Mapas - + Start game Iniciar juego - - - Lobby - Sala de Espera - Mods diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index 37afc3acd..5a8f80cd5 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -681,59 +681,31 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Вступні відео - + Active Активні - + Disabled Деактивований - + Enable Активувати - + Not Installed Не встановлено - + Install Встановити - - Chat - - - Form - - - - - Users in lobby - Гравців у лобі - - - - Global chat - Загальний чат - - - - type you message - введіть повідомлення - - - - send - Відправити - - FirstLaunchView @@ -1047,118 +1019,6 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс Авто (%1) - - Lobby - - - - Connect - Підключитися - - - - Username - Ім'я користувача - - - - Server - Сервер - - - - Session - Сесія - - - - Players - Гравці - - - - Resolve - Розв'язати - - - - New game - Нова гра - - - - Load game - Завантажити гру - - - - New room - Створити кімнату - - - - Join room - Приєднатися до кімнати - - - - Ready - Готовність - - - - Mods mismatch - Модифікації, що не збігаються - - - - Leave - Вийти з кімнати - - - - Kick player - Виключити гравця - - - - Players in the room - Гравці у кімнаті - - - - Disconnect - Від'єднатися - - - - No issues detected - Проблем не виявлено - - - - LobbyRoomRequest - - - Room settings - Налаштування кімнати - - - - Room name - Назва кімнати - - - - Maximum players - Максимум гравців - - - - Password (optional) - Пароль (за бажанням) - - MainWindow @@ -1178,21 +1038,16 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс - Lobby - Лобі - - - Help Допомога - + Map Editor Редактор мап - + Start game Грати diff --git a/launcher/translation/vietnamese.ts b/launcher/translation/vietnamese.ts index 175c609e3..58affaf3b 100644 --- a/launcher/translation/vietnamese.ts +++ b/launcher/translation/vietnamese.ts @@ -674,59 +674,31 @@ Toàn màn hình riêng biệt - Trò chơi chạy toàn màn hình và dùng đ Hiện thị giới thiệu - + Active Bật - + Disabled Tắt - + Enable Bật - + Not Installed Chưa cài đặt - + Install Cài đặt - - Chat - - - Form - - - - - Users in lobby - - - - - Global chat - - - - - type you message - - - - - send - - - FirstLaunchView @@ -1040,118 +1012,6 @@ Hiện tại chưa hỗ trợ Heroes® of Might and Magic® III HD!Tự động (%1) - - Lobby - - - - Connect - Kết nối - - - - Username - Tên đăng nhập - - - - Server - Máy chủ - - - - Session - Phiên - - - - Players - Người chơi - - - - Resolve - Phân tích - - - - New game - Tạo mới - - - - Load game - Tải lại - - - - New room - Tạo phòng - - - - Join room - Vào phòng - - - - Ready - Sẵn sàng - - - - Mods mismatch - Bản sửa đổi chưa giống - - - - Leave - Rời khỏi - - - - Kick player - Mời ra - - - - Players in the room - Người chơi trong phòng - - - - Disconnect - Thoát - - - - No issues detected - Không có vấn đề - - - - LobbyRoomRequest - - - Room settings - Cài đặt phòng - - - - Room name - Tên phòng - - - - Maximum players - Số người chơi tối đa - - - - Password (optional) - Mật khẩu (tùy chọn) - - MainWindow @@ -1165,25 +1025,20 @@ Hiện tại chưa hỗ trợ Heroes® of Might and Magic® III HD!Cài đặt - + Help - + Map Editor Tạo bản đồ - + Start game Chơi ngay - - - Lobby - Sảnh - Mods diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index 4ea8f5481..a4c416028 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -107,7 +107,7 @@ public: std::string getJsonKey() const override; void registerIcons(const IconRegistar & cb) const override; ArtifactID getId() const override; - virtual const IBonusBearer * getBonusBearer() const override; + const IBonusBearer * getBonusBearer() const override; std::string getDescriptionTranslated() const override; std::string getEventTranslated() const override; diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index f281a8d1f..4bfada15d 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -126,7 +126,7 @@ public: std::string getJsonKey() const override; void registerIcons(const IconRegistar & cb) const override; CreatureID getId() const override; - virtual const IBonusBearer * getBonusBearer() const override; + const IBonusBearer * getBonusBearer() const override; int32_t getAdvMapAmountMin() const override; int32_t getAdvMapAmountMax() const override; diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 8de208a9c..2d2065cfc 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -171,7 +171,7 @@ public: virtual void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out)const; //hero - virtual const CGHeroInstance * getHero(ObjectInstanceID objid) const override; + const CGHeroInstance * getHero(ObjectInstanceID objid) const override; const CGHeroInstance * getHeroWithSubid(int subid) const override; virtual int getHeroCount(PlayerColor player, bool includeGarrisoned) const; virtual bool getHeroInfo(const CGObjectInstance * hero, InfoAboutHero & dest, const CGObjectInstance * selectedObject = nullptr) const; @@ -183,7 +183,7 @@ public: //virtual const CGObjectInstance * getArmyInstance(ObjectInstanceID oid) const; //objects - virtual const CGObjectInstance * getObj(ObjectInstanceID objid, bool verbose = true) const override; + const CGObjectInstance * getObj(ObjectInstanceID objid, bool verbose = true) const override; virtual std::vector getBlockingObjs(int3 pos)const; virtual std::vector getVisitableObjs(int3 pos, bool verbose = true) const override; virtual std::vector getFlaggableObjects(int3 pos) const; diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index 656e72a0d..c3d286455 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -144,26 +144,26 @@ public: virtual std::string getBattleAIName() const = 0; //has to return name of the battle AI to be used //battle interface - virtual void activeStack(const BattleID & battleID, const CStack * stack) override; - virtual void yourTacticPhase(const BattleID & battleID, int distance) override; + void activeStack(const BattleID & battleID, const CStack * stack) override; + void yourTacticPhase(const BattleID & battleID, int distance) override; - virtual void battleNewRound(const BattleID & battleID) override; - virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; - virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; + void battleNewRound(const BattleID & battleID) override; + void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; + void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; virtual void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; - virtual void actionStarted(const BattleID & battleID, const BattleAction &action) override; - virtual void battleNewRoundFirst(const BattleID & battleID) override; - virtual void actionFinished(const BattleID & battleID, const BattleAction &action) override; - virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; + void actionStarted(const BattleID & battleID, const BattleAction &action) override; + void battleNewRoundFirst(const BattleID & battleID) override; + void actionFinished(const BattleID & battleID, const BattleAction &action) override; + void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) override; virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; - virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; - virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; - virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; + void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; + void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; + void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; virtual void battleUnitsChanged(const BattleID & battleID, const std::vector & units) override; - virtual void saveGame(BinarySerializer & h) override; - virtual void loadGame(BinaryDeserializer & h) override; + void saveGame(BinarySerializer & h) override; + void loadGame(BinaryDeserializer & h) override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index a882cda0b..b8ba71690 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -326,6 +326,8 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js { JsonNode classConf = node["mapObject"]; classConf["heroClass"].String() = identifier; + if (!node["compatibilityIdentifiers"].isNull()) + classConf["compatibilityIdentifiers"] = node["compatibilityIdentifiers"]; classConf.setMeta(scope); VLC->objtypeh->loadSubObject(identifier, classConf, index, heroClass->getIndex()); }); @@ -756,6 +758,9 @@ void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNod objects.emplace_back(object); registerObject(scope, "hero", name, object->getIndex()); + + for(const auto & compatID : data["compatibilityIdentifiers"].Vector()) + registerObject(scope, "hero", compatID.String(), object->getIndex()); } void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) @@ -767,6 +772,8 @@ void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNod objects[index] = object; registerObject(scope, "hero", name, object->getIndex()); + for(const auto & compatID : data["compatibilityIdentifiers"].Vector()) + registerObject(scope, "hero", compatID.String(), object->getIndex()); } ui32 CHeroHandler::level (TExpType experience) const diff --git a/lib/CHeroHandler.h b/lib/CHeroHandler.h index 1a3219f8c..1b066d029 100644 --- a/lib/CHeroHandler.h +++ b/lib/CHeroHandler.h @@ -31,11 +31,11 @@ class CRandomGenerator; class JsonSerializeFormat; class BattleField; -enum class EHeroGender : uint8_t +enum class EHeroGender : int8_t { + DEFAULT = -1, // from h3m, instance has same gender as hero type MALE = 0, FEMALE = 1, - DEFAULT = 0xff // from h3m, instance has same gender as hero type }; class DLL_LINKAGE CHero : public HeroType diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index 1996fb62b..3bfa34b2b 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -102,6 +102,7 @@ void GameSettings::load(const JsonNode & input) {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" }, {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom" }, {EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" }, + {EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES, "pathfinder", "originalFlyRules" }, {EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" }, {EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" }, }; diff --git a/lib/GameSettings.h b/lib/GameSettings.h index 4a5f5256d..65265147f 100644 --- a/lib/GameSettings.h +++ b/lib/GameSettings.h @@ -66,6 +66,7 @@ enum class EGameSettings PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, PATHFINDER_USE_WHIRLPOOL, + PATHFINDER_ORIGINAL_FLY_RULES, TOWNS_BUILDINGS_PER_TURN_CAP, TOWNS_STARTING_DWELLING_CHANCES, COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 25e7d4261..ae7df063b 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -51,8 +51,6 @@ #include "RiverHandler.h" #include "TerrainHandler.h" -#include "serializer/Connection.h" - VCMI_LIB_NAMESPACE_BEGIN void CPrivilegedInfoCallback::getFreeTiles(std::vector & tiles) const diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 23ac2da6b..f0a6334f8 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -72,6 +72,10 @@ JsonNode::JsonNode(JsonType Type) setType(Type); } +JsonNode::JsonNode(const std::byte *data, size_t datasize) + :JsonNode(reinterpret_cast(data), datasize) +{} + JsonNode::JsonNode(const char *data, size_t datasize) { JsonParser parser(data, datasize); @@ -417,6 +421,15 @@ JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) return ::resolvePointer(*this, jsonPointer); } +std::vector JsonNode::toBytes(bool compact) const +{ + std::string jsonString = toJson(compact); + auto dataBegin = reinterpret_cast(jsonString.data()); + auto dataEnd = dataBegin + jsonString.size(); + std::vector result(dataBegin, dataEnd); + return result; +} + std::string JsonNode::toJson(bool compact) const { std::ostringstream out; diff --git a/lib/JsonNode.h b/lib/JsonNode.h index 03527a2e8..75f9128b2 100644 --- a/lib/JsonNode.h +++ b/lib/JsonNode.h @@ -51,6 +51,7 @@ public: JsonNode(JsonType Type = JsonType::DATA_NULL); //Create tree from Json-formatted input explicit JsonNode(const char * data, size_t datasize); + explicit JsonNode(const std::byte * data, size_t datasize); //Create tree from JSON file explicit JsonNode(const JsonPath & fileURI); explicit JsonNode(const std::string & modName, const JsonPath & fileURI); @@ -116,6 +117,7 @@ public: const JsonNode & operator[](size_t child) const; std::string toJson(bool compact = false) const; + std::vector toBytes(bool compact = false) const; template void serialize(Handler &h) { diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index 194f7d561..73b896716 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -111,7 +111,7 @@ void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const if(i == si->playerInfos.cend() && !ignoreNoHuman) throw std::domain_error(VLC->generaltexth->translate("core.genrltxt.530")); - if(si->mapGenOptions && si->mode == StartInfo::NEW_GAME) + if(si->mapGenOptions && si->mode == EStartMode::NEW_GAME) { if(!si->mapGenOptions->checkOptions()) throw std::domain_error(VLC->generaltexth->translate("core.genrltxt.751")); @@ -123,7 +123,12 @@ bool LobbyInfo::isClientHost(int clientId) const return clientId == hostClientId; } -std::set LobbyInfo::getAllClientPlayers(int clientId) +bool LobbyInfo::isPlayerHost(const PlayerColor & color) const +{ + return vstd::contains(getAllClientPlayers(hostClientId), color); +} + +std::set LobbyInfo::getAllClientPlayers(int clientId) const { std::set players; for(auto & elem : si->playerInfos) diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 550503f40..4067124a8 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -98,12 +98,18 @@ struct DLL_LINKAGE PlayerSettings HeroTypeID getHeroValidated() const; }; +enum class EStartMode : int32_t +{ + NEW_GAME, + LOAD_GAME, + CAMPAIGN, + INVALID = 255 +}; + /// Struct which describes the difficulty, the turn time,.. of a heroes match. struct DLL_LINKAGE StartInfo { - enum EMode {NEW_GAME, LOAD_GAME, CAMPAIGN, INVALID = 255}; - - EMode mode; + EStartMode mode; ui8 difficulty; //0=easy; 4=impossible using TPlayerInfos = std::map; @@ -152,7 +158,7 @@ struct DLL_LINKAGE StartInfo h & campState; } - StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), + StartInfo() : mode(EStartMode::INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(nullptr))), fileURI("") { @@ -197,7 +203,6 @@ struct DLL_LINKAGE LobbyState struct DLL_LINKAGE LobbyInfo : public LobbyState { - boost::mutex stateMutex; std::string uuid; LobbyInfo() {} @@ -205,7 +210,8 @@ struct DLL_LINKAGE LobbyInfo : public LobbyState void verifyStateBeforeStart(bool ignoreNoHuman = false) const; bool isClientHost(int clientId) const; - std::set getAllClientPlayers(int clientId); + bool isPlayerHost(const PlayerColor & color) const; + std::set getAllClientPlayers(int clientId) const; std::vector getConnectedPlayerIdsForClient(int clientId) const; // Helpers for lobby state access diff --git a/lib/TextOperations.cpp b/lib/TextOperations.cpp index 87dc765e2..d234a848c 100644 --- a/lib/TextOperations.cpp +++ b/lib/TextOperations.cpp @@ -219,4 +219,10 @@ std::string TextOperations::getFormattedDateTimeLocal(std::time_t dt) return vstd::getFormattedDateTime(dt, Languages::getLanguageOptions(settings["general"]["language"].String()).dateTimeFormat); } +std::string TextOperations::getFormattedTimeLocal(std::time_t dt) +{ + return vstd::getFormattedDateTime(dt, "%H:%M"); +} + + VCMI_LIB_NAMESPACE_END diff --git a/lib/TextOperations.h b/lib/TextOperations.h index ba944d641..8c0f827f6 100644 --- a/lib/TextOperations.h +++ b/lib/TextOperations.h @@ -59,6 +59,9 @@ namespace TextOperations /// get formatted DateTime depending on the language selected DLL_LINKAGE std::string getFormattedDateTimeLocal(std::time_t dt); + + /// get formatted time (without date) + DLL_LINKAGE std::string getFormattedTimeLocal(std::time_t dt); }; diff --git a/lib/bonuses/Updaters.h b/lib/bonuses/Updaters.h index 05219d2e8..f96c552e9 100644 --- a/lib/bonuses/Updaters.h +++ b/lib/bonuses/Updaters.h @@ -49,7 +49,7 @@ public: std::shared_ptr createUpdatedBonus(const std::shared_ptr & b, const CBonusSystemNode & context) const override; virtual std::string toString() const override; - virtual JsonNode toJsonNode() const override; + JsonNode toJsonNode() const override; }; class DLL_LINKAGE TimesHeroLevelUpdater : public IUpdater @@ -62,7 +62,7 @@ public: std::shared_ptr createUpdatedBonus(const std::shared_ptr & b, const CBonusSystemNode & context) const override; virtual std::string toString() const override; - virtual JsonNode toJsonNode() const override; + JsonNode toJsonNode() const override; }; class DLL_LINKAGE TimesStackLevelUpdater : public IUpdater @@ -75,7 +75,7 @@ public: std::shared_ptr createUpdatedBonus(const std::shared_ptr & b, const CBonusSystemNode & context) const override; virtual std::string toString() const override; - virtual JsonNode toJsonNode() const override; + JsonNode toJsonNode() const override; }; class DLL_LINKAGE ArmyMovementUpdater : public IUpdater @@ -98,7 +98,7 @@ public: std::shared_ptr createUpdatedBonus(const std::shared_ptr & b, const CBonusSystemNode & context) const override; virtual std::string toString() const override; - virtual JsonNode toJsonNode() const override; + JsonNode toJsonNode() const override; }; class DLL_LINKAGE OwnerUpdater : public IUpdater @@ -111,7 +111,7 @@ public: std::shared_ptr createUpdatedBonus(const std::shared_ptr& b, const CBonusSystemNode& context) const override; virtual std::string toString() const override; - virtual JsonNode toJsonNode() const override; + JsonNode toJsonNode() const override; }; VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 7bebcdf66..070d26492 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -330,7 +330,7 @@ public: static BuildingID FORT_LEVEL(unsigned int level) { assert(level < 3); - return BuildingID(Type::TOWN_HALL + level); + return BuildingID(Type::FORT + level); } static std::string encode(int32_t index); diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h index 67ddc1b4a..9c7b93f0e 100644 --- a/lib/constants/Enumerations.h +++ b/lib/constants/Enumerations.h @@ -64,10 +64,10 @@ enum class EMarketMode : int8_t enum class EAiTactic : int8_t { NONE = -1, - RANDOM, - WARRIOR, - BUILDER, - EXPLORER + RANDOM = 0, + WARRIOR = 1, + BUILDER = 2, + EXPLORER = 3 }; enum class EBuildingState : int8_t diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 7463c12a0..98460d32f 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -189,10 +189,10 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog switch(scenarioOps->mode) { - case StartInfo::NEW_GAME: + case EStartMode::NEW_GAME: initNewGame(mapService, allowSavingRandomMap, progressTracking); break; - case StartInfo::CAMPAIGN: + case EStartMode::CAMPAIGN: initCampaign(); break; default: @@ -711,7 +711,7 @@ void CGameState::initFogOfWar() void CGameState::initStartingBonus() { - if (scenarioOps->mode == StartInfo::CAMPAIGN) + if (scenarioOps->mode == EStartMode::CAMPAIGN) return; // These are the single scenario bonuses; predefined // campaign bonuses are spread out over other init* functions. diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index c7ae984fd..46417dbe7 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -39,7 +39,7 @@ CampaignHeroReplacement::CampaignHeroReplacement(CGHeroInstance * hero, const Ob CGameStateCampaign::CGameStateCampaign(CGameState * owner): gameState(owner) { - assert(gameState->scenarioOps->mode == StartInfo::CAMPAIGN); + assert(gameState->scenarioOps->mode == EStartMode::CAMPAIGN); assert(gameState->scenarioOps->campState != nullptr); } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 53bb5c701..d9ad418a7 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1111,7 +1111,7 @@ std::string CGHeroInstance::getClassNameTextID() const { if (isCampaignGem()) return "core.genrltxt.735"; - return type->heroClass->getNameTranslated(); + return type->heroClass->getNameTextID(); } std::string CGHeroInstance::getNameTextID() const @@ -1524,28 +1524,6 @@ std::string CGHeroInstance::getHeroTypeName() const void CGHeroInstance::afterAddToMap(CMap * map) { - if(ID != Obj::RANDOM_HERO) - { - auto existingHero = std::find_if(map->objects.begin(), map->objects.end(), [&](const CGObjectInstance * o) ->bool - { - return o && (o->ID == Obj::HERO || o->ID == Obj::PRISON) && o->subID == subID && o != this; - }); - - if(existingHero != map->objects.end()) - { - if(settings["session"]["editor"].Bool()) - { - logGlobal->warn("Hero is already on the map at %s", (*existingHero)->visitablePos().toString()); - } - else - { - logGlobal->error("Hero is already on the map at %s", (*existingHero)->visitablePos().toString()); - - throw std::runtime_error("Hero is already on the map"); - } - } - } - if(ID != Obj::PRISON) { map->heroesOnMap.emplace_back(this); diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 61ca63750..5f6683a7f 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -23,7 +23,7 @@ class CGTownInstance; class CMap; struct TerrainTile; struct TurnInfo; -enum class EHeroGender : uint8_t; +enum class EHeroGender : int8_t; class DLL_LINKAGE CGHeroPlaceholder : public CGObjectInstance { diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index b75a31be7..6b74fc402 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -424,7 +424,7 @@ class DLL_LINKAGE CGTerrainPatch : public CGObjectInstance public: using CGObjectInstance::CGObjectInstance; - virtual bool isTile2Terrain() const override + bool isTile2Terrain() const override { return true; } diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index c0d97a0c0..b6b520f13 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -42,8 +42,12 @@ DisposedHero::DisposedHero() : heroId(0), portrait(255) } -CMapEvent::CMapEvent() : players(0), humanAffected(0), computerAffected(0), - firstOccurence(0), nextOccurence(0) +CMapEvent::CMapEvent() + : players(0) + , humanAffected(false) + , computerAffected(false) + , firstOccurence(0) + , nextOccurence(0) { } diff --git a/lib/mapping/CMapDefines.h b/lib/mapping/CMapDefines.h index 27ad56a32..e1032aff7 100644 --- a/lib/mapping/CMapDefines.h +++ b/lib/mapping/CMapDefines.h @@ -37,8 +37,8 @@ public: MetaString message; TResources resources; ui8 players; // affected players, bit field? - ui8 humanAffected; - ui8 computerAffected; + bool humanAffected; + bool computerAffected; ui32 firstOccurence; ui32 nextOccurence; /// specifies after how many days the event will occur the next time; 0 if event occurs only one time diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index 8144b857f..c67940ac5 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -117,7 +117,7 @@ void CMapHeader::setupEvents() } CMapHeader::CMapHeader() : version(EMapFormat::VCMI), height(72), width(72), - twoLevel(true), difficulty(1), levelLimit(0), howManyTeams(0), areAnyPlayers(false) + twoLevel(true), difficulty(EMapDifficulty::NORMAL), levelLimit(0), howManyTeams(0), areAnyPlayers(false) { setupEvents(); allowedHeroes = VLC->heroh->getDefaultAllowed(); @@ -149,7 +149,7 @@ void CMapHeader::registerMapStrings() if(maxStrings == 0 || mapLanguages.empty()) { - logGlobal->info("Map %s doesn't have any supported translation", name.toString()); + logGlobal->trace("Map %s doesn't have any supported translation", name.toString()); return; } diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index c343fcab7..4f72ec311 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -191,6 +191,15 @@ struct DLL_LINKAGE TriggeredEvent } }; +enum class EMapDifficulty : uint8_t +{ + EASY = 0, + NORMAL = 1, + HARD = 2, + EXPERT = 3, + IMPOSSIBLE = 4 +}; + /// The map header holds information about loss/victory condition,map format, version, players, height, width,... class DLL_LINKAGE CMapHeader { @@ -218,7 +227,7 @@ public: bool twoLevel; /// The default value is true. MetaString name; MetaString description; - ui8 difficulty; /// The default value is 1 representing a normal map difficulty. + EMapDifficulty difficulty; /// Specifies the maximum level to reach for a hero. A value of 0 states that there is no /// maximum level for heroes. This is the default value. ui8 levelLimit; @@ -255,7 +264,12 @@ public: h & width; h & height; h & twoLevel; - h & difficulty; + // FIXME: we should serialize enum's according to their underlying type + // should be fixed when we are making breaking change to save compatiblity + static_assert(Handler::Version::MINIMAL < Handler::Version::RELEASE_143); + uint8_t difficultyInteger = static_cast(difficulty); + h & difficultyInteger; + difficulty = static_cast(difficultyInteger); h & levelLimit; h & areAnyPlayers; h & players; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index aabe26abc..c9a2a9b40 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -184,10 +184,10 @@ void CMapLoaderH3M::readHeader() mapHeader->twoLevel = reader->readBool(); mapHeader->name.appendTextID(readLocalizedString("header.name")); mapHeader->description.appendTextID(readLocalizedString("header.description")); - mapHeader->difficulty = reader->readInt8(); + mapHeader->difficulty = static_cast(reader->readInt8Checked(0, 4)); if(features.levelAB) - mapHeader->levelLimit = reader->readUInt8(); + mapHeader->levelLimit = reader->readInt8Checked(0, std::min(100u, VLC->heroh->maxSupportedLevel())); else mapHeader->levelLimit = 0; @@ -218,7 +218,7 @@ void CMapLoaderH3M::readPlayerInfo() continue; } - playerInfo.aiTactic = static_cast(reader->readUInt8()); + playerInfo.aiTactic = static_cast(reader->readInt8Checked(-1, 3)); if(features.levelSOD) reader->skipUnused(1); //TODO: check meaning? @@ -261,8 +261,8 @@ void CMapLoaderH3M::readPlayerInfo() if(features.levelAB) { reader->skipUnused(1); //TODO: check meaning? - uint32_t heroCount = reader->readUInt32(); - for(int pp = 0; pp < heroCount; ++pp) + size_t heroCount = reader->readUInt32(); + for(size_t pp = 0; pp < heroCount; ++pp) { SHeroName vv; vv.heroId = reader->readHero(); @@ -274,39 +274,13 @@ void CMapLoaderH3M::readPlayerInfo() } } -enum class EVictoryConditionType : uint8_t -{ - ARTIFACT = 0, - GATHERTROOP = 1, - GATHERRESOURCE = 2, - BUILDCITY = 3, - BUILDGRAIL = 4, - BEATHERO = 5, - CAPTURECITY = 6, - BEATMONSTER = 7, - TAKEDWELLINGS = 8, - TAKEMINES = 9, - TRANSPORTITEM = 10, - HOTA_ELIMINATE_ALL_MONSTERS = 11, - HOTA_SURVIVE_FOR_DAYS = 12, - WINSTANDARD = 255 -}; - -enum class ELossConditionType : uint8_t -{ - LOSSCASTLE = 0, - LOSSHERO = 1, - TIMEEXPIRES = 2, - LOSSSTANDARD = 255 -}; - void CMapLoaderH3M::readVictoryLossConditions() { mapHeader->triggeredEvents.clear(); mapHeader->victoryMessage.clear(); mapHeader->defeatMessage.clear(); - auto vicCondition = static_cast(reader->readUInt8()); + auto vicCondition = static_cast(reader->readInt8Checked(-1, 12)); EventCondition victoryCondition(EventCondition::STANDARD_WIN); EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); @@ -395,9 +369,9 @@ void CMapLoaderH3M::readVictoryLossConditions() EventExpression::OperatorAll oper; EventCondition cond(EventCondition::HAVE_BUILDING); cond.position = reader->readInt3(); - cond.objectType = BuildingID::HALL_LEVEL(reader->readUInt8() + 1); + cond.objectType = BuildingID::HALL_LEVEL(reader->readInt8Checked(0,3) + 1); oper.expressions.emplace_back(cond); - cond.objectType = BuildingID::FORT_LEVEL(reader->readUInt8()); + cond.objectType = BuildingID::FORT_LEVEL(reader->readInt8Checked(0, 2)); oper.expressions.emplace_back(cond); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283"); @@ -578,7 +552,7 @@ void CMapLoaderH3M::readVictoryLossConditions() } // Read loss conditions - auto lossCond = static_cast(reader->readUInt8()); + auto lossCond = static_cast(reader->readInt8Checked(-1, 2)); if(lossCond == ELossConditionType::LOSSSTANDARD) { mapHeader->defeatIconIndex = 3; @@ -685,9 +659,9 @@ void CMapLoaderH3M::readAllowedHeroes() if(features.levelAB) { - uint32_t placeholdersQty = reader->readUInt32(); + size_t placeholdersQty = reader->readUInt32(); - for (uint32_t i = 0; i < placeholdersQty; ++i) + for (size_t i = 0; i < placeholdersQty; ++i) { auto heroID = reader->readHero(); mapHeader->reservedCampaignHeroes.insert(heroID); @@ -700,9 +674,9 @@ void CMapLoaderH3M::readDisposedHeroes() // Reading disposed heroes (20 bytes) if(features.levelSOD) { - ui8 disp = reader->readUInt8(); + size_t disp = reader->readUInt8(); map->disposedHeroes.resize(disp); - for(int g = 0; g < disp; ++g) + for(size_t g = 0; g < disp; ++g) { map->disposedHeroes[g].heroId = reader->readHero(); map->disposedHeroes[g].portrait = reader->readHeroPortrait(); @@ -801,10 +775,10 @@ void CMapLoaderH3M::readAllowedSpellsAbilities() void CMapLoaderH3M::readRumors() { - uint32_t rumorsCount = reader->readUInt32(); + size_t rumorsCount = reader->readUInt32(); assert(rumorsCount < 1000); // sanity check - for(int it = 0; it < rumorsCount; it++) + for(size_t it = 0; it < rumorsCount; it++) { Rumor ourRumor; ourRumor.name = readBasicString(); @@ -853,7 +827,7 @@ void CMapLoaderH3M::readPredefinedHeroes() for(int yy = 0; yy < howMany; ++yy) { hero->secSkills[yy].first = reader->readSkill(); - hero->secSkills[yy].second = reader->readUInt8(); + hero->secSkills[yy].second = reader->readInt8Checked(1,3); } } @@ -864,7 +838,7 @@ void CMapLoaderH3M::readPredefinedHeroes() hero->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", heroID, "biography")); // 0xFF is default, 00 male, 01 female - hero->gender = static_cast(reader->readUInt8()); + hero->gender = static_cast(reader->readInt8Checked(-1, 1)); assert(hero->gender == EHeroGender::MALE || hero->gender == EHeroGender::FEMALE || hero->gender == EHeroGender::DEFAULT); bool hasCustomSpells = reader->readBool(); @@ -910,8 +884,8 @@ void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) // bag artifacts // number of artifacts in hero's bag - int amount = reader->readUInt16(); - for(int i = 0; i < amount; ++i) + size_t amount = reader->readUInt16(); + for(size_t i = 0; i < amount; ++i) { loadArtifactToSlot(hero, ArtifactPosition::BACKPACK_START + static_cast(hero->artifactsInBackpack.size())); } @@ -1038,33 +1012,33 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi reward.heroExperience = reader->readUInt32(); reward.manaDiff = reader->readInt32(); - if(auto val = reader->readUInt8()) + if(auto val = reader->readInt8Checked(-3, 3)) reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(idToBeGiven)); - if(auto val = reader->readUInt8()) + if(auto val = reader->readInt8Checked(-3, 3)) reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(idToBeGiven)); reader->readResourses(reward.resources); for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) reward.primary.at(x) = reader->readUInt8(); - int gabn = reader->readUInt8(); //number of gained abilities - for(int oo = 0; oo < gabn; ++oo) + size_t gabn = reader->readUInt8(); //number of gained abilities + for(size_t oo = 0; oo < gabn; ++oo) { auto rId = reader->readSkill(); - auto rVal = reader->readUInt8(); + auto rVal = reader->readInt8Checked(1,3); reward.secondary[rId] = rVal; } - int gart = reader->readUInt8(); //number of gained artifacts - for(int oo = 0; oo < gart; ++oo) + size_t gart = reader->readUInt8(); //number of gained artifacts + for(size_t oo = 0; oo < gart; ++oo) reward.artifacts.push_back(reader->readArtifact()); - int gspel = reader->readUInt8(); //number of gained spells - for(int oo = 0; oo < gspel; ++oo) + size_t gspel = reader->readUInt8(); //number of gained spells + for(size_t oo = 0; oo < gspel; ++oo) reward.spells.push_back(reader->readSpell()); - int gcre = reader->readUInt8(); //number of gained creatures - for(int oo = 0; oo < gcre; ++oo) + size_t gcre = reader->readUInt8(); //number of gained creatures + for(size_t oo = 0; oo < gcre; ++oo) { auto rId = reader->readCreature(); auto rVal = reader->readUInt16(); @@ -1094,7 +1068,7 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob //type will be set during initialization object->putStack(SlotID(0), hlp); - object->character = reader->readInt8(); + object->character = reader->readInt8Checked(0, 4); bool hasMessage = reader->readBool(); if(hasMessage) @@ -1192,17 +1166,17 @@ CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::share CGObjectInstance * CMapLoaderH3M::readScholar(const int3 & position, std::shared_ptr objectTemplate) { - enum class ScholarBonusType : uint8_t { + enum class ScholarBonusType : int8_t { + RANDOM = -1, PRIM_SKILL = 0, SECONDARY_SKILL = 1, SPELL = 2, - RANDOM = 255 }; auto * object = readGeneric(position, objectTemplate); auto * rewardable = dynamic_cast(object); - uint8_t bonusTypeRaw = reader->readUInt8(); + uint8_t bonusTypeRaw = reader->readInt8Checked(-1, 2); auto bonusType = static_cast(bonusTypeRaw); auto bonusID = reader->readUInt8(); @@ -1477,7 +1451,7 @@ CGObjectInstance * CMapLoaderH3M::readBank(const int3 & mapPosition, std::shared int32_t guardsPresetIndex = reader->readInt32(); // presence of upgraded stack: -1 = random, 0 = never, 1 = always - int8_t upgradedStackPresence = reader->readInt8(); + int8_t upgradedStackPresence = reader->readInt8Checked(-1, 1); assert(vstd::iswithin(guardsPresetIndex, -1, 4)); assert(vstd::iswithin(upgradedStackPresence, -1, 1)); @@ -1807,7 +1781,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec for(int i = 0; i < skillsCount; ++i) { object->secSkills[i].first = reader->readSkill(); - object->secSkills[i].second = reader->readUInt8(); + object->secSkills[i].second = reader->readInt8Checked(1,3); } } @@ -1815,7 +1789,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec if(hasGarison) readCreatureSet(object, 7); - object->formation = static_cast(reader->readUInt8()); + object->formation = static_cast(reader->readInt8Checked(0, 1)); assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT); loadArtifactsOfHero(object); @@ -1828,7 +1802,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec if(hasCustomBiography) object->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "biography")); - object->gender = static_cast(reader->readUInt8()); + object->gender = static_cast(reader->readInt8Checked(-1, 1)); assert(object->gender == EHeroGender::MALE || object->gender == EHeroGender::FEMALE || object->gender == EHeroGender::DEFAULT); } else @@ -1938,30 +1912,12 @@ enum class ESeerHutRewardType : uint8_t CREATURE = 10, }; -enum class EQuestMission { - NONE = 0, - LEVEL = 1, - PRIMARY_SKILL = 2, - KILL_HERO = 3, - KILL_CREATURE = 4, - ARTIFACT = 5, - ARMY = 6, - RESOURCES = 7, - HERO = 8, - PLAYER = 9, - HOTA_MULTI = 10, - // end of H3 missions - KEYMASTER = 100, - HOTA_HERO_CLASS = 101, - HOTA_REACH_DATE = 102 -}; - void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven) { EQuestMission missionType = EQuestMission::NONE; if(features.levelAB) { - missionType = static_cast(readQuest(hut, position)); + missionType = readQuest(hut, position); } else { @@ -1981,7 +1937,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con if(missionType != EQuestMission::NONE) { - auto rewardType = static_cast(reader->readUInt8()); + auto rewardType = static_cast(reader->readInt8Checked(0, 10)); Rewardable::VisitInfo vinfo; auto & reward = vinfo.reward; switch(rewardType) @@ -2003,36 +1959,34 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con } case ESeerHutRewardType::MORALE: { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, reader->readUInt8(), BonusSourceID(idToBeGiven)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, reader->readInt8Checked(-3, 3), BonusSourceID(idToBeGiven)); break; } case ESeerHutRewardType::LUCK: { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, reader->readUInt8(), BonusSourceID(idToBeGiven)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, reader->readInt8Checked(-3, 3), BonusSourceID(idToBeGiven)); break; } case ESeerHutRewardType::RESOURCES: { - auto rId = reader->readUInt8(); + auto rId = reader->readGameResID(); auto rVal = reader->readUInt32(); - assert(rId < features.resourcesCount); - reward.resources[rId] = rVal; break; } case ESeerHutRewardType::PRIMARY_SKILL: { - auto rId = reader->readUInt8(); + auto rId = reader->readPrimary(); auto rVal = reader->readUInt8(); - reward.primary.at(rId) = rVal; + reward.primary.at(rId.getNum()) = rVal; break; } case ESeerHutRewardType::SECONDARY_SKILL: { auto rId = reader->readSkill(); - auto rVal = reader->readUInt8(); + auto rVal = reader->readInt8Checked(1,3); reward.secondary[rId] = rVal; break; @@ -2071,11 +2025,11 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con } } -int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) +EQuestMission CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) { - auto missionId = reader->readUInt8(); + auto missionId = static_cast(reader->readInt8Checked(0, 10)); - switch(static_cast(missionId)) + switch(missionId) { case EQuestMission::NONE: return missionId; @@ -2100,8 +2054,8 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } case EQuestMission::ARTIFACT: { - int artNumber = reader->readUInt8(); - for(int yy = 0; yy < artNumber; ++yy) + size_t artNumber = reader->readUInt8(); + for(size_t yy = 0; yy < artNumber; ++yy) { auto artid = reader->readArtifact(); guard->quest->mission.artifacts.push_back(artid); @@ -2111,9 +2065,9 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } case EQuestMission::ARMY: { - int typeNumber = reader->readUInt8(); + size_t typeNumber = reader->readUInt8(); guard->quest->mission.creatures.resize(typeNumber); - for(int hh = 0; hh < typeNumber; ++hh) + for(size_t hh = 0; hh < typeNumber; ++hh) { guard->quest->mission.creatures[hh].type = reader->readCreature().toCreature(); guard->quest->mission.creatures[hh].count = reader->readUInt16(); @@ -2143,7 +2097,7 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) if(missionSubID == 0) { - missionId = int(EQuestMission::HOTA_HERO_CLASS); + missionId = EQuestMission::HOTA_HERO_CLASS; std::set heroClasses; reader->readBitmaskHeroClassesSized(heroClasses, false); for(auto & hc : heroClasses) @@ -2152,7 +2106,7 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } if(missionSubID == 1) { - missionId = int(EQuestMission::HOTA_REACH_DATE); + missionId = EQuestMission::HOTA_REACH_DATE; guard->quest->mission.daysPassed = reader->readUInt32() + 1; break; } @@ -2194,7 +2148,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt if(hasGarrison) readCreatureSet(object, 7); - object->formation = static_cast(reader->readUInt8()); + object->formation = static_cast(reader->readInt8Checked(0, 1)); assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT); bool hasCustomBuildings = reader->readBool(); @@ -2252,7 +2206,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt else event.humanAffected = true; - event.computerAffected = reader->readUInt8(); + event.computerAffected = reader->readBool(); event.firstOccurence = reader->readUInt16(); event.nextOccurence = reader->readUInt8(); diff --git a/lib/mapping/MapFormatH3M.h b/lib/mapping/MapFormatH3M.h index a0064bdca..84100c45a 100644 --- a/lib/mapping/MapFormatH3M.h +++ b/lib/mapping/MapFormatH3M.h @@ -35,6 +35,50 @@ class SpellID; class PlayerColor; class int3; +enum class EQuestMission { + NONE = 0, + LEVEL = 1, + PRIMARY_SKILL = 2, + KILL_HERO = 3, + KILL_CREATURE = 4, + ARTIFACT = 5, + ARMY = 6, + RESOURCES = 7, + HERO = 8, + PLAYER = 9, + HOTA_MULTI = 10, + // end of H3 missions + KEYMASTER = 100, + HOTA_HERO_CLASS = 101, + HOTA_REACH_DATE = 102 +}; + +enum class EVictoryConditionType : int8_t +{ + WINSTANDARD = -1, + ARTIFACT = 0, + GATHERTROOP = 1, + GATHERRESOURCE = 2, + BUILDCITY = 3, + BUILDGRAIL = 4, + BEATHERO = 5, + CAPTURECITY = 6, + BEATMONSTER = 7, + TAKEDWELLINGS = 8, + TAKEMINES = 9, + TRANSPORTITEM = 10, + HOTA_ELIMINATE_ALL_MONSTERS = 11, + HOTA_SURVIVE_FOR_DAYS = 12 +}; + +enum class ELossConditionType : int8_t +{ + LOSSSTANDARD = -1, + LOSSCASTLE = 0, + LOSSHERO = 1, + TIMEEXPIRES = 2 +}; + class DLL_LINKAGE CMapLoaderH3M : public IMapLoader { public: @@ -204,7 +248,7 @@ private: * * @param guard the quest guard where that quest should be applied to */ - int readQuest(IQuestObject * guard, const int3 & position); + EQuestMission readQuest(IQuestObject * guard, const int3 & position); void readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven); diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 9fa8375fe..90ad01008 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -1156,6 +1156,21 @@ void CMapLoaderJson::readObjects() { return a->getObjTypeIndex() < b->getObjTypeIndex(); }); + + + std::set debugHeroesOnMap; + for (auto const & object : map->objects) + { + if(object->ID != Obj::HERO && object->ID != Obj::PRISON) + continue; + + auto * hero = dynamic_cast(object.get()); + + if (debugHeroesOnMap.count(hero->getHeroType())) + logGlobal->error("Hero is already on the map at %s", hero->visitablePos().toString()); + + debugHeroesOnMap.insert(hero->getHeroType()); + } } void CMapLoaderJson::readTranslations() diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 2714e19c8..d2db92dff 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -183,6 +183,13 @@ RiverId MapReaderH3M::readRiver() return result; } +PrimarySkill MapReaderH3M::readPrimary() +{ + PrimarySkill result(readUInt8()); + assert(result <= PrimarySkill::KNOWLEDGE ); + return result; +} + SecondarySkill MapReaderH3M::readSkill() { SecondarySkill result(readUInt8()); @@ -400,32 +407,35 @@ bool MapReaderH3M::readBool() return result != 0; } -ui8 MapReaderH3M::readUInt8() +int8_t MapReaderH3M::readInt8Checked(int8_t lowerLimit, int8_t upperLimit) +{ + int8_t result = readInt8(); + assert(result >= lowerLimit); + assert(result <= upperLimit); + return std::clamp(result, lowerLimit, upperLimit); +} + +uint8_t MapReaderH3M::readUInt8() { return reader->readUInt8(); } -si8 MapReaderH3M::readInt8() +int8_t MapReaderH3M::readInt8() { return reader->readInt8(); } -ui16 MapReaderH3M::readUInt16() +uint16_t MapReaderH3M::readUInt16() { return reader->readUInt16(); } -si16 MapReaderH3M::readInt16() -{ - return reader->readInt16(); -} - -ui32 MapReaderH3M::readUInt32() +uint32_t MapReaderH3M::readUInt32() { return reader->readUInt32(); } -si32 MapReaderH3M::readInt32() +int32_t MapReaderH3M::readInt32() { return reader->readInt32(); } @@ -435,9 +445,4 @@ std::string MapReaderH3M::readBaseString() return reader->readBaseString(); } -CBinaryReader & MapReaderH3M::getInternalReader() -{ - return *reader; -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapReaderH3M.h b/lib/mapping/MapReaderH3M.h index e2b5e01b8..42b446ba5 100644 --- a/lib/mapping/MapReaderH3M.h +++ b/lib/mapping/MapReaderH3M.h @@ -40,6 +40,7 @@ public: TerrainId readTerrain(); RoadId readRoad(); RiverId readRiver(); + PrimarySkill readPrimary(); SecondarySkill readSkill(); SpellID readSpell(); SpellID readSpell32(); @@ -70,17 +71,17 @@ public: bool readBool(); - ui8 readUInt8(); - si8 readInt8(); - ui16 readUInt16(); - si16 readInt16(); - ui32 readUInt32(); - si32 readInt32(); + uint8_t readUInt8(); + int8_t readInt8(); + int8_t readInt8Checked(int8_t lowerLimit, int8_t upperLimit); + + uint16_t readUInt16(); + + uint32_t readUInt32(); + int32_t readInt32(); std::string readBaseString(); - CBinaryReader & getInternalReader(); - private: template Identifier remapIdentifier(const Identifier & identifier); diff --git a/lib/network/NetworkConnection.cpp b/lib/network/NetworkConnection.cpp new file mode 100644 index 000000000..01e20d951 --- /dev/null +++ b/lib/network/NetworkConnection.cpp @@ -0,0 +1,99 @@ +/* + * NetworkConnection.cpp, 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 + * + */ +#include "StdInc.h" +#include "NetworkConnection.h" + +VCMI_LIB_NAMESPACE_BEGIN + +NetworkConnection::NetworkConnection(INetworkConnectionListener & listener, const std::shared_ptr & socket) + : socket(socket) + , listener(listener) +{ + socket->set_option(boost::asio::ip::tcp::no_delay(true)); + socket->set_option(boost::asio::socket_base::send_buffer_size(4194304)); + socket->set_option(boost::asio::socket_base::receive_buffer_size(4194304)); +} + +void NetworkConnection::start() +{ + boost::asio::async_read(*socket, + readBuffer, + boost::asio::transfer_exactly(messageHeaderSize), + [self = shared_from_this()](const auto & ec, const auto & endpoint) { self->onHeaderReceived(ec); }); +} + +void NetworkConnection::onHeaderReceived(const boost::system::error_code & ecHeader) +{ + if (ecHeader) + { + listener.onDisconnected(shared_from_this(), ecHeader.message()); + return; + } + + if (readBuffer.size() < messageHeaderSize) + throw std::runtime_error("Failed to read header!"); + + uint32_t messageSize; + readBuffer.sgetn(reinterpret_cast(&messageSize), sizeof(messageSize)); + + if (messageSize > messageMaxSize) + { + listener.onDisconnected(shared_from_this(), "Invalid packet size!"); + return; + } + + boost::asio::async_read(*socket, + readBuffer, + boost::asio::transfer_exactly(messageSize), + [self = shared_from_this(), messageSize](const auto & ecPayload, const auto & endpoint) { self->onPacketReceived(ecPayload, messageSize); }); +} + +void NetworkConnection::onPacketReceived(const boost::system::error_code & ec, uint32_t expectedPacketSize) +{ + if (ec) + { + listener.onDisconnected(shared_from_this(), ec.message()); + return; + } + + if (readBuffer.size() < expectedPacketSize) + { + throw std::runtime_error("Failed to read packet!"); + } + + std::vector message(expectedPacketSize); + readBuffer.sgetn(reinterpret_cast(message.data()), expectedPacketSize); + listener.onPacketReceived(shared_from_this(), message); + + start(); +} + +void NetworkConnection::sendPacket(const std::vector & message) +{ + boost::system::error_code ec; + + // create array with single element - boost::asio::buffer can be constructed from containers, but not from plain integer + std::array messageSize{static_cast(message.size())}; + + boost::asio::write(*socket, boost::asio::buffer(messageSize), ec ); + boost::asio::write(*socket, boost::asio::buffer(message), ec ); + + //Note: ignoring error code, intended +} + +void NetworkConnection::close() +{ + boost::system::error_code ec; + socket->close(ec); + + //NOTE: ignoring error code, intended +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkConnection.h b/lib/network/NetworkConnection.h new file mode 100644 index 000000000..beaaba376 --- /dev/null +++ b/lib/network/NetworkConnection.h @@ -0,0 +1,37 @@ +/* + * NetworkConnection.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 "NetworkDefines.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class NetworkConnection : public INetworkConnection, public std::enable_shared_from_this +{ + static const int messageHeaderSize = sizeof(uint32_t); + static const int messageMaxSize = 64 * 1024 * 1024; // arbitrary size to prevent potential massive allocation if we receive garbage input + + std::shared_ptr socket; + + NetworkBuffer readBuffer; + INetworkConnectionListener & listener; + + void onHeaderReceived(const boost::system::error_code & ec); + void onPacketReceived(const boost::system::error_code & ec, uint32_t expectedPacketSize); + +public: + NetworkConnection(INetworkConnectionListener & listener, const std::shared_ptr & socket); + + void start(); + void close() override; + void sendPacket(const std::vector & message) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkDefines.h b/lib/network/NetworkDefines.h new file mode 100644 index 000000000..6b86ff23a --- /dev/null +++ b/lib/network/NetworkDefines.h @@ -0,0 +1,24 @@ +/* + * NetworkDefines.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 + +#include "NetworkInterface.h" + +VCMI_LIB_NAMESPACE_BEGIN + +using NetworkContext = boost::asio::io_service; +using NetworkSocket = boost::asio::ip::tcp::socket; +using NetworkAcceptor = boost::asio::ip::tcp::acceptor; +using NetworkBuffer = boost::asio::streambuf; +using NetworkTimer = boost::asio::steady_timer; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkHandler.cpp b/lib/network/NetworkHandler.cpp new file mode 100644 index 000000000..ddb15e091 --- /dev/null +++ b/lib/network/NetworkHandler.cpp @@ -0,0 +1,71 @@ +/* + * NetworkHandler.cpp, 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 + * + */ +#include "StdInc.h" +#include "NetworkHandler.h" + +#include "NetworkServer.h" +#include "NetworkConnection.h" + +VCMI_LIB_NAMESPACE_BEGIN + +std::unique_ptr INetworkHandler::createHandler() +{ + return std::make_unique(); +} + +NetworkHandler::NetworkHandler() + : io(std::make_shared()) +{} + +std::unique_ptr NetworkHandler::createServerTCP(INetworkServerListener & listener) +{ + return std::make_unique(listener, io); +} + +void NetworkHandler::connectToRemote(INetworkClientListener & listener, const std::string & host, uint16_t port) +{ + auto socket = std::make_shared(*io); + boost::asio::ip::tcp::resolver resolver(*io); + auto endpoints = resolver.resolve(host, std::to_string(port)); + boost::asio::async_connect(*socket, endpoints, [socket, &listener](const boost::system::error_code& error, const boost::asio::ip::tcp::endpoint& endpoint) + { + if (error) + { + listener.onConnectionFailed(error.message()); + return; + } + auto connection = std::make_shared(listener, socket); + connection->start(); + + listener.onConnectionEstablished(connection); + }); +} + +void NetworkHandler::run() +{ + boost::asio::executor_work_guardget_executor())> work{io->get_executor()}; + io->run(); +} + +void NetworkHandler::createTimer(INetworkTimerListener & listener, std::chrono::milliseconds duration) +{ + auto timer = std::make_shared(*io, duration); + timer->async_wait([&listener, timer](const boost::system::error_code& error){ + if (!error) + listener.onTimer(); + }); +} + +void NetworkHandler::stop() +{ + io->stop(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkHandler.h b/lib/network/NetworkHandler.h new file mode 100644 index 000000000..c5b326c67 --- /dev/null +++ b/lib/network/NetworkHandler.h @@ -0,0 +1,31 @@ +/* + * NetworkHandler.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 "NetworkDefines.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class NetworkHandler : public INetworkHandler +{ + std::shared_ptr io; + +public: + NetworkHandler(); + + std::unique_ptr createServerTCP(INetworkServerListener & listener) override; + void connectToRemote(INetworkClientListener & listener, const std::string & host, uint16_t port) override; + void createTimer(INetworkTimerListener & listener, std::chrono::milliseconds duration) override; + + void run() override; + void stop() override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkInterface.h b/lib/network/NetworkInterface.h new file mode 100644 index 000000000..58bd11023 --- /dev/null +++ b/lib/network/NetworkInterface.h @@ -0,0 +1,106 @@ +/* + * NetworkHandler.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 + +VCMI_LIB_NAMESPACE_BEGIN + +/// Base class for connections with other services, either incoming or outgoing +class DLL_LINKAGE INetworkConnection : boost::noncopyable +{ +public: + virtual ~INetworkConnection() = default; + virtual void sendPacket(const std::vector & message) = 0; + virtual void close() = 0; +}; + +using NetworkConnectionPtr = std::shared_ptr; +using NetworkConnectionWeakPtr = std::weak_ptr; + +/// Base class for outgoing connections support +class DLL_LINKAGE INetworkClient : boost::noncopyable +{ +public: + virtual ~INetworkClient() = default; + + virtual bool isConnected() const = 0; + virtual void sendPacket(const std::vector & message) = 0; +}; + +/// Base class for incoming connections support +class DLL_LINKAGE INetworkServer : boost::noncopyable +{ +public: + virtual ~INetworkServer() = default; + + virtual void start(uint16_t port) = 0; +}; + +/// Base interface that must be implemented by user of networking API to handle any connection callbacks +class DLL_LINKAGE INetworkConnectionListener +{ +public: + virtual void onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) = 0; + virtual void onPacketReceived(const std::shared_ptr & connection, const std::vector & message) = 0; + + virtual ~INetworkConnectionListener() = default; +}; + +/// Interface that must be implemented by user of networking API to handle outgoing connection callbacks +class DLL_LINKAGE INetworkClientListener : public INetworkConnectionListener +{ +public: + virtual void onConnectionFailed(const std::string & errorMessage) = 0; + virtual void onConnectionEstablished(const std::shared_ptr &) = 0; +}; + +/// Interface that must be implemented by user of networking API to handle incoming connection callbacks +class DLL_LINKAGE INetworkServerListener : public INetworkConnectionListener +{ +public: + virtual void onNewConnection(const std::shared_ptr &) = 0; +}; + +/// Interface that must be implemented by user of networking API to handle timers on network thread +class DLL_LINKAGE INetworkTimerListener +{ +public: + virtual ~INetworkTimerListener() = default; + + virtual void onTimer() = 0; +}; + +/// Main class for handling of all network activity +class DLL_LINKAGE INetworkHandler : boost::noncopyable +{ +public: + virtual ~INetworkHandler() = default; + + /// Constructs default implementation + static std::unique_ptr createHandler(); + + /// Creates an instance of TCP server that allows to receiving connections on a local port + virtual std::unique_ptr createServerTCP(INetworkServerListener & listener) = 0; + + /// Creates an instance of TCP client that allows to establish single outgoing connection to a remote port + /// On success: INetworkTimerListener::onConnectionEstablished() will be called, established connection provided as parameter + /// On failure: INetworkTimerListener::onConnectionFailed will be called with human-readable error message + virtual void connectToRemote(INetworkClientListener & listener, const std::string & host, uint16_t port) = 0; + + /// Creates a timer that will be called once, after specified interval has passed + /// On success: INetworkTimerListener::onTimer() will be called + /// On failure: no-op + virtual void createTimer(INetworkTimerListener & listener, std::chrono::milliseconds duration) = 0; + + /// Starts network processing on this thread. Does not returns until networking processing has been terminated + virtual void run() = 0; + virtual void stop() = 0; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkServer.cpp b/lib/network/NetworkServer.cpp new file mode 100644 index 000000000..b408ed525 --- /dev/null +++ b/lib/network/NetworkServer.cpp @@ -0,0 +1,62 @@ +/* + * NetworkServer.cpp, 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 + * + */ +#include "StdInc.h" +#include "NetworkServer.h" +#include "NetworkConnection.h" + +VCMI_LIB_NAMESPACE_BEGIN + +NetworkServer::NetworkServer(INetworkServerListener & listener, const std::shared_ptr & context) + : io(context) + , listener(listener) +{ +} + +void NetworkServer::start(uint16_t port) +{ + acceptor = std::make_shared(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)); + startAsyncAccept(); +} + +void NetworkServer::startAsyncAccept() +{ + auto upcomingConnection = std::make_shared(*io); + acceptor->async_accept(*upcomingConnection, [this, upcomingConnection](const auto & ec) { connectionAccepted(upcomingConnection, ec); }); +} + +void NetworkServer::connectionAccepted(std::shared_ptr upcomingConnection, const boost::system::error_code & ec) +{ + if(ec) + { + throw std::runtime_error("Something wrong during accepting: " + ec.message()); + } + + logNetwork->info("We got a new connection! :)"); + auto connection = std::make_shared(*this, upcomingConnection); + connections.insert(connection); + connection->start(); + listener.onNewConnection(connection); + startAsyncAccept(); +} + +void NetworkServer::onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) +{ + logNetwork->info("Connection lost! Reason: %s", errorMessage); + assert(connections.count(connection)); + connections.erase(connection); + listener.onDisconnected(connection, errorMessage); +} + +void NetworkServer::onPacketReceived(const std::shared_ptr & connection, const std::vector & message) +{ + listener.onPacketReceived(connection, message); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkServer.h b/lib/network/NetworkServer.h new file mode 100644 index 000000000..8fc0e8988 --- /dev/null +++ b/lib/network/NetworkServer.h @@ -0,0 +1,35 @@ +/* + * NetworkServer.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 "NetworkDefines.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class NetworkServer : public INetworkConnectionListener, public INetworkServer +{ + std::shared_ptr io; + std::shared_ptr acceptor; + std::set> connections; + + INetworkServerListener & listener; + + void connectionAccepted(std::shared_ptr, const boost::system::error_code & ec); + void startAsyncAccept(); + + void onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) override; + void onPacketReceived(const std::shared_ptr & connection, const std::vector & message) override; +public: + NetworkServer(INetworkServerListener & listener, const std::shared_ptr & context); + + void start(uint16_t port) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h index c77705174..fe1d9181e 100644 --- a/lib/networkPacks/NetPackVisitor.h +++ b/lib/networkPacks/NetPackVisitor.h @@ -151,7 +151,8 @@ public: virtual void visitLobbyChatMessage(LobbyChatMessage & pack) {} virtual void visitLobbyGuiAction(LobbyGuiAction & pack) {} virtual void visitLobbyLoadProgress(LobbyLoadProgress & pack) {} - virtual void visitLobbyEndGame(LobbyEndGame & pack) {} + virtual void visitLobbyRestartGame(LobbyRestartGame & pack) {} + virtual void visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack) {} virtual void visitLobbyStartGame(LobbyStartGame & pack) {} virtual void visitLobbyChangeHost(LobbyChangeHost & pack) {} virtual void visitLobbyUpdateState(LobbyUpdateState & pack) {} diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index e7c1a9f53..00ed57775 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -708,9 +708,9 @@ void LobbyLoadProgress::visitTyped(ICPackVisitor & visitor) visitor.visitLobbyLoadProgress(*this); } -void LobbyEndGame::visitTyped(ICPackVisitor & visitor) +void LobbyRestartGame::visitTyped(ICPackVisitor & visitor) { - visitor.visitLobbyEndGame(*this); + visitor.visitLobbyRestartGame(*this); } void LobbyStartGame::visitTyped(ICPackVisitor & visitor) @@ -718,6 +718,11 @@ void LobbyStartGame::visitTyped(ICPackVisitor & visitor) visitor.visitLobbyStartGame(*this); } +void LobbyPrepareStartGame::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyPrepareStartGame(*this); +} + void LobbyChangeHost::visitTyped(ICPackVisitor & visitor) { visitor.visitLobbyChangeHost(*this); diff --git a/lib/networkPacks/PacksForLobby.h b/lib/networkPacks/PacksForLobby.h index ff7c242f3..ae0cf6635 100644 --- a/lib/networkPacks/PacksForLobby.h +++ b/lib/networkPacks/PacksForLobby.h @@ -29,7 +29,7 @@ struct DLL_LINKAGE CLobbyPackToPropagate : public CPackForLobby struct DLL_LINKAGE CLobbyPackToServer : public CPackForLobby { - virtual bool isForServer() const override; + bool isForServer() const override; }; struct DLL_LINKAGE LobbyClientConnected : public CLobbyPackToPropagate @@ -37,7 +37,7 @@ struct DLL_LINKAGE LobbyClientConnected : public CLobbyPackToPropagate // Set by client before sending pack to server std::string uuid; std::vector names; - StartInfo::EMode mode = StartInfo::INVALID; + EStartMode mode = EStartMode::INVALID; // Changed by server before announcing pack int clientId = -1; int hostClientId = -1; @@ -111,17 +111,21 @@ struct DLL_LINKAGE LobbyLoadProgress : public CLobbyPackToPropagate } }; -struct DLL_LINKAGE LobbyEndGame : public CLobbyPackToPropagate +struct DLL_LINKAGE LobbyRestartGame : public CLobbyPackToPropagate { - bool closeConnection = false; - bool restart = false; - void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h) { - h & closeConnection; - h & restart; + } +}; + +struct DLL_LINKAGE LobbyPrepareStartGame : public CLobbyPackToPropagate +{ + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler &h) + { } }; diff --git a/lib/pathfinder/CGPathNode.h b/lib/pathfinder/CGPathNode.h index 15363f419..28ee2f920 100644 --- a/lib/pathfinder/CGPathNode.h +++ b/lib/pathfinder/CGPathNode.h @@ -233,7 +233,7 @@ struct DLL_LINKAGE CDestinationNodeInfo : public PathNodeInfo CDestinationNodeInfo(); - virtual void setNode(CGameState * gs, CGPathNode * n) override; + void setNode(CGameState * gs, CGPathNode * n) override; virtual bool isBetterWay() const; }; diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 40fa2f650..4dcf8c5ed 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -483,7 +483,7 @@ bool CPathfinderHelper::passOneTurnLimitCheck(const PathNodeInfo & source) const return false; if(source.node->layer == EPathfindingLayer::AIR) { - return options.originalMovementRules && source.node->accessible == EPathAccessibility::ACCESSIBLE; + return options.originalFlyRules && source.node->accessible == EPathAccessibility::ACCESSIBLE; } return true; diff --git a/lib/pathfinder/NodeStorage.h b/lib/pathfinder/NodeStorage.h index eddb23201..afe19b10e 100644 --- a/lib/pathfinder/NodeStorage.h +++ b/lib/pathfinder/NodeStorage.h @@ -46,7 +46,7 @@ public: const PathfinderConfig * pathfinderConfig, const CPathfinderHelper * pathfinderHelper) override; - virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; + void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/pathfinder/PathfinderOptions.cpp b/lib/pathfinder/PathfinderOptions.cpp index 5b0ca709a..4b16b63f3 100644 --- a/lib/pathfinder/PathfinderOptions.cpp +++ b/lib/pathfinder/PathfinderOptions.cpp @@ -27,10 +27,10 @@ PathfinderOptions::PathfinderOptions() , useTeleportOneWay(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE)) , useTeleportOneWayRandom(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM)) , useTeleportWhirlpool(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_WHIRLPOOL)) + , originalFlyRules(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES)) , useCastleGate(false) , lightweightFlyingMode(false) , oneTurnSpecialLayersLimit(true) - , originalMovementRules(false) , turnLimit(std::numeric_limits::max()) , canUseCast(false) { diff --git a/lib/pathfinder/PathfinderOptions.h b/lib/pathfinder/PathfinderOptions.h index 4a1d4b985..c4cb6ef44 100644 --- a/lib/pathfinder/PathfinderOptions.h +++ b/lib/pathfinder/PathfinderOptions.h @@ -67,7 +67,9 @@ struct DLL_LINKAGE PathfinderOptions /// - Option should also allow same tile land <-> air layer transitions. /// Current implementation only allow go into (from) air layer only to neighbour tiles. /// I find it's reasonable limitation, but it's will make some movements more expensive than in H3. - bool originalMovementRules; + /// Further work can also be done to mimic SoD quirks if needed + /// (such as picking unoptimal paths on purpose when targeting guards or being interrupted on guarded resource tile when picking it during diagonal u-turn) + bool originalFlyRules; /// Max number of turns to compute. Default = infinite uint8_t turnLimit; @@ -104,7 +106,7 @@ public: SingleHeroPathfinderConfig(CPathsInfo & out, CGameState * gs, const CGHeroInstance * hero); virtual ~SingleHeroPathfinderConfig(); - virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override; + CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override; static std::vector> buildRuleSet(); }; diff --git a/lib/pathfinder/PathfindingRules.cpp b/lib/pathfinder/PathfindingRules.cpp index 1be7fe660..ca8e3df46 100644 --- a/lib/pathfinder/PathfindingRules.cpp +++ b/lib/pathfinder/PathfindingRules.cpp @@ -301,7 +301,7 @@ PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingRea if(source.guarded) { - if(!(pathfinderConfig->options.originalMovementRules && source.node->layer == EPathfindingLayer::AIR) + if(!(pathfinderConfig->options.originalFlyRules && source.node->layer == EPathfindingLayer::AIR) && !pathfinderConfig->options.ignoreGuards && (!destination.isGuardianTile || pathfinderHelper->getGuardiansCount(source.coord) > 1)) // Can step into tile of guard { @@ -386,14 +386,22 @@ void LayerTransitionRule::process( break; case EPathfindingLayer::AIR: - if(pathfinderConfig->options.originalMovementRules) + if(pathfinderConfig->options.originalFlyRules) { - if((source.node->accessible != EPathAccessibility::ACCESSIBLE && - source.node->accessible != EPathAccessibility::VISITABLE) && - (destination.node->accessible != EPathAccessibility::VISITABLE && - destination.node->accessible != EPathAccessibility::ACCESSIBLE)) + if(source.node->accessible != EPathAccessibility::ACCESSIBLE && + source.node->accessible != EPathAccessibility::VISITABLE && + destination.node->accessible != EPathAccessibility::VISITABLE && + destination.node->accessible != EPathAccessibility::ACCESSIBLE) { - destination.blocked = true; + if(destination.node->accessible == EPathAccessibility::BLOCKVIS) + { + if(source.nodeObject || (source.tile->blocked && destination.tile->blocked)) + { + destination.blocked = true; + } + } + else + destination.blocked = true; } } else if(destination.node->accessible != EPathAccessibility::ACCESSIBLE) diff --git a/lib/registerTypes/RegisterTypesLobbyPacks.h b/lib/registerTypes/RegisterTypesLobbyPacks.h index 8ef908b34..3eade0382 100644 --- a/lib/registerTypes/RegisterTypesLobbyPacks.h +++ b/lib/registerTypes/RegisterTypesLobbyPacks.h @@ -37,7 +37,8 @@ void registerTypesLobbyPacks(Serializer &s) // Only host client send s.template registerType(); s.template registerType(); - s.template registerType(); + s.template registerType(); + s.template registerType(); s.template registerType(); s.template registerType(); // Only server send diff --git a/lib/registerTypes/RegisterTypesMapObjects.h b/lib/registerTypes/RegisterTypesMapObjects.h index ddaac348a..5e8d96f32 100644 --- a/lib/registerTypes/RegisterTypesMapObjects.h +++ b/lib/registerTypes/RegisterTypesMapObjects.h @@ -54,7 +54,6 @@ void registerTypesMapObjects(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); - s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); @@ -133,6 +132,8 @@ void registerTypesMapObjects(Serializer &s) //s.template registerType(); s.template registerType(); + + s.template registerType(); } VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 807090d78..02543f967 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -434,7 +434,7 @@ void CMapGenerator::addHeaderInfo() m.twoLevel = mapGenOptions.getHasTwoLevels(); m.name.appendLocalString(EMetaText::GENERAL_TXT, 740); m.description.appendRawString(getMapDescription()); - m.difficulty = 1; + m.difficulty = EMapDifficulty::NORMAL; addPlayerInfo(); m.waterMap = (mapGenOptions.getWaterContent() != EWaterContent::EWaterContent::NONE); m.banWaterContent(); diff --git a/lib/rmg/CZonePlacer.cpp b/lib/rmg/CZonePlacer.cpp index 44e372dbe..9765a7b4e 100644 --- a/lib/rmg/CZonePlacer.cpp +++ b/lib/rmg/CZonePlacer.cpp @@ -20,13 +20,14 @@ #include "RmgMap.h" #include "Zone.h" #include "Functions.h" +#include "PenroseTiling.h" VCMI_LIB_NAMESPACE_BEGIN class CRandomGenerator; CZonePlacer::CZonePlacer(RmgMap & map) - : width(0), height(0), scaleX(0), scaleY(0), mapSize(0), + : width(0), height(0), mapSize(0), gravityConstant(1e-3f), stiffnessConstant(3e-3f), stifness(0), @@ -524,7 +525,7 @@ void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const std::vector prescaler = { 0, 0 }; for (int i = 0; i < 2; i++) - prescaler[i] = std::sqrt((width * height) / (totalSize[i] * 3.14f)); + prescaler[i] = std::sqrt((width * height) / (totalSize[i] * PI_CONSTANT)); mapSize = static_cast(sqrt(width * height)); for(const auto & zone : zones) { @@ -802,18 +803,8 @@ void CZonePlacer::moveOneZone(TZoneMap& zones, TForceVector& totalForces, TDista float CZonePlacer::metric (const int3 &A, const int3 &B) const { - float dx = abs(A.x - B.x) * scaleX; - float dy = abs(A.y - B.y) * scaleY; + return A.dist2dSQ(B); - /* - 1. Normal euclidean distance - 2. Sinus for extra curves - 3. Nonlinear mess for fuzzy edges - */ - - return dx * dx + dy * dy + - 5 * std::sin(dx * dy / 10) + - 25 * std::sin (std::sqrt(A.x * B.x) * (A.y - B.y) / 100 * (scaleX * scaleY)); } void CZonePlacer::assignZones(CRandomGenerator * rand) @@ -823,9 +814,6 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) auto width = map.getMapGenOptions().getWidth(); auto height = map.getMapGenOptions().getHeight(); - //scale to Medium map to ensure smooth results - scaleX = 72.f / width; - scaleY = 72.f / height; auto zones = map.getZones(); vstd::erase_if(zones, [](const std::pair> & pr) @@ -845,7 +833,13 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) return lhs.second / lhs.first->getSize() < rhs.second / rhs.first->getSize(); }; - auto moveZoneToCenterOfMass = [](const std::shared_ptr & zone) -> void + auto simpleCompareByDistance = [](const Dpair & lhs, const Dpair & rhs) -> bool + { + //bigger zones have smaller distance + return lhs.second < rhs.second; + }; + + auto moveZoneToCenterOfMass = [width, height](const std::shared_ptr & zone) -> void { int3 total(0, 0, 0); auto tiles = zone->area().getTiles(); @@ -855,17 +849,17 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) } int size = static_cast(tiles.size()); assert(size); - zone->setPos(int3(total.x / size, total.y / size, total.z / size)); + auto newPos = int3(total.x / size, total.y / size, total.z / size); + zone->setPos(newPos); + zone->setCenter(float3(float(newPos.x) / width, float(newPos.y) / height, newPos.z)); }; int levels = map.levels(); - /* - 1. Create Voronoi diagram - 2. find current center of mass for each zone. Move zone to that center to balance zones sizes - */ + // Find current center of mass for each zone. Move zone to that center to balance zones sizes int3 pos; + for(pos.z = 0; pos.z < levels; pos.z++) { for(pos.x = 0; pos.x < width; pos.x++) @@ -893,11 +887,28 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) moveZoneToCenterOfMass(zone.second); } - //assign actual tiles to each zone using nonlinear norm for fine edges - for(const auto & zone : zones) zone.second->clearTiles(); //now populate them again + // Assign zones to closest Penrose vertex + PenroseTiling penrose; + auto vertices = penrose.generatePenroseTiling(zones.size() / map.levels(), rand); + + std::map, std::set> vertexMapping; + + for (const auto & vertex : vertices) + { + distances.clear(); + for(const auto & zone : zones) + { + distances.emplace_back(zone.second, zone.second->getCenter().dist2dSQ(float3(vertex.x(), vertex.y(), 0))); + } + auto closestZone = boost::min_element(distances, compareByDistance)->first; + + vertexMapping[closestZone].insert(int3(vertex.x() * width, vertex.y() * height, closestZone->getPos().z)); //Closest vertex belongs to zone + } + + //Assign actual tiles to each zone for (pos.z = 0; pos.z < levels; pos.z++) { for (pos.x = 0; pos.x < width; pos.x++) @@ -905,22 +916,32 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) for (pos.y = 0; pos.y < height; pos.y++) { distances.clear(); - for(const auto & zone : zones) + for(const auto & zoneVertex : vertexMapping) { - if (zone.second->getPos().z == pos.z) - distances.emplace_back(zone.second, metric(pos, zone.second->getPos())); - else - distances.emplace_back(zone.second, std::numeric_limits::max()); + auto zone = zoneVertex.first; + for (const auto & vertex : zoneVertex.second) + { + if (zone->getCenter().z == pos.z) + distances.emplace_back(zone, metric(pos, vertex)); + else + distances.emplace_back(zone, std::numeric_limits::max()); + } } - auto zone = boost::min_element(distances, compareByDistance)->first; //closest tile belongs to zone - zone->area().add(pos); - map.setZoneID(pos, zone->getId()); + + //Tile closes to vertex belongs to zone + auto closestZone = boost::min_element(distances, simpleCompareByDistance)->first; + closestZone->area().add(pos); + map.setZoneID(pos, closestZone->getId()); } } } + //set position (town position) to center of mass of irregular zone for(const auto & zone : zones) { + if(zone.second->area().empty()) + throw rmgException("Empty zone after Penrose tiling"); + moveZoneToCenterOfMass(zone.second); //TODO: similiar for islands diff --git a/lib/rmg/CZonePlacer.h b/lib/rmg/CZonePlacer.h index 90b77b8f2..2eaf429ce 100644 --- a/lib/rmg/CZonePlacer.h +++ b/lib/rmg/CZonePlacer.h @@ -54,9 +54,7 @@ private: private: int width; int height; - //metric coeficients - float scaleX; - float scaleY; + //metric coeficient float mapSize; float gravityConstant; diff --git a/lib/rmg/PenroseTiling.cpp b/lib/rmg/PenroseTiling.cpp new file mode 100644 index 000000000..e6ee0e9f4 --- /dev/null +++ b/lib/rmg/PenroseTiling.cpp @@ -0,0 +1,165 @@ +/* + * PenroseTiling.cpp, 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 + * + */ + +// Adapted from https://github.com/mpizzzle/penrose by Michael Percival + +#include "StdInc.h" +#include "PenroseTiling.h" + +VCMI_LIB_NAMESPACE_BEGIN + + +Point2D Point2D::operator * (float scale) const +{ + return Point2D(x() * scale, y() * scale); +} + +Point2D Point2D::operator + (const Point2D& other) const +{ + return Point2D(x() + other.x(), y() + other.y()); +} + +bool Point2D::operator < (const Point2D& other) const +{ + if (x() < other.x()) + { + return true; + } + else + { + return y() < other.y(); + } +} + +Triangle::Triangle(bool t_123, const TIndices & inds): + tiling(t_123), + indices(inds) +{} + +Triangle::~Triangle() +{ + for (auto * triangle : subTriangles) + { + if (triangle) + { + delete triangle; + triangle = nullptr; + } + } +} + +Point2D Point2D::rotated(float radians) const +{ + float cosAngle = cos(radians); + float sinAngle = sin(radians); + + // Apply rotation matrix transformation + float newX = x() * cosAngle - y() * sinAngle; + float newY = x() * sinAngle + y() * cosAngle; + + return Point2D(newX, newY); +} + +void PenroseTiling::split(Triangle& p, std::vector& points, + std::array, 5>& indices, uint32_t depth) +{ + uint32_t s = points.size(); + TIndices& i = p.indices; + + const auto p2 = P2; + + if (depth > 0) + { + if (p.tiling ^ !p2) + { + points.push_back(Point2D((points[i[0]] * (1.0f - PHI) ) + (points[i[2]]) * PHI)); + points.push_back(Point2D((points[i[p2]] * (1.0f - PHI)) + (points[i[!p2]] * PHI))); + + auto * t1 = new Triangle(p2, TIndices({ i[(!p2) + 1], p2 ? i[2] : s, p2 ? s : i[1] })); + auto * t2 = new Triangle(true, TIndices({ p2 ? i[1] : s, s + 1, p2 ? s : i[1] })); + auto * t3 = new Triangle(false, TIndices({ s, s + 1, i[0] })); + + p.subTriangles = { t1, t2, t3 }; + } + else + { + points.push_back(Point2D((points[i[p2 * 2]] * (1.0f - PHI)) + (points[i[!p2]]) * PHI)); + + auto * t1 = new Triangle(true, TIndices({ i[2], s, i[1] })); + auto * t2 = new Triangle(false, TIndices({ i[(!p2) + 1], s, i[0] })); + + p.subTriangles = { t1, t2 }; + } + + for (auto& t : p.subTriangles) + { + if (depth == 1) + { + for (uint32_t k = 0; k < 3; ++k) + { + if (k != (t->tiling ^ !p2 ? 2 : 1)) + { + indices[indices.size() - 1].push_back(t->indices[k]); + indices[indices.size() - 1].push_back(t->indices[((k + 1) % 3)]); + } + } + + indices[t->tiling + (p.tiling ? 0 : 2)].insert(indices[t->tiling + (p.tiling ? 0 : 2)].end(), t->indices.begin(), t->indices.end()); + } + + // Split recursively + split(*t, points, indices, depth - 1); + } + } + + return; +} + +std::set PenroseTiling::generatePenroseTiling(size_t numZones, CRandomGenerator * rand) +{ + float scale = 100.f / (numZones * 1.5f + 20); + float polyAngle = (2 * PI_CONSTANT) / POLY; + + float randomAngle = rand->nextDouble(0, 2 * PI_CONSTANT); + + std::vector points = { Point2D(0.0f, 0.0f), Point2D(0.0f, 1.0f).rotated(randomAngle) }; + std::array, 5> indices; + + for (uint32_t i = 1; i < POLY; ++i) + { + Point2D next = points[i].rotated(polyAngle); + points.push_back(next); + } + + for (auto& p : points) + { + p.x(p.x() * scale * BASE_SIZE); + } + + std::set finalPoints; + + for (uint32_t i = 0; i < POLY; i++) + { + std::array p = { (i % (POLY + 1)) + 1, ((i + 1) % POLY) + 1 }; + + Triangle t(true, TIndices({ 0, p[i & 1], p[!(i & 1)] })); + + split(t, points, indices, DEPTH); + } + + vstd::copy_if(points, vstd::set_inserter(finalPoints), [](const Point2D point) + { + return vstd::isbetween(point.x(), 0.f, 1.0f) && vstd::isbetween(point.y(), 0.f, 1.0f); + }); + + return finalPoints; +} + +VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/rmg/PenroseTiling.h b/lib/rmg/PenroseTiling.h new file mode 100644 index 000000000..bdda4c7c9 --- /dev/null +++ b/lib/rmg/PenroseTiling.h @@ -0,0 +1,71 @@ +/* + * PenroseTiling.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 "../GameConstants.h" +#include "../CRandomGenerator.h" +#include +#include + +VCMI_LIB_NAMESPACE_BEGIN + +using namespace boost::geometry; +typedef std::array TIndices; + +const float PI_CONSTANT = 3.141592f; + +class Point2D : public model::d2::point_xy +{ +public: + using point_xy::point_xy; + + Point2D operator * (float scale) const; + Point2D operator + (const Point2D& other) const; + Point2D rotated(float radians) const; + + bool operator < (const Point2D& other) const; +}; + +Point2D rotatePoint(const Point2D& point, double radians, const Point2D& origin); + +class Triangle +{ +public: + ~Triangle(); + + const bool tiling; + TIndices indices; + + std::vector subTriangles; + + Triangle(bool t_123, const TIndices & inds); +}; + +class PenroseTiling +{ + +public: + const float PHI = 1.0 / ((1.0 + std::sqrt(5.0)) / 2); + const uint32_t POLY = 10; // Number of symmetries? + + const float BASE_SIZE = 1.25f; + const uint32_t DEPTH = 7; //Recursion depth + + const bool P2 = false; // Tiling type + + std::set generatePenroseTiling(size_t numZones, CRandomGenerator * rand); + +private: + void split(Triangle& p, std::vector& points, std::array, 5>& indices, uint32_t depth); + +}; + +VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/rmg/Zone.cpp b/lib/rmg/Zone.cpp index e48e3e43b..09f563727 100644 --- a/lib/rmg/Zone.cpp +++ b/lib/rmg/Zone.cpp @@ -116,6 +116,7 @@ void Zone::initFreeTiles() if(dAreaFree.empty()) { + // Fixme: This might fail fot water zone, which doesn't need to have a tile in its center of the mass dAreaPossible.erase(pos); dAreaFree.add(pos); //zone must have at least one free tile where other paths go - for instance in the center } @@ -250,21 +251,30 @@ void Zone::fractalize() treasureDensity += t.density; } - if (treasureValue > 400) + if (getType() == ETemplateZoneType::WATER) { - // A quater at max density - marginFactor = (0.25f + ((std::max(0, (600 - treasureValue))) / (600.f - 400)) * 0.75f); + // Set very little obstacles on water + spanFactor = 0.2; } - else if (treasureValue < 125) + else //Scale with treasure density { - //Dense obstacles - spanFactor *= (treasureValue / 125.f); - vstd::amax(spanFactor, 0.15f); + if (treasureValue > 400) + { + // A quater at max density + marginFactor = (0.25f + ((std::max(0, (600 - treasureValue))) / (600.f - 400)) * 0.75f); + } + else if (treasureValue < 125) + { + //Dense obstacles + spanFactor *= (treasureValue / 125.f); + vstd::amax(spanFactor, 0.15f); + } + if (treasureDensity <= 10) + { + vstd::amin(spanFactor, 0.1f + 0.01f * treasureDensity); //Add extra obstacles to fill up space } - if (treasureDensity <= 10) - { - vstd::amin(spanFactor, 0.1f + 0.01f * treasureDensity); //Add extra obstacles to fill up space } + float blockDistance = minDistance * spanFactor; //More obstacles in the Underground freeDistance = freeDistance * marginFactor; vstd::amax(freeDistance, 4 * 4); diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 6bc51579c..8dcc24c25 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -579,11 +579,15 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD for (auto id : adjacentZones) { - auto manager = map.getZones().at(id)->getModificator(); - if (manager) + auto otherZone = map.getZones().at(id); + if ((otherZone->getType() == ETemplateZoneType::WATER) == (zone.getType() == ETemplateZoneType::WATER)) { - // TODO: Update distances for perimeter of guarded object, not just treasures - manager->updateDistances(object); + // Do not update other zone if only one is water + auto manager = otherZone->getModificator(); + if (manager) + { + manager->updateDistances(object); + } } } } diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index b34addee5..222ed2364 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -23,9 +23,13 @@ protected: public: CLoaderBase(IBinaryReader * r): reader(r){}; - inline int read(void * data, unsigned size) + inline void read(void * data, unsigned size, bool reverseEndianess) { - return reader->read(data, size); + auto bytePtr = reinterpret_cast(data); + + reader->read(bytePtr, size); + if(reverseEndianess) + std::reverse(bytePtr, bytePtr + size); }; }; @@ -170,11 +174,7 @@ public: template < class T, typename std::enable_if < std::is_fundamental::value && !std::is_same::value, int >::type = 0 > void load(T &data) { - unsigned length = sizeof(data); - char * dataPtr = reinterpret_cast(&data); - this->read(dataPtr,length); - if(reverseEndianess) - std::reverse(dataPtr, dataPtr + length); + this->read(static_cast(&data), sizeof(data), reverseEndianess); } template < typename T, typename std::enable_if < is_serializeable::value, int >::type = 0 > @@ -439,7 +439,7 @@ public: { ui32 length = readAndCheckLength(); data.resize(length); - this->read((void*)data.c_str(),length); + this->read(static_cast(data.data()), length, false); } template diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index 9f465e539..07d152ad3 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -23,9 +23,9 @@ protected: public: CSaverBase(IBinaryWriter * w): writer(w){}; - inline int write(const void * data, unsigned size) + inline void write(const void * data, unsigned size) { - return writer->write(data, size); + writer->write(reinterpret_cast(data), size); }; }; @@ -145,7 +145,7 @@ public: void save(const T &data) { // save primitive - simply dump binary data to output - this->write(&data,sizeof(data)); + this->write(static_cast(&data), sizeof(data)); } template < typename T, typename std::enable_if < std::is_enum::value, int >::type = 0 > @@ -312,7 +312,7 @@ public: void save(const std::string &data) { save(ui32(data.length())); - this->write(data.c_str(),(unsigned int)data.size()); + this->write(static_cast(data.data()), data.size()); } template void save(const std::pair &data) diff --git a/lib/serializer/CLoadFile.cpp b/lib/serializer/CLoadFile.cpp index 2e50fa6c1..ef43b8e27 100644 --- a/lib/serializer/CLoadFile.cpp +++ b/lib/serializer/CLoadFile.cpp @@ -21,7 +21,7 @@ CLoadFile::CLoadFile(const boost::filesystem::path & fname, ESerializationVersio //must be instantiated in .cpp file for access to complete types of all member fields CLoadFile::~CLoadFile() = default; -int CLoadFile::read(void * data, unsigned size) +int CLoadFile::read(std::byte * data, unsigned size) { sfile->read(reinterpret_cast(data), size); return size; @@ -92,7 +92,7 @@ void CLoadFile::clear() void CLoadFile::checkMagicBytes(const std::string &text) { std::string loaded = text; - read((void *)loaded.data(), static_cast(text.length())); + read(reinterpret_cast(loaded.data()), text.length()); if(loaded != text) throw std::runtime_error("Magic bytes doesn't match!"); } diff --git a/lib/serializer/CLoadFile.h b/lib/serializer/CLoadFile.h index 405c1f99c..800872038 100644 --- a/lib/serializer/CLoadFile.h +++ b/lib/serializer/CLoadFile.h @@ -23,7 +23,7 @@ public: CLoadFile(const boost::filesystem::path & fname, ESerializationVersion minimalVersion = ESerializationVersion::CURRENT); //throws! virtual ~CLoadFile(); - int read(void * data, unsigned size) override; //throws! + int read(std::byte * data, unsigned size) override; //throws! void openNextFile(const boost::filesystem::path & fname, ESerializationVersion minimalVersion); //throws! void clear(); diff --git a/lib/serializer/CMemorySerializer.cpp b/lib/serializer/CMemorySerializer.cpp index 39a647335..13f3a59cf 100644 --- a/lib/serializer/CMemorySerializer.cpp +++ b/lib/serializer/CMemorySerializer.cpp @@ -12,21 +12,21 @@ VCMI_LIB_NAMESPACE_BEGIN -int CMemorySerializer::read(void * data, unsigned size) +int CMemorySerializer::read(std::byte * data, unsigned size) { if(buffer.size() < readPos + size) throw std::runtime_error(boost::str(boost::format("Cannot read past the buffer (accessing index %d, while size is %d)!") % (readPos + size - 1) % buffer.size())); - std::memcpy(data, buffer.data() + readPos, size); + std::copy_n(buffer.data() + readPos, size, data); readPos += size; return size; } -int CMemorySerializer::write(const void * data, unsigned size) +int CMemorySerializer::write(const std::byte * data, unsigned size) { auto oldSize = buffer.size(); //and the pos to write from buffer.resize(oldSize + size); - std::memcpy(buffer.data() + oldSize, data, size); + std::copy_n(data, size, buffer.data() + oldSize); return size; } diff --git a/lib/serializer/CMemorySerializer.h b/lib/serializer/CMemorySerializer.h index 586220724..caaa4cc24 100644 --- a/lib/serializer/CMemorySerializer.h +++ b/lib/serializer/CMemorySerializer.h @@ -18,15 +18,15 @@ VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE CMemorySerializer : public IBinaryReader, public IBinaryWriter { - std::vector buffer; + std::vector buffer; size_t readPos; //index of the next byte to be read public: BinaryDeserializer iser; BinarySerializer oser; - int read(void * data, unsigned size) override; //throws! - int write(const void * data, unsigned size) override; + int read(std::byte * data, unsigned size) override; //throws! + int write(const std::byte * data, unsigned size) override; CMemorySerializer(); diff --git a/lib/serializer/CSaveFile.cpp b/lib/serializer/CSaveFile.cpp index e575a88a3..d406342b4 100644 --- a/lib/serializer/CSaveFile.cpp +++ b/lib/serializer/CSaveFile.cpp @@ -21,9 +21,9 @@ CSaveFile::CSaveFile(const boost::filesystem::path &fname) //must be instantiated in .cpp file for access to complete types of all member fields CSaveFile::~CSaveFile() = default; -int CSaveFile::write(const void * data, unsigned size) +int CSaveFile::write(const std::byte * data, unsigned size) { - sfile->write((char *)data,size); + sfile->write(reinterpret_cast(data), size); return size; } @@ -66,7 +66,7 @@ void CSaveFile::clear() void CSaveFile::putMagicBytes(const std::string &text) { - write(text.c_str(), static_cast(text.length())); + write(reinterpret_cast(text.c_str()), text.length()); } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CSaveFile.h b/lib/serializer/CSaveFile.h index f1b823bf2..857b7f159 100644 --- a/lib/serializer/CSaveFile.h +++ b/lib/serializer/CSaveFile.h @@ -23,7 +23,7 @@ public: CSaveFile(const boost::filesystem::path &fname); //throws! ~CSaveFile(); - int write(const void * data, unsigned size) override; + int write(const std::byte * data, unsigned size) override; void openNextFile(const boost::filesystem::path &fname); //throws! void clear(); diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 6907c2008..d0f90c758 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -49,7 +49,7 @@ struct VectorizedObjectInfo }; /// Base class for serializers capable of reading or writing data -class DLL_LINKAGE CSerializer +class DLL_LINKAGE CSerializer : boost::noncopyable { template, bool> = true> static int32_t idToNumber(const Numeric &t) @@ -175,14 +175,14 @@ struct VectorizedIDType class DLL_LINKAGE IBinaryReader : public virtual CSerializer { public: - virtual int read(void * data, unsigned size) = 0; + virtual int read(std::byte * data, unsigned size) = 0; }; /// Base class for serializers class DLL_LINKAGE IBinaryWriter : public virtual CSerializer { public: - virtual int write(const void * data, unsigned size) = 0; + virtual int write(const std::byte * data, unsigned size) = 0; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index e17d57141..c72921d2f 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -10,357 +10,159 @@ #include "StdInc.h" #include "Connection.h" -#include "../networkPacks/NetPacksBase.h" -#include "../gameState/CGameState.h" +#include "BinaryDeserializer.h" +#include "BinarySerializer.h" -#include +#include "../gameState/CGameState.h" +#include "../networkPacks/NetPacksBase.h" +#include "../network/NetworkInterface.h" VCMI_LIB_NAMESPACE_BEGIN -using namespace boost; -using namespace boost::asio::ip; - -struct ConnectionBuffers +class DLL_LINKAGE ConnectionPackWriter final : public IBinaryWriter { - boost::asio::streambuf readBuffer; - boost::asio::streambuf writeBuffer; +public: + std::vector buffer; + + int write(const std::byte * data, unsigned size) final; }; -void CConnection::init() +class DLL_LINKAGE ConnectionPackReader final : public IBinaryReader { - enableBufferedWrite = false; - enableBufferedRead = false; - connectionBuffers = std::make_unique(); +public: + const std::vector * buffer; + size_t position; - socket->set_option(boost::asio::ip::tcp::no_delay(true)); - try - { - socket->set_option(boost::asio::socket_base::send_buffer_size(4194304)); - socket->set_option(boost::asio::socket_base::receive_buffer_size(4194304)); - } - catch (const boost::system::system_error & e) - { - logNetwork->error("error setting socket option: %s", e.what()); - } + int read(std::byte * data, unsigned size) final; +}; - enableSmartPointerSerialization(); - disableStackSendingByID(); -#ifndef VCMI_ENDIAN_BIG - myEndianess = true; -#else - myEndianess = false; -#endif - connected = true; - std::string pom; - //we got connection - oser & std::string("Aiya!\n") & name & uuid & myEndianess; //identify ourselves - iser & pom & pom & contactUuid & contactEndianess; - logNetwork->info("Established connection with %s. UUID: %s", pom, contactUuid); - mutexRead = std::make_shared(); - mutexWrite = std::make_shared(); - - iser.version = ESerializationVersion::CURRENT; -} - -CConnection::CConnection(const std::string & host, ui16 port, std::string Name, std::string UUID): - io_service(std::make_shared()), - iser(this), - oser(this), - name(std::move(Name)), - uuid(std::move(UUID)) +int ConnectionPackWriter::write(const std::byte * data, unsigned size) { - int i = 0; - boost::system::error_code error = asio::error::host_not_found; - socket = std::make_shared(*io_service); - - tcp::resolver resolver(*io_service); - tcp::resolver::iterator end; - tcp::resolver::iterator pom; - tcp::resolver::iterator endpoint_iterator = resolver.resolve(tcp::resolver::query(host, std::to_string(port)), error); - if(error) - { - logNetwork->error("Problem with resolving: \n%s", error.message()); - throw std::runtime_error("Problem with resolving"); - } - pom = endpoint_iterator; - if(pom != end) - logNetwork->info("Found endpoints:"); - else - { - logNetwork->error("Critical problem: No endpoints found!"); - throw std::runtime_error("No endpoints found!"); - } - while(pom != end) - { - logNetwork->info("\t%d:%s", i, (boost::asio::ip::tcp::endpoint&)*pom); - pom++; - } - i=0; - while(endpoint_iterator != end) - { - logNetwork->info("Trying connection to %s(%d)", (boost::asio::ip::tcp::endpoint&)*endpoint_iterator, i++); - socket->connect(*endpoint_iterator, error); - if(!error) - { - init(); - return; - } - else - { - throw std::runtime_error("Failed to connect!"); - } - endpoint_iterator++; - } + buffer.insert(buffer.end(), data, data + size); + return size; } -CConnection::CConnection(std::shared_ptr Socket, std::string Name, std::string UUID): - iser(this), - oser(this), - socket(std::move(Socket)), - name(std::move(Name)), - uuid(std::move(UUID)) +int ConnectionPackReader::read(std::byte * data, unsigned size) { - init(); + if (position + size > buffer->size()) + throw std::runtime_error("End of file reached when reading received network pack!"); + + std::copy_n(buffer->begin() + position, size, data); + position += size; + return size; } -CConnection::CConnection(const std::shared_ptr & acceptor, - const std::shared_ptr & io_service, - std::string Name, - std::string UUID): - io_service(io_service), - iser(this), - oser(this), - name(std::move(Name)), - uuid(std::move(UUID)) + +CConnection::CConnection(std::weak_ptr networkConnection) + : networkConnection(networkConnection) + , packReader(std::make_unique()) + , packWriter(std::make_unique()) + , deserializer(std::make_unique(packReader.get())) + , serializer(std::make_unique(packWriter.get())) + , connectionID(-1) { - boost::system::error_code error = asio::error::host_not_found; - socket = std::make_shared(*io_service); - acceptor->accept(*socket,error); - if (error) - { - logNetwork->error("Error on accepting: %s", error.message()); - socket.reset(); - throw std::runtime_error("Can't establish connection :("); - } - init(); + assert(networkConnection.lock() != nullptr); + + enterLobbyConnectionMode(); + deserializer->version = ESerializationVersion::CURRENT; } -void CConnection::flushBuffers() -{ - if(!enableBufferedWrite) - return; - - if (!socket) - throw std::runtime_error("Can't write to closed socket!"); - - try - { - asio::write(*socket, connectionBuffers->writeBuffer); - } - catch(...) - { - //connection has been lost - connected = false; - throw; - } - - enableBufferedWrite = false; -} - -int CConnection::write(const void * data, unsigned size) -{ - if (!socket) - throw std::runtime_error("Can't write to closed socket!"); - - try - { - if(enableBufferedWrite) - { - std::ostream ostream(&connectionBuffers->writeBuffer); - - ostream.write(static_cast(data), size); - - return size; - } - - int ret = static_cast(asio::write(*socket, asio::const_buffers_1(asio::const_buffer(data, size)))); - return ret; - } - catch(...) - { - //connection has been lost - connected = false; - throw; - } -} - -int CConnection::read(void * data, unsigned size) -{ - try - { - if(enableBufferedRead) - { - auto available = connectionBuffers->readBuffer.size(); - - while(available < size) - { - auto bytesRead = socket->read_some(connectionBuffers->readBuffer.prepare(1024)); - connectionBuffers->readBuffer.commit(bytesRead); - available = connectionBuffers->readBuffer.size(); - } - - std::istream istream(&connectionBuffers->readBuffer); - - istream.read(static_cast(data), size); - - return size; - } - - int ret = static_cast(asio::read(*socket,asio::mutable_buffers_1(asio::mutable_buffer(data,size)))); - return ret; - } - catch(...) - { - //connection has been lost - connected = false; - throw; - } -} - -CConnection::~CConnection() -{ - close(); - - if(handler) - { - // ugly workaround to avoid self-join if last strong reference to shared_ptr that owns this class has been released in this very thread, e.g. on netpack processing - if (boost::this_thread::get_id() != handler->get_id()) - handler->join(); - else - handler->detach(); - } -} - -template -CConnection & CConnection::operator&(const T &t) { -// throw std::exception(); -//XXX this is temporaly ? solution to fix gcc (4.3.3, other?) compilation -// problem for more details contact t0@czlug.icis.pcz.pl or impono@gmail.com -// do not remove this exception it shoudnt be called - return *this; -} - -void CConnection::close() -{ - if(socket) - { - try - { - socket->shutdown(boost::asio::ip::tcp::socket::shutdown_receive); - } - catch (const boost::system::system_error & e) - { - logNetwork->error("error closing socket: %s", e.what()); - } - - socket->close(); - socket.reset(); - } -} - -bool CConnection::isOpen() const -{ - return socket && connected; -} - -void CConnection::reportState(vstd::CLoggerBase * out) -{ - out->debug("CConnection"); - if(socket && socket->is_open()) - { - out->debug("\tWe have an open and valid socket"); - out->debug("\t %d bytes awaiting", socket->available()); - } -} - -CPack * CConnection::retrievePack() -{ - enableBufferedRead = true; - - CPack * pack = nullptr; - boost::unique_lock lock(*mutexRead); - iser & pack; - logNetwork->trace("Received CPack of type %s", (pack ? typeid(*pack).name() : "nullptr")); - if(pack == nullptr) - logNetwork->error("Received a nullptr CPack! You should check whether client and server ABI matches."); - - enableBufferedRead = false; - - return pack; -} +CConnection::~CConnection() = default; void CConnection::sendPack(const CPack * pack) { - boost::unique_lock lock(*mutexWrite); + auto connectionPtr = networkConnection.lock(); + + if (!connectionPtr) + throw std::runtime_error("Attempt to send packet on a closed connection!"); + + *serializer & pack; + logNetwork->trace("Sending a pack of type %s", typeid(*pack).name()); - enableBufferedWrite = true; + connectionPtr->sendPacket(packWriter->buffer); + packWriter->buffer.clear(); +} - oser & pack; +CPack * CConnection::retrievePack(const std::vector & data) +{ + CPack * result; - flushBuffers(); + packReader->buffer = &data; + packReader->position = 0; + + *deserializer & result; + + logNetwork->trace("Received CPack of type %s", (result ? typeid(*result).name() : "nullptr")); + return result; +} + +bool CConnection::isMyConnection(const std::shared_ptr & otherConnection) const +{ + return otherConnection != nullptr && networkConnection.lock() == otherConnection; +} + +std::shared_ptr CConnection::getConnection() +{ + return networkConnection.lock(); } void CConnection::disableStackSendingByID() { - CSerializer::sendStackInstanceByIds = false; + packReader->sendStackInstanceByIds = false; + packWriter->sendStackInstanceByIds = false; } void CConnection::enableStackSendingByID() { - CSerializer::sendStackInstanceByIds = true; -} - -void CConnection::disableSmartPointerSerialization() -{ - iser.smartPointerSerialization = oser.smartPointerSerialization = false; -} - -void CConnection::enableSmartPointerSerialization() -{ - iser.smartPointerSerialization = oser.smartPointerSerialization = true; + packReader->sendStackInstanceByIds = true; + packWriter->sendStackInstanceByIds = true; } void CConnection::enterLobbyConnectionMode() { - iser.loadedPointers.clear(); - oser.savedPointers.clear(); + deserializer->loadedPointers.clear(); + serializer->savedPointers.clear(); disableSmartVectorMemberSerialization(); disableSmartPointerSerialization(); + disableStackSendingByID(); +} + +void CConnection::setCallback(IGameCallback * cb) +{ + deserializer->cb = cb; } void CConnection::enterGameplayConnectionMode(CGameState * gs) { enableStackSendingByID(); disableSmartPointerSerialization(); - addStdVecItems(gs); - iser.cb = gs->callback; + + setCallback(gs->callback); + enableSmartVectorMemberSerializatoin(gs); +} + +void CConnection::disableSmartPointerSerialization() +{ + deserializer->smartPointerSerialization = false; + serializer->smartPointerSerialization = false; +} + +void CConnection::enableSmartPointerSerialization() +{ + deserializer->smartPointerSerialization = true; + serializer->smartPointerSerialization = true; } void CConnection::disableSmartVectorMemberSerialization() { - CSerializer::smartVectorMembersSerialization = false; + packReader->smartVectorMembersSerialization = false; + packWriter->smartVectorMembersSerialization = false; } -void CConnection::enableSmartVectorMemberSerializatoin() +void CConnection::enableSmartVectorMemberSerializatoin(CGameState * gs) { - CSerializer::smartVectorMembersSerialization = true; -} - -std::string CConnection::toString() const -{ - boost::format fmt("Connection with %s (ID: %d UUID: %s)"); - fmt % name % connectionID % uuid; - return fmt.str(); + packWriter->addStdVecItems(gs); + packReader->addStdVecItems(gs); } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/Connection.h b/lib/serializer/Connection.h index 518223246..3d14f261b 100644 --- a/lib/serializer/Connection.h +++ b/lib/serializer/Connection.h @@ -9,123 +9,52 @@ */ #pragma once -#include "BinaryDeserializer.h" -#include "BinarySerializer.h" - -#if BOOST_VERSION >= 107000 // Boost version >= 1.70 -#include -using TSocket = boost::asio::basic_stream_socket; -using TAcceptor = boost::asio::basic_socket_acceptor; -#else -namespace boost -{ - namespace asio - { - namespace ip - { - class tcp; - } - -#if BOOST_VERSION >= 106600 // Boost version >= 1.66 - class io_context; - typedef io_context io_service; -#else - class io_service; -#endif - - template class stream_socket_service; - template - class basic_stream_socket; - - template class socket_acceptor_service; - template - class basic_socket_acceptor; - } - class mutex; -} - -typedef boost::asio::basic_stream_socket < boost::asio::ip::tcp , boost::asio::stream_socket_service > TSocket; -typedef boost::asio::basic_socket_acceptor > TAcceptor; -#endif - - VCMI_LIB_NAMESPACE_BEGIN +class BinaryDeserializer; +class BinarySerializer; struct CPack; -struct ConnectionBuffers; +class INetworkConnection; +class ConnectionPackReader; +class ConnectionPackWriter; +class CGameState; +class IGameCallback; -/// Main class for network communication -/// Allows establishing connection and bidirectional read-write -class DLL_LINKAGE CConnection - : public IBinaryReader, public IBinaryWriter, public std::enable_shared_from_this +/// Wrapper class for game connection +/// Handles serialization and deserialization of data received from network +class DLL_LINKAGE CConnection : boost::noncopyable { - void init(); - void reportState(vstd::CLoggerBase * out) override; + /// Non-owning pointer to underlying connection + std::weak_ptr networkConnection; - int write(const void * data, unsigned size) override; - int read(void * data, unsigned size) override; - void flushBuffers(); - - std::shared_ptr io_service; //can be empty if connection made from socket - - bool enableBufferedWrite; - bool enableBufferedRead; - std::unique_ptr connectionBuffers; - -public: - BinaryDeserializer iser; - BinarySerializer oser; - - std::shared_ptr mutexRead; - std::shared_ptr mutexWrite; - std::shared_ptr socket; - bool connected; - bool myEndianess, contactEndianess; //true if little endian, if endianness is different we'll have to revert received multi-byte vars - std::string contactUuid; - std::string name; //who uses this connection - std::string uuid; - - int connectionID; - std::shared_ptr handler; - - CConnection(const std::string & host, ui16 port, std::string Name, std::string UUID); - CConnection(const std::shared_ptr & acceptor, const std::shared_ptr & Io_service, std::string Name, std::string UUID); - CConnection(std::shared_ptr Socket, std::string Name, std::string UUID); //use immediately after accepting connection into socket - - void close(); - bool isOpen() const; - template - CConnection &operator&(const T&); - virtual ~CConnection(); - - CPack * retrievePack(); - void sendPack(const CPack * pack); + std::unique_ptr packReader; + std::unique_ptr packWriter; + std::unique_ptr deserializer; + std::unique_ptr serializer; void disableStackSendingByID(); void enableStackSendingByID(); void disableSmartPointerSerialization(); void enableSmartPointerSerialization(); void disableSmartVectorMemberSerialization(); - void enableSmartVectorMemberSerializatoin(); + void enableSmartVectorMemberSerializatoin(CGameState * gs); + +public: + bool isMyConnection(const std::shared_ptr & otherConnection) const; + std::shared_ptr getConnection(); + + std::string uuid; + int connectionID; + + explicit CConnection(std::weak_ptr networkConnection); + ~CConnection(); + + void sendPack(const CPack * pack); + CPack * retrievePack(const std::vector & data); void enterLobbyConnectionMode(); + void setCallback(IGameCallback * cb); void enterGameplayConnectionMode(CGameState * gs); - - std::string toString() const; - - template - CConnection & operator>>(T &t) - { - iser & t; - return * this; - } - - template - CConnection & operator<<(const T &t) - { - oser & t; - return * this; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/JsonSerializeFormat.cpp b/lib/serializer/JsonSerializeFormat.cpp index 4da52c445..bbf576a96 100644 --- a/lib/serializer/JsonSerializeFormat.cpp +++ b/lib/serializer/JsonSerializeFormat.cpp @@ -135,9 +135,16 @@ void JsonSerializeFormat::readLICPart(const JsonNode & part, const JsonSerialize { const std::string & identifier = index.String(); - const si32 rawId = decoder(identifier); - if(rawId != -1) + try + { + const si32 rawId = decoder(identifier); value.insert(rawId); + } + catch (const IdentifierResolutionException & e) + { + // downgrade exception to warning (printed as part of exception generation + // this is usually caused by loading allowed heroes / artifacts list from vmap's + } } } diff --git a/lobby/CMakeLists.txt b/lobby/CMakeLists.txt new file mode 100644 index 000000000..b4169ab09 --- /dev/null +++ b/lobby/CMakeLists.txt @@ -0,0 +1,45 @@ +set(lobby_SRCS + StdInc.cpp + + EntryPoint.cpp + LobbyDatabase.cpp + LobbyServer.cpp + SQLiteConnection.cpp +) + +set(lobby_HEADERS + StdInc.h + + LobbyDatabase.h + LobbyDefines.h + LobbyServer.h + SQLiteConnection.h +) + +assign_source_group(${lobby_SRCS} ${lobby_HEADERS}) + +add_executable(vcmilobby ${lobby_SRCS} ${lobby_HEADERS}) +set(lobby_LIBS vcmi) + +if(CMAKE_SYSTEM_NAME MATCHES FreeBSD OR HAIKU) + set(lobby_LIBS execinfo ${lobby_LIBS}) +endif() + +target_link_libraries(vcmilobby PRIVATE ${lobby_LIBS} ${SQLite3_LIBRARIES}) + +target_include_directories(vcmilobby PRIVATE ${SQLite3_INCLUDE_DIRS}) +target_include_directories(vcmilobby PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +if(WIN32) + set_target_properties(vcmilobby + PROPERTIES + OUTPUT_NAME "VCMI_lobby" + PROJECT_LABEL "VCMI_lobby" + ) +endif() + +vcmi_set_output_dir(vcmilobby "") +enable_pch(vcmilobby) + +install(TARGETS vcmilobby DESTINATION ${BIN_DIR}) + diff --git a/lobby/EntryPoint.cpp b/lobby/EntryPoint.cpp new file mode 100644 index 000000000..48a6cd783 --- /dev/null +++ b/lobby/EntryPoint.cpp @@ -0,0 +1,37 @@ +/* + * EntryPoint.cpp, 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 + * + */ +#include "StdInc.h" + +#include "LobbyServer.h" + +#include "../lib/logging/CBasicLogConfigurator.h" +#include "../lib/VCMIDirs.h" + +static const int LISTENING_PORT = 30303; + +int main(int argc, const char * argv[]) +{ +#ifndef VCMI_IOS + console = new CConsoleHandler(); +#endif + CBasicLogConfigurator logConfig(VCMIDirs::get().userLogsPath() / "VCMI_Lobby_log.txt", console); + logConfig.configureDefault(); + + auto databasePath = VCMIDirs::get().userDataPath() / "vcmiLobby.db"; + logGlobal->info("Opening database %s", databasePath.string()); + + LobbyServer server(databasePath); + logGlobal->info("Starting server on port %d", LISTENING_PORT); + + server.start(LISTENING_PORT); + server.run(); + + return 0; +} diff --git a/lobby/LobbyDatabase.cpp b/lobby/LobbyDatabase.cpp new file mode 100644 index 000000000..f4aa5d9b4 --- /dev/null +++ b/lobby/LobbyDatabase.cpp @@ -0,0 +1,540 @@ +/* + * LobbyServer.cpp, 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 + * + */ +#include "StdInc.h" +#include "LobbyDatabase.h" + +#include "SQLiteConnection.h" + +void LobbyDatabase::createTables() +{ + static const std::string createChatMessages = R"( + CREATE TABLE IF NOT EXISTS chatMessages ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + senderName TEXT, + roomType TEXT, + messageText TEXT, + creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + )"; + + static const std::string createTableGameRooms = R"( + CREATE TABLE IF NOT EXISTS gameRooms ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + roomID TEXT, + hostAccountID TEXT, + status INTEGER NOT NULL DEFAULT 0, + playerLimit INTEGER NOT NULL, + creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + )"; + + static const std::string createTableGameRoomPlayers = R"( + CREATE TABLE IF NOT EXISTS gameRoomPlayers ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + roomID TEXT, + accountID TEXT + ); + )"; + + static const std::string createTableAccounts = R"( + CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + accountID TEXT, + displayName TEXT, + online INTEGER NOT NULL, + lastLoginTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + )"; + + static const std::string createTableAccountCookies = R"( + CREATE TABLE IF NOT EXISTS accountCookies ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + accountID TEXT, + cookieUUID TEXT, + creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + )"; + + static const std::string createTableGameRoomInvites = R"( + CREATE TABLE IF NOT EXISTS gameRoomInvites ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + roomID TEXT, + accountID TEXT + ); + )"; + + database->prepare(createChatMessages)->execute(); + database->prepare(createTableGameRoomPlayers)->execute(); + database->prepare(createTableGameRooms)->execute(); + database->prepare(createTableAccounts)->execute(); + database->prepare(createTableAccountCookies)->execute(); + database->prepare(createTableGameRoomInvites)->execute(); +} + +void LobbyDatabase::clearOldData() +{ + static const std::string removeActiveAccounts = R"( + UPDATE accounts + SET online = 0 + WHERE online <> 0 + )"; + + static const std::string removeActiveRooms = R"( + UPDATE gameRooms + SET status = 5 + WHERE status <> 5 + )"; + + database->prepare(removeActiveAccounts)->execute(); + database->prepare(removeActiveRooms)->execute(); +} + +void LobbyDatabase::prepareStatements() +{ + // INSERT INTO + + static const std::string insertChatMessageText = R"( + INSERT INTO chatMessages(senderName, messageText) VALUES( ?, ?); + )"; + + static const std::string insertAccountText = R"( + INSERT INTO accounts(accountID, displayName, online) VALUES(?,?,0); + )"; + + static const std::string insertAccessCookieText = R"( + INSERT INTO accountCookies(accountID, cookieUUID) VALUES(?,?); + )"; + + static const std::string insertGameRoomText = R"( + INSERT INTO gameRooms(roomID, hostAccountID, status, playerLimit) VALUES(?, ?, 0, 8); + )"; + + static const std::string insertGameRoomPlayersText = R"( + INSERT INTO gameRoomPlayers(roomID, accountID) VALUES(?,?); + )"; + + static const std::string insertGameRoomInvitesText = R"( + INSERT INTO gameRoomInvites(roomID, accountID) VALUES(?,?); + )"; + + // DELETE FROM + + static const std::string deleteGameRoomPlayersText = R"( + DELETE FROM gameRoomPlayers WHERE roomID = ? AND accountID = ? + )"; + + static const std::string deleteGameRoomInvitesText = R"( + DELETE FROM gameRoomInvites WHERE roomID = ? AND accountID = ? + )"; + + // UPDATE + + static const std::string setAccountOnlineText = R"( + UPDATE accounts + SET online = ? + WHERE accountID = ? + )"; + + static const std::string setGameRoomStatusText = R"( + UPDATE gameRooms + SET status = ? + WHERE roomID = ? + )"; + + static const std::string updateAccountLoginTimeText = R"( + UPDATE accounts + SET lastLoginTime = CURRENT_TIMESTAMP + WHERE accountID = ? + )"; + + // SELECT FROM + + static const std::string getRecentMessageHistoryText = R"( + SELECT senderName, displayName, messageText, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',cm.creationTime) AS secondsElapsed + FROM chatMessages cm + LEFT JOIN accounts on accountID = senderName + WHERE secondsElapsed < 60*60*18 + ORDER BY cm.creationTime DESC + LIMIT 100 + )"; + + static const std::string getIdleGameRoomText = R"( + SELECT roomID + FROM gameRooms + WHERE hostAccountID = ? AND status = 0 + LIMIT 1 + )"; + + static const std::string getGameRoomStatusText = R"( + SELECT status + FROM gameRooms + WHERE roomID = ? + )"; + + static const std::string getAccountGameRoomText = R"( + SELECT grp.roomID + FROM gameRoomPlayers grp + LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID + WHERE accountID = ? AND status IN (1, 2) + LIMIT 1 + )"; + + static const std::string getActiveAccountsText = R"( + SELECT accountID, displayName + FROM accounts + WHERE online = 1 + )"; + + static const std::string getActiveGameRoomsText = R"( + SELECT roomID, hostAccountID, displayName, status, playerLimit + FROM gameRooms + LEFT JOIN accounts ON hostAccountID = accountID + WHERE status = 1 + )"; + + static const std::string countRoomUsedSlotsText = R"( + SELECT COUNT(accountID) + FROM gameRoomPlayers + WHERE roomID = ? + )"; + + static const std::string countRoomTotalSlotsText = R"( + SELECT playerLimit + FROM gameRooms + WHERE roomID = ? + )"; + + static const std::string getAccountDisplayNameText = R"( + SELECT displayName + FROM accounts + WHERE accountID = ? + )"; + + static const std::string isAccountCookieValidText = R"( + SELECT COUNT(accountID) + FROM accountCookies + WHERE accountID = ? AND cookieUUID = ? + )"; + + static const std::string isGameRoomCookieValidText = R"( + SELECT COUNT(roomID) + FROM gameRooms + LEFT JOIN accountCookies ON accountCookies.accountID = gameRooms.hostAccountID + WHERE roomID = ? AND cookieUUID = ? AND strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',creationTime) < ? + )"; + + static const std::string isPlayerInGameRoomText = R"( + SELECT COUNT(accountID) + FROM gameRoomPlayers grp + LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID + WHERE accountID = ? AND grp.roomID = ? AND status IN (1, 2) + )"; + + static const std::string isPlayerInAnyGameRoomText = R"( + SELECT COUNT(accountID) + FROM gameRoomPlayers grp + LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID + WHERE accountID = ? AND status IN (1, 2) + )"; + + static const std::string isAccountIDExistsText = R"( + SELECT COUNT(accountID) + FROM accounts + WHERE accountID = ? + )"; + + static const std::string isAccountNameExistsText = R"( + SELECT COUNT(displayName) + FROM accounts + WHERE displayName = ? + )"; + + insertChatMessageStatement = database->prepare(insertChatMessageText); + insertAccountStatement = database->prepare(insertAccountText); + insertAccessCookieStatement = database->prepare(insertAccessCookieText); + insertGameRoomStatement = database->prepare(insertGameRoomText); + insertGameRoomPlayersStatement = database->prepare(insertGameRoomPlayersText); + insertGameRoomInvitesStatement = database->prepare(insertGameRoomInvitesText); + + deleteGameRoomPlayersStatement = database->prepare(deleteGameRoomPlayersText); + deleteGameRoomInvitesStatement = database->prepare(deleteGameRoomInvitesText); + + setAccountOnlineStatement = database->prepare(setAccountOnlineText); + setGameRoomStatusStatement = database->prepare(setGameRoomStatusText); + updateAccountLoginTimeStatement = database->prepare(updateAccountLoginTimeText); + + getRecentMessageHistoryStatement = database->prepare(getRecentMessageHistoryText); + getIdleGameRoomStatement = database->prepare(getIdleGameRoomText); + getGameRoomStatusStatement = database->prepare(getGameRoomStatusText); + getAccountGameRoomStatement = database->prepare(getAccountGameRoomText); + getActiveAccountsStatement = database->prepare(getActiveAccountsText); + getActiveGameRoomsStatement = database->prepare(getActiveGameRoomsText); + getAccountDisplayNameStatement = database->prepare(getAccountDisplayNameText); + countRoomUsedSlotsStatement = database->prepare(countRoomUsedSlotsText); + countRoomTotalSlotsStatement = database->prepare(countRoomTotalSlotsText); + + isAccountCookieValidStatement = database->prepare(isAccountCookieValidText); + isPlayerInGameRoomStatement = database->prepare(isPlayerInGameRoomText); + isPlayerInAnyGameRoomStatement = database->prepare(isPlayerInAnyGameRoomText); + isAccountIDExistsStatement = database->prepare(isAccountIDExistsText); + isAccountNameExistsStatement = database->prepare(isAccountNameExistsText); +} + +LobbyDatabase::~LobbyDatabase() = default; + +LobbyDatabase::LobbyDatabase(const boost::filesystem::path & databasePath) +{ + database = SQLiteInstance::open(databasePath, true); + createTables(); + clearOldData(); + prepareStatements(); +} + +void LobbyDatabase::insertChatMessage(const std::string & sender, const std::string & roomType, const std::string & roomName, const std::string & messageText) +{ + insertChatMessageStatement->executeOnce(sender, messageText); +} + +bool LobbyDatabase::isPlayerInGameRoom(const std::string & accountID) +{ + bool result = false; + + isPlayerInAnyGameRoomStatement->setBinds(accountID); + if(isPlayerInAnyGameRoomStatement->execute()) + isPlayerInAnyGameRoomStatement->getColumns(result); + isPlayerInAnyGameRoomStatement->reset(); + + return result; +} + +bool LobbyDatabase::isPlayerInGameRoom(const std::string & accountID, const std::string & roomID) +{ + bool result = false; + + isPlayerInGameRoomStatement->setBinds(accountID, roomID); + if(isPlayerInGameRoomStatement->execute()) + isPlayerInGameRoomStatement->getColumns(result); + isPlayerInGameRoomStatement->reset(); + + return result; +} + +std::vector LobbyDatabase::getRecentMessageHistory() +{ + std::vector result; + + while(getRecentMessageHistoryStatement->execute()) + { + LobbyChatMessage message; + getRecentMessageHistoryStatement->getColumns(message.accountID, message.displayName, message.messageText, message.age); + result.push_back(message); + } + getRecentMessageHistoryStatement->reset(); + + return result; +} + +void LobbyDatabase::setAccountOnline(const std::string & accountID, bool isOnline) +{ + setAccountOnlineStatement->executeOnce(isOnline ? 1 : 0, accountID); +} + +void LobbyDatabase::setGameRoomStatus(const std::string & roomID, LobbyRoomState roomStatus) +{ + setGameRoomStatusStatement->executeOnce(vstd::to_underlying(roomStatus), roomID); +} + +void LobbyDatabase::insertPlayerIntoGameRoom(const std::string & accountID, const std::string & roomID) +{ + insertGameRoomPlayersStatement->executeOnce(roomID, accountID); +} + +void LobbyDatabase::deletePlayerFromGameRoom(const std::string & accountID, const std::string & roomID) +{ + deleteGameRoomPlayersStatement->executeOnce(roomID, accountID); +} + +void LobbyDatabase::deleteGameRoomInvite(const std::string & targetAccountID, const std::string & roomID) +{ + deleteGameRoomInvitesStatement->executeOnce(roomID, targetAccountID); +} + +void LobbyDatabase::insertGameRoomInvite(const std::string & targetAccountID, const std::string & roomID) +{ + insertGameRoomInvitesStatement->executeOnce(roomID, targetAccountID); +} + +void LobbyDatabase::insertGameRoom(const std::string & roomID, const std::string & hostAccountID) +{ + insertGameRoomStatement->executeOnce(roomID, hostAccountID); +} + +void LobbyDatabase::insertAccount(const std::string & accountID, const std::string & displayName) +{ + insertAccountStatement->executeOnce(accountID, displayName); +} + +void LobbyDatabase::insertAccessCookie(const std::string & accountID, const std::string & accessCookieUUID) +{ + insertAccessCookieStatement->executeOnce(accountID, accessCookieUUID); +} + +void LobbyDatabase::updateAccountLoginTime(const std::string & accountID) +{ + updateAccountLoginTimeStatement->executeOnce(accountID); +} + +std::string LobbyDatabase::getAccountDisplayName(const std::string & accountID) +{ + std::string result; + + getAccountDisplayNameStatement->setBinds(accountID); + if(getAccountDisplayNameStatement->execute()) + getAccountDisplayNameStatement->getColumns(result); + getAccountDisplayNameStatement->reset(); + + return result; +} + +LobbyCookieStatus LobbyDatabase::getAccountCookieStatus(const std::string & accountID, const std::string & accessCookieUUID) +{ + bool result = false; + + isAccountCookieValidStatement->setBinds(accountID, accessCookieUUID); + if(isAccountCookieValidStatement->execute()) + isAccountCookieValidStatement->getColumns(result); + isAccountCookieValidStatement->reset(); + + return result ? LobbyCookieStatus::VALID : LobbyCookieStatus::INVALID; +} + +LobbyInviteStatus LobbyDatabase::getAccountInviteStatus(const std::string & accountID, const std::string & roomID) +{ + assert(0); + return {}; +} + +LobbyRoomState LobbyDatabase::getGameRoomStatus(const std::string & roomID) +{ + int result = -1; + + getGameRoomStatusStatement->setBinds(roomID); + if(getGameRoomStatusStatement->execute()) + getGameRoomStatusStatement->getColumns(result); + getGameRoomStatusStatement->reset(); + + if (result != -1) + return static_cast(result); + return LobbyRoomState::CLOSED; +} + +uint32_t LobbyDatabase::getGameRoomFreeSlots(const std::string & roomID) +{ + uint32_t usedSlots = 0; + uint32_t totalSlots = 0; + + countRoomUsedSlotsStatement->setBinds(roomID); + if(countRoomUsedSlotsStatement->execute()) + countRoomUsedSlotsStatement->getColumns(usedSlots); + countRoomUsedSlotsStatement->reset(); + + countRoomTotalSlotsStatement->setBinds(roomID); + if(countRoomTotalSlotsStatement->execute()) + countRoomTotalSlotsStatement->getColumns(totalSlots); + countRoomTotalSlotsStatement->reset(); + + + if (totalSlots > usedSlots) + return totalSlots - usedSlots; + return 0; +} + +bool LobbyDatabase::isAccountNameExists(const std::string & displayName) +{ + bool result = false; + + isAccountNameExistsStatement->setBinds(displayName); + if(isAccountNameExistsStatement->execute()) + isAccountNameExistsStatement->getColumns(result); + isAccountNameExistsStatement->reset(); + return result; +} + +bool LobbyDatabase::isAccountIDExists(const std::string & accountID) +{ + bool result = false; + + isAccountIDExistsStatement->setBinds(accountID); + if(isAccountIDExistsStatement->execute()) + isAccountIDExistsStatement->getColumns(result); + isAccountIDExistsStatement->reset(); + return result; +} + +std::vector LobbyDatabase::getActiveGameRooms() +{ + std::vector result; + + while(getActiveGameRoomsStatement->execute()) + { + LobbyGameRoom entry; + getActiveGameRoomsStatement->getColumns(entry.roomID, entry.hostAccountID, entry.hostAccountDisplayName, entry.roomStatus, entry.playersLimit); + result.push_back(entry); + } + getActiveGameRoomsStatement->reset(); + + for (auto & room : result) + { + countRoomUsedSlotsStatement->setBinds(room.roomID); + if(countRoomUsedSlotsStatement->execute()) + countRoomUsedSlotsStatement->getColumns(room.playersCount); + countRoomUsedSlotsStatement->reset(); + } + return result; +} + +std::vector LobbyDatabase::getActiveAccounts() +{ + std::vector result; + + while(getActiveAccountsStatement->execute()) + { + LobbyAccount entry; + getActiveAccountsStatement->getColumns(entry.accountID, entry.displayName); + result.push_back(entry); + } + getActiveAccountsStatement->reset(); + return result; +} + +std::string LobbyDatabase::getIdleGameRoom(const std::string & hostAccountID) +{ + std::string result; + + getIdleGameRoomStatement->setBinds(hostAccountID); + if(getIdleGameRoomStatement->execute()) + getIdleGameRoomStatement->getColumns(result); + + getIdleGameRoomStatement->reset(); + return result; +} + +std::string LobbyDatabase::getAccountGameRoom(const std::string & accountID) +{ + std::string result; + + getAccountGameRoomStatement->setBinds(accountID); + if(getAccountGameRoomStatement->execute()) + getAccountGameRoomStatement->getColumns(result); + + getAccountGameRoomStatement->reset(); + return result; +} diff --git a/lobby/LobbyDatabase.h b/lobby/LobbyDatabase.h new file mode 100644 index 000000000..d1fbd3c60 --- /dev/null +++ b/lobby/LobbyDatabase.h @@ -0,0 +1,96 @@ +/* + * LobbyDatabase.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 "LobbyDefines.h" + +class SQLiteInstance; +class SQLiteStatement; + +using SQLiteInstancePtr = std::unique_ptr; +using SQLiteStatementPtr = std::unique_ptr; + +class LobbyDatabase +{ + SQLiteInstancePtr database; + + SQLiteStatementPtr insertChatMessageStatement; + SQLiteStatementPtr insertAccountStatement; + SQLiteStatementPtr insertAccessCookieStatement; + SQLiteStatementPtr insertGameRoomStatement; + SQLiteStatementPtr insertGameRoomPlayersStatement; + SQLiteStatementPtr insertGameRoomInvitesStatement; + + SQLiteStatementPtr deleteGameRoomPlayersStatement; + SQLiteStatementPtr deleteGameRoomInvitesStatement; + + SQLiteStatementPtr setAccountOnlineStatement; + SQLiteStatementPtr setGameRoomStatusStatement; + SQLiteStatementPtr updateAccountLoginTimeStatement; + + SQLiteStatementPtr getRecentMessageHistoryStatement; + SQLiteStatementPtr getIdleGameRoomStatement; + SQLiteStatementPtr getGameRoomStatusStatement; + SQLiteStatementPtr getActiveGameRoomsStatement; + SQLiteStatementPtr getActiveAccountsStatement; + SQLiteStatementPtr getAccountGameRoomStatement; + SQLiteStatementPtr getAccountDisplayNameStatement; + SQLiteStatementPtr countRoomUsedSlotsStatement; + SQLiteStatementPtr countRoomTotalSlotsStatement; + + SQLiteStatementPtr isAccountCookieValidStatement; + SQLiteStatementPtr isGameRoomCookieValidStatement; + SQLiteStatementPtr isPlayerInGameRoomStatement; + SQLiteStatementPtr isPlayerInAnyGameRoomStatement; + SQLiteStatementPtr isAccountIDExistsStatement; + SQLiteStatementPtr isAccountNameExistsStatement; + + void prepareStatements(); + void createTables(); + void clearOldData(); + +public: + explicit LobbyDatabase(const boost::filesystem::path & databasePath); + ~LobbyDatabase(); + + void setAccountOnline(const std::string & accountID, bool isOnline); + void setGameRoomStatus(const std::string & roomID, LobbyRoomState roomStatus); + + void insertPlayerIntoGameRoom(const std::string & accountID, const std::string & roomID); + void deletePlayerFromGameRoom(const std::string & accountID, const std::string & roomID); + + void deleteGameRoomInvite(const std::string & targetAccountID, const std::string & roomID); + void insertGameRoomInvite(const std::string & targetAccountID, const std::string & roomID); + + void insertGameRoom(const std::string & roomID, const std::string & hostAccountID); + void insertAccount(const std::string & accountID, const std::string & displayName); + void insertAccessCookie(const std::string & accountID, const std::string & accessCookieUUID); + void insertChatMessage(const std::string & sender, const std::string & roomType, const std::string & roomID, const std::string & messageText); + + void updateAccountLoginTime(const std::string & accountID); + + std::vector getActiveGameRooms(); + std::vector getActiveAccounts(); + std::vector getRecentMessageHistory(); + + std::string getIdleGameRoom(const std::string & hostAccountID); + std::string getAccountGameRoom(const std::string & accountID); + std::string getAccountDisplayName(const std::string & accountID); + + LobbyCookieStatus getAccountCookieStatus(const std::string & accountID, const std::string & accessCookieUUID); + LobbyInviteStatus getAccountInviteStatus(const std::string & accountID, const std::string & roomID); + LobbyRoomState getGameRoomStatus(const std::string & roomID); + uint32_t getGameRoomFreeSlots(const std::string & roomID); + + bool isPlayerInGameRoom(const std::string & accountID); + bool isPlayerInGameRoom(const std::string & accountID, const std::string & roomID); + bool isAccountNameExists(const std::string & displayName); + bool isAccountIDExists(const std::string & accountID); +}; diff --git a/lobby/LobbyDefines.h b/lobby/LobbyDefines.h new file mode 100644 index 000000000..b2b323f65 --- /dev/null +++ b/lobby/LobbyDefines.h @@ -0,0 +1,57 @@ +/* + * LobbyDefines.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 + +struct LobbyAccount +{ + std::string accountID; + std::string displayName; +}; + +struct LobbyGameRoom +{ + std::string roomID; + std::string hostAccountID; + std::string hostAccountDisplayName; + std::string roomStatus; + uint32_t playersCount; + uint32_t playersLimit; +}; + +struct LobbyChatMessage +{ + std::string accountID; + std::string displayName; + std::string messageText; + std::chrono::seconds age; +}; + +enum class LobbyCookieStatus : int32_t +{ + INVALID, + VALID +}; + +enum class LobbyInviteStatus : int32_t +{ + NOT_INVITED, + INVITED, + DECLINED +}; + +enum class LobbyRoomState : int32_t +{ + IDLE = 0, // server is ready but no players are in the room + PUBLIC = 1, // host has joined and allows anybody to join + PRIVATE = 2, // host has joined but only allows those he invited to join + //BUSY = 3, // match is ongoing + //CANCELLED = 4, // game room was cancelled without starting the game + CLOSED = 5, // game room was closed after playing for some time +}; diff --git a/lobby/LobbyServer.cpp b/lobby/LobbyServer.cpp new file mode 100644 index 000000000..70fd2470a --- /dev/null +++ b/lobby/LobbyServer.cpp @@ -0,0 +1,592 @@ +/* + * LobbyServer.cpp, 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 + * + */ +#include "StdInc.h" +#include "LobbyServer.h" + +#include "LobbyDatabase.h" + +#include "../lib/JsonNode.h" + +#include +#include + +bool LobbyServer::isAccountNameValid(const std::string & accountName) const +{ + if(accountName.size() < 4) + return false; + + if(accountName.size() < 20) + return false; + + for(const auto & c : accountName) + if(!std::isalnum(c)) + return false; + + return true; +} + +std::string LobbyServer::sanitizeChatMessage(const std::string & inputString) const +{ + // TODO: sanitize message and remove any "weird" symbols from it + return inputString; +} + +NetworkConnectionPtr LobbyServer::findAccount(const std::string & accountID) const +{ + for(const auto & account : activeAccounts) + if(account.second == accountID) + return account.first; + + return nullptr; +} + +NetworkConnectionPtr LobbyServer::findGameRoom(const std::string & gameRoomID) const +{ + for(const auto & account : activeGameRooms) + if(account.second == gameRoomID) + return account.first; + + return nullptr; +} + +void LobbyServer::sendMessage(const NetworkConnectionPtr & target, const JsonNode & json) +{ + target->sendPacket(json.toBytes(true)); +} + +void LobbyServer::sendAccountCreated(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & accountCookie) +{ + JsonNode reply; + reply["type"].String() = "accountCreated"; + reply["accountID"].String() = accountID; + reply["accountCookie"].String() = accountCookie; + sendMessage(target, reply); +} + +void LobbyServer::sendInviteReceived(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & gameRoomID) +{ + JsonNode reply; + reply["type"].String() = "inviteReceived"; + reply["accountID"].String() = accountID; + reply["gameRoomID"].String() = gameRoomID; + sendMessage(target, reply); +} + +void LobbyServer::sendOperationFailed(const NetworkConnectionPtr & target, const std::string & reason) +{ + JsonNode reply; + reply["type"].String() = "operationFailed"; + reply["reason"].String() = reason; + sendMessage(target, reply); +} + +void LobbyServer::sendLoginSuccess(const NetworkConnectionPtr & target, const std::string & accountCookie, const std::string & displayName) +{ + JsonNode reply; + reply["type"].String() = "loginSuccess"; + reply["accountCookie"].String() = accountCookie; + if(!displayName.empty()) + reply["displayName"].String() = displayName; + sendMessage(target, reply); +} + +void LobbyServer::sendChatHistory(const NetworkConnectionPtr & target, const std::vector & history) +{ + JsonNode reply; + reply["type"].String() = "chatHistory"; + + for(const auto & message : boost::adaptors::reverse(history)) + { + JsonNode jsonEntry; + + jsonEntry["accountID"].String() = message.accountID; + jsonEntry["displayName"].String() = message.displayName; + jsonEntry["messageText"].String() = message.messageText; + jsonEntry["ageSeconds"].Integer() = message.age.count(); + + reply["messages"].Vector().push_back(jsonEntry); + } + + sendMessage(target, reply); +} + +void LobbyServer::broadcastActiveAccounts() +{ + auto activeAccountsStats = database->getActiveAccounts(); + + JsonNode reply; + reply["type"].String() = "activeAccounts"; + + for(const auto & account : activeAccountsStats) + { + JsonNode jsonEntry; + jsonEntry["accountID"].String() = account.accountID; + jsonEntry["displayName"].String() = account.displayName; + jsonEntry["status"].String() = "In Lobby"; // TODO: in room status, in match status, offline status(?) + reply["accounts"].Vector().push_back(jsonEntry); + } + + for(const auto & connection : activeAccounts) + sendMessage(connection.first, reply); +} + +JsonNode LobbyServer::prepareActiveGameRooms() +{ + auto activeGameRoomStats = database->getActiveGameRooms(); + JsonNode reply; + reply["type"].String() = "activeGameRooms"; + + for(const auto & gameRoom : activeGameRoomStats) + { + JsonNode jsonEntry; + jsonEntry["gameRoomID"].String() = gameRoom.roomID; + jsonEntry["hostAccountID"].String() = gameRoom.hostAccountID; + jsonEntry["hostAccountDisplayName"].String() = gameRoom.hostAccountDisplayName; + jsonEntry["description"].String() = "TODO: ROOM DESCRIPTION"; + jsonEntry["playersCount"].Integer() = gameRoom.playersCount; + jsonEntry["playersLimit"].Integer() = gameRoom.playersLimit; + reply["gameRooms"].Vector().push_back(jsonEntry); + } + + return reply; +} + +void LobbyServer::broadcastActiveGameRooms() +{ + auto reply = prepareActiveGameRooms(); + + for(const auto & connection : activeAccounts) + sendMessage(connection.first, reply); +} + +void LobbyServer::sendAccountJoinsRoom(const NetworkConnectionPtr & target, const std::string & accountID) +{ + JsonNode reply; + reply["type"].String() = "accountJoinsRoom"; + reply["accountID"].String() = accountID; + sendMessage(target, reply); +} + +void LobbyServer::sendJoinRoomSuccess(const NetworkConnectionPtr & target, const std::string & gameRoomID, bool proxyMode) +{ + JsonNode reply; + reply["type"].String() = "joinRoomSuccess"; + reply["gameRoomID"].String() = gameRoomID; + reply["proxyMode"].Bool() = proxyMode; + sendMessage(target, reply); +} + +void LobbyServer::sendChatMessage(const NetworkConnectionPtr & target, const std::string & roomMode, const std::string & roomName, const std::string & accountID, const std::string & displayName, const std::string & messageText) +{ + JsonNode reply; + reply["type"].String() = "chatMessage"; + reply["messageText"].String() = messageText; + reply["accountID"].String() = accountID; + reply["displayName"].String() = displayName; + reply["roomMode"].String() = roomMode; + reply["roomName"].String() = roomName; + + sendMessage(target, reply); +} + +void LobbyServer::onNewConnection(const NetworkConnectionPtr & connection) +{ + // no-op - waiting for incoming data +} + +void LobbyServer::onDisconnected(const NetworkConnectionPtr & connection, const std::string & errorMessage) +{ + if(activeAccounts.count(connection)) + { + database->setAccountOnline(activeAccounts.at(connection), false); + activeAccounts.erase(connection); + } + + if(activeGameRooms.count(connection)) + { + database->setGameRoomStatus(activeGameRooms.at(connection), LobbyRoomState::CLOSED); + activeGameRooms.erase(connection); + } + + if(activeProxies.count(connection)) + { + const auto & otherConnection = activeProxies.at(connection); + + if (otherConnection) + otherConnection->close(); + + activeProxies.erase(connection); + activeProxies.erase(otherConnection); + } + + broadcastActiveAccounts(); + broadcastActiveGameRooms(); +} + +void LobbyServer::onPacketReceived(const NetworkConnectionPtr & connection, const std::vector & message) +{ + // proxy connection - no processing, only redirect + if(activeProxies.count(connection)) + { + auto lockedPtr = activeProxies.at(connection); + if(lockedPtr) + return lockedPtr->sendPacket(message); + + logGlobal->info("Received unexpected message for inactive proxy!"); + } + + JsonNode json(message.data(), message.size()); + + // TODO: check for json parsing errors + // TODO: validate json based on received message type + + std::string messageType = json["type"].String(); + + // communication messages from vcmiclient + if(activeAccounts.count(connection)) + { + std::string accountName = activeAccounts.at(connection); + logGlobal->info("%s: Received message of type %s", accountName, messageType); + + if(messageType == "sendChatMessage") + return receiveSendChatMessage(connection, json); + + if(messageType == "openGameRoom") + return receiveOpenGameRoom(connection, json); + + if(messageType == "joinGameRoom") + return receiveJoinGameRoom(connection, json); + + if(messageType == "sendInvite") + return receiveSendInvite(connection, json); + + if(messageType == "declineInvite") + return receiveDeclineInvite(connection, json); + + logGlobal->warn("%s: Unknown message type: %s", accountName, messageType); + return; + } + + // communication messages from vcmiserver + if(activeGameRooms.count(connection)) + { + std::string roomName = activeGameRooms.at(connection); + logGlobal->info("%s: Received message of type %s", roomName, messageType); + + if(messageType == "leaveGameRoom") + return receiveLeaveGameRoom(connection, json); + + logGlobal->warn("%s: Unknown message type: %s", roomName, messageType); + return; + } + + logGlobal->info("(unauthorised): Received message of type %s", messageType); + + // unauthorized connections - permit only login or register attempts + if(messageType == "clientLogin") + return receiveClientLogin(connection, json); + + if(messageType == "clientRegister") + return receiveClientRegister(connection, json); + + if(messageType == "serverLogin") + return receiveServerLogin(connection, json); + + if(messageType == "clientProxyLogin") + return receiveClientProxyLogin(connection, json); + + if(messageType == "serverProxyLogin") + return receiveServerProxyLogin(connection, json); + + connection->close(); + logGlobal->info("(unauthorised): Unknown message type %s", messageType); +} + +void LobbyServer::receiveSendChatMessage(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string accountID = activeAccounts[connection]; + std::string messageText = json["messageText"].String(); + std::string messageTextClean = sanitizeChatMessage(messageText); + std::string displayName = database->getAccountDisplayName(accountID); + + if(messageTextClean.empty()) + return sendOperationFailed(connection, "No printable characters in sent message!"); + + database->insertChatMessage(accountID, "global", "english", messageText); + + for(const auto & otherConnection : activeAccounts) + sendChatMessage(otherConnection.first, "global", "english", accountID, displayName, messageText); +} + +void LobbyServer::receiveClientRegister(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string displayName = json["displayName"].String(); + std::string language = json["language"].String(); + + if(isAccountNameValid(displayName)) + return sendOperationFailed(connection, "Illegal account name"); + + if(database->isAccountNameExists(displayName)) + return sendOperationFailed(connection, "Account name already in use"); + + std::string accountCookie = boost::uuids::to_string(boost::uuids::random_generator()()); + std::string accountID = boost::uuids::to_string(boost::uuids::random_generator()()); + + database->insertAccount(accountID, displayName); + database->insertAccessCookie(accountID, accountCookie); + + sendAccountCreated(connection, accountID, accountCookie); +} + +void LobbyServer::receiveClientLogin(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string accountID = json["accountID"].String(); + std::string accountCookie = json["accountCookie"].String(); + std::string language = json["language"].String(); + std::string version = json["version"].String(); + + if(!database->isAccountIDExists(accountID)) + return sendOperationFailed(connection, "Account not found"); + + auto clientCookieStatus = database->getAccountCookieStatus(accountID, accountCookie); + + if(clientCookieStatus == LobbyCookieStatus::INVALID) + return sendOperationFailed(connection, "Authentification failure"); + + database->updateAccountLoginTime(accountID); + database->setAccountOnline(accountID, true); + + std::string displayName = database->getAccountDisplayName(accountID); + + activeAccounts[connection] = accountID; + + sendLoginSuccess(connection, accountCookie, displayName); + sendChatHistory(connection, database->getRecentMessageHistory()); + + // send active game rooms list to new account + // and update acount list to everybody else including new account + broadcastActiveAccounts(); + sendMessage(connection, prepareActiveGameRooms()); +} + +void LobbyServer::receiveServerLogin(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string gameRoomID = json["gameRoomID"].String(); + std::string accountID = json["accountID"].String(); + std::string accountCookie = json["accountCookie"].String(); + std::string version = json["version"].String(); + + auto clientCookieStatus = database->getAccountCookieStatus(accountID, accountCookie); + + if(clientCookieStatus == LobbyCookieStatus::INVALID) + { + sendOperationFailed(connection, "Invalid credentials"); + } + else + { + database->insertGameRoom(gameRoomID, accountID); + activeGameRooms[connection] = gameRoomID; + sendLoginSuccess(connection, accountCookie, {}); + broadcastActiveGameRooms(); + } +} + +void LobbyServer::receiveClientProxyLogin(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string gameRoomID = json["gameRoomID"].String(); + std::string accountID = json["accountID"].String(); + std::string accountCookie = json["accountCookie"].String(); + + auto clientCookieStatus = database->getAccountCookieStatus(accountID, accountCookie); + + if(clientCookieStatus != LobbyCookieStatus::INVALID) + { + for(auto & proxyEntry : awaitingProxies) + { + if(proxyEntry.accountID != accountID) + continue; + if(proxyEntry.roomID != gameRoomID) + continue; + + proxyEntry.accountConnection = connection; + + auto gameRoomConnection = proxyEntry.roomConnection.lock(); + + if(gameRoomConnection) + { + activeProxies[gameRoomConnection] = connection; + activeProxies[connection] = gameRoomConnection; + } + return; + } + } + + sendOperationFailed(connection, "Invalid credentials"); + connection->close(); +} + +void LobbyServer::receiveServerProxyLogin(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string gameRoomID = json["gameRoomID"].String(); + std::string guestAccountID = json["guestAccountID"].String(); + std::string accountCookie = json["accountCookie"].String(); + + // FIXME: find host account ID and validate his cookie + //auto clientCookieStatus = database->getAccountCookieStatus(hostAccountID, accountCookie, accountCookieLifetime); + + //if(clientCookieStatus != LobbyCookieStatus::INVALID) + { + NetworkConnectionPtr targetAccount = findAccount(guestAccountID); + + if(targetAccount == nullptr) + { + sendOperationFailed(connection, "Invalid credentials"); + return; // unknown / disconnected account + } + + sendJoinRoomSuccess(targetAccount, gameRoomID, true); + + AwaitingProxyState proxy; + proxy.accountID = guestAccountID; + proxy.roomID = gameRoomID; + proxy.roomConnection = connection; + awaitingProxies.push_back(proxy); + return; + } + + //connection->close(); +} + +void LobbyServer::receiveOpenGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string hostAccountID = json["hostAccountID"].String(); + std::string accountID = activeAccounts[connection]; + + if(database->isPlayerInGameRoom(accountID)) + return sendOperationFailed(connection, "Player already in the room!"); + + std::string gameRoomID = database->getIdleGameRoom(hostAccountID); + if(gameRoomID.empty()) + return sendOperationFailed(connection, "Failed to find idle server to join!"); + + std::string roomType = json["roomType"].String(); + if(roomType != "public" && roomType != "private") + return sendOperationFailed(connection, "Invalid room type!"); + + if(roomType == "public") + database->setGameRoomStatus(gameRoomID, LobbyRoomState::PUBLIC); + if(roomType == "private") + database->setGameRoomStatus(gameRoomID, LobbyRoomState::PRIVATE); + + database->insertPlayerIntoGameRoom(accountID, gameRoomID); + broadcastActiveGameRooms(); + sendJoinRoomSuccess(connection, gameRoomID, false); +} + +void LobbyServer::receiveJoinGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string gameRoomID = json["gameRoomID"].String(); + std::string accountID = activeAccounts[connection]; + + if(database->isPlayerInGameRoom(accountID)) + return sendOperationFailed(connection, "Player already in the room!"); + + NetworkConnectionPtr targetRoom = findGameRoom(gameRoomID); + + if(targetRoom == nullptr) + return sendOperationFailed(connection, "Failed to find game room to join!"); + + auto roomStatus = database->getGameRoomStatus(gameRoomID); + + if(roomStatus != LobbyRoomState::PRIVATE && roomStatus != LobbyRoomState::PUBLIC) + return sendOperationFailed(connection, "Room does not accepts new players!"); + + if(roomStatus == LobbyRoomState::PRIVATE) + { + if(database->getAccountInviteStatus(accountID, gameRoomID) != LobbyInviteStatus::INVITED) + return sendOperationFailed(connection, "You are not permitted to join private room without invite!"); + } + + if(database->getGameRoomFreeSlots(gameRoomID) == 0) + return sendOperationFailed(connection, "Room is already full!"); + + database->insertPlayerIntoGameRoom(accountID, gameRoomID); + sendAccountJoinsRoom(targetRoom, accountID); + //No reply to client - will be sent once match server establishes proxy connection with lobby + + broadcastActiveGameRooms(); +} + +void LobbyServer::receiveLeaveGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string accountID = json["accountID"].String(); + std::string gameRoomID = activeGameRooms[connection]; + + if(!database->isPlayerInGameRoom(accountID, gameRoomID)) + return sendOperationFailed(connection, "You are not in the room!"); + + database->deletePlayerFromGameRoom(accountID, gameRoomID); + + broadcastActiveGameRooms(); +} + +void LobbyServer::receiveSendInvite(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string senderName = activeAccounts[connection]; + std::string accountID = json["accountID"].String(); + std::string gameRoomID = database->getAccountGameRoom(senderName); + + auto targetAccount = findAccount(accountID); + + if(!targetAccount) + return sendOperationFailed(connection, "Invalid account to invite!"); + + if(!database->isPlayerInGameRoom(senderName)) + return sendOperationFailed(connection, "You are not in the room!"); + + if(database->isPlayerInGameRoom(accountID)) + return sendOperationFailed(connection, "This player is already in a room!"); + + if(database->getAccountInviteStatus(accountID, gameRoomID) != LobbyInviteStatus::NOT_INVITED) + return sendOperationFailed(connection, "This player is already invited!"); + + database->insertGameRoomInvite(accountID, gameRoomID); + sendInviteReceived(targetAccount, senderName, gameRoomID); +} + +void LobbyServer::receiveDeclineInvite(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string accountID = activeAccounts[connection]; + std::string gameRoomID = json["gameRoomID"].String(); + + if(database->getAccountInviteStatus(accountID, gameRoomID) != LobbyInviteStatus::INVITED) + return sendOperationFailed(connection, "No active invite found!"); + + database->deleteGameRoomInvite(accountID, gameRoomID); +} + +LobbyServer::~LobbyServer() = default; + +LobbyServer::LobbyServer(const boost::filesystem::path & databasePath) + : database(std::make_unique(databasePath)) + , networkHandler(INetworkHandler::createHandler()) + , networkServer(networkHandler->createServerTCP(*this)) +{ +} + +void LobbyServer::start(uint16_t port) +{ + networkServer->start(port); +} + +void LobbyServer::run() +{ + networkHandler->run(); +} diff --git a/lobby/LobbyServer.h b/lobby/LobbyServer.h new file mode 100644 index 000000000..e61b0111c --- /dev/null +++ b/lobby/LobbyServer.h @@ -0,0 +1,92 @@ +/* + * LobbyServer.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 "../lib/network/NetworkInterface.h" +#include "LobbyDefines.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class LobbyDatabase; + +class LobbyServer final : public INetworkServerListener +{ + struct AwaitingProxyState + { + std::string accountID; + std::string roomID; + NetworkConnectionWeakPtr accountConnection; + NetworkConnectionWeakPtr roomConnection; + }; + + /// list of connected proxies. All messages received from (key) will be redirected to (value) connection + std::map activeProxies; + + /// list of half-established proxies from server that are still waiting for client to connect + std::vector awaitingProxies; + + /// list of logged in accounts (vcmiclient's) + std::map activeAccounts; + + /// list of currently logged in game rooms (vcmiserver's) + std::map activeGameRooms; + + std::unique_ptr database; + std::unique_ptr networkHandler; + std::unique_ptr networkServer; + + std::string sanitizeChatMessage(const std::string & inputString) const; + bool isAccountNameValid(const std::string & accountName) const; + + NetworkConnectionPtr findAccount(const std::string & accountID) const; + NetworkConnectionPtr findGameRoom(const std::string & gameRoomID) const; + + void onNewConnection(const NetworkConnectionPtr & connection) override; + void onDisconnected(const NetworkConnectionPtr & connection, const std::string & errorMessage) override; + void onPacketReceived(const NetworkConnectionPtr & connection, const std::vector & message) override; + + void sendMessage(const NetworkConnectionPtr & target, const JsonNode & json); + + void broadcastActiveAccounts(); + void broadcastActiveGameRooms(); + + JsonNode prepareActiveGameRooms(); + + void sendChatMessage(const NetworkConnectionPtr & target, const std::string & roomMode, const std::string & roomName, const std::string & accountID, const std::string & displayName, const std::string & messageText); + void sendAccountCreated(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & accountCookie); + void sendOperationFailed(const NetworkConnectionPtr & target, const std::string & reason); + void sendLoginSuccess(const NetworkConnectionPtr & target, const std::string & accountCookie, const std::string & displayName); + void sendChatHistory(const NetworkConnectionPtr & target, const std::vector &); + void sendAccountJoinsRoom(const NetworkConnectionPtr & target, const std::string & accountID); + void sendJoinRoomSuccess(const NetworkConnectionPtr & target, const std::string & gameRoomID, bool proxyMode); + void sendInviteReceived(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & gameRoomID); + + void receiveClientRegister(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveClientLogin(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveServerLogin(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveClientProxyLogin(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveServerProxyLogin(const NetworkConnectionPtr & connection, const JsonNode & json); + + void receiveSendChatMessage(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveOpenGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveJoinGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveLeaveGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveSendInvite(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveDeclineInvite(const NetworkConnectionPtr & connection, const JsonNode & json); + +public: + explicit LobbyServer(const boost::filesystem::path & databasePath); + ~LobbyServer(); + + void start(uint16_t port); + void run(); +}; diff --git a/lobby/SQLiteConnection.cpp b/lobby/SQLiteConnection.cpp new file mode 100644 index 000000000..439ad839b --- /dev/null +++ b/lobby/SQLiteConnection.cpp @@ -0,0 +1,194 @@ +/* + * SQLiteConnection.cpp, 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 + * + */ +#include "StdInc.h" +#include "SQLiteConnection.h" + +#include + +[[noreturn]] static void handleSQLiteError(sqlite3 * connection) +{ + const char * message = sqlite3_errmsg(connection); + throw std::runtime_error(std::string("SQLite error: ") + message); +} + +static void checkSQLiteError(sqlite3 * connection, int result) +{ + if(result != SQLITE_OK) + handleSQLiteError(connection); +} + +SQLiteStatement::SQLiteStatement(SQLiteInstance & instance, sqlite3_stmt * statement) + : m_instance(instance) + , m_statement(statement) +{ +} + +SQLiteStatement::~SQLiteStatement() +{ + sqlite3_finalize(m_statement); +} + +bool SQLiteStatement::execute() +{ + int result = sqlite3_step(m_statement); + + switch(result) + { + case SQLITE_DONE: + return false; + case SQLITE_ROW: + return true; + default: + checkSQLiteError(m_instance.m_connection, result); + return false; + } +} + +void SQLiteStatement::reset() +{ + int result = sqlite3_reset(m_statement); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::clear() +{ + int result = sqlite3_clear_bindings(m_statement); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const double & value) +{ + int result = sqlite3_bind_double(m_statement, static_cast(index), value); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const bool & value) +{ + int result = sqlite3_bind_int(m_statement, static_cast(value), value); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const uint8_t & value) +{ + int result = sqlite3_bind_int(m_statement, static_cast(index), value); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const uint16_t & value) +{ + int result = sqlite3_bind_int(m_statement, static_cast(index), value); + checkSQLiteError(m_instance.m_connection, result); +} +void SQLiteStatement::setBindSingle(size_t index, const uint32_t & value) +{ + int result = sqlite3_bind_int(m_statement, static_cast(index), value); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const int32_t & value) +{ + int result = sqlite3_bind_int(m_statement, static_cast(index), value); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const int64_t & value) +{ + int result = sqlite3_bind_int64(m_statement, static_cast(index), value); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const std::string & value) +{ + int result = sqlite3_bind_text(m_statement, static_cast(index), value.data(), static_cast(value.size()), SQLITE_STATIC); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const char * value) +{ + int result = sqlite3_bind_text(m_statement, static_cast(index), value, -1, SQLITE_STATIC); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::getColumnSingle(size_t index, double & value) +{ + value = sqlite3_column_double(m_statement, static_cast(index)); +} + +void SQLiteStatement::getColumnSingle(size_t index, bool & value) +{ + value = sqlite3_column_int(m_statement, static_cast(index)) != 0; +} + +void SQLiteStatement::getColumnSingle(size_t index, uint8_t & value) +{ + value = static_cast(sqlite3_column_int(m_statement, static_cast(index))); +} + +void SQLiteStatement::getColumnSingle(size_t index, uint16_t & value) +{ + value = static_cast(sqlite3_column_int(m_statement, static_cast(index))); +} + +void SQLiteStatement::getColumnSingle(size_t index, int32_t & value) +{ + value = sqlite3_column_int(m_statement, static_cast(index)); +} + +void SQLiteStatement::getColumnSingle(size_t index, uint32_t & value) +{ + value = sqlite3_column_int(m_statement, static_cast(index)); +} + +void SQLiteStatement::getColumnSingle(size_t index, int64_t & value) +{ + value = sqlite3_column_int64(m_statement, static_cast(index)); +} + +void SQLiteStatement::getColumnSingle(size_t index, std::string & value) +{ + const auto * value_raw = sqlite3_column_text(m_statement, static_cast(index)); + value = reinterpret_cast(value_raw); +} + +SQLiteInstancePtr SQLiteInstance::open(const boost::filesystem::path & db_path, bool allow_write) +{ + int flags = allow_write ? (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) : SQLITE_OPEN_READONLY; + + sqlite3 * connection; + int result = sqlite3_open_v2(db_path.c_str(), &connection, flags, nullptr); + + if(result == SQLITE_OK) + return SQLiteInstancePtr(new SQLiteInstance(connection)); + + sqlite3_close(connection); + handleSQLiteError(connection); +} + +SQLiteInstance::SQLiteInstance(sqlite3 * connection) + : m_connection(connection) +{ +} + +SQLiteInstance::~SQLiteInstance() +{ + sqlite3_close(m_connection); +} + +SQLiteStatementPtr SQLiteInstance::prepare(const std::string & sql_text) +{ + sqlite3_stmt * statement; + int result = sqlite3_prepare_v2(m_connection, sql_text.data(), static_cast(sql_text.size()), &statement, nullptr); + + if(result == SQLITE_OK) + return SQLiteStatementPtr(new SQLiteStatement(*this, statement)); + + sqlite3_finalize(statement); + handleSQLiteError(m_connection); +} diff --git a/lobby/SQLiteConnection.h b/lobby/SQLiteConnection.h new file mode 100644 index 000000000..834caec88 --- /dev/null +++ b/lobby/SQLiteConnection.h @@ -0,0 +1,115 @@ +/* + * SQLiteConnection.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 + +using sqlite3 = struct sqlite3; +using sqlite3_stmt = struct sqlite3_stmt; + +class SQLiteInstance; +class SQLiteStatement; + +using SQLiteInstancePtr = std::unique_ptr; +using SQLiteStatementPtr = std::unique_ptr; + +class SQLiteStatement : boost::noncopyable +{ +public: + friend class SQLiteInstance; + + bool execute(); + void reset(); + void clear(); + + ~SQLiteStatement(); + + template + void executeOnce(const Args &... args) + { + setBinds(args...); + execute(); + reset(); + } + + template + void setBinds(const Args &... args) + { + setBindSingle(1, args...); // The leftmost SQL parameter has an index of 1 + } + + template + void getColumns(Args &... args) + { + getColumnSingle(0, args...); // The leftmost column of the result set has the index 0 + } + +private: + void setBindSingle(size_t index, const double & value); + void setBindSingle(size_t index, const bool & value); + void setBindSingle(size_t index, const uint8_t & value); + void setBindSingle(size_t index, const uint16_t & value); + void setBindSingle(size_t index, const uint32_t & value); + void setBindSingle(size_t index, const int32_t & value); + void setBindSingle(size_t index, const int64_t & value); + void setBindSingle(size_t index, const std::string & value); + void setBindSingle(size_t index, const char * value); + + void getColumnSingle(size_t index, double & value); + void getColumnSingle(size_t index, bool & value); + void getColumnSingle(size_t index, uint8_t & value); + void getColumnSingle(size_t index, uint16_t & value); + void getColumnSingle(size_t index, uint32_t & value); + void getColumnSingle(size_t index, int32_t & value); + void getColumnSingle(size_t index, int64_t & value); + void getColumnSingle(size_t index, std::string & value); + + template + void getColumnSingle(size_t index, std::chrono::duration & value) + { + int64_t durationValue = 0; + getColumnSingle(index, durationValue); + value = std::chrono::duration(durationValue); + } + + SQLiteStatement(SQLiteInstance & instance, sqlite3_stmt * statement); + + template + void setBindSingle(size_t index, T const & arg, const Args &... args) + { + setBindSingle(index, arg); + setBindSingle(index + 1, args...); + } + + template + void getColumnSingle(size_t index, T & arg, Args &... args) + { + getColumnSingle(index, arg); + getColumnSingle(index + 1, args...); + } + + SQLiteInstance & m_instance; + sqlite3_stmt * m_statement; +}; + +class SQLiteInstance : boost::noncopyable +{ +public: + friend class SQLiteStatement; + + static SQLiteInstancePtr open(const boost::filesystem::path & db_path, bool allow_write); + + ~SQLiteInstance(); + + SQLiteStatementPtr prepare(const std::string & statement); + +private: + explicit SQLiteInstance(sqlite3 * connection); + + sqlite3 * m_connection; +}; diff --git a/lobby/StdInc.cpp b/lobby/StdInc.cpp new file mode 100644 index 000000000..dd7f66cb8 --- /dev/null +++ b/lobby/StdInc.cpp @@ -0,0 +1,2 @@ +// Creates the precompiled header +#include "StdInc.h" diff --git a/lobby/StdInc.h b/lobby/StdInc.h new file mode 100644 index 000000000..d03216bdf --- /dev/null +++ b/lobby/StdInc.h @@ -0,0 +1,14 @@ +/* + * StdInc.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 "../Global.h" + +VCMI_LIB_USING_NAMESPACE diff --git a/mapeditor/mapsettings/generalsettings.cpp b/mapeditor/mapsettings/generalsettings.cpp index a15599e84..74f405232 100644 --- a/mapeditor/mapsettings/generalsettings.cpp +++ b/mapeditor/mapsettings/generalsettings.cpp @@ -35,23 +35,23 @@ void GeneralSettings::initialize(MapController & c) //set difficulty switch(controller->map()->difficulty) { - case 0: + case EMapDifficulty::EASY: ui->diffRadio1->setChecked(true); break; - case 1: + case EMapDifficulty::NORMAL: ui->diffRadio2->setChecked(true); break; - case 2: + case EMapDifficulty::HARD: ui->diffRadio3->setChecked(true); break; - case 3: + case EMapDifficulty::EXPERT: ui->diffRadio4->setChecked(true); break; - case 4: + case EMapDifficulty::IMPOSSIBLE: ui->diffRadio5->setChecked(true); break; }; @@ -67,11 +67,11 @@ void GeneralSettings::update() controller->map()->levelLimit = 0; //set difficulty - if(ui->diffRadio1->isChecked()) controller->map()->difficulty = 0; - if(ui->diffRadio2->isChecked()) controller->map()->difficulty = 1; - if(ui->diffRadio3->isChecked()) controller->map()->difficulty = 2; - if(ui->diffRadio4->isChecked()) controller->map()->difficulty = 3; - if(ui->diffRadio5->isChecked()) controller->map()->difficulty = 4; + if(ui->diffRadio1->isChecked()) controller->map()->difficulty = EMapDifficulty::EASY; + if(ui->diffRadio2->isChecked()) controller->map()->difficulty = EMapDifficulty::NORMAL; + if(ui->diffRadio3->isChecked()) controller->map()->difficulty = EMapDifficulty::HARD; + if(ui->diffRadio4->isChecked()) controller->map()->difficulty = EMapDifficulty::EXPERT; + if(ui->diffRadio5->isChecked()) controller->map()->difficulty = EMapDifficulty::IMPOSSIBLE; } void GeneralSettings::on_heroLevelLimitCheck_toggled(bool checked) diff --git a/scripting/lua/LuaSpellEffect.h b/scripting/lua/LuaSpellEffect.h index effb620e7..54b11d4c2 100644 --- a/scripting/lua/LuaSpellEffect.h +++ b/scripting/lua/LuaSpellEffect.h @@ -35,7 +35,7 @@ public: LuaSpellEffectFactory(const Script * script_); virtual ~LuaSpellEffectFactory(); - virtual Effect * create() const override; + Effect * create() const override; private: const Script * script; diff --git a/scripting/lua/api/BonusSystem.h b/scripting/lua/api/BonusSystem.h index 657243670..ebaabc1e0 100644 --- a/scripting/lua/api/BonusSystem.h +++ b/scripting/lua/api/BonusSystem.h @@ -44,7 +44,7 @@ public: static int toJsonNode(lua_State * L); protected: - virtual void adjustStaticTable(lua_State * L) const override; + void adjustStaticTable(lua_State * L) const override; }; class BonusListProxy : public SharedWrapper @@ -56,7 +56,7 @@ public: static std::shared_ptr index(std::shared_ptr self, int key); protected: - virtual void adjustMetatable(lua_State * L) const override; + void adjustMetatable(lua_State * L) const override; }; class BonusBearerProxy : public OpaqueWrapper diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 789c0682a..1fcca7397 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -63,6 +63,7 @@ #include "../lib/serializer/CSaveFile.h" #include "../lib/serializer/CLoadFile.h" +#include "../lib/serializer/Connection.h" #include "../lib/spells/CSpellHandler.h" @@ -444,7 +445,10 @@ void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill wh void CGameHandler::handleClientDisconnection(std::shared_ptr c) { if(lobby->getState() == EServerState::SHUTDOWN || !gs || !gs->scenarioOps) + { + assert(0); // game should have shut down before reaching this point! return; + } for(auto & playerConnections : connections) { @@ -970,11 +974,11 @@ void CGameHandler::onNewTurn() synchronizeArtifactHandlerLists(); //new day events may have changed them. TODO better of managing that } -void CGameHandler::run(bool resume) +void CGameHandler::start(bool resume) { LOG_TRACE_PARAMS(logGlobal, "resume=%d", resume); - for (auto cc : lobby->connections) + for (auto cc : lobby->activeConnections) { auto players = lobby->getAllClientPlayers(cc->connectionID); std::stringstream sbuffer; @@ -982,10 +986,7 @@ void CGameHandler::run(bool resume) for (PlayerColor color : players) { sbuffer << color << " "; - { - boost::unique_lock lock(gsm); - connections[color].insert(cc); - } + connections[color].insert(cc); } logGlobal->info(sbuffer.str()); } @@ -1005,18 +1006,11 @@ void CGameHandler::run(bool resume) events::GameResumed::defaultExecute(serverEventBus.get()); turnOrder->onGameStarted(); +} - //wait till game is done - auto clockLast = std::chrono::steady_clock::now(); - while(lobby->getState() == EServerState::GAMEPLAY) - { - const auto clockDuration = std::chrono::steady_clock::now() - clockLast; - const int timePassed = std::chrono::duration_cast(clockDuration).count(); - clockLast += clockDuration; - turnTimerHandler.update(timePassed); - boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); - - } +void CGameHandler::tick(int millisecondsPassed) +{ + turnTimerHandler.update(millisecondsPassed); } void CGameHandler::giveSpells(const CGTownInstance *t, const CGHeroInstance *h) @@ -1195,7 +1189,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo if (leavingTile == LEAVING_TILE) leaveTile(); - if (isInTheMap(guardPos)) + if (lookForGuards == CHECK_FOR_GUARDS && isInTheMap(guardPos)) tmh.attackedFrom = std::make_optional(guardPos); tmh.result = result; @@ -1678,13 +1672,8 @@ void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) void CGameHandler::sendToAllClients(CPackForClient * pack) { logNetwork->trace("\tSending to all clients: %s", typeid(*pack).name()); - for (auto c : lobby->connections) - { - if(!c->isOpen()) - continue; - + for (auto c : lobby->activeConnections) c->sendPack(pack); - } } void CGameHandler::sendAndApply(CPackForClient * pack) @@ -3196,8 +3185,6 @@ bool CGameHandler::setFormation(ObjectInstanceID hid, EArmyFormation formation) bool CGameHandler::queryReply(QueryID qid, std::optional answer, PlayerColor player) { - boost::unique_lock lock(gsm); - logGlobal->trace("Player %s attempts answering query %d with answer:", player, qid); if (answer) logGlobal->trace("%d", *answer); @@ -3633,7 +3620,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) if(p->human) { - lobby->setState(EServerState::GAMEPLAY_ENDED); + lobby->setState(EServerState::SHUTDOWN); } } else diff --git a/server/CGameHandler.h b/server/CGameHandler.h index b2136df6b..67b9bf8d3 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -73,7 +73,6 @@ public: std::map>> connections; //player color -> connection to client with interface of that player //queries stuff - boost::recursive_mutex gsm; ui32 QID; SpellCastEnvironment * spellEnv; @@ -261,7 +260,8 @@ public: bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id); - void run(bool resume); + void start(bool resume); + void tick(int millisecondsPassed); bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & arts); void spawnWanderingMonsters(CreatureID creatureID); diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 0263fef96..efc6a0432 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -15,7 +15,9 @@ set(server_SRCS processors/PlayerMessageProcessor.cpp processors/TurnOrderProcessor.cpp + EntryPoint.cpp CGameHandler.cpp + GlobalLobbyProcessor.cpp ServerSpellCastEnvironment.cpp CVCMIServer.cpp NetPacksServer.cpp @@ -41,6 +43,7 @@ set(server_HEADERS processors/TurnOrderProcessor.h CGameHandler.h + GlobalLobbyProcessor.h ServerSpellCastEnvironment.h CVCMIServer.h LobbyNetPackVisitors.h diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 8f4ecaf75..1ec3d3299 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -8,53 +8,23 @@ * */ #include "StdInc.h" -#include - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/campaign/CampaignState.h" -#include "../lib/CThreadHelper.h" -#include "../lib/serializer/Connection.h" -#include "../lib/CArtHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CTownHandler.h" -#include "../lib/CBuildingHandler.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/CCreatureHandler.h" -#include "zlib.h" #include "CVCMIServer.h" -#include "../lib/StartInfo.h" -#include "../lib/mapping/CMapHeader.h" -#include "../lib/rmg/CMapGenOptions.h" -#include "LobbyNetPackVisitors.h" -#ifdef VCMI_ANDROID -#include -#include -#include "lib/CAndroidVMHelper.h" -#endif -#include "../lib/VCMI_Lib.h" -#include "../lib/VCMIDirs.h" + #include "CGameHandler.h" +#include "GlobalLobbyProcessor.h" +#include "LobbyNetPackVisitors.h" #include "processors/PlayerMessageProcessor.h" -#include "../lib/mapping/CMapInfo.h" -#include "../lib/GameConstants.h" -#include "../lib/logging/CBasicLogConfigurator.h" -#include "../lib/CConfigHandler.h" -#include "../lib/ScopeGuard.h" -#include "../lib/serializer/CMemorySerializer.h" -#include "../lib/serializer/Cast.h" -#include "../lib/UnlockGuard.h" - -// for applier +#include "../lib/CHeroHandler.h" #include "../lib/registerTypes/RegisterTypesLobbyPacks.h" +#include "../lib/serializer/CMemorySerializer.h" +#include "../lib/serializer/Connection.h" // UUID generation #include #include #include - -#include "../lib/gameState/CGameState.h" +#include template class CApplyOnServer; @@ -81,11 +51,8 @@ public: if(checker.getResult()) { - boost::unique_lock stateLock(srv->stateMutex); ApplyOnServerNetPackVisitor applier(*srv); - ptr->visit(applier); - return applier.getResult(); } else @@ -117,179 +84,175 @@ public: } }; -std::string SERVER_NAME_AFFIX = "server"; -std::string SERVER_NAME = GameConstants::VCMI_VERSION + std::string(" (") + SERVER_NAME_AFFIX + ')'; +class CVCMIServerPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) +{ +private: + CVCMIServer & handler; + std::shared_ptr gh; + +public: + CVCMIServerPackVisitor(CVCMIServer & handler, std::shared_ptr gh) + :handler(handler), gh(gh) + { + } + + bool callTyped() override { return false; } + + void visitForLobby(CPackForLobby & packForLobby) override + { + handler.handleReceivedPack(std::unique_ptr(&packForLobby)); + } + + void visitForServer(CPackForServer & serverPack) override + { + if (gh) + gh->handleReceivedPack(&serverPack); + else + logNetwork->error("Received pack for game server while in lobby!"); + } + + void visitForClient(CPackForClient & clientPack) override + { + } +}; CVCMIServer::CVCMIServer(boost::program_options::variables_map & opts) - : port(3030), io(std::make_shared()), state(EServerState::LOBBY), cmdLineOptions(opts), currentClientId(1), currentPlayerId(1), restartGameplay(false) + : currentClientId(1) + , currentPlayerId(1) + , cmdLineOptions(opts) { uuid = boost::uuids::to_string(boost::uuids::random_generator()()); logNetwork->trace("CVCMIServer created! UUID: %s", uuid); applier = std::make_shared>(); registerTypesLobbyPacks(*applier); + networkHandler = INetworkHandler::createHandler(); + + if(cmdLineOptions.count("lobby")) + lobbyProcessor = std::make_unique(*this); + else + startAcceptingIncomingConnections(); +} + +CVCMIServer::~CVCMIServer() = default; + +void CVCMIServer::startAcceptingIncomingConnections() +{ + uint16_t port = 3030; + if(cmdLineOptions.count("port")) - port = cmdLineOptions["port"].as(); + port = cmdLineOptions["port"].as(); logNetwork->info("Port %d will be used", port); - try - { - acceptor = std::make_shared(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)); - } - catch(...) - { - logNetwork->info("Port %d is busy, trying to use random port instead", port); - if(cmdLineOptions.count("run-by-client")) - { - logNetwork->error("Port must be specified when run-by-client is used!!"); -#if (defined(__ANDROID_API__) && __ANDROID_API__ < 21) || (defined(__MINGW32__)) || defined(VCMI_APPLE) - ::exit(0); -#else - std::quick_exit(0); -#endif - } - acceptor = std::make_shared(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); - port = acceptor->local_endpoint().port(); - } + + networkServer = networkHandler->createServerTCP(*this); + networkServer->start(port); logNetwork->info("Listening for connections at port %d", port); } -CVCMIServer::~CVCMIServer() +void CVCMIServer::onNewConnection(const std::shared_ptr & connection) { - announceQueue.clear(); + if(getState() == EServerState::LOBBY) + { + activeConnections.push_back(std::make_shared(connection)); + activeConnections.back()->enterLobbyConnectionMode(); + } + else + { + // TODO: reconnection support + connection->close(); + } +} - if(announceLobbyThread) - announceLobbyThread->join(); +void CVCMIServer::onPacketReceived(const std::shared_ptr & connection, const std::vector & message) +{ + std::shared_ptr c = findConnection(connection); + auto pack = c->retrievePack(message); + pack->c = c; + CVCMIServerPackVisitor visitor(*this, this->gh); + pack->visit(visitor); } void CVCMIServer::setState(EServerState value) { - state.store(value); + assert(state != EServerState::SHUTDOWN); // do not attempt to restart dying server + state = value; + + if (state == EServerState::SHUTDOWN) + networkHandler->stop(); } EServerState CVCMIServer::getState() const { - return state.load(); + return state; +} + +std::shared_ptr CVCMIServer::findConnection(const std::shared_ptr & netConnection) +{ + for(const auto & gameConnection : activeConnections) + { + if (gameConnection->isMyConnection(netConnection)) + return gameConnection; + } + + throw std::runtime_error("Unknown connection received in CVCMIServer::findConnection"); } void CVCMIServer::run() { +#if defined(VCMI_ANDROID) && !defined(SINGLE_PROCESS_APP) if(!restartGameplay) { - this->announceLobbyThread = std::make_unique(&CVCMIServer::threadAnnounceLobby, this); - - startAsyncAccept(); - if(!remoteConnectionsThread && cmdLineOptions.count("lobby")) - { - remoteConnectionsThread = std::make_unique(&CVCMIServer::establishRemoteConnections, this); - } - -#if defined(VCMI_ANDROID) -#ifndef SINGLE_PROCESS_APP CAndroidVMHelper vmHelper; vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "onServerReady"); + } #endif -#endif - } - - while(state == EServerState::LOBBY || state == EServerState::GAMEPLAY_STARTING) - boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); - - logNetwork->info("Thread handling connections ended"); - - if(state == EServerState::GAMEPLAY) - { - gh->run(si->mode == StartInfo::LOAD_GAME); - } - while(state == EServerState::GAMEPLAY_ENDED) - boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); + networkHandler->run(); } -void CVCMIServer::establishRemoteConnections() +void CVCMIServer::onTimer() { - setThreadName("establishConnection"); + // we might receive onTimer call after transitioning from GAMEPLAY to LOBBY state, e.g. on game restart + if (getState() != EServerState::GAMEPLAY) + return; - //wait for host connection - while(connections.empty()) - boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); - - uuid = cmdLineOptions["lobby-uuid"].as(); - int numOfConnections = cmdLineOptions["connections"].as(); - for(int i = 0; i < numOfConnections; ++i) - connectToRemote(); -} + static const auto serverUpdateInterval = std::chrono::milliseconds(100); -void CVCMIServer::connectToRemote() -{ - std::shared_ptr c; - try - { - auto address = cmdLineOptions["lobby"].as(); - int port = cmdLineOptions["lobby-port"].as(); - - logNetwork->info("Establishing connection to remote at %s:%d with uuid %s", address, port, uuid); - c = std::make_shared(address, port, SERVER_NAME, uuid); - } - catch(...) - { - logNetwork->error("\nCannot establish remote connection!"); - } - - if(c) - { - connections.insert(c); - remoteConnections.insert(c); - c->handler = std::make_shared(&CVCMIServer::threadHandleClient, this, c); - } -} + auto timeNow = std::chrono::steady_clock::now(); + auto timePassedBefore = lastTimerUpdateTime - gameplayStartTime; + auto timePassedNow = timeNow - gameplayStartTime; -void CVCMIServer::threadAnnounceLobby() -{ - setThreadName("announceLobby"); - while(state != EServerState::SHUTDOWN) - { - { - boost::unique_lock myLock(mx); - while(!announceQueue.empty()) - { - announcePack(std::move(announceQueue.front())); - announceQueue.pop_front(); - } + lastTimerUpdateTime = timeNow; - if(acceptor) - { - io->reset(); - io->poll(); - } - } + auto msPassedBefore = std::chrono::duration_cast(timePassedBefore); + auto msPassedNow = std::chrono::duration_cast(timePassedNow); + auto msDelta = msPassedNow - msPassedBefore; - boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); - } + if (msDelta.count()) + gh->tick(msDelta.count()); + networkHandler->createTimer(*this, serverUpdateInterval); } void CVCMIServer::prepareToRestart() { - if(state == EServerState::GAMEPLAY) + if(getState() != EServerState::GAMEPLAY) { - restartGameplay = true; - * si = * gh->gs->initialOpts; - si->seedToBeUsed = si->seedPostInit = 0; - state = EServerState::LOBBY; - if (si->campState) - { - assert(si->campState->currentScenario().has_value()); - campaignMap = si->campState->currentScenario().value_or(CampaignScenarioID(0)); - campaignBonus = si->campState->getBonusID(campaignMap).value_or(-1); - } - // FIXME: dirry hack to make sure old CGameHandler::run is finished - boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); + assert(0); + return; + } + + * si = * gh->gs->initialOpts; + si->seedToBeUsed = si->seedPostInit = 0; + setState(EServerState::LOBBY); + if (si->campState) + { + assert(si->campState->currentScenario().has_value()); + campaignMap = si->campState->currentScenario().value_or(CampaignScenarioID(0)); + campaignBonus = si->campState->getBonusID(campaignMap).value_or(-1); } - for(auto c : connections) - { + for(auto c : activeConnections) c->enterLobbyConnectionMode(); - c->disableStackSendingByID(); - } - boost::unique_lock queueLock(mx); + gh = nullptr; } @@ -298,18 +261,20 @@ bool CVCMIServer::prepareToStartGame() Load::ProgressAccumulator progressTracking; Load::Progress current(1); progressTracking.include(current); - auto currentProgress = std::numeric_limits::max(); - - auto progressTrackingThread = boost::thread([this, &progressTracking, ¤tProgress]() + + auto progressTrackingThread = boost::thread([this, &progressTracking]() { + auto currentProgress = std::numeric_limits::max(); + while(!progressTracking.finished()) { if(progressTracking.get() != currentProgress) { + //FIXME: UNGUARDED MULTITHREADED ACCESS!!! currentProgress = progressTracking.get(); std::unique_ptr loadProgress(new LobbyLoadProgress); loadProgress->progress = currentProgress; - addToAnnounceQueue(std::move(loadProgress)); + announcePack(std::move(loadProgress)); } boost::this_thread::sleep(boost::posix_time::milliseconds(50)); } @@ -318,7 +283,7 @@ bool CVCMIServer::prepareToStartGame() gh = std::make_shared(this); switch(si->mode) { - case StartInfo::CAMPAIGN: + case EStartMode::CAMPAIGN: logNetwork->info("Preparing to start new campaign"); si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(nullptr)); si->fileURI = mi->fileURI; @@ -327,14 +292,14 @@ bool CVCMIServer::prepareToStartGame() gh->init(si.get(), progressTracking); break; - case StartInfo::NEW_GAME: + case EStartMode::NEW_GAME: logNetwork->info("Preparing to start new game"); si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(nullptr)); si->fileURI = mi->fileURI; gh->init(si.get(), progressTracking); break; - case StartInfo::LOAD_GAME: + case EStartMode::LOAD_GAME: logNetwork->info("Preparing to start loaded game"); if(!gh->load(si->mapname)) { @@ -355,154 +320,56 @@ bool CVCMIServer::prepareToStartGame() return true; } -void CVCMIServer::startGameImmidiately() +void CVCMIServer::startGameImmediately() { - for(auto c : connections) + for(auto c : activeConnections) c->enterGameplayConnectionMode(gh->gs); - state = EServerState::GAMEPLAY; + gh->start(si->mode == EStartMode::LOAD_GAME); + setState(EServerState::GAMEPLAY); + lastTimerUpdateTime = gameplayStartTime = std::chrono::steady_clock::now(); + onTimer(); } -void CVCMIServer::startAsyncAccept() +void CVCMIServer::onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) { - assert(!upcomingConnection); - assert(acceptor); + logNetwork->error("Network error receiving a pack. Connection has been closed"); -#if BOOST_VERSION >= 107000 // Boost version >= 1.70 - upcomingConnection = std::make_shared(acceptor->get_executor()); -#else - upcomingConnection = std::make_shared(acceptor->get_io_service()); -#endif - acceptor->async_accept(*upcomingConnection, std::bind(&CVCMIServer::connectionAccepted, this, _1)); -} + std::shared_ptr c = findConnection(connection); + vstd::erase(activeConnections, c); -void CVCMIServer::connectionAccepted(const boost::system::error_code & ec) -{ - if(ec) + if(activeConnections.empty() || hostClientId == c->connectionID) { - if(state != EServerState::SHUTDOWN) - logNetwork->info("Something wrong during accepting: %s", ec.message()); + setState(EServerState::SHUTDOWN); return; } - try + if(gh && getState() == EServerState::GAMEPLAY) { - if(state == EServerState::LOBBY || !hangingConnections.empty()) - { - logNetwork->info("We got a new connection! :)"); - auto c = std::make_shared(upcomingConnection, SERVER_NAME, uuid); - upcomingConnection.reset(); - connections.insert(c); - c->handler = std::make_shared(&CVCMIServer::threadHandleClient, this, c); - } - } - catch(std::exception & e) - { - logNetwork->error("Failure processing new connection! %s", e.what()); - upcomingConnection.reset(); - } + gh->handleClientDisconnection(c); - startAsyncAccept(); -} - -class CVCMIServerPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) -{ -private: - CVCMIServer & handler; - std::shared_ptr gh; - -public: - CVCMIServerPackVisitor(CVCMIServer & handler, std::shared_ptr gh) - :handler(handler), gh(gh) - { - } - - virtual bool callTyped() override { return false; } - - virtual void visitForLobby(CPackForLobby & packForLobby) override - { - handler.handleReceivedPack(std::unique_ptr(&packForLobby)); - } - - virtual void visitForServer(CPackForServer & serverPack) override - { - if (gh) - gh->handleReceivedPack(&serverPack); - else - logNetwork->error("Received pack for game server while in lobby!"); - } - - virtual void visitForClient(CPackForClient & clientPack) override - { - } -}; - -void CVCMIServer::threadHandleClient(std::shared_ptr c) -{ - setThreadName("handleClient"); - c->enterLobbyConnectionMode(); - - while(c->connected) - { - CPack * pack; - - try - { - pack = c->retrievePack(); - pack->c = c; - } - catch(boost::system::system_error & e) - { - if (e.code() == boost::asio::error::eof) - logNetwork->error("Network error receiving a pack. Connection has been closed"); - else - logNetwork->error("Network error receiving a pack. Connection %s dies. What happened: %s", c->toString(), e.what()); - - hangingConnections.insert(c); - connections.erase(c); - if(connections.empty() || hostClient == c) - state = EServerState::SHUTDOWN; - - if(gh && state == EServerState::GAMEPLAY) - { - gh->handleClientDisconnection(c); - } - break; - } - - CVCMIServerPackVisitor visitor(*this, this->gh); - pack->visit(visitor); - } - - boost::unique_lock queueLock(mx); - - if(c->connected) - { auto lcd = std::make_unique(); lcd->c = c; lcd->clientId = c->connectionID; handleReceivedPack(std::move(lcd)); } - - logNetwork->info("Thread listening for %s ended", c->toString()); - c->handler.reset(); } void CVCMIServer::handleReceivedPack(std::unique_ptr pack) { CBaseForServerApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack.get())); if(apply->applyOnServerBefore(this, pack.get())) - addToAnnounceQueue(std::move(pack)); + announcePack(std::move(pack)); } void CVCMIServer::announcePack(std::unique_ptr pack) { - for(auto c : connections) + for(auto c : activeConnections) { // FIXME: we need to avoid sending something to client that not yet get answer for LobbyClientConnected // Until UUID set we only pass LobbyClientConnected to this client - if(c->uuid == uuid && !dynamic_cast(pack.get())) - continue; + //if(c->uuid == uuid && !dynamic_cast(pack.get())) + // continue; c->sendPack(pack.get()); } @@ -515,7 +382,7 @@ void CVCMIServer::announceMessage(const std::string & txt) logNetwork->info("Show message: %s", txt); auto cm = std::make_unique(); cm->message = txt; - addToAnnounceQueue(std::move(cm)); + announcePack(std::move(cm)); } void CVCMIServer::announceTxt(const std::string & txt, const std::string & playerName) @@ -524,25 +391,18 @@ void CVCMIServer::announceTxt(const std::string & txt, const std::string & playe auto cm = std::make_unique(); cm->playerName = playerName; cm->message = txt; - addToAnnounceQueue(std::move(cm)); -} - -void CVCMIServer::addToAnnounceQueue(std::unique_ptr pack) -{ - boost::unique_lock queueLock(mx); - announceQueue.push_back(std::move(pack)); + announcePack(std::move(cm)); } bool CVCMIServer::passHost(int toConnectionId) { - for(auto c : connections) + for(auto c : activeConnections) { if(isClientHost(c->connectionID)) continue; if(c->connectionID != toConnectionId) continue; - hostClient = c; hostClientId = c->connectionID; announceTxt(boost::str(boost::format("Pass host to connection %d") % toConnectionId)); return true; @@ -550,41 +410,38 @@ bool CVCMIServer::passHost(int toConnectionId) return false; } -void CVCMIServer::clientConnected(std::shared_ptr c, std::vector & names, std::string uuid, StartInfo::EMode mode) +void CVCMIServer::clientConnected(std::shared_ptr c, std::vector & names, const std::string & uuid, EStartMode mode) { - if(state == EServerState::LOBBY) - c->connectionID = currentClientId++; + assert(getState() == EServerState::LOBBY); - if(!hostClient) + c->connectionID = currentClientId++; + + if(hostClientId == -1) { - hostClient = c; hostClientId = c->connectionID; si->mode = mode; } logNetwork->info("Connection with client %d established. UUID: %s", c->connectionID, c->uuid); - if(state == EServerState::LOBBY) + for(auto & name : names) { - for(auto & name : names) + logNetwork->info("Client %d player: %s", c->connectionID, name); + ui8 id = currentPlayerId++; + + ClientPlayer cp; + cp.connection = c->connectionID; + cp.name = name; + playerNames.insert(std::make_pair(id, cp)); + announceTxt(boost::str(boost::format("%s (pid %d cid %d) joins the game") % name % id % c->connectionID)); + + //put new player in first slot with AI + for(auto & elem : si->playerInfos) { - logNetwork->info("Client %d player: %s", c->connectionID, name); - ui8 id = currentPlayerId++; - - ClientPlayer cp; - cp.connection = c->connectionID; - cp.name = name; - playerNames.insert(std::make_pair(id, cp)); - announceTxt(boost::str(boost::format("%s (pid %d cid %d) joins the game") % name % id % c->connectionID)); - - //put new player in first slot with AI - for(auto & elem : si->playerInfos) + if(elem.second.isControlledByAI() && !elem.second.compOnly) { - if(elem.second.isControlledByAI() && !elem.second.compOnly) - { - setPlayerConnectedId(elem.second, id); - break; - } + setPlayerConnectedId(elem.second, id); + break; } } } @@ -592,47 +449,43 @@ void CVCMIServer::clientConnected(std::shared_ptr c, std::vector c) { - connections -= c; - if(connections.empty() || hostClient == c) - { - state = EServerState::SHUTDOWN; - return; - } - - PlayerReinitInterface startAiPack; - startAiPack.playerConnectionId = PlayerSettings::PLAYER_AI; - - for(auto it = playerNames.begin(); it != playerNames.end();) - { - if(it->second.connection != c->connectionID) - { - ++it; - continue; - } + c->getConnection()->close(); + vstd::erase(activeConnections, c); - int id = it->first; - std::string playerLeftMsgText = boost::str(boost::format("%s (pid %d cid %d) left the game") % id % playerNames[id].name % c->connectionID); - announceTxt(playerLeftMsgText); //send lobby text, it will be ignored for non-lobby clients - auto * playerSettings = si->getPlayersSettings(id); - if(!playerSettings) - { - ++it; - continue; - } - - it = playerNames.erase(it); - setPlayerConnectedId(*playerSettings, PlayerSettings::PLAYER_AI); - - if(gh && si && state == EServerState::GAMEPLAY) - { - gh->playerMessages->broadcastMessage(playerSettings->color, playerLeftMsgText); - gh->connections[playerSettings->color].insert(hostClient); - startAiPack.players.push_back(playerSettings->color); - } - } - - if(!startAiPack.players.empty()) - gh->sendAndApply(&startAiPack); +// PlayerReinitInterface startAiPack; +// startAiPack.playerConnectionId = PlayerSettings::PLAYER_AI; +// +// for(auto it = playerNames.begin(); it != playerNames.end();) +// { +// if(it->second.connection != c->connectionID) +// { +// ++it; +// continue; +// } +// +// int id = it->first; +// std::string playerLeftMsgText = boost::str(boost::format("%s (pid %d cid %d) left the game") % id % playerNames[id].name % c->connectionID); +// announceTxt(playerLeftMsgText); //send lobby text, it will be ignored for non-lobby clients +// auto * playerSettings = si->getPlayersSettings(id); +// if(!playerSettings) +// { +// ++it; +// continue; +// } +// +// it = playerNames.erase(it); +// setPlayerConnectedId(*playerSettings, PlayerSettings::PLAYER_AI); +// +// if(gh && si && state == EServerState::GAMEPLAY) +// { +// gh->playerMessages->broadcastMessage(playerSettings->color, playerLeftMsgText); +// // gh->connections[playerSettings->color].insert(hostClient); +// startAiPack.players.push_back(playerSettings->color); +// } +// } +// +// if(!startAiPack.players.empty()) +// gh->sendAndApply(&startAiPack); } void CVCMIServer::reconnectPlayer(int connId) @@ -640,7 +493,7 @@ void CVCMIServer::reconnectPlayer(int connId) PlayerReinitInterface startAiPack; startAiPack.playerConnectionId = connId; - if(gh && si && state == EServerState::GAMEPLAY) + if(gh && si && getState() == EServerState::GAMEPLAY) { for(auto it = playerNames.begin(); it != playerNames.end(); ++it) { @@ -686,7 +539,7 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr mapInfo, if(mi->scenarioOptionsOfSave) { si = CMemorySerializer::deepCopy(*mi->scenarioOptionsOfSave); - si->mode = StartInfo::LOAD_GAME; + si->mode = EStartMode::LOAD_GAME; if(si->campState) campaignMap = si->campState->currentScenario().value(); @@ -702,7 +555,7 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr mapInfo, } } } - else if(si->mode == StartInfo::NEW_GAME || si->mode == StartInfo::CAMPAIGN) + else if(si->mode == EStartMode::NEW_GAME || si->mode == EStartMode::CAMPAIGN) { if(mi->campaign) return; @@ -753,10 +606,9 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr mapInfo, void CVCMIServer::updateAndPropagateLobbyState() { - boost::unique_lock stateLock(stateMutex); // Update player settings for RMG // TODO: find appropriate location for this code - if(si->mapGenOptions && si->mode == StartInfo::NEW_GAME) + if(si->mapGenOptions && si->mode == EStartMode::NEW_GAME) { for(const auto & psetPair : si->playerInfos) { @@ -772,7 +624,7 @@ void CVCMIServer::updateAndPropagateLobbyState() auto lus = std::make_unique(); lus->state = *this; - addToAnnounceQueue(std::move(lus)); + announcePack(std::move(lus)); } void CVCMIServer::setPlayer(PlayerColor clickedColor) @@ -1090,169 +942,10 @@ ui8 CVCMIServer::getIdOfFirstUnallocatedPlayer() const if(!si->getPlayersSettings(i->first)) return i->first; } - return 0; } -static void handleCommandOptions(int argc, const char * argv[], boost::program_options::variables_map & options) +INetworkHandler & CVCMIServer::getNetworkHandler() { - namespace po = boost::program_options; - po::options_description opts("Allowed options"); - opts.add_options() - ("help,h", "display help and exit") - ("version,v", "display version information and exit") - ("run-by-client", "indicate that server launched by client on same machine") - ("uuid", po::value(), "") - ("port", po::value(), "port at which server will listen to connections from client") - ("lobby", po::value(), "address to remote lobby") - ("lobby-port", po::value(), "port at which server connect to remote lobby") - ("lobby-uuid", po::value(), "") - ("connections", po::value(), "amount of connections to remote lobby"); - - if(argc > 1) - { - try - { - po::store(po::parse_command_line(argc, argv, opts), options); - } - catch(boost::program_options::error & e) - { - std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; - } - } - -#ifdef SINGLE_PROCESS_APP - options.emplace("run-by-client", po::variable_value{true, true}); -#endif - - po::notify(options); - -#ifndef SINGLE_PROCESS_APP - if(options.count("help")) - { - auto time = std::time(nullptr); - printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); - printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); - printf("This is free software; see the source for copying conditions. There is NO\n"); - printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); - printf("\n"); - std::cout << opts; - exit(0); - } - - if(options.count("version")) - { - printf("%s\n", GameConstants::VCMI_VERSION.c_str()); - std::cout << VCMIDirs::get().genHelpString(); - exit(0); - } -#endif + return *networkHandler; } - -#ifdef SINGLE_PROCESS_APP -#define main server_main -#endif - -#if VCMI_ANDROID_DUAL_PROCESS -void CVCMIServer::create() -{ - const int argc = 1; - const char * argv[argc] = { "android-server" }; -#else -int main(int argc, const char * argv[]) -{ -#endif - -#if !defined(VCMI_ANDROID) && !defined(SINGLE_PROCESS_APP) - // Correct working dir executable folder (not bundle folder) so we can use executable relative paths - boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path()); -#endif - -#ifndef VCMI_IOS - console = new CConsoleHandler(); -#endif - CBasicLogConfigurator logConfig(VCMIDirs::get().userLogsPath() / "VCMI_Server_log.txt", console); - logConfig.configureDefault(); - logGlobal->info(SERVER_NAME); - - boost::program_options::variables_map opts; - handleCommandOptions(argc, argv, opts); - preinitDLL(console, false); - logConfig.configure(); - - loadDLLClasses(); - srand((ui32)time(nullptr)); - -#ifdef SINGLE_PROCESS_APP - boost::condition_variable * cond = reinterpret_cast(const_cast(argv[0])); - cond->notify_one(); -#endif - - try - { - boost::asio::io_service io_service; - CVCMIServer server(opts); - - try - { - while(server.getState() != EServerState::SHUTDOWN) - { - server.run(); - } - io_service.run(); - } - catch(boost::system::system_error & e) //for boost errors just log, not crash - probably client shut down connection - { - logNetwork->error(e.what()); - server.setState(EServerState::SHUTDOWN); - } - } - catch(boost::system::system_error & e) - { - logNetwork->error(e.what()); - //catch any startup errors (e.g. can't access port) errors - //and return non-zero status so client can detect error - throw; - } -#if VCMI_ANDROID_DUAL_PROCESS - CAndroidVMHelper envHelper; - envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "killServer"); -#endif - logConfig.deconfigure(); - vstd::clear_pointer(VLC); - -#if !VCMI_ANDROID_DUAL_PROCESS - return 0; -#endif -} - -#if VCMI_ANDROID_DUAL_PROCESS -extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_createServer(JNIEnv * env, jclass cls) -{ - __android_log_write(ANDROID_LOG_INFO, "VCMI", "Got jni call to init server"); - CAndroidVMHelper::cacheVM(env); - - CVCMIServer::create(); -} - -extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_initClassloader(JNIEnv * baseEnv, jclass cls) -{ - CAndroidVMHelper::initClassloader(baseEnv); -} -#elif defined(SINGLE_PROCESS_APP) -void CVCMIServer::create(boost::condition_variable * cond, const std::vector & args) -{ - std::vector argv = {cond}; - for(auto & a : args) - argv.push_back(a.c_str()); - main(argv.size(), reinterpret_cast(&*argv.begin())); -} - -#ifdef VCMI_ANDROID -void CVCMIServer::reuseClientJNIEnv(void * jniEnv) -{ - CAndroidVMHelper::initClassloader(jniEnv); - CAndroidVMHelper::alwaysUseLoadedClass = true; -} -#endif // VCMI_ANDROID -#endif // VCMI_ANDROID_DUAL_PROCESS diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index 1bcac979a..63da18f76 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -9,10 +9,10 @@ */ #pragma once -#include "../lib/serializer/Connection.h" +#include "../lib/network/NetworkInterface.h" #include "../lib/StartInfo.h" -#include +#include #if defined(VCMI_ANDROID) && !defined(SINGLE_PROCESS_APP) #define VCMI_ANDROID_DUAL_PROCESS 1 @@ -24,6 +24,7 @@ class CMapInfo; struct CPackForLobby; +class CConnection; struct StartInfo; struct LobbyInfo; struct PlayerSettings; @@ -36,73 +37,79 @@ VCMI_LIB_NAMESPACE_END class CGameHandler; class CBaseForServerApply; class CBaseForGHApply; +class GlobalLobbyProcessor; enum class EServerState : ui8 { LOBBY, - GAMEPLAY_STARTING, GAMEPLAY, - GAMEPLAY_ENDED, SHUTDOWN }; -class CVCMIServer : public LobbyInfo +class CVCMIServer : public LobbyInfo, public INetworkServerListener, public INetworkTimerListener { - std::atomic restartGameplay; // FIXME: this is just a hack - std::shared_ptr io; - std::shared_ptr acceptor; - std::shared_ptr upcomingConnection; - std::list> announceQueue; - boost::recursive_mutex mx; + /// Network server instance that receives and processes incoming connections on active socket + std::unique_ptr networkServer; + std::unique_ptr lobbyProcessor; + + std::chrono::steady_clock::time_point gameplayStartTime; + std::chrono::steady_clock::time_point lastTimerUpdateTime; + + std::unique_ptr networkHandler; + std::shared_ptr> applier; - std::unique_ptr announceLobbyThread; - std::unique_ptr remoteConnectionsThread; - std::atomic state; + EServerState state = EServerState::LOBBY; + + std::shared_ptr findConnection(const std::shared_ptr &); + + int currentClientId; + ui8 currentPlayerId; public: - std::shared_ptr gh; - ui16 port; + /// List of all active connections + std::vector> activeConnections; + // INetworkListener impl + void onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) override; + void onPacketReceived(const std::shared_ptr & connection, const std::vector & message) override; + void onNewConnection(const std::shared_ptr &) override; + void onTimer() override; + + std::shared_ptr gh; boost::program_options::variables_map cmdLineOptions; - std::set> connections; - std::set> remoteConnections; - std::set> hangingConnections; //keep connections of players disconnected during the game - - std::atomic currentClientId; - std::atomic currentPlayerId; - std::shared_ptr hostClient; CVCMIServer(boost::program_options::variables_map & opts); ~CVCMIServer(); + void run(); + bool prepareToStartGame(); void prepareToRestart(); - void startGameImmidiately(); + void startGameImmediately(); + void startAcceptingIncomingConnections(); - void establishRemoteConnections(); - void connectToRemote(); - void startAsyncAccept(); - void connectionAccepted(const boost::system::error_code & ec); void threadHandleClient(std::shared_ptr c); - void threadAnnounceLobby(); - void handleReceivedPack(std::unique_ptr pack); void announcePack(std::unique_ptr pack); bool passHost(int toConnectionId); void announceTxt(const std::string & txt, const std::string & playerName = "system"); - void announceMessage(const std::string & txt); - void addToAnnounceQueue(std::unique_ptr pack); void setPlayerConnectedId(PlayerSettings & pset, ui8 player) const; void updateStartInfoOnMapChange(std::shared_ptr mapInfo, std::shared_ptr mapGenOpt = {}); - void clientConnected(std::shared_ptr c, std::vector & names, std::string uuid, StartInfo::EMode mode); + void clientConnected(std::shared_ptr c, std::vector & names, const std::string & uuid, EStartMode mode); void clientDisconnected(std::shared_ptr c); void reconnectPlayer(int connId); + void announceMessage(const std::string & txt); + + void handleReceivedPack(std::unique_ptr pack); + void updateAndPropagateLobbyState(); + INetworkHandler & getNetworkHandler(); + void setState(EServerState value); EServerState getState() const; diff --git a/server/EntryPoint.cpp b/server/EntryPoint.cpp new file mode 100644 index 000000000..145027801 --- /dev/null +++ b/server/EntryPoint.cpp @@ -0,0 +1,170 @@ +/* + * EntryPoint.cpp, 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 + * + */ +#include "StdInc.h" + +#include "CVCMIServer.h" + +#include "../lib/CConsoleHandler.h" +#include "../lib/logging/CBasicLogConfigurator.h" +#include "../lib/VCMIDirs.h" +#include "../lib/VCMI_Lib.h" + +#ifdef VCMI_ANDROID +#include +#include +#include "lib/CAndroidVMHelper.h" +#endif + +#include + +const std::string SERVER_NAME_AFFIX = "server"; +const std::string SERVER_NAME = GameConstants::VCMI_VERSION + std::string(" (") + SERVER_NAME_AFFIX + ')'; + +static void handleCommandOptions(int argc, const char * argv[], boost::program_options::variables_map & options) +{ + namespace po = boost::program_options; + po::options_description opts("Allowed options"); + opts.add_options() + ("help,h", "display help and exit") + ("version,v", "display version information and exit") + ("run-by-client", "indicate that server launched by client on same machine") + ("port", po::value(), "port at which server will listen to connections from client") + ("lobby", "start server in lobby mode in which server connects to a global lobby"); + + if(argc > 1) + { + try + { + po::store(po::parse_command_line(argc, argv, opts), options); + } + catch(boost::program_options::error & e) + { + std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; + } + } + +#ifdef SINGLE_PROCESS_APP + options.emplace("run-by-client", po::variable_value{true, true}); +#endif + + po::notify(options); + +#ifndef SINGLE_PROCESS_APP + if(options.count("help")) + { + auto time = std::time(nullptr); + printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); + printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); + printf("This is free software; see the source for copying conditions. There is NO\n"); + printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); + printf("\n"); + std::cout << opts; + exit(0); + } + + if(options.count("version")) + { + printf("%s\n", GameConstants::VCMI_VERSION.c_str()); + std::cout << VCMIDirs::get().genHelpString(); + exit(0); + } +#endif +} + +#ifdef SINGLE_PROCESS_APP +#define main server_main +#endif + +#if VCMI_ANDROID_DUAL_PROCESS +void CVCMIServer::create() +{ + const int argc = 1; + const char * argv[argc] = { "android-server" }; +#else +int main(int argc, const char * argv[]) +{ +#endif + +#if !defined(VCMI_ANDROID) && !defined(SINGLE_PROCESS_APP) + // Correct working dir executable folder (not bundle folder) so we can use executable relative paths + boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path()); +#endif + +#ifndef VCMI_IOS + console = new CConsoleHandler(); +#endif + CBasicLogConfigurator logConfig(VCMIDirs::get().userLogsPath() / "VCMI_Server_log.txt", console); + logConfig.configureDefault(); + logGlobal->info(SERVER_NAME); + + boost::program_options::variables_map opts; + handleCommandOptions(argc, argv, opts); + preinitDLL(console, false); + logConfig.configure(); + + loadDLLClasses(); + std::srand(static_cast(time(nullptr))); + + { + CVCMIServer server(opts); + +#ifdef SINGLE_PROCESS_APP + boost::condition_variable * cond = reinterpret_cast(const_cast(argv[0])); + cond->notify_one(); +#endif + + server.run(); + + // CVCMIServer destructor must be called here - before VLC cleanup + } + + +#if VCMI_ANDROID_DUAL_PROCESS + CAndroidVMHelper envHelper; + envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "killServer"); +#endif + logConfig.deconfigure(); + vstd::clear_pointer(VLC); + +#if !VCMI_ANDROID_DUAL_PROCESS + return 0; +#endif +} + +#if VCMI_ANDROID_DUAL_PROCESS +extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_createServer(JNIEnv * env, jclass cls) +{ + __android_log_write(ANDROID_LOG_INFO, "VCMI", "Got jni call to init server"); + CAndroidVMHelper::cacheVM(env); + + CVCMIServer::create(); +} + +extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_initClassloader(JNIEnv * baseEnv, jclass cls) +{ + CAndroidVMHelper::initClassloader(baseEnv); +} +#elif defined(SINGLE_PROCESS_APP) +void CVCMIServer::create(boost::condition_variable * cond, const std::vector & args) +{ + std::vector argv = {cond}; + for(auto & a : args) + argv.push_back(a.c_str()); + main(argv.size(), reinterpret_cast(&*argv.begin())); +} + +#ifdef VCMI_ANDROID +void CVCMIServer::reuseClientJNIEnv(void * jniEnv) +{ + CAndroidVMHelper::initClassloader(jniEnv); + CAndroidVMHelper::alwaysUseLoadedClass = true; +} +#endif // VCMI_ANDROID +#endif // VCMI_ANDROID_DUAL_PROCESS diff --git a/server/GlobalLobbyProcessor.cpp b/server/GlobalLobbyProcessor.cpp new file mode 100644 index 000000000..df42d9fb4 --- /dev/null +++ b/server/GlobalLobbyProcessor.cpp @@ -0,0 +1,145 @@ +/* + * GlobalLobbyProcessor.cpp, 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 + * + */ +#include "StdInc.h" +#include "GlobalLobbyProcessor.h" + +#include "CVCMIServer.h" +#include "../lib/CConfigHandler.h" + +GlobalLobbyProcessor::GlobalLobbyProcessor(CVCMIServer & owner) + : owner(owner) +{ + logGlobal->info("Connecting to lobby server"); + establishNewConnection(); +} + +void GlobalLobbyProcessor::establishNewConnection() +{ + std::string hostname = settings["lobby"]["hostname"].String(); + int16_t port = settings["lobby"]["port"].Integer(); + owner.getNetworkHandler().connectToRemote(*this, hostname, port); +} + +void GlobalLobbyProcessor::onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) +{ + if (connection == controlConnection) + { + owner.setState(EServerState::SHUTDOWN); + return; + } + else + { + if (owner.getState() == EServerState::LOBBY) + { + for (auto const & proxy : proxyConnections) + { + if (proxy.second == connection) + { + JsonNode message; + message["type"].String() = "leaveGameRoom"; + message["accountID"].String() = proxy.first; + controlConnection->sendPacket(message.toBytes(true)); + break; + } + } + } + + // player disconnected + owner.onDisconnected(connection, errorMessage); + } +} + +void GlobalLobbyProcessor::onPacketReceived(const std::shared_ptr & connection, const std::vector & message) +{ + if (connection == controlConnection) + { + JsonNode json(message.data(), message.size()); + + if(json["type"].String() == "operationFailed") + return receiveOperationFailed(json); + + if(json["type"].String() == "loginSuccess") + return receiveLoginSuccess(json); + + if(json["type"].String() == "accountJoinsRoom") + return receiveAccountJoinsRoom(json); + + logGlobal->error("Received unexpected message from lobby server of type '%s' ", json["type"].String()); + } + else + { + // received game message via proxy connection + owner.onPacketReceived(connection, message); + } +} + +void GlobalLobbyProcessor::receiveOperationFailed(const JsonNode & json) +{ + logGlobal->info("Lobby: Failed to login into a lobby server!"); + + owner.setState(EServerState::SHUTDOWN); +} + +void GlobalLobbyProcessor::receiveLoginSuccess(const JsonNode & json) +{ + // no-op, wait just for any new commands from lobby + logGlobal->info("Lobby: Succesfully connected to lobby server"); + owner.startAcceptingIncomingConnections(); +} + +void GlobalLobbyProcessor::receiveAccountJoinsRoom(const JsonNode & json) +{ + std::string accountID = json["accountID"].String(); + + logGlobal->info("Lobby: Account %s will join our room!", accountID); + assert(proxyConnections.count(accountID) == 0); + + proxyConnections[accountID] = nullptr; + establishNewConnection(); +} + +void GlobalLobbyProcessor::onConnectionFailed(const std::string & errorMessage) +{ + owner.setState(EServerState::SHUTDOWN); +} + +void GlobalLobbyProcessor::onConnectionEstablished(const std::shared_ptr & connection) +{ + if (controlConnection == nullptr) + { + controlConnection = connection; + logGlobal->info("Connection to lobby server established"); + + JsonNode toSend; + toSend["type"].String() = "serverLogin"; + toSend["gameRoomID"].String() = owner.uuid; + toSend["accountID"] = settings["lobby"]["accountID"]; + toSend["accountCookie"] = settings["lobby"]["accountCookie"]; + connection->sendPacket(toSend.toBytes(true)); + } + else + { + // Proxy connection for a player + std::string guestAccountID; + for (auto const & proxies : proxyConnections) + if (proxies.second == nullptr) + guestAccountID = proxies.first; + + JsonNode toSend; + toSend["type"].String() = "serverProxyLogin"; + toSend["gameRoomID"].String() = owner.uuid; + toSend["guestAccountID"].String() = guestAccountID; + toSend["accountCookie"] = settings["lobby"]["accountCookie"]; + connection->sendPacket(toSend.toBytes(true)); + + proxyConnections[guestAccountID] = connection; + owner.onNewConnection(connection); + } +} diff --git a/server/GlobalLobbyProcessor.h b/server/GlobalLobbyProcessor.h new file mode 100644 index 000000000..4c81cc70c --- /dev/null +++ b/server/GlobalLobbyProcessor.h @@ -0,0 +1,39 @@ +/* + * GlobalLobbyProcessor.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 "../lib/network/NetworkInterface.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class CVCMIServer; + +class GlobalLobbyProcessor : public INetworkClientListener +{ + CVCMIServer & owner; + + NetworkConnectionPtr controlConnection; + std::map proxyConnections; + + void onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) override; + void onPacketReceived(const std::shared_ptr & connection, const std::vector & message) override; + void onConnectionFailed(const std::string & errorMessage) override; + void onConnectionEstablished(const std::shared_ptr &) override; + + void receiveOperationFailed(const JsonNode & json); + void receiveLoginSuccess(const JsonNode & json); + void receiveAccountJoinsRoom(const JsonNode & json); + + void establishNewConnection(); +public: + explicit GlobalLobbyProcessor(CVCMIServer & owner); +}; diff --git a/server/LobbyNetPackVisitors.h b/server/LobbyNetPackVisitors.h index 6304f561a..ed0a566bd 100644 --- a/server/LobbyNetPackVisitors.h +++ b/server/LobbyNetPackVisitors.h @@ -28,15 +28,16 @@ public: return result; } - virtual void visitForLobby(CPackForLobby & pack) override; - virtual void visitLobbyClientConnected(LobbyClientConnected & pack) override; - virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; - virtual void visitLobbyEndGame(LobbyEndGame & pack) override; - virtual void visitLobbyStartGame(LobbyStartGame & pack) override; - virtual void visitLobbyChangeHost(LobbyChangeHost & pack) override; - virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override; - virtual void visitLobbyChatMessage(LobbyChatMessage & pack) override; - virtual void visitLobbyGuiAction(LobbyGuiAction & pack) override; + void visitForLobby(CPackForLobby & pack) override; + void visitLobbyClientConnected(LobbyClientConnected & pack) override; + void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; + void visitLobbyRestartGame(LobbyRestartGame & pack) override; + void visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack) override; + void visitLobbyStartGame(LobbyStartGame & pack) override; + void visitLobbyChangeHost(LobbyChangeHost & pack) override; + void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override; + void visitLobbyChatMessage(LobbyChatMessage & pack) override; + void visitLobbyGuiAction(LobbyGuiAction & pack) override; }; class ApplyOnServerAfterAnnounceNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) @@ -50,12 +51,12 @@ public: { } - virtual void visitForLobby(CPackForLobby & pack) override; - virtual void visitLobbyClientConnected(LobbyClientConnected & pack) override; - virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; - virtual void visitLobbyEndGame(LobbyEndGame & pack) override; - virtual void visitLobbyStartGame(LobbyStartGame & pack) override; - virtual void visitLobbyChangeHost(LobbyChangeHost & pack) override; + void visitForLobby(CPackForLobby & pack) override; + void visitLobbyClientConnected(LobbyClientConnected & pack) override; + void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; + void visitLobbyRestartGame(LobbyRestartGame & pack) override; + void visitLobbyStartGame(LobbyStartGame & pack) override; + void visitLobbyChangeHost(LobbyChangeHost & pack) override; }; class ApplyOnServerNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) @@ -75,21 +76,21 @@ public: return result; } - virtual void visitLobbyClientConnected(LobbyClientConnected & pack) override; - virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; - virtual void visitLobbySetMap(LobbySetMap & pack) override; - virtual void visitLobbySetCampaign(LobbySetCampaign & pack) override; - virtual void visitLobbySetCampaignMap(LobbySetCampaignMap & pack) override; - virtual void visitLobbySetCampaignBonus(LobbySetCampaignBonus & pack) override; - virtual void visitLobbyEndGame(LobbyEndGame & pack) override; - virtual void visitLobbyStartGame(LobbyStartGame & pack) override; - virtual void visitLobbyChangeHost(LobbyChangeHost & pack) override; - virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override; - virtual void visitLobbySetPlayer(LobbySetPlayer & pack) override; - virtual void visitLobbySetPlayerName(LobbySetPlayerName & pack) override; - virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) override; - virtual void visitLobbySetExtraOptions(LobbySetExtraOptions & pack) override; - virtual void visitLobbySetSimturns(LobbySetSimturns & pack) override; - virtual void visitLobbySetDifficulty(LobbySetDifficulty & pack) override; - virtual void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) override; + void visitLobbyClientConnected(LobbyClientConnected & pack) override; + void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; + void visitLobbySetMap(LobbySetMap & pack) override; + void visitLobbySetCampaign(LobbySetCampaign & pack) override; + void visitLobbySetCampaignMap(LobbySetCampaignMap & pack) override; + void visitLobbySetCampaignBonus(LobbySetCampaignBonus & pack) override; + void visitLobbyRestartGame(LobbyRestartGame & pack) override; + void visitLobbyStartGame(LobbyStartGame & pack) override; + void visitLobbyChangeHost(LobbyChangeHost & pack) override; + void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override; + void visitLobbySetPlayer(LobbySetPlayer & pack) override; + void visitLobbySetPlayerName(LobbySetPlayerName & pack) override; + void visitLobbySetTurnTime(LobbySetTurnTime & pack) override; + void visitLobbySetExtraOptions(LobbySetExtraOptions & pack) override; + void visitLobbySetSimturns(LobbySetSimturns & pack) override; + void visitLobbySetDifficulty(LobbySetDifficulty & pack) override; + void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) override; }; diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index 2df75fd98..d312befdd 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -13,13 +13,11 @@ #include "CVCMIServer.h" #include "CGameHandler.h" -#include "../lib/serializer/Connection.h" #include "../lib/StartInfo.h" #include "../lib/CRandomGenerator.h" - -// Campaigns #include "../lib/campaign/CampaignState.h" +#include "../lib/serializer/Connection.h" void ClientPermissionsCheckerNetPackVisitor::visitForLobby(CPackForLobby & pack) { @@ -40,67 +38,11 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitForLobby(CPackForLobby & pac void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack) { - if(srv.gh) - { - for(auto & connection : srv.hangingConnections) - { - if(connection->uuid == pack.uuid) - { - { - result = true; - return; - } - } - } - } - - if(srv.getState() == EServerState::LOBBY) - { - result = true; - return; - } - - //disconnect immediately and ignore this client - srv.connections.erase(pack.c); - if(pack.c && pack.c->isOpen()) - { - pack.c->close(); - pack.c->connected = false; - } - { - result = false; - return; - } + result = srv.getState() == EServerState::LOBBY; } void ApplyOnServerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack) { - if(srv.gh) - { - for(auto & connection : srv.hangingConnections) - { - if(connection->uuid == pack.uuid) - { - logNetwork->info("Reconnection player"); - pack.c->connectionID = connection->connectionID; - for(auto & playerConnection : srv.gh->connections) - { - for(auto & existingConnection : playerConnection.second) - { - if(existingConnection == connection) - { - playerConnection.second.erase(existingConnection); - playerConnection.second.insert(pack.c); - break; - } - } - } - srv.hangingConnections.erase(connection); - break; - } - } - } - srv.clientConnected(pack.c, pack.names, pack.uuid, pack.mode); // Server need to pass some data to newly connected client pack.clientId = pack.c->connectionID; @@ -116,15 +58,17 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientConnected(LobbyCl // Until UUID set we only pass LobbyClientConnected to this client pack.c->uuid = pack.uuid; srv.updateAndPropagateLobbyState(); - if(srv.getState() == EServerState::GAMEPLAY) - { - //immediately start game - std::unique_ptr startGameForReconnectedPlayer(new LobbyStartGame); - startGameForReconnectedPlayer->initializedStartInfo = srv.si; - startGameForReconnectedPlayer->initializedGameState = srv.gh->gameState(); - startGameForReconnectedPlayer->clientId = pack.c->connectionID; - srv.addToAnnounceQueue(std::move(startGameForReconnectedPlayer)); - } + +// FIXME: what is this??? We do NOT support reconnection into ongoing game - at the very least queries and battles are NOT serialized +// if(srv.getState() == EServerState::GAMEPLAY) +// { +// //immediately start game +// std::unique_ptr startGameForReconnectedPlayer(new LobbyStartGame); +// startGameForReconnectedPlayer->initializedStartInfo = srv.si; +// startGameForReconnectedPlayer->initializedGameState = srv.gh->gameState(); +// startGameForReconnectedPlayer->clientId = pack.c->connectionID; +// srv.announcePack(std::move(startGameForReconnectedPlayer)); +// } } void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) @@ -143,7 +87,7 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientDisconnected(LobbyC return; } - if(pack.c->uuid != srv.cmdLineOptions["uuid"].as()) + if(pack.c->connectionID != srv.hostClientId) { result = false; return; @@ -156,38 +100,28 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientDisconnected(LobbyC void ApplyOnServerNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) { srv.clientDisconnected(pack.c); - pack.c->close(); - pack.c->connected = false; - result = true; } void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) { - if(pack.c && pack.c->isOpen()) - { - boost::unique_lock lock(*pack.c->mutexWrite); - pack.c->close(); - pack.c->connected = false; - } - if(pack.shutdownServer) { logNetwork->info("Client requested shutdown, server will close itself..."); srv.setState(EServerState::SHUTDOWN); return; } - else if(srv.connections.empty()) + else if(srv.activeConnections.empty()) { logNetwork->error("Last connection lost, server will close itself..."); srv.setState(EServerState::SHUTDOWN); } - else if(pack.c == srv.hostClient) + else if(pack.c->connectionID == srv.hostClientId) { auto ph = std::make_unique(); - auto newHost = *RandomGeneratorUtil::nextItem(srv.connections, CRandomGenerator::getDefault()); + auto newHost = srv.activeConnections.front(); ph->newHostConnectionId = newHost->connectionID; - srv.addToAnnounceQueue(std::move(ph)); + srv.announcePack(std::move(ph)); } srv.updateAndPropagateLobbyState(); @@ -219,7 +153,7 @@ void ApplyOnServerNetPackVisitor::visitLobbySetMap(LobbySetMap & pack) void ApplyOnServerNetPackVisitor::visitLobbySetCampaign(LobbySetCampaign & pack) { srv.si->mapname = pack.ourCampaign->getFilename(); - srv.si->mode = StartInfo::CAMPAIGN; + srv.si->mode = EStartMode::CAMPAIGN; srv.si->campState = pack.ourCampaign; srv.si->turnTimerInfo = TurnTimerInfo{}; @@ -258,26 +192,27 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction result = srv.isClientHost(pack.c->connectionID); } -void ClientPermissionsCheckerNetPackVisitor::visitLobbyEndGame(LobbyEndGame & pack) +void ClientPermissionsCheckerNetPackVisitor::visitLobbyRestartGame(LobbyRestartGame & pack) { result = srv.isClientHost(pack.c->connectionID); } -void ApplyOnServerNetPackVisitor::visitLobbyEndGame(LobbyEndGame & pack) +void ApplyOnServerNetPackVisitor::visitLobbyRestartGame(LobbyRestartGame & pack) { srv.prepareToRestart(); result = true; } -void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyEndGame(LobbyEndGame & pack) +void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyRestartGame(LobbyRestartGame & pack) { - boost::unique_lock stateLock(srv.stateMutex); - for(auto & c : srv.connections) - { + for(const auto & c : srv.activeConnections) c->enterLobbyConnectionMode(); - c->disableStackSendingByID(); - } +} + +void ClientPermissionsCheckerNetPackVisitor::visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack) +{ + result = srv.isClientHost(pack.c->connectionID); } void ClientPermissionsCheckerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) @@ -306,18 +241,16 @@ void ApplyOnServerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) pack.initializedStartInfo = std::make_shared(*srv.gh->getStartInfo(true)); pack.initializedGameState = srv.gh->gameState(); - - srv.setState(EServerState::GAMEPLAY_STARTING); result = true; } void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) { if(pack.clientId == -1) //do not restart game for single client only - srv.startGameImmidiately(); + srv.startGameImmediately(); else { - for(auto & c : srv.connections) + for(const auto & c : srv.activeConnections) { if(c->connectionID == pack.clientId) { diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index ffa256225..fc5b297fb 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -20,11 +20,11 @@ #include "../lib/IGameCallback.h" #include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/gameState/CGameState.h" #include "../lib/battle/IBattleState.h" #include "../lib/battle/BattleAction.h" #include "../lib/battle/Unit.h" -#include "../lib/serializer/Connection.h" #include "../lib/spells/CSpellHandler.h" #include "../lib/spells/ISpellMechanics.h" #include "../lib/serializer/Cast.h" diff --git a/server/ServerNetPackVisitors.h b/server/ServerNetPackVisitors.h index 487af47de..c2cc0de42 100644 --- a/server/ServerNetPackVisitors.h +++ b/server/ServerNetPackVisitors.h @@ -28,34 +28,34 @@ public: return result; } - virtual void visitSaveGame(SaveGame & pack) override; - virtual void visitGamePause(GamePause & pack) override; - virtual void visitEndTurn(EndTurn & pack) override; - virtual void visitDismissHero(DismissHero & pack) override; - virtual void visitMoveHero(MoveHero & pack) override; - virtual void visitCastleTeleportHero(CastleTeleportHero & pack) override; - virtual void visitArrangeStacks(ArrangeStacks & pack) override; - virtual void visitBulkMoveArmy(BulkMoveArmy & pack) override; - virtual void visitBulkSplitStack(BulkSplitStack & pack) override; - virtual void visitBulkMergeStacks(BulkMergeStacks & pack) override; - virtual void visitBulkSmartSplitStack(BulkSmartSplitStack & pack) override; - virtual void visitDisbandCreature(DisbandCreature & pack) override; - virtual void visitBuildStructure(BuildStructure & pack) override; - virtual void visitRecruitCreatures(RecruitCreatures & pack) override; - virtual void visitUpgradeCreature(UpgradeCreature & pack) override; - virtual void visitGarrisonHeroSwap(GarrisonHeroSwap & pack) override; - virtual void visitExchangeArtifacts(ExchangeArtifacts & pack) override; - virtual void visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) override; - virtual void visitAssembleArtifacts(AssembleArtifacts & pack) override; - virtual void visitEraseArtifactByClient(EraseArtifactByClient & pack) override; - virtual void visitBuyArtifact(BuyArtifact & pack) override; - virtual void visitTradeOnMarketplace(TradeOnMarketplace & pack) override; - virtual void visitSetFormation(SetFormation & pack) override; - virtual void visitHireHero(HireHero & pack) override; - virtual void visitBuildBoat(BuildBoat & pack) override; - virtual void visitQueryReply(QueryReply & pack) override; - virtual void visitMakeAction(MakeAction & pack) override; - virtual void visitDigWithHero(DigWithHero & pack) override; - virtual void visitCastAdvSpell(CastAdvSpell & pack) override; - virtual void visitPlayerMessage(PlayerMessage & pack) override; + void visitSaveGame(SaveGame & pack) override; + void visitGamePause(GamePause & pack) override; + void visitEndTurn(EndTurn & pack) override; + void visitDismissHero(DismissHero & pack) override; + void visitMoveHero(MoveHero & pack) override; + void visitCastleTeleportHero(CastleTeleportHero & pack) override; + void visitArrangeStacks(ArrangeStacks & pack) override; + void visitBulkMoveArmy(BulkMoveArmy & pack) override; + void visitBulkSplitStack(BulkSplitStack & pack) override; + void visitBulkMergeStacks(BulkMergeStacks & pack) override; + void visitBulkSmartSplitStack(BulkSmartSplitStack & pack) override; + void visitDisbandCreature(DisbandCreature & pack) override; + void visitBuildStructure(BuildStructure & pack) override; + void visitRecruitCreatures(RecruitCreatures & pack) override; + void visitUpgradeCreature(UpgradeCreature & pack) override; + void visitGarrisonHeroSwap(GarrisonHeroSwap & pack) override; + void visitExchangeArtifacts(ExchangeArtifacts & pack) override; + void visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) override; + void visitAssembleArtifacts(AssembleArtifacts & pack) override; + void visitEraseArtifactByClient(EraseArtifactByClient & pack) override; + void visitBuyArtifact(BuyArtifact & pack) override; + void visitTradeOnMarketplace(TradeOnMarketplace & pack) override; + void visitSetFormation(SetFormation & pack) override; + void visitHireHero(HireHero & pack) override; + void visitBuildBoat(BuildBoat & pack) override; + void visitQueryReply(QueryReply & pack) override; + void visitMakeAction(MakeAction & pack) override; + void visitDigWithHero(DigWithHero & pack) override; + void visitCastAdvSpell(CastAdvSpell & pack) override; + void visitPlayerMessage(PlayerMessage & pack) override; }; diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 84a05c740..247ee4804 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -29,7 +29,6 @@ TurnTimerHandler::TurnTimerHandler(CGameHandler & gh): void TurnTimerHandler::onGameplayStart(PlayerColor player) { - std::lock_guard guard(mx); if(const auto * si = gameHandler.getStartInfo()) { timers[player] = si->turnTimerInfo; @@ -45,7 +44,6 @@ void TurnTimerHandler::onGameplayStart(PlayerColor player) void TurnTimerHandler::setTimerEnabled(PlayerColor player, bool enabled) { - std::lock_guard guard(mx); assert(player.isValidPlayer()); timers[player].isActive = enabled; sendTimerUpdate(player); @@ -53,7 +51,6 @@ void TurnTimerHandler::setTimerEnabled(PlayerColor player, bool enabled) void TurnTimerHandler::setEndTurnAllowed(PlayerColor player, bool enabled) { - std::lock_guard guard(mx); assert(player.isValidPlayer()); endTurnAllowed[player] = enabled; } @@ -69,7 +66,6 @@ void TurnTimerHandler::sendTimerUpdate(PlayerColor player) void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) { - std::lock_guard guard(mx); if(const auto * si = gameHandler.getStartInfo()) { if(si->turnTimerInfo.isEnabled()) @@ -87,7 +83,6 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) void TurnTimerHandler::update(int waitTime) { - std::lock_guard guard(mx); if(!gameHandler.getStartInfo()->turnTimerInfo.isEnabled()) return; @@ -122,7 +117,6 @@ bool TurnTimerHandler::timerCountDown(int & timer, int initialTimer, PlayerColor void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) { - std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs || !si->turnTimerInfo.isEnabled()) @@ -164,7 +158,6 @@ bool TurnTimerHandler::isPvpBattle(const BattleID & battleID) const void TurnTimerHandler::onBattleStart(const BattleID & battleID) { - std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs) @@ -192,7 +185,6 @@ void TurnTimerHandler::onBattleStart(const BattleID & battleID) void TurnTimerHandler::onBattleEnd(const BattleID & battleID) { - std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs) @@ -221,7 +213,6 @@ void TurnTimerHandler::onBattleEnd(const BattleID & battleID) void TurnTimerHandler::onBattleNextStack(const BattleID & battleID, const CStack & stack) { - std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs || !gs->getBattle(battleID)) @@ -248,7 +239,6 @@ void TurnTimerHandler::onBattleNextStack(const BattleID & battleID, const CStack void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime) { - std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs) diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index c97eff721..8f6fae5a6 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -29,7 +29,6 @@ class TurnTimerHandler std::map timers; std::map lastUpdate; std::map endTurnAllowed; - std::recursive_mutex mx; void onPlayerMakingTurn(PlayerColor player, int waitTime); void onBattleLoop(const BattleID & battleID, int waitTime); diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index a27be3606..94d585803 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -584,10 +584,10 @@ void BattleFlowProcessor::stackEnchantedTrigger(const CBattleInfoCallback & batt auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); for(auto b : bl) { - const CSpell * sp = b->subtype.as().toSpell(); - if(!sp) + if (!b->subtype.as().hasValue()) continue; + const CSpell * sp = b->subtype.as().toSpell(); const int32_t val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); const int32_t level = ((val > 3) ? (val - 3) : val); diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 4cce9c846..65c18f9e9 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -13,7 +13,6 @@ #include "../CGameHandler.h" #include "../CVCMIServer.h" -#include "../../lib/serializer/Connection.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/modding/IdentifierStorage.h" @@ -22,11 +21,13 @@ #include "../../lib/StartInfo.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/modding/IdentifierStorage.h" #include "../../lib/modding/ModScope.h" #include "../../lib/mapping/CMap.h" #include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/networkPacks/StackLocation.h" +#include "../../lib/serializer/Connection.h" PlayerMessageProcessor::PlayerMessageProcessor() :gameHandler(nullptr) @@ -62,10 +63,7 @@ bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::st std::vector words; boost::split(words, message, boost::is_any_of(" ")); - bool isHost = false; - for(auto & c : gameHandler->connections[player]) - if(gameHandler->gameLobby()->isClientHost(c->connectionID)) - isHost = true; + bool isHost = gameHandler->gameLobby()->isPlayerHost(player); if(!isHost || words.size() < 2 || words[0] != "game") return false; diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 0d762794e..2266036a5 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -233,7 +233,7 @@ void TurnOrderProcessor::doStartNewDay() if(!activePlayer) { - gameHandler->gameLobby()->setState(EServerState::GAMEPLAY_ENDED); + gameHandler->gameLobby()->setState(EServerState::SHUTDOWN); return; } diff --git a/server/queries/BattleQueries.h b/server/queries/BattleQueries.h index 52d8e94ee..871b62e5d 100644 --- a/server/queries/BattleQueries.h +++ b/server/queries/BattleQueries.h @@ -28,10 +28,10 @@ public: CBattleQuery(CGameHandler * owner); CBattleQuery(CGameHandler * owner, const IBattleInfo * Bi); //TODO - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - virtual bool blocksPack(const CPack *pack) const override; - virtual void onRemoval(PlayerColor color) override; - virtual void onExposure(QueryPtr topQuery) override; + void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + bool blocksPack(const CPack *pack) const override; + void onRemoval(PlayerColor color) override; + void onExposure(QueryPtr topQuery) override; }; class CBattleDialogQuery : public CDialogQuery @@ -41,5 +41,5 @@ public: const IBattleInfo * bi; - virtual void onRemoval(PlayerColor color) override; + void onRemoval(PlayerColor color) override; }; diff --git a/server/queries/CQuery.cpp b/server/queries/CQuery.cpp index ad23dcc25..0580b75ed 100644 --- a/server/queries/CQuery.cpp +++ b/server/queries/CQuery.cpp @@ -49,8 +49,6 @@ CQuery::CQuery(CGameHandler * gameHandler) : owner(gameHandler->queries.get()) , gh(gameHandler) { - boost::unique_lock l(QueriesProcessor::mx); - static QueryID QID = QueryID(0); queryID = ++QID; diff --git a/server/queries/CQuery.h b/server/queries/CQuery.h index cf6e18254..5773ecadd 100644 --- a/server/queries/CQuery.h +++ b/server/queries/CQuery.h @@ -68,8 +68,8 @@ class CDialogQuery : public CQuery { public: CDialogQuery(CGameHandler * owner); - virtual bool endsByPlayerAnswer() const override; - virtual bool blocksPack(const CPack *pack) const override; + bool endsByPlayerAnswer() const override; + bool blocksPack(const CPack *pack) const override; void setReply(std::optional reply) override; protected: std::optional answer; diff --git a/server/queries/MapQueries.h b/server/queries/MapQueries.h index 5668dad02..7a6cabf3c 100644 --- a/server/queries/MapQueries.h +++ b/server/queries/MapQueries.h @@ -46,9 +46,9 @@ public: CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance *Obj, const CGHeroInstance *Hero, int3 Tile); - virtual bool blocksPack(const CPack *pack) const override; - virtual void onRemoval(PlayerColor color) override; - virtual void onExposure(QueryPtr topQuery) override; + bool blocksPack(const CPack *pack) const override; + void onRemoval(PlayerColor color) override; + void onExposure(QueryPtr topQuery) override; }; //Created when hero attempts move and something happens @@ -60,11 +60,11 @@ public: bool visitDestAfterVictory; //if hero moved to guarded tile and it should be visited once guard is defeated const CGHeroInstance *hero; - virtual void onExposure(QueryPtr topQuery) override; + void onExposure(QueryPtr topQuery) override; CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory = false); - virtual void onAdding(PlayerColor color) override; - virtual void onRemoval(PlayerColor color) override; + void onAdding(PlayerColor color) override; + void onRemoval(PlayerColor color) override; }; class CGarrisonDialogQuery : public CDialogQuery //used also for hero exchange dialogs @@ -73,8 +73,8 @@ public: std::array exchangingArmies; CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance *up, const CArmedInstance *down); - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - virtual bool blocksPack(const CPack *pack) const override; + void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + bool blocksPack(const CPack *pack) const override; }; //yes/no and component selection dialogs @@ -85,7 +85,7 @@ public: CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog &bd); - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; }; class OpenWindowQuery : public CDialogQuery @@ -105,7 +105,7 @@ public: CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog &td); - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; }; class CHeroLevelUpDialogQuery : public CDialogQuery @@ -113,8 +113,8 @@ class CHeroLevelUpDialogQuery : public CDialogQuery public: CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp &Hlu, const CGHeroInstance * Hero); - virtual void onRemoval(PlayerColor color) override; - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + void onRemoval(PlayerColor color) override; + void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; HeroLevelUp hlu; const CGHeroInstance * hero; @@ -125,8 +125,8 @@ class CCommanderLevelUpDialogQuery : public CDialogQuery public: CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp &Clu, const CGHeroInstance * Hero); - virtual void onRemoval(PlayerColor color) override; - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + void onRemoval(PlayerColor color) override; + void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; CommanderLevelUp clu; const CGHeroInstance * hero; diff --git a/server/queries/QueriesProcessor.cpp b/server/queries/QueriesProcessor.cpp index 259e33a3e..73918cbf4 100644 --- a/server/queries/QueriesProcessor.cpp +++ b/server/queries/QueriesProcessor.cpp @@ -12,8 +12,6 @@ #include "CQuery.h" -boost::mutex QueriesProcessor::mx; - void QueriesProcessor::popQuery(PlayerColor player, QueryPtr query) { LOG_TRACE_PARAMS(logGlobal, "player='%s', query='%s'", player % query); diff --git a/server/queries/QueriesProcessor.h b/server/queries/QueriesProcessor.h index d0fd6df35..b46cd24fd 100644 --- a/server/queries/QueriesProcessor.h +++ b/server/queries/QueriesProcessor.h @@ -23,8 +23,6 @@ private: std::map> queries; //player => stack of queries public: - static boost::mutex mx; - void addQuery(QueryPtr query); void popQuery(const CQuery &query); void popQuery(QueryPtr query); diff --git a/test/CVcmiTestConfig.h b/test/CVcmiTestConfig.h index 5a1fb007d..9052fa80b 100644 --- a/test/CVcmiTestConfig.h +++ b/test/CVcmiTestConfig.h @@ -15,6 +15,6 @@ class CVcmiTestConfig : public ::testing::Environment { public: - virtual void SetUp() override; - virtual void TearDown() override; + void SetUp() override; + void TearDown() override; }; diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index 921f76108..1f2aa6053 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -142,7 +142,7 @@ public: si.mapname = "anything";//does not matter, map service mocked si.difficulty = 0; si.mapfileChecksum = 0; - si.mode = StartInfo::NEW_GAME; + si.mode = EStartMode::NEW_GAME; si.seedToBeUsed = 42; std::unique_ptr header = mapService.loadMapHeader(ResourcePath(si.mapname));