diff --git a/CCallback.cpp b/CCallback.cpp index 6dd134f07..0266f6b31 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -270,17 +270,10 @@ void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroIn { assert(townOrTavern); assert(hero); - ui8 i=0; - for(; iplayers[*player].availableHeroes.size(); i++) - { - if(gs->players[*player].availableHeroes[i] == hero) - { - HireHero pack(i, townOrTavern->id); - pack.player = *player; - sendRequest(&pack); - return; - } - } + + HireHero pack(HeroTypeID(hero->subID), townOrTavern->id); + pack.player = *player; + sendRequest(&pack); } void CCallback::save( const std::string &fname ) diff --git a/ChangeLog.md b/ChangeLog.md index 3e2476f5c..6fab77cdf 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,129 @@ # 1.2.1 -> 1.3.0 (unreleased) +### GENERAL: +* Implemented automatic interface scaling to any resolution supported by monitor +* Implemented UI scaling option to scale game interface +* Game resolution and UI scaling can now be changed without game restart +* Fixed multiple issues with borderless fullscreen mode +* On mobile systems game will now always run at native resolution with configurable UI scaling +* Implemented support for Horn of the Abyss map format +* Implemented option to replay results of quick combat +* Added translations to French and Chinese +* All in-game cheats are now case-insensitive +* Added high-definition icon for Windows +* Fix crash on connecting to server on FreeBSD and Flatpak builds + +### TOUCHSCREEN SUPPORT: +* VCMI will now properly recognizes touch screen input +* Implemented long tap gesture that shows popup window. Tap once more to close popup +* Long tap gesture duration can now be configured in settings +* Implemented swipe gesture for scrolling through lists +* All windows that have sliders in UI can now be scrolled using swipe gesture +* Implemented swipe gesture for attack direction selection: swipe from enemy position to position you want to attack from +* Implemented pinch gesture for zooming adventure map +* Implemented haptic feedback (vibration) for long press gesture + +### LAUNCHER: +* Launcher will now attempt to automatically detect language of OS on first launch +* Added "About" tab with information about project and environment +* Added separate options for Allied AI and Enemy AI for adventure map +* Patially fixed displaying of download progress for mods +* Fixed potential crash on opening mod information for mods with a changelog + +### MAP EDITOR: +* Fixed crash on cutting random town +* Added option to export entire map as an image +* Added validation for placing multiple heroes into starting town +* It is now possible to have single player on a map +* It is now possible to configure teams in editor + +### AI PLAYER: +* Fixed potential crash on accessing market (VCAI) +* Fixed potentially infinite turns (VCAI) + +### GAME MECHANICS +* Implemented hero backpack limit (disabled by default) +* Fixed Admiral's Hat movement points calculation +* It is now possible to access Shipwrecks from coast +* Hero path will now be correctly updated on equipping/unequipping Levitation Boots or Angel Wings +* It is no longer possible to abort movement while hero is flying over water +* Fixed digging for Grail +* Implemented "Survive beyond a time limit" victory condition +* Implemented "Defeat all monsters" victory condition +* 100% damage resistance or damage reduction will make unit immune to a spell +* Game will now randomly select obligatory skill for hero on levelup instead of always picking Fire Magic +* Fixed duration of bonuses from visitable object such as Idol of Fortune +* Rescued hero from prison will now correctly reveal map around him +* Lighthouses will no longer give movement bonus on land + +### CAMPAIGNS: +* Fixed transfer of artifacts into next scenario +* Fixed crash on advancing to next scenario with heroes from mods +* Fixed handling of "Start with building" campaign bonus +* Fixed incorrect starting level of heroes in campaigns +* Game will now play correct music track on scenario selection window +* Dracon woll now correctly start without spellbook in Dragon Slayer campaign +* Fixed frequent crash on moving to next scenario during campaign + +### RANDOM MAP GENERATOR: +* Improved zone placement, shape and connections +* Improved zone passability for better gameplay +* Improved treasure distribution and treasure values to match SoD closely +* RMG will now respect road settings set in menu +* Tweaked many original templates so they allow new terrains and factions +* Added "bannedTowns", "bannedTerrains", "bannedMonsters" zone properties +* Added "road" property to connections +* Support for "wide" connections +* Support for new "fictive" and "repulsive" connections +* RMG will now run faster, utilizing many CPU cores + +### INTERFACE: +* Adventure map is now scalable and can be used with any resolution without mods +* Adventure map interface is now correctly blocked during enemy turn +* It is now possible to zoom in or out using mouse wheel or pinch gesture +* It is now possible to reset zoom via Backspace hotkey +* Receiving a message in chat will now play sound +* Map grid will now correctly display on map start +* Fixed multiple issues with incorrect updates of save/load game screen +* Fixed missing fortifications level icon in town tooltip +* Fixed positioning of resource label in Blacksmith window +* Status bar on inactive windows will no longer show any tooltip from active window +* Fixed highlighting of possible artifact placements when exchanging with allied hero +* Implemented sound of flying movement (for Fly spell or Angel Wings) +* Last symbol of entered cheat/chat message will no longer trigger hotkey + +### BATTLES: +* Implemented Tower moat (Land Mines) +* Implemented defence reduction for units in moat +* Fixed movement through moat of double-hexed units +* Fixed removal of Land Mines and Fire Walls +* Obstacles will now corectly show up either below or above unit +* It is now possible to teleport a unit through destroyed walls +* Added distinct overlay image for showing movement range of highlighted unit +* Added overlay for displaying shooting range penalties of units + +### MODDING: +* Implemented initial version of VCMI campaign format +* Implemented spell cast as possible reward for configurable object +* Implemented support for configurable buildings in towns +* Implemented support for placing prison, tavern and heroes on water +* Implemented support for new boat types +* It is now possible for boats to use other movement layers, such as "air" +* It is now possible to use growing artifacts on artifacts that can be used by hero +* It is now possible to configure town moat +* Palette-cycling animation of terrains and rivers can now be configured in json +* Game will now correctly resolve identifier in unexpected form (e.g. 'bless' vs 'spell.bless' vs 'core:bless') +* Creature specialties that use short form ( "creature" : "pikeman" ) will now correctly affect all creature upgrades +* It is now possible to configure spells for Shrines +* It is now possible to configure upgrade costs per level for Hill Forts +* It is now possible to configure boat type for Shipyards on adventure map and in town +* Implemented support for HotA-style adventure map images for monsters, with offset +* Replaced (SCHOOL)_SPELL_DMG_PREMY with SPELL_DAMAGE bonus (uses school as subtype). +* Removed bonuses (SCHOOL)_SPELLS - replaced with SPELLS_OF_SCHOOL +* Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance +* MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses + # 1.2.0 -> 1.2.1 ### GENERAL: diff --git a/client/CMT.cpp b/client/CMT.cpp index 417ca2f2a..3f4fe83b4 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -434,7 +434,8 @@ void playIntro() { if(CCS->videoh->openAndPlayVideo("3DOLOGO.SMK", 0, 1, true, true)) { - CCS->videoh->openAndPlayVideo("AZVS.SMK", 0, 1, true, true); + if (CCS->videoh->openAndPlayVideo("NWCLOGO.SMK", 0, 1, true, true)) + CCS->videoh->openAndPlayVideo("H3INTRO.SMK", 0, 1, true, true); } } diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index aa36f8443..d2a3e6537 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -415,7 +415,10 @@ void CPlayerInterface::heroKilled(const CGHeroInstance* hero) EVENT_HANDLER_CALLED_BY_CLIENT; LOG_TRACE_PARAMS(logGlobal, "Hero %s killed handler for player %s", hero->getNameTranslated() % playerID); - localState->removeWanderingHero(hero); + // if hero is not in town garrison + if (vstd::contains(localState->getWanderingHeroes(), hero)) + localState->removeWanderingHero(hero); + adventureInt->onHeroChanged(hero); localState->erasePath(hero); } @@ -517,7 +520,7 @@ void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) if(town->garrisonHero) //wandering hero moved to the garrison { // This method also gets called on hero recruitment -> garrisoned hero is already in garrison - if(town->garrisonHero->tempOwner == playerID && !vstd::contains(localState->getWanderingHeroes(), town->visitingHero)) + if(town->garrisonHero->tempOwner == playerID && vstd::contains(localState->getWanderingHeroes(), town->garrisonHero)) localState->removeWanderingHero(town->garrisonHero); } @@ -537,7 +540,9 @@ void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) castleInt->garr->setArmy(town->visitingHero, 1); castleInt->garr->recreateSlots(); castleInt->heroes->update(); - castleInt->redraw(); + + // Perform totalRedraw to update hero list on adventure map + GH.windows().totalRedraw(); } for (auto ki : GH.windows().findWindows()) @@ -604,9 +609,11 @@ void CPlayerInterface::garrisonsChanged(std::vector ob void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) //what: 1 - built, 2 - demolished { EVENT_HANDLER_CALLED_BY_CLIENT; + adventureInt->onTownChanged(town); + if (castleInt) { - castleInt->townlist->update(town); + castleInt->townlist->updateElement(town); if (castleInt->town == town) { @@ -621,8 +628,10 @@ void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID build break; } } + + // Perform totalRedraw in order to force redraw of updated town list icon from adventure map + GH.windows().totalRedraw(); } - adventureInt->onTownChanged(town); } void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) @@ -1490,6 +1499,13 @@ void CPlayerInterface::objectRemovedAfter() { EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->onMapTilesChanged(boost::none); + + // visiting or garrisoned hero removed - recreate castle window + if (castleInt) + openTownWindow(castleInt->town); + + for (auto ki : GH.windows().findWindows()) + ki->heroRemoved(); } void CPlayerInterface::playerBlocked(int reason, bool start) @@ -1988,8 +2004,17 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path) int soundChannel = -1; std::string soundName; - auto getMovementSoundFor = [&](const CGHeroInstance * hero, int3 posPrev, int3 posNext) -> std::string + auto getMovementSoundFor = [&](const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) -> std::string { + if (moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL) + return ""; + + if (moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK) + return ""; + + if (moveType == EPathNodeAction::BLOCKING_VISIT) + return ""; + // flying movement sound if (hero->hasBonusOfType(BonusType::FLYING_MOVEMENT)) return "HORSE10.wav"; @@ -2041,8 +2066,11 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path) } if(i != path.nodes.size() - 1) { - soundName = getMovementSoundFor(h, prevCoord, nextCoord); - soundChannel = CCS->soundh->playSound(soundName, -1); + soundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); + if (!soundName.empty()) + soundChannel = CCS->soundh->playSound(soundName, -1); + else + soundChannel = -1; } continue; } @@ -2055,14 +2083,17 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path) { // Start a new sound for the hero movement or let the existing one carry on. - std::string newSoundName = getMovementSoundFor(h, prevCoord, nextCoord); + std::string newSoundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); if(newSoundName != soundName) { soundName = newSoundName; CCS->soundh->stopSound(soundChannel); - soundChannel = CCS->soundh->playSound(soundName, -1); + if (!soundName.empty()) + soundChannel = CCS->soundh->playSound(soundName, -1); + else + soundChannel = -1; } } diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 59d6feee0..ecce5366f 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -92,7 +92,7 @@ void AdventureMapInterface::onHeroMovementStarted(const CGHeroInstance * hero) void AdventureMapInterface::onHeroChanged(const CGHeroInstance *h) { - widget->getHeroList()->update(h); + widget->getHeroList()->updateElement(h); if (h && h == LOCPLINT->localState->getCurrentHero() && !widget->getInfoBar()->showingComponents()) widget->getInfoBar()->showSelection(); @@ -102,7 +102,7 @@ void AdventureMapInterface::onHeroChanged(const CGHeroInstance *h) void AdventureMapInterface::onTownChanged(const CGTownInstance * town) { - widget->getTownList()->update(town); + widget->getTownList()->updateElement(town); if (town && town == LOCPLINT->localState->getCurrentTown() && !widget->getInfoBar()->showingComponents()) widget->getInfoBar()->showSelection(); @@ -365,8 +365,8 @@ void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID) widget->getInfoBar()->showSelection(); } - widget->getHeroList()->update(); - widget->getTownList()->update(); + widget->getHeroList()->updateWidget(); + widget->getTownList()->updateWidget(); const CGHeroInstance * heroToSelect = nullptr; @@ -833,5 +833,8 @@ void AdventureMapInterface::onScreenResize() widget->getMinimap()->update(); widget->getInfoBar()->showSelection(); + if (LOCPLINT && LOCPLINT->localState->getCurrentArmy()) + widget->getMapView()->onCenteredObject(LOCPLINT->localState->getCurrentArmy()); + adjustActiveness(); } diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index 7c190debf..23c325ea9 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -280,21 +280,15 @@ void CHeroList::select(const CGHeroInstance * hero) selectIndex(vstd::find_pos(LOCPLINT->localState->getWanderingHeroes(), hero)); } -void CHeroList::update(const CGHeroInstance * hero) +void CHeroList::updateElement(const CGHeroInstance * hero) { - //this hero is already present, update its status - for(auto & elem : listBox->getItems()) - { - auto item = std::dynamic_pointer_cast(elem); - if(item && item->hero == hero && vstd::contains(LOCPLINT->localState->getWanderingHeroes(), hero)) - { - item->update(); - return; - } - } - //simplest solution for now: reset list and restore selection + updateWidget(); +} +void CHeroList::updateWidget() +{ listBox->resize(LOCPLINT->localState->getWanderingHeroes().size()); + listBox->reset(); if (LOCPLINT->localState->getCurrentHero()) select(LOCPLINT->localState->getCurrentHero()); @@ -363,14 +357,17 @@ void CTownList::select(const CGTownInstance * town) selectIndex(vstd::find_pos(LOCPLINT->localState->getOwnedTowns(), town)); } -void CTownList::update(const CGTownInstance *) +void CTownList::updateElement(const CGTownInstance * town) { - //simplest solution for now: reset list and restore selection + updateWidget(); +} +void CTownList::updateWidget() +{ listBox->resize(LOCPLINT->localState->getOwnedTowns().size()); + listBox->reset(); if (LOCPLINT->localState->getCurrentTown()) select(LOCPLINT->localState->getCurrentTown()); CList::update(); } - diff --git a/client/adventureMap/CList.h b/client/adventureMap/CList.h index 385ed277d..0cefd814e 100644 --- a/client/adventureMap/CList.h +++ b/client/adventureMap/CList.h @@ -77,6 +77,9 @@ protected: virtual std::shared_ptr createItem(size_t index) = 0; + /// should be called when list is invalidated + void update(); + public: /// functions that will be called when selection changes CFunctionList onSelect; @@ -87,8 +90,6 @@ public: void setScrollUpButton(std::shared_ptr button); void setScrollDownButton(std::shared_ptr button); - /// should be called when list is invalidated - void update(); /// set of methods to switch selection void selectIndex(int which); @@ -137,7 +138,10 @@ public: void select(const CGHeroInstance * hero = nullptr); /// Update hero. Will add or remove it from the list if needed - void update(const CGHeroInstance * hero = nullptr); + void updateElement(const CGHeroInstance * hero); + + /// Update all heroes + void updateWidget(); }; /// List of towns which is shown at the right of the adventure map screen or in the town screen @@ -167,6 +171,9 @@ public: void select(const CGTownInstance * town = nullptr); /// Update town. Will add or remove it from the list if needed - void update(const CGTownInstance * town = nullptr); + void updateElement(const CGTownInstance * town); + + /// Update all towns + void updateWidget(); }; diff --git a/client/eventsSDL/InputSourceTouch.cpp b/client/eventsSDL/InputSourceTouch.cpp index 8e627ac27..31274e0dd 100644 --- a/client/eventsSDL/InputSourceTouch.cpp +++ b/client/eventsSDL/InputSourceTouch.cpp @@ -223,10 +223,12 @@ void InputSourceTouch::handleUpdate() if (currentTime > lastTapTimeTicks + params.longTouchTimeMilliseconds) { GH.events().dispatchShowPopup(GH.getCursorPosition()); - hapticFeedback(); if (GH.windows().isTopWindowPopup()) + { + hapticFeedback(); state = TouchState::TAP_DOWN_LONG; + } } } } diff --git a/client/gui/CIntObject.cpp b/client/gui/CIntObject.cpp index 156f94811..93d0ebc70 100644 --- a/client/gui/CIntObject.cpp +++ b/client/gui/CIntObject.cpp @@ -305,6 +305,7 @@ CKeyShortcut::CKeyShortcut() CKeyShortcut::CKeyShortcut(EShortcut key) : assignedKey(key) + , shortcutPressed(false) { } diff --git a/client/render/IScreenHandler.h b/client/render/IScreenHandler.h index b9d248956..51cc3dda2 100644 --- a/client/render/IScreenHandler.h +++ b/client/render/IScreenHandler.h @@ -37,4 +37,7 @@ public: /// Converts provided rect from logical coordinates into coordinates within window, accounting for scaling and viewport virtual Rect convertLogicalPointsToWindow(const Rect & input) const = 0; + + /// Dimensions of render output + virtual Point getRenderResolution() const = 0; }; diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index b0c5c1fd2..8de235d47 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -46,7 +46,7 @@ std::tuple ScreenHandler::getSupportedScalingRange() const // arbitrary limit on *downscaling*. Allow some downscaling, if requested by user. Should be generally limited to 100+ for all but few devices static const double minimalScaling = 50; - Point renderResolution = getActualRenderResolution(); + Point renderResolution = getRenderResolution(); double reservedAreaWidth = settings["video"]["reservedWidth"].Float(); Point availableResolution = Point(renderResolution.x * (1 - reservedAreaWidth), renderResolution.y); @@ -85,7 +85,7 @@ Rect ScreenHandler::convertLogicalPointsToWindow(const Rect & input) const Point ScreenHandler::getPreferredLogicalResolution() const { - Point renderResolution = getActualRenderResolution(); + Point renderResolution = getRenderResolution(); double reservedAreaWidth = settings["video"]["reservedWidth"].Float(); Point availableResolution = Point(renderResolution.x * (1 - reservedAreaWidth), renderResolution.y); @@ -99,7 +99,7 @@ Point ScreenHandler::getPreferredLogicalResolution() const return logicalResolution; } -Point ScreenHandler::getActualRenderResolution() const +Point ScreenHandler::getRenderResolution() const { assert(mainRenderer != nullptr); diff --git a/client/renderSDL/ScreenHandler.h b/client/renderSDL/ScreenHandler.h index 5bd5ae30b..c7a057144 100644 --- a/client/renderSDL/ScreenHandler.h +++ b/client/renderSDL/ScreenHandler.h @@ -39,9 +39,6 @@ class ScreenHandler final : public IScreenHandler /// This value is what player views as window size Point getPreferredWindowResolution() const; - /// Dimensions of render output, usually same as window size except for high-DPI screens on macOS / iOS - Point getActualRenderResolution() const; - EWindowMode getPreferredWindowMode() const; /// Returns index of display on which window should be created @@ -86,6 +83,9 @@ public: /// Fills screen with black color, erasing any existing content void clearScreen() final; + /// Dimensions of render output, usually same as window size except for high-DPI screens on macOS / iOS + Point getRenderResolution() const final; + std::vector getSupportedResolutions() const final; std::vector getSupportedResolutions(int displayIndex) const; std::tuple getSupportedScalingRange() const final; diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index cefd5c059..331962d7a 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -319,9 +319,6 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) noDismiss = true; } - for(auto ki : GH.windows().findWindows()) - noDismiss = true; - //if player only have one hero and no towns if(!LOCPLINT->cb->howManyTowns() && LOCPLINT->cb->howManyHeroes() == 1) noDismiss = true; @@ -329,7 +326,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) if(curHero->isMissionCritical()) noDismiss = true; - dismissButton->block(!!curHero->visitedTown || noDismiss); + dismissButton->block(noDismiss); if(curHero->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION)) == 0) { diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index f0dbb73f6..1369ad42c 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -15,6 +15,7 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" +#include "../PlayerLocalState.h" #include "../adventureMap/CResDataBar.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" @@ -638,6 +639,11 @@ void CKingdomInterface::townChanged(const CGTownInstance *town) townList->townChanged(town); } +void CKingdomInterface::heroRemoved() +{ + tabArea->reset(); +} + void CKingdomInterface::updateGarrisons() { if(auto garrison = std::dynamic_pointer_cast(tabArea->getItem())) @@ -694,11 +700,12 @@ void CKingdHeroList::updateGarrisons() std::shared_ptr CKingdHeroList::createHeroItem(size_t index) { ui32 picCount = 4; // OVSLOT contains 4 images - size_t heroesCount = LOCPLINT->cb->howManyHeroes(false); - if(index < heroesCount) + auto heroesList = LOCPLINT->localState->getWanderingHeroes(); + + if(index < heroesList.size()) { - auto hero = std::make_shared(LOCPLINT->cb->getHeroBySerial((int)index, false)); + auto hero = std::make_shared(heroesList[index]); addSet(hero->heroArts); return hero; } @@ -745,10 +752,11 @@ void CKingdTownList::updateGarrisons() std::shared_ptr CKingdTownList::createTownItem(size_t index) { ui32 picCount = 4; // OVSLOT contains 4 images - size_t townsCount = LOCPLINT->cb->howManyTowns(); - if(index < townsCount) - return std::make_shared(LOCPLINT->cb->getTownBySerial((int)index)); + auto townsList = LOCPLINT->localState->getOwnedTowns(); + + if(index < townsList.size()) + return std::make_shared(townsList[index]); else return std::make_shared("OVSLOT", (index-2) % picCount ); } diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index df47ee33e..e305f2e16 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -246,6 +246,7 @@ public: CKingdomInterface(); void townChanged(const CGTownInstance *town); + void heroRemoved(); void updateGarrisons() override; void artifactRemoved(const ArtifactLocation &artLoc) override; void artifactMoved(const ArtifactLocation &artLoc, const ArtifactLocation &destLoc, bool withRedraw) override; diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 1e4bc1a35..0929ae724 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -158,23 +158,33 @@ void CRecruitmentWindow::select(std::shared_ptr card) void CRecruitmentWindow::buy() { CreatureID crid = selected->creature->getId(); - SlotID dstslot = dst-> getSlotFor(crid); + SlotID dstslot = dst->getSlotFor(crid); if(!dstslot.validSlot() && (selected->creature->warMachine == ArtifactID::NONE)) //no available slot { - std::string txt; - if(dst->ID == Obj::HERO) + std::pair toMerge; + bool allowMerge = CGI->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED); + + if (allowMerge && dst->mergableStacks(toMerge)) { - txt = CGI->generaltexth->allTexts[425]; //The %s would join your hero, but there aren't enough provisions to support them. - boost::algorithm::replace_first(txt, "%s", slider->getValue() > 1 ? CGI->creh->objects[crid]->getNamePluralTranslated() : CGI->creh->objects[crid]->getNameSingularTranslated()); + LOCPLINT->cb->mergeStacks( dst, dst, toMerge.first, toMerge.second); } else { - txt = CGI->generaltexth->allTexts[17]; //There is no room in the garrison for this army. - } + std::string txt; + if(dst->ID == Obj::HERO) + { + txt = CGI->generaltexth->allTexts[425]; //The %s would join your hero, but there aren't enough provisions to support them. + boost::algorithm::replace_first(txt, "%s", slider->getValue() > 1 ? CGI->creh->objects[crid]->getNamePluralTranslated() : CGI->creh->objects[crid]->getNameSingularTranslated()); + } + else + { + txt = CGI->generaltexth->allTexts[17]; //There is no room in the garrison for this army. + } - LOCPLINT->showInfoDialog(txt); - return; + LOCPLINT->showInfoDialog(txt); + return; + } } onRecruit(crid, slider->getValue()); diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index eb2ffae0e..ae747f90e 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -224,25 +224,19 @@ void GeneralOptionsTab::updateResolutionSelector() std::shared_ptr resolutionButton = widget("resolutionButton"); std::shared_ptr resolutionLabel = widget("resolutionLabel"); - if (settings["video"]["fullscreen"].Bool() && !settings["video"]["realFullscreen"].Bool()) + if (resolutionButton) { - if (resolutionButton) + if (settings["video"]["fullscreen"].Bool() && !settings["video"]["realFullscreen"].Bool()) resolutionButton->disable(); - - if (resolutionLabel) - resolutionLabel->setText(resolutionToLabelString(GH.screenDimensions().x, GH.screenDimensions().y)); - } - else - { - const auto & currentResolution = settings["video"]["resolution"]; - - if (resolutionButton) + else resolutionButton->enable(); - - if (resolutionLabel) - resolutionLabel->setText(resolutionToLabelString(currentResolution["width"].Integer(), currentResolution["height"].Integer())); } + if (resolutionLabel) + { + Point resolution = GH.screenHandler().getRenderResolution(); + resolutionLabel->setText(resolutionToLabelString(resolution.x, resolution.y)); + } } void GeneralOptionsTab::selectGameResolution() @@ -370,6 +364,11 @@ void GeneralOptionsTab::setGameScaling(int index) gameRes["scaling"].Float() = scaling; widget("scalingLabel")->setText(scalingToLabelString(scaling)); + + GH.dispatchMainThread([](){ + boost::unique_lock lock(*CPlayerInterface::pim); + GH.onScreenResize(); + }); } void GeneralOptionsTab::selectLongTouchDuration() diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index f325bfa4f..70c41549a 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -68,6 +68,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/gameState/CGameState.cpp ${MAIN_LIB_DIR}/gameState/CGameStateCampaign.cpp ${MAIN_LIB_DIR}/gameState/InfoAboutArmy.cpp + ${MAIN_LIB_DIR}/gameState/TavernHeroesPool.cpp ${MAIN_LIB_DIR}/logging/CBasicLogConfigurator.cpp ${MAIN_LIB_DIR}/logging/CLogger.cpp @@ -394,6 +395,8 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/gameState/EVictoryLossCheckResult.h ${MAIN_LIB_DIR}/gameState/InfoAboutArmy.h ${MAIN_LIB_DIR}/gameState/SThievesGuildInfo.h + ${MAIN_LIB_DIR}/gameState/TavernHeroesPool.h + ${MAIN_LIB_DIR}/gameState/TavernSlot.h ${MAIN_LIB_DIR}/gameState/QuestInfo.h ${MAIN_LIB_DIR}/logging/CBasicLogConfigurator.h diff --git a/config/battleStartpos.json b/config/battleStartpos.json index 255a6ec6e..5921cc071 100644 --- a/config/battleStartpos.json +++ b/config/battleStartpos.json @@ -4,12 +4,12 @@ { "name" : "attackerLoose", // loose formation, attacker "levels": [ - [ 86 ], - [ 35, 137 ], - [ 35, 86, 137 ], - [ 1, 69, 103, 171 ], - [ 1, 35, 86, 137, 171 ], - [ 1, 35, 69, 103, 137, 171 ], + [ 86 ], + [ 35, 137 ], + [ 35, 86, 137 ], + [ 1, 69, 103, 171 ], + [ 1, 35, 86, 137, 171 ], + [ 1, 35, 69, 103, 137, 171 ], [ 1, 35, 69, 86, 103, 137, 171 ] ] }, @@ -17,12 +17,12 @@ { "name" : "defenderLoose", // loose formation, defender "levels": [ - [ 100 ], - [ 49, 151 ], - [ 49, 100, 151 ], - [ 15, 83, 117, 185 ], - [ 15, 49, 100, 151, 185 ], - [ 15, 49, 83, 117, 151, 185 ], + [ 100 ], + [ 49, 151 ], + [ 49, 100, 151 ], + [ 15, 83, 117, 185 ], + [ 15, 49, 100, 151, 185 ], + [ 15, 49, 83, 117, 151, 185 ], [ 15, 49, 83, 100, 117, 151, 185 ] ] }, @@ -30,26 +30,26 @@ { "name" : "attackerTight", // tight formation, attacker "levels": [ - [ 86 ], - [ 69, 103 ], - [ 69, 86, 103 ], - [ 52, 69, 103, 120 ], - [ 52, 69, 86, 103, 120 ], - [ 35, 52, 69, 103, 120, 137 ], - [ 35, 52, 69, 86, 103, 120, 137 ] + [ 86 ], + [ 69, 103 ], + [ 69, 86, 103 ], + [ 35, 69, 103, 137 ], + [ 35, 69, 86, 103, 137 ], + [ 1, 35, 69, 103, 137, 171 ], + [ 1, 35, 69, 86, 103, 137, 171 ] ] }, { "name" : "defenderTight", // tight formation, defender "levels": [ - [ 100 ], - [ 83, 117 ], - [ 83, 100, 117 ], - [ 66, 83, 117, 134 ], - [ 66, 83, 100, 117, 134 ], - [ 49, 66, 83, 117, 134, 151 ], - [ 49, 66, 83, 100, 117, 134, 151 ] + [ 100 ], + [ 83, 117 ], + [ 83, 100, 117 ], + [ 49, 83, 117, 151 ], + [ 49, 83, 100, 117, 151 ], + [ 15, 49, 83, 117, 151, 185 ], + [ 15, 49, 83, 100, 117, 151, 185 ] ] }, diff --git a/config/filesystem.json b/config/filesystem.json index d4c3880e7..0a388c31b 100644 --- a/config/filesystem.json +++ b/config/filesystem.json @@ -9,25 +9,25 @@ { "DATA/" : [ - {"type" : "lod", "path" : "Data/H3ab_bmp.lod"}, - {"type" : "lod", "path" : "Data/H3bitmap.lod"}, - {"type" : "lod", "path" : "Data/h3abp_bm.lod"}, // Polish version of H3 only - {"type" : "lod", "path" : "Data/H3pbitma.lod"}, // Polish version of H3 only + {"type" : "lod", "path" : "Data/H3ab_bmp.lod"}, // Contains H3:AB data + {"type" : "lod", "path" : "Data/h3abp_bm.lod"}, // Localized versions only, contains H3:AB patch data + {"type" : "lod", "path" : "Data/H3bitmap.lod"}, // Contains H3:SoD data (overrides H3:AB data) + {"type" : "lod", "path" : "Data/H3pbitma.lod"}, // Localized versions only, contains H3:SoD patch data {"type" : "dir", "path" : "Data"} ], "SPRITES/": [ - {"type" : "lod", "path" : "Data/H3ab_spr.lod"}, - {"type" : "lod", "path" : "Data/H3sprite.lod"}, - {"type" : "lod", "path" : "Data/h3abp_sp.lod"}, // Polish version of H3 only - {"type" : "lod", "path" : "Data/H3psprit.lod"}, // Polish version of H3 only + {"type" : "lod", "path" : "Data/H3ab_spr.lod"}, // Contains H3:AB data + {"type" : "lod", "path" : "Data/H3sprite.lod"}, // Localized versions only, contains H3:AB patch data + {"type" : "lod", "path" : "Data/h3abp_sp.lod"}, // Contains H3:SoD data (overrides H3:AB data) +// {"type" : "lod", "path" : "Data/H3psprit.lod"}, // Localized versions only, contains H3:SoD patch data. Unused? Has corrupted data, e.g. lock icon for artifacts {"type" : "dir", "path" : "Sprites"} ], "SOUNDS/": [ {"type" : "snd", "path" : "Data/H3ab_ahd.snd"}, - {"type" : "snd", "path" : "Data/Heroes3-cd2.snd"}, {"type" : "snd", "path" : "Data/Heroes3.snd"}, + {"type" : "snd", "path" : "Data/Heroes3-cd2.snd"}, //WoG have overriden sounds with .82m extension in Data {"type" : "dir", "path" : "Data", "depth": 0} ], diff --git a/config/gameConfig.json b/config/gameConfig.json index 92c1158c8..048d23e12 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -311,6 +311,8 @@ "accumulateWhenNeutral" : false, // if enabled, dwellings owned by players will accumulate creatures "accumulateWhenOwned" : false + // if enabled, game will attempt to merge slots in army on recruit if all slots in hero army are in use + "mergeOnRecruit" : true }, "markets" : diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 3a2a16664..412776089 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -63,12 +63,12 @@ }, "language" : { "type" : "string", - "enum" : [ "english", "czech", "chinese", "german", "hungarian", "italian", "korean", "polish", "russian", "spanish", "ukrainian" ], + "enum" : [ "english", "czech", "chinese", "french", "german", "hungarian", "italian", "korean", "polish", "russian", "spanish", "ukrainian" ], "default" : "english" }, "gameDataLanguage" : { "type" : "string", - "enum" : [ "auto", "english", "czech", "chinese", "german", "hungarian", "italian", "korean", "polish", "russian", "spanish", "ukrainian", "other_cp1250", "other_cp1251", "other_cp1252" ], + "enum" : [ "auto", "english", "czech", "chinese", "french", "german", "hungarian", "italian", "korean", "polish", "russian", "spanish", "ukrainian", "other_cp1250", "other_cp1251", "other_cp1252" ], "default" : "auto" }, "lastSave" : { diff --git a/config/widgets/settings/battleOptionsTab.json b/config/widgets/settings/battleOptionsTab.json index d08d4b39e..a0ce1e424 100644 --- a/config/widgets/settings/battleOptionsTab.json +++ b/config/widgets/settings/battleOptionsTab.json @@ -245,7 +245,7 @@ ] }, { - "index": 12, + "index": 9, "type": "toggleButton", "image": "settingsWindow/button46", "help": "vcmi.battleOptions.animationsSpeed5", @@ -261,7 +261,7 @@ ] }, { - "index": 24, + "index": 18, "type": "toggleButton", "image": "settingsWindow/button46", "help": "vcmi.battleOptions.animationsSpeed6", diff --git a/launcher/firstLaunch/firstlaunch_moc.ui b/launcher/firstLaunch/firstlaunch_moc.ui index 0e39e72cd..62fd64d1f 100644 --- a/launcher/firstLaunch/firstlaunch_moc.ui +++ b/launcher/firstLaunch/firstlaunch_moc.ui @@ -7,7 +7,7 @@ 0 0 745 - 389 + 397 @@ -96,7 +96,7 @@ - 0 + 2 @@ -616,7 +616,7 @@ Heroes® of Might and Magic® III HD is currently not supported! Horn of the Abyss - true + false @@ -638,7 +638,7 @@ Heroes® of Might and Magic® III HD is currently not supported! Heroes III Translation - true + false @@ -699,10 +699,10 @@ Heroes® of Might and Magic® III HD is currently not supported! - High Definition Support + Interface Improvements - true + false @@ -724,7 +724,7 @@ Heroes® of Might and Magic® III HD is currently not supported! In The Wake of Gods - true + false @@ -769,7 +769,7 @@ Heroes® of Might and Magic® III HD is currently not supported! - Install support for playing Heroes III in resolutions higher than 800x600 + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles true diff --git a/launcher/mainwindow_moc.cpp b/launcher/mainwindow_moc.cpp index 3d74fe1e9..99db438c9 100644 --- a/launcher/mainwindow_moc.cpp +++ b/launcher/mainwindow_moc.cpp @@ -153,6 +153,7 @@ void MainWindow::enterSetup() ui->startEditorButton->setEnabled(false); ui->lobbyButton->setEnabled(false); ui->settingsButton->setEnabled(false); + ui->aboutButton->setEnabled(false); ui->modslistButton->setEnabled(false); ui->tabListWidget->setCurrentIndex(TabRows::SETUP); } @@ -166,6 +167,7 @@ void MainWindow::exitSetup() ui->startEditorButton->setEnabled(true); ui->lobbyButton->setEnabled(true); ui->settingsButton->setEnabled(true); + ui->aboutButton->setEnabled(true); ui->modslistButton->setEnabled(true); ui->tabListWidget->setCurrentIndex(TabRows::MODS); } diff --git a/launcher/settingsView/csettingsview_moc.cpp b/launcher/settingsView/csettingsview_moc.cpp index d8b37611c..530156c85 100644 --- a/launcher/settingsView/csettingsview_moc.cpp +++ b/launcher/settingsView/csettingsview_moc.cpp @@ -280,12 +280,6 @@ void CSettingsView::on_comboBoxDisplayIndex_currentIndexChanged(int index) fillValidResolutionsForScreen(index); } -void CSettingsView::on_comboBoxPlayerAI_currentTextChanged(const QString & arg1) -{ - Settings node = settings.write["server"]["playerAI"]; - node->String() = arg1.toUtf8().data(); -} - void CSettingsView::on_comboBoxFriendlyAI_currentTextChanged(const QString & arg1) { Settings node = settings.write["server"]["friendlyAI"]; @@ -500,6 +494,19 @@ void CSettingsView::on_spinBoxFramerateLimit_valueChanged(int arg1) node->Float() = arg1; } +void CSettingsView::on_comboBoxEnemyPlayerAI_currentTextChanged(const QString &arg1) +{ + Settings node = settings.write["server"]["playerAI"]; + node->String() = arg1.toUtf8().data(); +} + + +void CSettingsView::on_comboBoxAlliedPlayerAI_currentTextChanged(const QString &arg1) +{ + Settings node = settings.write["server"]["alliedAI"]; + node->String() = arg1.toUtf8().data(); +} + void CSettingsView::on_checkBoxAutoSavePrefix_stateChanged(int arg1) { diff --git a/launcher/settingsView/csettingsview_moc.h b/launcher/settingsView/csettingsview_moc.h index 9e3369a3f..6e27d9896 100644 --- a/launcher/settingsView/csettingsview_moc.h +++ b/launcher/settingsView/csettingsview_moc.h @@ -35,7 +35,6 @@ public slots: private slots: void on_comboBoxResolution_currentTextChanged(const QString & arg1); void on_comboBoxFullScreen_currentIndexChanged(int index); - void on_comboBoxPlayerAI_currentTextChanged(const QString & arg1); void on_comboBoxFriendlyAI_currentTextChanged(const QString & arg1); void on_comboBoxNeutralAI_currentTextChanged(const QString & arg1); void on_comboBoxEnemyAI_currentTextChanged(const QString & arg1); @@ -63,6 +62,10 @@ private slots: void on_spinBoxFramerateLimit_valueChanged(int arg1); + void on_comboBoxEnemyPlayerAI_currentTextChanged(const QString &arg1); + + void on_comboBoxAlliedPlayerAI_currentTextChanged(const QString &arg1); + void on_checkBoxAutoSavePrefix_stateChanged(int arg1); void on_spinBoxAutoSaveLimit_valueChanged(int arg1); diff --git a/launcher/translation/chinese.ts b/launcher/translation/chinese.ts index 289258f76..048c534c2 100644 --- a/launcher/translation/chinese.ts +++ b/launcher/translation/chinese.ts @@ -710,6 +710,11 @@ Heroes® of Might and Magic® III HD is currently not supported! The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually 自动检测英雄无敌3语言失败。请手动选择英雄无敌3语言 + + + Interface Improvements + + Install a translation of Heroes III in your preferred language @@ -722,8 +727,12 @@ Heroes® of Might and Magic® III HD is currently not supported! + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + + + Install support for playing Heroes III in resolutions higher than 800x600 - 安装英雄无敌3的800x600以上分辨率支持 + 安装英雄无敌3的800x600以上分辨率支持 @@ -813,9 +822,8 @@ Heroes® of Might and Magic® III HD is currently not supported! 英雄无敌3翻译 - High Definition Support - 高分辨率支持 + 高分辨率支持 diff --git a/launcher/translation/english.ts b/launcher/translation/english.ts index d52b036fc..aa772876c 100644 --- a/launcher/translation/english.ts +++ b/launcher/translation/english.ts @@ -678,6 +678,11 @@ Heroes® of Might and Magic® III HD is currently not supported! The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually + + + Interface Improvements + + Install a translation of Heroes III in your preferred language @@ -690,7 +695,7 @@ Heroes® of Might and Magic® III HD is currently not supported! - Install support for playing Heroes III in resolutions higher than 800x600 + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles @@ -780,11 +785,6 @@ Heroes® of Might and Magic® III HD is currently not supported! Heroes III Translation - - - High Definition Support - - In The Wake of Gods diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index dd445428f..c00ec4ba2 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -257,7 +257,7 @@ Impressions écran - + %p% (%v KB out of %m KB) %p% (%v Ko sur %m Ko) @@ -780,9 +780,8 @@ Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écra - Install support for playing Heroes III in resolutions higher than 800x600 - Installer un support pour jouer à Heroes III avec des résolutions supérieures à 800x600 + Installer un support pour jouer à Heroes III avec des résolutions supérieures à 800x600 @@ -860,7 +859,7 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge If you don't have a copy of Heroes III installed, you can use our automatic installation tool 'vcmibuilder', which only requires the GoG.com Heroes III installer. Please visit our wiki for detailed instructions. - Si vous n"avez pas installé de copie de Heroes III, vous pouvez utiliser notre outil d"installation automatique "vcmibuilder", qui ne nécessite que le programme d"installation de GoG.com Heroes III. Veuillez visiter notre wiki pour des instructions détaillées. + Si vous n"avez pas installé de copie de Heroes III, vous pouvez utiliser notre outil d"installation automatique "vcmibuilder", qui ne nécessite que le programme d"installation de GoG.com Heroes III. Veuillez visiter notre wiki pour des instructions détaillées. @@ -920,8 +919,12 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge + Interface Improvements + + + High Definition Support - Support de Haute Définition + Support de Haute Définition @@ -933,6 +936,11 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher En option, vous pouvez installer des mods supplémentaires soit maintenant, soit à tout moment plus tard, à l"aide du lanceur VCMI + + + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index e589f2bcc..c667db77d 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -714,6 +714,11 @@ Heroes III: HD Edition wird derzeit nicht unterstützt The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually Automatische Erkennung der Sprache fehlgeschlagen. Bitte wählen Sie die Sprache Ihrer Heroes III Kopie + + + Interface Improvements + + Install a translation of Heroes III in your preferred language @@ -726,8 +731,12 @@ Heroes III: HD Edition wird derzeit nicht unterstützt + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + + + Install support for playing Heroes III in resolutions higher than 800x600 - Installieren Sie Unterstützung für das Spielen von Heroes III in anderen Auflösungen als 800x600 + Installieren Sie Unterstützung für das Spielen von Heroes III in anderen Auflösungen als 800x600 @@ -817,9 +826,8 @@ Heroes III: HD Edition wird derzeit nicht unterstützt Heroes III Übersetzung - High Definition Support - Unterstützung für hohe Auflösungen + Unterstützung für hohe Auflösungen diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index b16450d0e..62ad8bb1c 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -714,6 +714,11 @@ Heroes III: HD Edition nie jest obecnie wspierane! The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually Automatyczna detekcja języka nie powiodła się. Proszę wybrać język twojego Heroes III + + + Interface Improvements + + Install a translation of Heroes III in your preferred language @@ -726,8 +731,12 @@ Heroes III: HD Edition nie jest obecnie wspierane! + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + + + Install support for playing Heroes III in resolutions higher than 800x600 - Zainstaluj wsparcie dla grania w Heroes III w rozdzielczości innej niż 800x600 + Zainstaluj wsparcie dla grania w Heroes III w rozdzielczości innej niż 800x600 @@ -817,9 +826,8 @@ Heroes III: HD Edition nie jest obecnie wspierane! Tłumaczenie Heroes III - High Definition Support - Wsparcie High Definition + Wsparcie High Definition diff --git a/launcher/translation/russian.ts b/launcher/translation/russian.ts index 3cabefd7f..cfb060df8 100644 --- a/launcher/translation/russian.ts +++ b/launcher/translation/russian.ts @@ -708,6 +708,11 @@ Heroes® of Might and Magic® III HD is currently not supported! The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually Язык Героев III не был определен. Пожалуйста, выберите язык вашей копии Героев III + + + Interface Improvements + + Install a translation of Heroes III in your preferred language @@ -720,8 +725,12 @@ Heroes® of Might and Magic® III HD is currently not supported! + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + + + Install support for playing Heroes III in resolutions higher than 800x600 - Установить поддержку запуска Героев III в разрешениях, отличных от 800x600 + Установить поддержку запуска Героев III в разрешениях, отличных от 800x600 @@ -811,9 +820,8 @@ Heroes® of Might and Magic® III HD is currently not supported! Перевод Героев III - High Definition Support - Поддержка высоких разрешений + Поддержка высоких разрешений diff --git a/launcher/translation/spanish.ts b/launcher/translation/spanish.ts index d7fd84ebf..eca998ccc 100644 --- a/launcher/translation/spanish.ts +++ b/launcher/translation/spanish.ts @@ -702,6 +702,16 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Your Heroes III language has been successfully detected. Se ha detectado con éxito el idioma de tu Heroes III. + + + Interface Improvements + + + + + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + + Select your language @@ -781,9 +791,8 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Traducción de Heroes III. - High Definition Support - Soporte para resoluciones en Alta Definición + Soporte para resoluciones en Alta Definición @@ -801,9 +810,8 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Opcionalmente, puedes instalar mods adicionales ya sea ahora o en cualquier momento posterior, utilizando el lanzador de VCMI. - Install support for playing Heroes III in resolutions higher than 800x600 - Instalar soporte para jugar Heroes III en resoluciones superiores a 800x600 + Instalar soporte para jugar Heroes III en resoluciones superiores a 800x600 diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index e8dfab4f3..69f1f51c9 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -718,6 +718,11 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually Не вдалося визначити мову гри. Будь ласка, виберіть мову вашої копії Heroes III + + + Interface Improvements + Удосконалення нтерфейсу + Install a translation of Heroes III in your preferred language @@ -730,8 +735,12 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + Встановити різноманітні покращення інтерфейсу, такі як покращений інтерфейс випадкових карт та вибір варіантів дій у боях + + Install support for playing Heroes III in resolutions higher than 800x600 - Встановити підтримку для гри в Heroes III у роздільних здатностях, більших за 800x600 + Встановити підтримку для гри в Heroes III у роздільних здатностях, більших за 800x600 @@ -821,9 +830,8 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс Переклад Heroes III - High Definition Support - Підтримка високих роздільних здатностей + Підтримка високих роздільних здатностей diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 88bce3d83..f2195f49f 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -1097,7 +1097,7 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars case 'F': b.type = BonusType::FLYING; break; case 'm': - b.type = BonusType::MORALE; break; + b.type = BonusType::MORALE; b.val = 1; b.valType = BonusValueType::INDEPENDENT_MAX; break; diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 97be80869..16b10d9d8 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -13,6 +13,7 @@ #include "gameState/CGameState.h" #include "gameState/InfoAboutArmy.h" #include "gameState/SThievesGuildInfo.h" +#include "gameState/TavernHeroesPool.h" #include "CGeneralTextHandler.h" #include "StartInfo.h" // for StartInfo #include "battle/BattleInfo.h" // for BattleInfo @@ -99,13 +100,6 @@ const PlayerState * CGameInfoCallback::getPlayerState(PlayerColor color, bool ve } } -const CTown * CGameInfoCallback::getNativeTown(PlayerColor color) const -{ - const PlayerSettings *ps = getPlayerSettings(color); - ERROR_RET_VAL_IF(!ps, "There is no such player!", nullptr); - return (*VLC->townh)[ps->castle]->town; -} - const CGObjectInstance * CGameInfoCallback::getObjByQuestIdentifier(int identifier) const { if(gs->map->questIdentifierToId.empty()) @@ -486,13 +480,10 @@ std::vector CGameInfoCallback::getAvailableHeroes(const //ERROR_RET_VAL_IF(!isOwnedOrVisited(townOrTavern), "Town or tavern must be owned or visited!", ret); //TODO: town needs to be owned, advmap tavern needs to be visited; to be reimplemented when visit tracking is done const CGTownInstance * town = getTown(townOrTavern->id); + if(townOrTavern->ID == Obj::TAVERN || (town && town->hasBuilt(BuildingID::TAVERN))) - { - range::copy(gs->players[*player].availableHeroes, std::back_inserter(ret)); - vstd::erase_if(ret, [](const CGHeroInstance * h) { - return h == nullptr; - }); - } + return gs->heroesPool->getHeroesFor(*player); + return ret; } diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 6e57fa34a..8bca8a99d 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -108,7 +108,6 @@ public: // std::string getTavernRumor(const CGObjectInstance * townOrTavern) const; // EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements // virtual bool getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject = nullptr) const; -// const CTown *getNativeTown(PlayerColor color) const; //from gs // const TeamState *getTeam(TeamID teamID) const; @@ -206,7 +205,6 @@ public: virtual std::string getTavernRumor(const CGObjectInstance * townOrTavern) const; virtual EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements virtual bool getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject = nullptr) const; - virtual const CTown *getNativeTown(PlayerColor color) const; //from gs virtual const TeamState *getTeam(TeamID teamID) const; diff --git a/lib/CPlayerState.cpp b/lib/CPlayerState.cpp index 4abfe8f3d..41d2676d9 100644 --- a/lib/CPlayerState.cpp +++ b/lib/CPlayerState.cpp @@ -35,7 +35,6 @@ PlayerState::PlayerState(PlayerState && other) noexcept: std::swap(visitedObjects, other.visitedObjects); std::swap(heroes, other.heroes); std::swap(towns, other.towns); - std::swap(availableHeroes, other.availableHeroes); std::swap(dwellings, other.dwellings); std::swap(quests, other.quests); } diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index 26f7075b5..cfb99617c 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -33,7 +33,6 @@ public: std::set visitedObjects; // as a std::set, since most accesses here will be from visited status checks std::vector > heroes; std::vector > towns; - std::vector > availableHeroes; //heroes available in taverns std::vector > dwellings; //used for town growth std::vector quests; //store info about all received quests @@ -74,7 +73,6 @@ public: h & status; h & heroes; h & towns; - h & availableHeroes; h & dwellings; h & quests; h & visitedObjects; diff --git a/lib/CRandomGenerator.cpp b/lib/CRandomGenerator.cpp index 0052e3c25..4f896444b 100644 --- a/lib/CRandomGenerator.cpp +++ b/lib/CRandomGenerator.cpp @@ -20,6 +20,11 @@ CRandomGenerator::CRandomGenerator() resetSeed(); } +CRandomGenerator::CRandomGenerator(int seed) +{ + setSeed(seed); +} + void CRandomGenerator::setSeed(int seed) { rand.seed(seed); diff --git a/lib/CRandomGenerator.h b/lib/CRandomGenerator.h index 5b76adcb1..8e27b46fa 100644 --- a/lib/CRandomGenerator.h +++ b/lib/CRandomGenerator.h @@ -30,6 +30,9 @@ public: /// current thread ID. CRandomGenerator(); + /// Seeds the generator with provided initial seed + explicit CRandomGenerator(int seed); + void setSeed(int seed); /// Resets the seed to the product of the current time in milliseconds and the diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp index 76dcc54ed..c440277de 100644 --- a/lib/GameConstants.cpp +++ b/lib/GameConstants.cpp @@ -40,6 +40,7 @@ VCMI_LIB_NAMESPACE_BEGIN const HeroTypeID HeroTypeID::NONE = HeroTypeID(-1); +const ObjectInstanceID ObjectInstanceID::NONE = ObjectInstanceID(-1); const SlotID SlotID::COMMANDER_SLOT_PLACEHOLDER = SlotID(-2); const SlotID SlotID::SUMMONED_SLOT_PLACEHOLDER = SlotID(-3); diff --git a/lib/GameConstants.h b/lib/GameConstants.h index 4a58b77a5..93a3085a3 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -313,6 +313,8 @@ class ObjectInstanceID : public BaseForID { INSTID_LIKE_CLASS_COMMON(ObjectInstanceID, si32) + DLL_LINKAGE static const ObjectInstanceID NONE; + friend class CGameInfoCallback; friend class CNonConstInfoCallback; }; @@ -357,9 +359,11 @@ class PlayerColor : public BaseForID enum EPlayerColor { - PLAYER_LIMIT_I = 8 + PLAYER_LIMIT_I = 8, }; + using Mask = uint8_t; + DLL_LINKAGE static const PlayerColor SPECTATOR; //252 DLL_LINKAGE static const PlayerColor CANNOT_DETERMINE; //253 DLL_LINKAGE static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks) diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index c42e6525d..5abdedc6c 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -68,6 +68,7 @@ void GameSettings::load(const JsonNode & input) {EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT, "creatures", "weeklyGrowthPercent" }, {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, "dwellings", "accumulateWhenNeutral" }, {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED, "dwellings", "accumulateWhenOwned" }, + {EGameSettings::DWELLINGS_MERGE_ON_RECRUIT, "dwellings", "mergeOnRecruit" }, {EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP, "heroes", "perPlayerOnMapCap" }, {EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP, "heroes", "perPlayerTotalCap" }, {EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, "heroes", "retreatOnWinWithoutTroops" }, diff --git a/lib/GameSettings.h b/lib/GameSettings.h index 8fdb6b831..e933fa197 100644 --- a/lib/GameSettings.h +++ b/lib/GameSettings.h @@ -32,6 +32,7 @@ enum class EGameSettings CREATURES_WEEKLY_GROWTH_PERCENT, DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, DWELLINGS_ACCUMULATE_WHEN_OWNED, + DWELLINGS_MERGE_ON_RECRUIT, HEROES_PER_PLAYER_ON_MAP_CAP, HEROES_PER_PLAYER_TOTAL_CAP, HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index a898f5367..3e0a9c543 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -36,6 +36,7 @@ #include "StartInfo.h" #include "gameState/CGameState.h" #include "gameState/CGameStateCampaign.h" +#include "gameState/TavernHeroesPool.h" #include "mapping/CMap.h" #include "CPlayerState.h" #include "GameSettings.h" diff --git a/lib/NetPackVisitor.h b/lib/NetPackVisitor.h index 226b4a18a..f312339d2 100644 --- a/lib/NetPackVisitor.h +++ b/lib/NetPackVisitor.h @@ -36,7 +36,7 @@ public: virtual void visitSetMana(SetMana & pack) {} virtual void visitSetMovePoints(SetMovePoints & pack) {} virtual void visitFoWChange(FoWChange & pack) {} - virtual void visitSetAvailableHeroes(SetAvailableHeroes & pack) {} + virtual void visitSetAvailableHeroes(SetAvailableHero & pack) {} virtual void visitGiveBonus(GiveBonus & pack) {} virtual void visitChangeObjPos(ChangeObjPos & pack) {} virtual void visitPlayerEndsGame(PlayerEndsGame & pack) {} @@ -162,4 +162,4 @@ public: virtual void visitLobbyShowMessage(LobbyShowMessage & pack) {} }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/NetPacks.h b/lib/NetPacks.h index e3a6f25c5..d56dac4f8 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -19,6 +19,7 @@ #include "battle/BattleAction.h" #include "battle/CObstacleInstance.h" #include "gameState/EVictoryLossCheckResult.h" +#include "gameState/TavernSlot.h" #include "gameState/QuestInfo.h" #include "mapObjects/CGHeroInstance.h" #include "mapping/CMapDefines.h" @@ -330,23 +331,26 @@ struct DLL_LINKAGE FoWChange : public CPackForClient } }; -struct DLL_LINKAGE SetAvailableHeroes : public CPackForClient +struct DLL_LINKAGE SetAvailableHero : public CPackForClient { - SetAvailableHeroes() + SetAvailableHero() { - for(auto & i : army) - i.clear(); + army.clear(); } void applyGs(CGameState * gs); + TavernHeroSlot slotID; + TavernSlotRole roleID; PlayerColor player; - si32 hid[GameConstants::AVAILABLE_HEROES_PER_PLAYER]; //-1 if no hero - CSimpleArmy army[GameConstants::AVAILABLE_HEROES_PER_PLAYER]; + HeroTypeID hid; //HeroTypeID::NONE if no hero + CSimpleArmy army; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { + h & slotID; + h & roleID; h & player; h & hid; h & army; @@ -692,7 +696,7 @@ struct DLL_LINKAGE HeroRecruited : public CPackForClient { void applyGs(CGameState * gs) const; - si32 hid = -1; //subID of hero + HeroTypeID hid; //subID of hero ObjectInstanceID tid; ObjectInstanceID boatId; int3 tile; @@ -2437,12 +2441,12 @@ struct DLL_LINKAGE SetFormation : public CPackForServer struct DLL_LINKAGE HireHero : public CPackForServer { HireHero() = default; - HireHero(si32 HID, const ObjectInstanceID & TID) + HireHero(HeroTypeID HID, const ObjectInstanceID & TID) : hid(HID) , tid(TID) { } - si32 hid = 0; //available hero serial + HeroTypeID hid; //available hero serial ObjectInstanceID tid; //town (tavern) id PlayerColor player; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index b8368d741..eb1a1ce7f 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -20,6 +20,7 @@ #include "spells/CSpellHandler.h" #include "CCreatureHandler.h" #include "gameState/CGameState.h" +#include "gameState/TavernHeroesPool.h" #include "CStack.h" #include "battle/BattleInfo.h" #include "CTownHandler.h" @@ -151,7 +152,7 @@ void FoWChange::visitTyped(ICPackVisitor & visitor) visitor.visitFoWChange(*this); } -void SetAvailableHeroes::visitTyped(ICPackVisitor & visitor) +void SetAvailableHero::visitTyped(ICPackVisitor & visitor) { visitor.visitSetAvailableHeroes(*this); } @@ -939,18 +940,9 @@ void FoWChange::applyGs(CGameState *gs) } } -void SetAvailableHeroes::applyGs(CGameState *gs) +void SetAvailableHero::applyGs(CGameState *gs) { - PlayerState *p = gs->getPlayerState(player); - p->availableHeroes.clear(); - - for (int i = 0; i < GameConstants::AVAILABLE_HEROES_PER_PLAYER; i++) - { - CGHeroInstance *h = (hid[i]>=0 ? gs->hpool.heroesPool[hid[i]].get() : nullptr); - if(h && army[i]) - h->setToArmy(army[i]); - p->availableHeroes.emplace_back(h); - } + gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID); } void GiveBonus::applyGs(CGameState *gs) @@ -1132,7 +1124,16 @@ void RemoveObject::applyGs(CGameState *gs) PlayerState * p = gs->getPlayerState(beatenHero->tempOwner); gs->map->heroesOnMap -= beatenHero; p->heroes -= beatenHero; - beatenHero->detachFrom(*beatenHero->whereShouldBeAttachedOnSiege(gs)); + + + auto * siegeNode = beatenHero->whereShouldBeAttachedOnSiege(gs); + + // FIXME: workaround: + // hero should be attached to siegeNode after battle + // however this code might also be called on dismissing hero while in town + if (siegeNode && vstd::contains(beatenHero->getParentNodes(), siegeNode)) + beatenHero->detachFrom(*siegeNode); + beatenHero->tempOwner = PlayerColor::NEUTRAL; //no one owns beaten hero vstd::erase_if(beatenHero->artifactsInBackpack, [](const ArtSlotInfo& asi) { @@ -1150,11 +1151,8 @@ void RemoveObject::applyGs(CGameState *gs) beatenHero->inTownGarrison = false; } //return hero to the pool, so he may reappear in tavern - gs->hpool.heroesPool[beatenHero->subID] = beatenHero; - - if(!vstd::contains(gs->hpool.pavailable, beatenHero->subID)) - gs->hpool.pavailable[beatenHero->subID] = 0xff; + gs->heroesPool->addHeroToPool(beatenHero); gs->map->objects[id.getNum()] = nullptr; //If hero on Boat is removed, the Boat disappears @@ -1379,8 +1377,7 @@ void SetHeroesInTown::applyGs(CGameState * gs) const void HeroRecruited::applyGs(CGameState * gs) const { - assert(vstd::contains(gs->hpool.heroesPool, hid)); - CGHeroInstance *h = gs->hpool.heroesPool[hid]; + CGHeroInstance *h = gs->heroesPool->takeHeroFromPool(hid); CGTownInstance *t = gs->getTown(tid); PlayerState *p = gs->getPlayerState(player); @@ -1411,7 +1408,6 @@ void HeroRecruited::applyGs(CGameState * gs) const } } - gs->hpool.heroesPool.erase(hid); if(h->id == ObjectInstanceID()) { h->id = ObjectInstanceID(static_cast(gs->map->objects.size())); @@ -2021,26 +2017,17 @@ void NewTurn::applyGs(CGameState *gs) { CGHeroInstance *hero = gs->getHero(h.id); if(!hero) - { - // retreated or surrendered hero who has not been reset yet - for(auto& hp : gs->hpool.heroesPool) - { - if(hp.second->id == h.id) - { - hero = hp.second; - break; - } - } - } - if(!hero) { logGlobal->error("Hero %d not found in NewTurn::applyGs", h.id.getNum()); continue; } + hero->setMovementPoints(h.move); hero->mana = h.mana; } + gs->heroesPool->onNewDay(); + for(const auto & re : res) { assert(re.first < PlayerColor::PLAYER_LIMIT); diff --git a/lib/Rect.h b/lib/Rect.h index 981f62693..6197a4c18 100644 --- a/lib/Rect.h +++ b/lib/Rect.h @@ -160,7 +160,7 @@ public: h & x; h & y; h & w; - h & h; + h & this->h; } }; diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 495892d82..31b4dfe15 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -35,9 +35,9 @@ struct DLL_LINKAGE PlayerSettings }; Ebonus bonus; - si16 castle; - si32 hero, - heroPortrait; //-1 if default, else ID + FactionID castle; + HeroTypeID hero; + HeroTypeID heroPortrait; //-1 if default, else ID std::string heroName; PlayerColor color; //from 0 - diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 52c0ebc53..7108232d9 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -12,6 +12,7 @@ #include "EVictoryLossCheckResult.h" #include "InfoAboutArmy.h" +#include "TavernHeroesPool.h" #include "CGameStateCampaign.h" #include "SThievesGuildInfo.h" @@ -102,81 +103,6 @@ static CGObjectInstance * createObject(const Obj & id, int subid, const int3 & p return nobj; } -CGHeroInstance * CGameState::HeroesPool::pickHeroFor(bool native, - const PlayerColor & player, - const CTown * town, - std::map> & available, - CRandomGenerator & rand, - const CHeroClass * bannedClass) const -{ - CGHeroInstance *ret = nullptr; - - if(player>=PlayerColor::PLAYER_LIMIT) - { - logGlobal->error("Cannot pick hero for faction %s. Wrong owner!", town->faction->getJsonKey()); - return nullptr; - } - - std::vector pool; - - if(native) - { - for(auto & elem : available) - { - if(pavailable.find(elem.first)->second & 1<type->heroClass->faction == town->faction->getIndex()) - { - pool.push_back(elem.second); //get all available heroes - } - } - if(pool.empty()) - { - logGlobal->error("Cannot pick native hero for %s. Picking any...", player.getStr()); - return pickHeroFor(false, player, town, available, rand); - } - else - { - ret = *RandomGeneratorUtil::nextItem(pool, rand); - } - } - else - { - int sum = 0; - int r; - - for(auto & elem : available) - { - if (pavailable.find(elem.first)->second & (1<type->heroClass != bannedClass) ) // and his class is not same as other hero - { - pool.push_back(elem.second); - sum += elem.second->type->heroClass->selectionProbability[town->faction->getId()]; //total weight - } - } - if(pool.empty() || sum == 0) - { - logGlobal->error("There are no heroes available for player %s!", player.getStr()); - return nullptr; - } - - r = rand.nextInt(sum - 1); - for (auto & elem : pool) - { - r -= elem->type->heroClass->selectionProbability[town->faction->getId()]; - if(r < 0) - { - ret = elem; - break; - } - } - if(!ret) - ret = pool.back(); - } - - available.erase(ret->subID); - return ret; -} - HeroTypeID CGameState::pickNextHeroType(const PlayerColor & owner) { const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(owner); @@ -459,6 +385,7 @@ int CGameState::getDate(Date::EDateType mode) const CGameState::CGameState() { gs = this; + heroesPool = std::make_unique(); applier = std::make_shared>(); registerTypesClientPacks1(*applier); registerTypesClientPacks2(*applier); @@ -469,9 +396,6 @@ CGameState::~CGameState() { map.dellNull(); curB.dellNull(); - - for(auto ptr : hpool.heroesPool) // clean hero pool - ptr.second.dellNull(); } void CGameState::preInit(Services * services) @@ -951,8 +875,7 @@ void CGameState::initHeroes() if(!vstd::contains(heroesToCreate, HeroTypeID(ph->subID))) continue; ph->initHero(getRandomGenerator()); - hpool.heroesPool[ph->subID] = ph; - hpool.pavailable[ph->subID] = 0xff; + heroesPool->addHeroToPool(ph); heroesToCreate.erase(ph->type->getId()); map->allHeroes[ph->subID] = ph; @@ -965,14 +888,11 @@ void CGameState::initHeroes() int typeID = htype.getNum(); map->allHeroes[typeID] = vhi; - hpool.heroesPool[typeID] = vhi; - hpool.pavailable[typeID] = 0xff; + heroesPool->addHeroToPool(vhi); } for(auto & elem : map->disposedHeroes) - { - hpool.pavailable[elem.heroId] = elem.players; - } + heroesPool->setAvailability(elem.heroId, elem.players); if (campaign) campaign->initHeroes(); @@ -2067,17 +1987,6 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) #undef FILL_FIELD } -std::map > CGameState::unusedHeroesFromPool() -{ - std::map > pool = hpool.heroesPool; - for(const auto & player : players) - for(auto availableHero : player.second.availableHeroes) - if(availableHero) - pool.erase((*availableHero).subID); - - return pool; -} - void CGameState::buildBonusSystemTree() { buildGlobalTeamPlayerTree(); diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 99548f5b7..0281aa18e 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -29,6 +29,7 @@ struct EventCondition; struct CampaignTravel; class CStackInstance; class CGameStateCampaign; +class TavernHeroesPool; struct SThievesGuildInfo; template class CApplier; @@ -78,25 +79,10 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EVictoryLossCheck class DLL_LINKAGE CGameState : public CNonConstInfoCallback { friend class CGameStateCampaign; + public: - struct DLL_LINKAGE HeroesPool - { - std::map > heroesPool; //[subID] - heroes available to buy; nullptr if not available - std::map pavailable; // [subid] -> which players can recruit hero (binary flags) - - CGHeroInstance * pickHeroFor(bool native, - const PlayerColor & player, - const CTown * town, - std::map> & available, - CRandomGenerator & rand, - const CHeroClass * bannedClass = nullptr) const; - - template void serialize(Handler &h, const int version) - { - h & heroesPool; - h & pavailable; - } - } hpool; //we have here all heroes available on this map that are not hired + //we have here all heroes available on this map that are not hired + std::unique_ptr heroesPool; CGameState(); virtual ~CGameState(); @@ -142,7 +128,6 @@ public: bool checkForStandardLoss(const PlayerColor & player) const; //checks if given player lost the game void obtainPlayersStats(SThievesGuildInfo & tgi, int level); //fills tgi with info about other players that is available at given level of thieves' guild - std::map > unusedHeroesFromPool(); //heroes pool without heroes that are available in taverns bool isVisible(int3 pos, const std::optional & player) const override; bool isVisible(const CGObjectInstance * obj, const std::optional & player) const override; @@ -169,7 +154,7 @@ public: h & map; h & players; h & teams; - h & hpool; + h & heroesPool; h & globalEffects; h & rand; h & rumor; diff --git a/lib/gameState/TavernHeroesPool.cpp b/lib/gameState/TavernHeroesPool.cpp new file mode 100644 index 000000000..895c052e2 --- /dev/null +++ b/lib/gameState/TavernHeroesPool.cpp @@ -0,0 +1,142 @@ +/* + * TavernHeroesPool.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 "TavernHeroesPool.h" + +#include "../mapObjects/CGHeroInstance.h" +#include "../CHeroHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +TavernHeroesPool::~TavernHeroesPool() +{ + for(const auto & ptr : heroesPool) // clean hero pool + delete ptr.second; +} + +std::map TavernHeroesPool::unusedHeroesFromPool() const +{ + std::map pool = heroesPool; + for(const auto & slot : currentTavern) + pool.erase(HeroTypeID(slot.hero->subID)); + + return pool; +} + +TavernSlotRole TavernHeroesPool::getSlotRole(HeroTypeID hero) const +{ + for (auto const & slot : currentTavern) + { + if (HeroTypeID(slot.hero->subID) == hero) + return slot.role; + } + return TavernSlotRole::NONE; +} + +void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role) +{ + vstd::erase_if(currentTavern, [&](const TavernSlot & entry){ + return entry.player == player && entry.slot == slot; + }); + + if (hero == HeroTypeID::NONE) + return; + + CGHeroInstance * h = heroesPool[hero]; + + if (h && army) + h->setToArmy(army); + + TavernSlot newSlot; + newSlot.hero = h; + newSlot.player = player; + newSlot.role = role; + newSlot.slot = slot; + + currentTavern.push_back(newSlot); + + boost::range::sort(currentTavern, [](const TavernSlot & left, const TavernSlot & right) + { + if (left.slot == right.slot) + return left.player < right.player; + else + return left.slot < right.slot; + }); +} + +bool TavernHeroesPool::isHeroAvailableFor(HeroTypeID hero, PlayerColor color) const +{ + if (perPlayerAvailability.count(hero)) + return perPlayerAvailability.at(hero) & (1 << color.getNum()); + + return true; +} + +std::vector TavernHeroesPool::getHeroesFor(PlayerColor color) const +{ + std::vector result; + + for(const auto & slot : currentTavern) + { + if (slot.player == color) + result.push_back(slot.hero); + } + + return result; +} + +CGHeroInstance * TavernHeroesPool::takeHeroFromPool(HeroTypeID hero) +{ + assert(heroesPool.count(hero)); + + CGHeroInstance * result = heroesPool[hero]; + heroesPool.erase(hero); + + vstd::erase_if(currentTavern, [&](const TavernSlot & entry){ + return entry.hero->type->getId() == hero; + }); + + assert(result); + return result; +} + +void TavernHeroesPool::onNewDay() +{ + for(auto & hero : heroesPool) + { + assert(hero.second); + if(!hero.second) + continue; + + hero.second->setMovementPoints(hero.second->movementPointsLimit(true)); + hero.second->mana = hero.second->manaLimit(); + } + + for (auto & slot : currentTavern) + { + if (slot.role == TavernSlotRole::RETREATED_TODAY) + slot.role = TavernSlotRole::RETREATED; + + if (slot.role == TavernSlotRole::SURRENDERED_TODAY) + slot.role = TavernSlotRole::SURRENDERED; + } +} + +void TavernHeroesPool::addHeroToPool(CGHeroInstance * hero) +{ + heroesPool[HeroTypeID(hero->subID)] = hero; +} + +void TavernHeroesPool::setAvailability(HeroTypeID hero, PlayerColor::Mask mask) +{ + perPlayerAvailability[hero] = mask; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/TavernHeroesPool.h b/lib/gameState/TavernHeroesPool.h new file mode 100644 index 000000000..97c54879c --- /dev/null +++ b/lib/gameState/TavernHeroesPool.h @@ -0,0 +1,87 @@ +/* + * TavernHeroesPool.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 "TavernSlot.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class CTown; +class CRandomGenerator; +class CHeroClass; +class CGameState; +class CSimpleArmy; + +class DLL_LINKAGE TavernHeroesPool +{ + struct TavernSlot + { + CGHeroInstance * hero; + TavernHeroSlot slot; + TavernSlotRole role; + PlayerColor player; + + template void serialize(Handler &h, const int version) + { + h & hero; + h & slot; + h & role; + h & player; + } + }; + + /// list of all heroes in pool, including those currently present in taverns + std::map heroesPool; + + /// list of which players are able to purchase specific hero + /// if hero is not present in list, he is available for everyone + std::map perPlayerAvailability; + + /// list of heroes currently available in taverns + std::vector currentTavern; + +public: + ~TavernHeroesPool(); + + /// Returns heroes currently availabe in tavern of a specific player + std::vector getHeroesFor(PlayerColor color) const; + + /// returns heroes in pool without heroes that are available in taverns + std::map unusedHeroesFromPool() const; + + /// Returns true if hero is available to a specific player + bool isHeroAvailableFor(HeroTypeID hero, PlayerColor color) const; + + TavernSlotRole getSlotRole(HeroTypeID hero) const; + + CGHeroInstance * takeHeroFromPool(HeroTypeID hero); + + /// reset mana and movement points for all heroes in pool + void onNewDay(); + + void addHeroToPool(CGHeroInstance * hero); + + /// Marks hero as available to only specific set of players + void setAvailability(HeroTypeID hero, PlayerColor::Mask mask); + + /// Makes hero available in tavern of specified player + void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role); + + template void serialize(Handler &h, const int version) + { + h & heroesPool; + h & perPlayerAvailability; + h & currentTavern; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/TavernSlot.h b/lib/gameState/TavernSlot.h new file mode 100644 index 000000000..192fd047d --- /dev/null +++ b/lib/gameState/TavernSlot.h @@ -0,0 +1,35 @@ +/* + * TavernSlot.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 + +enum class TavernHeroSlot : int8_t +{ + NONE = -1, + NATIVE, // 1st / left slot in tavern, contains hero native to player's faction on new week + RANDOM // 2nd / right slot in tavern, contains hero of random class +}; + +enum class TavernSlotRole : int8_t +{ + NONE = -1, + + SINGLE_UNIT, // hero was added after buying hero from this slot, and only has 1 creature in army + FULL_ARMY, // hero was added to tavern on new week and still has full army + + RETREATED, // hero was owned by player before, but have retreated from battle and only has 1 creature in army + RETREATED_TODAY, + + SURRENDERED, // hero was owned by player before, but have surrendered in battle and kept some troops + SURRENDERED_TODAY, +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 18926795b..22265c0cb 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -57,6 +57,8 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER_CLASS("shrine", ShrineInstanceConstructor); SET_HANDLER_CLASS("hillFort", HillFortInstanceConstructor); SET_HANDLER_CLASS("shipyard", ShipyardInstanceConstructor); + SET_HANDLER_CLASS("monster", CreatureInstanceConstructor); + SET_HANDLER_CLASS("resource", ResourceInstanceConstructor); SET_HANDLER_CLASS("static", CObstacleConstructor); SET_HANDLER_CLASS("", CObstacleConstructor); @@ -73,7 +75,6 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER("artifact", CGArtifact); SET_HANDLER("borderGate", CGBorderGate); SET_HANDLER("borderGuard", CGBorderGuard); - SET_HANDLER("monster", CGCreature); SET_HANDLER("denOfThieves", CGDenOfthieves); SET_HANDLER("event", CGEvent); SET_HANDLER("garrison", CGGarrison); @@ -87,7 +88,6 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER("pandora", CGPandoraBox); SET_HANDLER("prison", CGHeroInstance); SET_HANDLER("questGuard", CGQuestGuard); - SET_HANDLER("resource", CGResource); SET_HANDLER("scholar", CGScholar); SET_HANDLER("seerHut", CGSeerHut); SET_HANDLER("sign", CGSignBottle); diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index 4a21d8825..d786d7f04 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -35,6 +35,26 @@ bool CObstacleConstructor::isStaticObject() return true; } +bool CreatureInstanceConstructor::hasNameTextID() const +{ + return true; +} + +std::string CreatureInstanceConstructor::getNameTextID() const +{ + return VLC->creatures()->getByIndex(getSubIndex())->getNamePluralTextID(); +} + +bool ResourceInstanceConstructor::hasNameTextID() const +{ + return true; +} + +std::string ResourceInstanceConstructor::getNameTextID() const +{ + return TextIdentifier("core", "restypes", getSubIndex()).get(); +} + void CTownInstanceConstructor::initTypeData(const JsonNode & input) { VLC->modh->identifiers.requestIdentifier("faction", input["faction"], [&](si32 index) @@ -86,6 +106,16 @@ void CTownInstanceConstructor::randomizeObject(CGTownInstance * object, CRandomG object->appearance = templ; } +bool CTownInstanceConstructor::hasNameTextID() const +{ + return true; +} + +std::string CTownInstanceConstructor::getNameTextID() const +{ + return faction->getNameTextID(); +} + void CHeroInstanceConstructor::initTypeData(const JsonNode & input) { VLC->modh->identifiers.requestIdentifier( @@ -133,6 +163,16 @@ void CHeroInstanceConstructor::randomizeObject(CGHeroInstance * object, CRandomG } +bool CHeroInstanceConstructor::hasNameTextID() const +{ + return true; +} + +std::string CHeroInstanceConstructor::getNameTextID() const +{ + return heroClass->getNameTextID(); +} + void BoatInstanceConstructor::initTypeData(const JsonNode & input) { layer = EPathfindingLayer::SAIL; diff --git a/lib/mapObjectConstructors/CommonConstructors.h b/lib/mapObjectConstructors/CommonConstructors.h index 4a58d699e..53e73d8d8 100644 --- a/lib/mapObjectConstructors/CommonConstructors.h +++ b/lib/mapObjectConstructors/CommonConstructors.h @@ -13,6 +13,7 @@ #include "../LogicalExpression.h" #include "../mapObjects/MiscObjects.h" +#include "../mapObjects/CGCreature.h" VCMI_LIB_NAMESPACE_BEGIN @@ -22,6 +23,7 @@ class CGTownInstance; class CGHeroInstance; class CGMarket; class CHeroClass; +class CGCreature; class CBank; class CGBoat; class CFaction; @@ -33,6 +35,20 @@ public: bool isStaticObject() override; }; +class CreatureInstanceConstructor : public CDefaultObjectTypeHandler +{ +public: + bool hasNameTextID() const override; + std::string getNameTextID() const override; +}; + +class ResourceInstanceConstructor : public CDefaultObjectTypeHandler +{ +public: + bool hasNameTextID() const override; + std::string getNameTextID() const override; +}; + class CTownInstanceConstructor : public CDefaultObjectTypeHandler { JsonNode filtersJson; @@ -48,6 +64,9 @@ public: void randomizeObject(CGTownInstance * object, CRandomGenerator & rng) const override; void afterLoadFinalization() override; + bool hasNameTextID() const override; + std::string getNameTextID() const override; + template void serialize(Handler &h, const int version) { h & filtersJson; @@ -72,6 +91,9 @@ public: void randomizeObject(CGHeroInstance * object, CRandomGenerator & rng) const override; void afterLoadFinalization() override; + bool hasNameTextID() const override; + std::string getNameTextID() const override; + template void serialize(Handler &h, const int version) { h & filtersJson; diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 5d17b5c48..3253e3e36 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -324,6 +324,21 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const { if(count) //there are available creatures { + + if (VLC->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED)) + { + SlotID testSlot = h->getSlotFor(crid); + if(!testSlot.validSlot()) //no available slot - try merging army of visiting hero + { + std::pair toMerge; + if (h->mergableStacks(toMerge)) + { + cb->moveStack(StackLocation(h, toMerge.first), StackLocation(h, toMerge.second), -1); //merge toMerge.first into toMerge.second + assert(!h->hasStackAtSlot(toMerge.first)); //we have now a new free slot + } + } + } + SlotID slot = h->getSlotFor(crid); if(!slot.validSlot()) //no available slot { diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index 58a3568d2..a6ef1e973 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -56,10 +56,10 @@ struct DLL_LINKAGE DisposedHero { DisposedHero(); - ui32 heroId; - ui32 portrait; /// The portrait id of the hero, -1 is default. + HeroTypeID heroId; + HeroTypeID portrait; /// The portrait id of the hero, -1 is default. std::string name; - ui8 players; /// Who can hire this hero (bitfield). + PlayerColor::Mask players; /// Who can hire this hero (bitfield). template void serialize(Handler & h, const int version) diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index c23a60074..d4a154816 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -107,6 +107,8 @@ void registerTypesMapObjectTypes(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); + s.template registerType(); + s.template registerType(); #define REGISTER_GENERIC_HANDLER(TYPENAME) s.template registerType >() @@ -239,7 +241,7 @@ void registerTypesClientPacks1(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(); diff --git a/lib/registerTypes/TypesLobbyPacks.cpp b/lib/registerTypes/TypesLobbyPacks.cpp index a347447f4..8ac2734a7 100644 --- a/lib/registerTypes/TypesLobbyPacks.cpp +++ b/lib/registerTypes/TypesLobbyPacks.cpp @@ -14,6 +14,7 @@ #include "../StartInfo.h" #include "../gameState/CGameState.h" #include "../gameState/CGameStateCampaign.h" +#include "../gameState/TavernHeroesPool.h" #include "../mapping/CMap.h" #include "../CModHandler.h" #include "../mapObjects/CObjectHandler.h" diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 2e75c38a1..72315f76e 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -606,6 +606,8 @@ bool ObjectManager::addGuard(rmg::Object & object, si32 strength, bool zoneGuard auto & instance = object.addInstance(*guard); instance.setPosition(guardPos - object.getPosition()); instance.setAnyTemplate(); //terrain is irrelevant for monsters, but monsters need some template now + //Make up for extra offset in HotA creature templates + instance.setPosition(instance.getPosition() + instance.object().getVisitableOffset()); return true; } diff --git a/lib/spells/TargetCondition.cpp b/lib/spells/TargetCondition.cpp index 7c43b15a5..cc0cbf0d5 100644 --- a/lib/spells/TargetCondition.cpp +++ b/lib/spells/TargetCondition.cpp @@ -541,7 +541,7 @@ void TargetCondition::loadConditions(const JsonNode & source, bool exclusive, bo CModHandler::parseIdentifier(keyValue.first, scope, type, identifier); - item = itemFactory->createConfigurable(scope, type, identifier); + item = itemFactory->createConfigurable(keyValue.second.meta, type, identifier); } if(item) diff --git a/lib/spells/effects/Timed.cpp b/lib/spells/effects/Timed.cpp index a00104e6a..8fac34e2d 100644 --- a/lib/spells/effects/Timed.cpp +++ b/lib/spells/effects/Timed.cpp @@ -250,7 +250,10 @@ void Timed::serializeJsonUnitEffect(JsonSerializeFormat & handler) auto guard = handler.enterStruct(p.first); const JsonNode & bonusNode = handler.getCurrent(); auto b = JsonUtils::parseBonus(bonusNode); - bonus.push_back(b); + if (b) + bonus.push_back(b); + else + logMod->error("Failed to parse bonus '%s'!", p.first); } } } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index e430070ed..00412911a 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -8,6 +8,14 @@ * */ #include "StdInc.h" +#include "CGameHandler.h" + +#include "HeroPoolProcessor.h" +#include "ServerNetPackVisitors.h" +#include "ServerSpellCastEnvironment.h" +#include "CVCMIServer.h" + +#include "PlayerMessageProcessor.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/FileInfo.h" @@ -35,7 +43,6 @@ #include "../lib/GameSettings.h" #include "../lib/battle/BattleInfo.h" #include "../lib/CondSh.h" -#include "ServerNetPackVisitors.h" #include "../lib/VCMI_Lib.h" #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapService.h" @@ -44,9 +51,6 @@ #include "../lib/ScopeGuard.h" #include "../lib/CSoundBase.h" #include "../lib/TerrainHandler.h" -#include "CGameHandler.h" -#include "ServerSpellCastEnvironment.h" -#include "CVCMIServer.h" #include "../lib/CCreatureSet.h" #include "../lib/CThreadHelper.h" #include "../lib/GameConstants.h" @@ -294,6 +298,11 @@ events::EventBus * CGameHandler::eventBus() const return serverEventBus.get(); } +CVCMIServer * CGameHandler::gameLobby() const +{ + return lobby; +} + void CGameHandler::levelUpHero(const CGHeroInstance * hero, SecondarySkill skill) { changeSecSkill(hero, skill, 1, 0); @@ -868,24 +877,12 @@ void CGameHandler::battleAfterLevelUp(const BattleResult &result) std::set playerColors = {finishingBattle->loser, finishingBattle->victor}; checkVictoryLossConditions(playerColors); - if (result.result == BattleResult::SURRENDER || result.result == BattleResult::ESCAPE) //loser has escaped or surrendered - { - SetAvailableHeroes sah; - sah.player = finishingBattle->loser; - sah.hid[0] = finishingBattle->loserHero->subID; - if (result.result == BattleResult::ESCAPE) //retreat - { - sah.army[0].clear(); - sah.army[0].setCreature(SlotID(0), finishingBattle->loserHero->type->initialArmy.at(0).creature, 1); - } + if (result.result == BattleResult::SURRENDER) + heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero); - if (const CGHeroInstance *another = getPlayerState(finishingBattle->loser)->availableHeroes.at(0)) - sah.hid[1] = another->subID; - else - sah.hid[1] = -1; + if (result.result == BattleResult::ESCAPE) + heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero); - sendAndApply(&sah); - } if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) { @@ -893,20 +890,7 @@ void CGameHandler::battleAfterLevelUp(const BattleResult &result) sendAndApply(&ro); if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) - { - SetAvailableHeroes sah; - sah.player = finishingBattle->victor; - sah.hid[0] = finishingBattle->winnerHero->subID; - sah.army[0].clear(); - sah.army[0].setCreature(SlotID(0), finishingBattle->winnerHero->type->initialArmy.at(0).creature, 1); - - if (const CGHeroInstance *another = getPlayerState(finishingBattle->victor)->availableHeroes.at(0)) - sah.hid[1] = another->subID; - else - sah.hid[1] = -1; - - sendAndApply(&sah); - } + heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); } finishingBattle.reset(); @@ -1240,7 +1224,7 @@ void CGameHandler::handleClientDisconnection(std::shared_ptr c) if(playerConnection != playerConnections.second.end()) { std::string messageText = boost::str(boost::format("%s (cid %d) was disconnected") % playerSettings->name % c->connectionID); - playerMessage(playerId, messageText, ObjectInstanceID{}); + playerMessages->broadcastMessage(playerId, messageText); } } } @@ -1576,6 +1560,8 @@ int CGameHandler::moveStack(int stack, BattleHex dest) CGameHandler::CGameHandler(CVCMIServer * lobby) : lobby(lobby) + , heroPool(std::make_unique(this)) + , playerMessages(std::make_unique(this)) , complainNoCreatures("No creatures to split") , complainNotEnoughCreatures("Cannot split that stack, not enough creatures!") , complainInvalidSlot("Invalid slot accessed!") @@ -1765,27 +1751,6 @@ void CGameHandler::newTurn() } } - std::map > pool = gs->hpool.heroesPool; - - for (auto& hp : pool) - { - auto hero = hp.second; - if (hero->isInitialized() && hero->stacks.size()) - { - // reset retreated or surrendered heroes - auto maxmove = hero->movementPointsLimit(true); - // if movement is greater than maxmove, we should decrease it - if (hero->movementPointsRemaining() != maxmove || hero->mana < hero->manaLimit()) - { - NewTurn::Hero hth; - hth.id = hero->id; - hth.move = maxmove; - hth.mana = hero->getManaNewTurn(); - n.heroes.insert(hth); - } - } - } - for (auto & elem : gs->players) { if (elem.first == PlayerColor::NEUTRAL) @@ -1797,29 +1762,7 @@ void CGameHandler::newTurn() hadGold.insert(playerGold); if (newWeek) //new heroes in tavern - { - SetAvailableHeroes sah; - sah.player = elem.first; - - //pick heroes and their armies - CHeroClass *banned = nullptr; - for (int j = 0; j < GameConstants::AVAILABLE_HEROES_PER_PLAYER; j++) - { - //first hero - native if possible, second hero -> any other class - if (CGHeroInstance *h = gs->hpool.pickHeroFor(j == 0, elem.first, getNativeTown(elem.first), pool, getRandomGenerator(), banned)) - { - sah.hid[j] = h->subID; - h->initArmy(getRandomGenerator(), &sah.army[j]); - banned = h->type->heroClass; - } - else - { - sah.hid[j] = -1; - } - } - - sendAndApply(&sah); - } + heroPool->onNewWeek(elem.first); n.res[elem.first] = elem.second.resources; @@ -2709,14 +2652,6 @@ void CGameHandler::changeSpells(const CGHeroInstance * hero, bool give, const st sendAndApply(&cs); } -void CGameHandler::sendMessageTo(std::shared_ptr c, const std::string &message) -{ - SystemMessage sm; - sm.text = message; - boost::unique_lock lock(*c->mutexWrite); - *(c.get()) << &sm; -} - void CGameHandler::giveHeroBonus(GiveBonus * bonus) { sendAndApply(bonus); @@ -2927,10 +2862,8 @@ bool CGameHandler::isPlayerOwns(CPackForServer * pack, ObjectInstanceID id) void CGameHandler::throwNotAllowedAction(CPackForServer * pack) { if(pack->c) - { - SystemMessage temp_message("You are not allowed to perform this action!"); - pack->c->sendPack(&temp_message); - } + playerMessages->sendSystemMessage(pack->c, "You are not allowed to perform this action!"); + logNetwork->error("Player is not allowed to perform this action!"); throw ExceptionNotAllowedAction(); } @@ -2940,11 +2873,9 @@ void CGameHandler::wrongPlayerMessage(CPackForServer * pack, PlayerColor expecte std::ostringstream oss; oss << "You were identified as player " << getPlayerAt(pack->c) << " while expecting " << expectedplayer; logNetwork->error(oss.str()); + if(pack->c) - { - SystemMessage temp_message(oss.str()); - pack->c->sendPack(&temp_message); - } + playerMessages->sendSystemMessage(pack->c, oss.str()); } void CGameHandler::throwOnWrongOwner(CPackForServer * pack, ObjectInstanceID id) @@ -3628,7 +3559,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, sendAndApply(&fw); if(t->visitingHero) - objectVisited(t, t->visitingHero); + visitCastleObjects(t, t->visitingHero); if(t->garrisonHero) visitCastleObjects(t, t->garrisonHero); @@ -3659,13 +3590,6 @@ bool CGameHandler::razeStructure (ObjectInstanceID tid, BuildingID bid) return true; } -void CGameHandler::sendMessageToAll(const std::string &message) -{ - SystemMessage sm; - sm.text = message; - sendToAllClients(&sm); -} - bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dstid, CreatureID crid, ui32 cram, si32 fromLvl) { const CGDwelling * dw = static_cast(getObj(objid)); @@ -4383,93 +4307,6 @@ bool CGameHandler::setFormation(ObjectInstanceID hid, ui8 formation) return true; } -bool CGameHandler::hireHero(const CGObjectInstance *obj, ui8 hid, PlayerColor player) -{ - const PlayerState * p = getPlayerState(player); - const CGTownInstance * t = getTown(obj->id); - - //common preconditions -// if ((p->resources.at(EGameResID::GOLD)= GameConstants::MAX_HEROES_PER_PLAYER && complain("Cannot hire hero, only 8 wandering heroes are allowed!"))) - if ((p->resources[EGameResID::GOLD] < GameConstants::HERO_GOLD_COST && complain("Not enough gold for buying hero!")) - || ((getHeroCount(player, false) >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) && complain("Cannot hire hero, too many wandering heroes already!"))) - || ((getHeroCount(player, true) >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP) && complain("Cannot hire hero, too many heroes garrizoned and wandering already!")))) - { - return false; - } - - if (t) //tavern in town - { - if ((!t->hasBuilt(BuildingID::TAVERN) && complain("No tavern!")) - || (t->visitingHero && complain("There is visiting hero - no place!"))) - { - return false; - } - } - else if (obj->ID == Obj::TAVERN) - { - if (getTile(obj->visitablePos())->visitableObjects.back() != obj && complain("Tavern entry must be unoccupied!")) - { - return false; - } - } - - const CGHeroInstance *nh = p->availableHeroes.at(hid); - if (!nh) - { - complain ("Hero is not available for hiring!"); - return false; - } - - HeroRecruited hr; - hr.tid = obj->id; - hr.hid = nh->subID; - hr.player = player; - hr.tile = nh->convertFromVisitablePos(obj->visitablePos()); - if (getTile(hr.tile)->isWater()) - { - //Create a new boat for hero - createObject(obj->visitablePos(), Obj::BOAT, nh->getBoatType().getNum()); - - hr.boatId = getTopObj(hr.tile)->id; - } - sendAndApply(&hr); - - std::map > pool = gs->unusedHeroesFromPool(); - - const CGHeroInstance *theOtherHero = p->availableHeroes.at(!hid); - const CGHeroInstance *newHero = nullptr; - if (theOtherHero) //on XXL maps all heroes can be imprisoned :( - { - newHero = gs->hpool.pickHeroFor(false, player, getNativeTown(player), pool, getRandomGenerator(), theOtherHero->type->heroClass); - } - - SetAvailableHeroes sah; - sah.player = player; - - if (newHero) - { - sah.hid[hid] = newHero->subID; - sah.army[hid].clear(); - sah.army[hid].setCreature(SlotID(0), newHero->type->initialArmy[0].creature, 1); - } - else - { - sah.hid[hid] = -1; - } - - sah.hid[!hid] = theOtherHero ? theOtherHero->subID : -1; - sendAndApply(&sah); - - giveResource(player, EGameResID::GOLD, -GameConstants::HERO_GOLD_COST); - - if(t) - { - objectVisited(t, nh); - } - return true; -} - bool CGameHandler::queryReply(QueryID qid, const JsonNode & answer, PlayerColor player) { boost::unique_lock lock(gsm); @@ -4977,140 +4814,6 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) return ok; } -void CGameHandler::playerMessage(PlayerColor player, const std::string &message, ObjectInstanceID currObj) -{ - bool cheated = false; - - std::vector words; - boost::split(words, message, boost::is_any_of(" ")); - - bool isHost = false; - for(auto & c : connections[player]) - if(lobby->isClientHost(c->connectionID)) - isHost = true; - - if(isHost && words.size() >= 2 && words[0] == "game") - { - if(words[1] == "exit" || words[1] == "quit" || words[1] == "end") - { - SystemMessage temp_message("game was terminated"); - sendAndApply(&temp_message); - lobby->state = EServerState::SHUTDOWN; - return; - } - if(words.size() == 3 && words[1] == "save") - { - save("Saves/" + words[2]); - SystemMessage temp_message("game saved as " + words[2]); - sendAndApply(&temp_message); - return; - } - if(words.size() == 3 && words[1] == "kick") - { - auto playername = words[2]; - PlayerColor playerToKick(PlayerColor::CANNOT_DETERMINE); - if(std::all_of(playername.begin(), playername.end(), ::isdigit)) - playerToKick = PlayerColor(std::stoi(playername)); - else - { - for(auto & c : connections) - { - if(c.first.getStr(false) == playername) - playerToKick = c.first; - } - } - - if(playerToKick != PlayerColor::CANNOT_DETERMINE) - { - PlayerCheated pc; - pc.player = playerToKick; - pc.losingCheatCode = true; - sendAndApply(&pc); - checkVictoryLossConditionsForPlayer(playerToKick); - } - return; - } - } - - int obj = 0; - if (words.size() == 2 && words[0] != "vcmiexp" && words[0] != "vcmiolorin") - { - obj = std::atoi(words[1].c_str()); - if (obj) - currObj = ObjectInstanceID(obj); - } - - const CGHeroInstance * hero = getHero(currObj); - const CGTownInstance * town = getTown(currObj); - if (!town && hero) - town = hero->visitedTown; - - if(words.size() > 1 && (words[0] == "vcmiarmy" || words[0] == "vcminissi" || words[0] == "vcmiexp" || words[0] == "vcmiolorin")) - { - std::string cheatCodeWithOneParameter = std::string(words[0]) + " " + words[1]; - handleCheatCode(cheatCodeWithOneParameter, player, hero, town, cheated); - } - else if (words.size() == 1 || obj) - { - handleCheatCode(words[0], player, hero, town, cheated); - } - else - { - for (const auto & i : gs->players) - { - if (i.first == PlayerColor::NEUTRAL) - continue; - if (words[1] == "ai") - { - if (i.second.human) - continue; - } - else if (words[1] != "all" && words[1] != i.first.getStr()) - continue; - - if (words[0] == "vcmiformenos" || words[0] == "vcmieagles" || words[0] == "vcmiungoliant" - || words[0] == "vcmiresources" || words[0] == "vcmimap" || words[0] == "vcmihidemap") - { - handleCheatCode(words[0], i.first, nullptr, nullptr, cheated); - } - else if (words[0] == "vcmiarmenelos" || words[0] == "vcmibuild") - { - for (const auto & t : i.second.towns) - { - handleCheatCode(words[0], i.first, nullptr, t, cheated); - } - } - else - { - for (const auto & h : i.second.heroes) - { - handleCheatCode(words[0], i.first, h, nullptr, cheated); - } - } - } - } - - if (cheated) - { - if(!getPlayerSettings(player)->isControlledByAI()) - { - SystemMessage temp_message(VLC->generaltexth->allTexts[260]); - sendAndApply(&temp_message); - } - - if(!player.isSpectator()) - checkVictoryLossConditionsForPlayer(player);//Player enter win code or got required art\creature - } - else - { - if(!getPlayerSettings(player)->isControlledByAI()) - { - PlayerMessageClient temp_message(player, message); - sendAndApply(&temp_message); - } - } -} - bool CGameHandler::makeCustomAction(BattleAction & ba) { switch(ba.actionType) @@ -5473,7 +5176,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) bool CGameHandler::complain(const std::string &problem) { - sendMessageToAll("Server encountered a problem: " + problem); + playerMessages->broadcastSystemMessage("Server encountered a problem: " + problem); logGlobal->error(problem); return true; } @@ -6865,225 +6568,6 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) } } -void CGameHandler::handleCheatCode(std::string & cheat, PlayerColor player, const CGHeroInstance * hero, const CGTownInstance * town, bool & cheated) -{ - //Make cheat case-insensitive - std::transform(cheat.begin(), cheat.end(), cheat.begin(), [](unsigned char c){ return std::tolower(c); }); - - if (cheat == "vcmiistari" || cheat == "vcmispells") - { - cheated = true; - if (!hero) return; - ///Give hero spellbook - if (!hero->hasSpellbook()) - giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); - - ///Give all spells with bonus (to allow banned spells) - GiveBonus giveBonus(GiveBonus::ETarget::HERO); - giveBonus.id = hero->id.getNum(); - giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, 0); - //start with level 0 to skip abilities - for (int level = 1; level <= GameConstants::SPELL_LEVELS; level++) - { - giveBonus.bonus.subtype = level; - sendAndApply(&giveBonus); - } - - ///Give mana - SetMana sm; - sm.hid = hero->id; - sm.val = 999; - sm.absolute = true; - sendAndApply(&sm); - } - else if (cheat == "vcmiarmenelos" || cheat == "vcmibuild") - { - cheated = true; - if (!town) return; - ///Build all buildings in selected town - for (auto & build : town->town->buildings) - { - if (!town->hasBuilt(build.first) - && !build.second->getNameTranslated().empty() - && build.first != BuildingID::SHIP) - { - buildStructure(town->id, build.first, true); - } - } - } - else if (cheat == "vcmiainur" || cheat == "vcmiangband" || cheat == "vcmiglaurung" || cheat == "vcmiarchangel" - || cheat == "vcmiblackknight" || cheat == "vcmicrystal" || cheat == "vcmiazure" || cheat == "vcmifaerie") - { - cheated = true; - if (!hero) return; - ///Gives N creatures into each slot - std::map> creatures; - creatures.insert(std::make_pair("vcmiainur", std::make_pair("archangel", 5))); //5 archangels - creatures.insert(std::make_pair("vcmiangband", std::make_pair("blackKnight", 10))); //10 black knights - creatures.insert(std::make_pair("vcmiglaurung", std::make_pair("crystalDragon", 5000))); //5000 crystal dragons - creatures.insert(std::make_pair("vcmiarchangel", std::make_pair("archangel", 5))); //5 archangels - creatures.insert(std::make_pair("vcmiblackknight", std::make_pair("blackKnight", 10))); //10 black knights - creatures.insert(std::make_pair("vcmicrystal", std::make_pair("crystalDragon", 5000))); //5000 crystal dragons - creatures.insert(std::make_pair("vcmiazure", std::make_pair("azureDragon", 5000))); //5000 azure dragons - creatures.insert(std::make_pair("vcmifaerie", std::make_pair("fairieDragon", 5000))); //5000 faerie dragons - - const int32_t creatureIdentifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "creature", creatures[cheat].first, false).value(); - const CCreature * creature = VLC->creh->objects.at(creatureIdentifier); - for (int i = 0; i < GameConstants::ARMY_SIZE; i++) - if (!hero->hasStackAtSlot(SlotID(i))) - insertNewStack(StackLocation(hero, SlotID(i)), creature, creatures[cheat].second); - } - else if (boost::starts_with(cheat, "vcmiarmy") || boost::starts_with(cheat, "vcminissi")) - { - cheated = true; - if (!hero) return; - - std::vector words; - boost::split(words, cheat, boost::is_any_of(" ")); - - if(words.size() < 2) - return; - - std::string creatureIdentifier = words[1]; - - std::optional creatureId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "creature", creatureIdentifier, false); - - if(creatureId.has_value()) - { - const auto * creature = CreatureID(creatureId.value()).toCreature(); - - for (int i = 0; i < GameConstants::ARMY_SIZE; i++) - if (!hero->hasStackAtSlot(SlotID(i))) - insertNewStack(StackLocation(hero, SlotID(i)), creature, 5 * std::pow(10, i)); - } - } - else if (cheat == "vcminoldor" || cheat == "vcmimachines") - { - cheated = true; - if (!hero) return; - ///Give all war machines to hero - if (!hero->getArt(ArtifactPosition::MACH1)) - giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::BALLISTA], ArtifactPosition::MACH1); - if (!hero->getArt(ArtifactPosition::MACH2)) - giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::AMMO_CART], ArtifactPosition::MACH2); - if (!hero->getArt(ArtifactPosition::MACH3)) - giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::FIRST_AID_TENT], ArtifactPosition::MACH3); - } - else if (cheat == "vcmiforgeofnoldorking" || cheat == "vcmiartifacts") - { - cheated = true; - if (!hero) return; - ///Give hero all artifacts except war machines, spell scrolls and spell book - for(int g = 7; g < VLC->arth->objects.size(); ++g) //including artifacts from mods - { - if(VLC->arth->objects[g]->canBePutAt(hero)) - giveHeroNewArtifact(hero, VLC->arth->objects[g], ArtifactPosition::FIRST_AVAILABLE); - } - } - else if (cheat == "vcmiglorfindel" || cheat == "vcmilevel") - { - cheated = true; - if (!hero) return; - ///selected hero gains a new level - changePrimSkill(hero, PrimarySkill::EXPERIENCE, VLC->heroh->reqExp(hero->level + 1) - VLC->heroh->reqExp(hero->level)); - } - else if (boost::starts_with(cheat, "vcmiexp") || boost::starts_with(cheat, "vcmiolorin")) - { - cheated = true; - if (!hero) return; - - std::vector words; - boost::split(words, cheat, boost::is_any_of(" ")); - - if(words.size() < 2) - return; - - std::string expAmount = words[1]; - long expAmountProcessed = 0; - - try - { - expAmountProcessed = std::stol(expAmount); - } - catch(std::exception&) - { - logGlobal->error("Could not parse experience amount for vcmiexp cheat"); - } - - if(expAmountProcessed > 1) - { - changePrimSkill(hero, PrimarySkill::EXPERIENCE, expAmountProcessed); - } - } - else if (cheat == "vcminahar" || cheat == "vcmimove") - { - cheated = true; - if (!hero) return; - ///Give 1000000 movement points to hero - SetMovePoints smp; - smp.hid = hero->id; - smp.val = 1000000; - sendAndApply(&smp); - - GiveBonus gb(GiveBonus::ETarget::HERO); - gb.bonus.type = BonusType::FREE_SHIP_BOARDING; - gb.bonus.duration = BonusDuration::ONE_DAY; - gb.bonus.source = BonusSource::OTHER; - gb.id = hero->id.getNum(); - giveHeroBonus(&gb); - } - else if (cheat == "vcmiformenos" || cheat == "vcmiresources") - { - cheated = true; - ///Give resources to player - TResources resources; - resources[EGameResID::GOLD] = 100000; - for (GameResID i = EGameResID::WOOD; i < EGameResID::GOLD; ++i) - resources[i] = 100; - - giveResources(player, resources); - } - else if (cheat == "vcmisilmaril" || cheat == "vcmiwin") - { - cheated = true; - ///Player wins - PlayerCheated pc; - pc.player = player; - pc.winningCheatCode = true; - sendAndApply(&pc); - } - else if (cheat == "vcmimelkor" || cheat == "vcmilose") - { - cheated = true; - ///Player looses - PlayerCheated pc; - pc.player = player; - pc.losingCheatCode = true; - sendAndApply(&pc); - } - else if (cheat == "vcmieagles" || cheat == "vcmiungoliant" || cheat == "vcmimap" || cheat == "vcmihidemap") - { - cheated = true; - ///Reveal or conceal FoW - FoWChange fc; - fc.mode = ((cheat == "vcmieagles" || cheat == "vcmimap") ? 1 : 0); - fc.player = player; - const auto & fowMap = gs->getPlayerTeam(player)->fogOfWarMap; - auto hlp_tab = new int3[gs->map->width * gs->map->height * (gs->map->levels())]; - int lastUnc = 0; - - for(int z = 0; z < gs->map->levels(); z++) - for(int x = 0; x < gs->map->width; x++) - for(int y = 0; y < gs->map->height; y++) - if(!(*fowMap)[z][x][y] || !fc.mode) - hlp_tab[lastUnc++] = int3(x, y, z); - - fc.tiles.insert(hlp_tab, hlp_tab + lastUnc); - delete [] hlp_tab; - sendAndApply(&fc); - } -} - void CGameHandler::removeObstacle(const CObstacleInstance & obstacle) { BattleObstaclesChanged obsRem; @@ -7394,3 +6878,11 @@ void CGameHandler::createObject(const int3 & visitablePosition, Obj type, int32_ no.targetPos = visitablePosition; sendAndApply(&no); } + +void CGameHandler::deserializationFix() +{ + //FIXME: pointer to GameHandler itself can't be deserialized at the moment since GameHandler is top-level entity in serialization + // restore any places that requires such pointer manually + heroPool->gameHandler = this; + playerMessages->gameHandler = this; +} diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 24e97067d..771ba90f1 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -46,9 +46,11 @@ template class CApplier; VCMI_LIB_NAMESPACE_END +class HeroPoolProcessor; class CGameHandler; class CVCMIServer; class CBaseForGHApply; +class PlayerMessageProcessor; struct PlayerStatus { @@ -97,13 +99,18 @@ class CGameHandler : public IGameCallback, public CBattleInfoCallback, public En CVCMIServer * lobby; std::shared_ptr> applier; std::unique_ptr battleThread; + public: + std::unique_ptr heroPool; + using FireShieldInfo = std::vector>; //use enums as parameters, because doMove(sth, true, false, true) is not readable enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS}; enum EVisitDest {VISIT_DEST, DONT_VISIT_DEST}; enum ELEaveTile {LEAVING_TILE, REMAINING_ON_TILE}; + std::unique_ptr playerMessages; + std::map>> connections; //player color -> connection to client with interface of that player PlayerStatuses states; //player color -> player state @@ -119,6 +126,7 @@ public: const GameCb * game() const override; vstd::CLoggerBase * logger() const override; events::EventBus * eventBus() const override; + CVCMIServer * gameLobby() const; bool isValidObject(const CGObjectInstance *obj) const; bool isBlockedByQueries(const CPack *pack, PlayerColor player); @@ -145,6 +153,7 @@ public: void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); void setBattleResult(BattleResult::EResult resultType, int victoriusSide); + CGameHandler() = default; CGameHandler(CVCMIServer * lobby); ~CGameHandler(); @@ -230,7 +239,6 @@ public: PlayerColor getPlayerAt(std::shared_ptr c) const; bool hasPlayerAt(PlayerColor player, std::shared_ptr c) const; - void playerMessage(PlayerColor player, const std::string &message, ObjectInstanceID currObj); void updateGateState(); bool makeBattleAction(BattleAction &ba); bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) @@ -240,7 +248,6 @@ public: void removeObstacle(const CObstacleInstance &obstacle); bool queryReply( QueryID qid, const JsonNode & answer, PlayerColor player ); - bool hireHero( const CGObjectInstance *obj, ui8 hid, PlayerColor player ); bool buildBoat( ObjectInstanceID objid, PlayerColor player ); bool setFormation( ObjectInstanceID hid, ui8 formation ); bool tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2); @@ -283,7 +290,12 @@ public: h & QID; h & states; h & finishingBattle; + h & heroPool; h & getRandomGenerator(); + h & playerMessages; + + if (!h.saving) + deserializationFix(); #if SCRIPTING_ENABLED JsonNode scriptsState; @@ -295,8 +307,6 @@ public: #endif } - void sendMessageToAll(const std::string &message); - void sendMessageTo(std::shared_ptr c, const std::string &message); void sendToAllClients(CPackForClient * pack); void sendAndApply(CPackForClient * pack) override; void applyAndSend(CPackForClient * pack); @@ -346,7 +356,11 @@ public: void attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & slot); void spawnWanderingMonsters(CreatureID creatureID); - void handleCheatCode(std::string & cheat, PlayerColor player, const CGHeroInstance * hero, const CGTownInstance * town, bool & cheated); + + // Check for victory and loss conditions + void checkVictoryLossConditionsForPlayer(PlayerColor player); + void checkVictoryLossConditions(const std::set & playerColors); + void checkVictoryLossConditionsForAll(); CRandomGenerator & getRandomGenerator(); @@ -355,6 +369,8 @@ public: scripting::Pool * getContextPool() const override; #endif + std::list generatePlayerTurnOrder() const; + friend class CVCMIServer; private: std::unique_ptr serverEventBus; @@ -363,16 +379,12 @@ private: #endif void reinitScripting(); + void deserializationFix(); + - std::list generatePlayerTurnOrder() const; void makeStackDoNothing(const CStack * next); void getVictoryLossMessage(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult, InfoWindow & out) const; - // Check for victory and loss conditions - void checkVictoryLossConditionsForPlayer(PlayerColor player); - void checkVictoryLossConditions(const std::set & playerColors); - void checkVictoryLossConditionsForAll(); - const std::string complainNoCreatures; const std::string complainNotEnoughCreatures; const std::string complainInvalidSlot; diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 4c09889c0..4239d07ba 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -2,6 +2,8 @@ set(server_SRCS StdInc.cpp CGameHandler.cpp + HeroPoolProcessor.cpp + PlayerMessageProcessor.cpp ServerSpellCastEnvironment.cpp CQuery.cpp CVCMIServer.cpp @@ -13,6 +15,8 @@ set(server_HEADERS StdInc.h CGameHandler.h + HeroPoolProcessor.h + PlayerMessageProcessor.h ServerSpellCastEnvironment.h CQuery.h CVCMIServer.h diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index b49cef5a2..d8ce1fc34 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -39,6 +39,7 @@ #include "../lib/VCMI_Lib.h" #include "../lib/VCMIDirs.h" #include "CGameHandler.h" +#include "PlayerMessageProcessor.h" #include "../lib/mapping/CMapInfo.h" #include "../lib/GameConstants.h" #include "../lib/logging/CBasicLogConfigurator.h" @@ -605,7 +606,7 @@ void CVCMIServer::clientDisconnected(std::shared_ptr c) if(gh && si && state == EServerState::GAMEPLAY) { - gh->playerMessage(playerSettings->color, playerLeftMsgText, ObjectInstanceID{}); + gh->playerMessages->broadcastMessage(playerSettings->color, playerLeftMsgText); gh->connections[playerSettings->color].insert(hostClient); startAiPack.players.push_back(playerSettings->color); } @@ -633,7 +634,7 @@ void CVCMIServer::reconnectPlayer(int connId) continue; std::string messageText = boost::str(boost::format("%s (cid %d) is connected") % playerSettings->name % connId); - gh->playerMessage(playerSettings->color, messageText, ObjectInstanceID{}); + gh->playerMessages->broadcastMessage(playerSettings->color, messageText); startAiPack.players.push_back(playerSettings->color); } @@ -822,7 +823,7 @@ void CVCMIServer::setPlayer(PlayerColor clickedColor) void CVCMIServer::optionNextCastle(PlayerColor player, int dir) { PlayerSettings & s = si->playerInfos[player]; - si16 & cur = s.castle; + FactionID & cur = s.castle; auto & allowed = getPlayerInfo(player.getNum()).allowedFactions; const bool allowRandomTown = getPlayerInfo(player.getNum()).isFactionRandom; @@ -856,7 +857,7 @@ void CVCMIServer::optionNextCastle(PlayerColor player, int dir) else { assert(dir >= -1 && dir <= 1); //othervice std::advance may go out of range - auto iter = allowed.find(FactionID(cur)); + auto iter = allowed.find(cur); std::advance(iter, dir); cur = *iter; } diff --git a/server/HeroPoolProcessor.cpp b/server/HeroPoolProcessor.cpp new file mode 100644 index 000000000..9923123dd --- /dev/null +++ b/server/HeroPoolProcessor.cpp @@ -0,0 +1,397 @@ +/* + * HeroPoolProcessor.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 "HeroPoolProcessor.h" + +#include "CGameHandler.h" + +#include "../lib/CHeroHandler.h" +#include "../lib/CPlayerState.h" +#include "../lib/GameSettings.h" +#include "../lib/NetPacks.h" +#include "../lib/StartInfo.h" +#include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/gameState/TavernHeroesPool.h" +#include "../lib/gameState/TavernSlot.h" + +HeroPoolProcessor::HeroPoolProcessor() + : gameHandler(nullptr) +{ +} + +HeroPoolProcessor::HeroPoolProcessor(CGameHandler * gameHandler) + : gameHandler(gameHandler) +{ +} + +bool HeroPoolProcessor::playerEndedTurn(const PlayerColor & player) +{ + // our player is acting right now and have not ended turn + if (player == gameHandler->gameState()->currentPlayer) + return false; + + auto turnOrder = gameHandler->generatePlayerTurnOrder(); + + for (auto const & entry : turnOrder) + { + // our player is yet to start turn + if (entry == gameHandler->gameState()->currentPlayer) + return false; + + // our player have finished turn + if (entry == player) + return true; + } + + assert(false); + return false; +} + +TavernHeroSlot HeroPoolProcessor::selectSlotForRole(const PlayerColor & player, TavernSlotRole roleID) +{ + const auto & heroesPool = gameHandler->gameState()->heroesPool; + + const auto & heroes = heroesPool->getHeroesFor(player); + + // if tavern has empty slot - use it + if (heroes.size() == 0) + return TavernHeroSlot::NATIVE; + + if (heroes.size() == 1) + return TavernHeroSlot::RANDOM; + + // try to find "better" slot to overwrite + // we want to avoid overwriting retreated heroes when tavern still has slot with random hero + // as well as avoid overwriting surrendered heroes if we can overwrite retreated hero + auto roleLeft = heroesPool->getSlotRole(HeroTypeID(heroes[0]->subID)); + auto roleRight = heroesPool->getSlotRole(HeroTypeID(heroes[1]->subID)); + + if (roleLeft > roleRight) + return TavernHeroSlot::RANDOM; + + if (roleLeft < roleRight) + return TavernHeroSlot::NATIVE; + + // both slots are equal in "value", so select randomly + if (getRandomGenerator(player).nextInt(100) > 50) + return TavernHeroSlot::RANDOM; + else + return TavernHeroSlot::NATIVE; +} + +void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHeroInstance * hero) +{ + SetAvailableHero sah; + if (playerEndedTurn(color)) + sah.roleID = TavernSlotRole::SURRENDERED_TODAY; + else + sah.roleID = TavernSlotRole::SURRENDERED; + + sah.slotID = selectSlotForRole(color, sah.roleID); + sah.player = color; + sah.hid = hero->subID; + gameHandler->sendAndApply(&sah); +} + +void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroInstance * hero) +{ + SetAvailableHero sah; + if (playerEndedTurn(color)) + sah.roleID = TavernSlotRole::RETREATED_TODAY; + else + sah.roleID = TavernSlotRole::RETREATED; + + sah.slotID = selectSlotForRole(color, sah.roleID); + sah.player = color; + sah.hid = hero->subID; + sah.army.clear(); + sah.army.setCreature(SlotID(0), hero->type->initialArmy.at(0).creature, 1); + + gameHandler->sendAndApply(&sah); +} + +void HeroPoolProcessor::clearHeroFromSlot(const PlayerColor & color, TavernHeroSlot slot) +{ + SetAvailableHero sah; + sah.player = color; + sah.roleID = TavernSlotRole::NONE; + sah.slotID = slot; + sah.hid = HeroTypeID::NONE; + gameHandler->sendAndApply(&sah); +} + +void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveArmy) +{ + SetAvailableHero sah; + sah.player = color; + sah.slotID = slot; + + CGHeroInstance *newHero = pickHeroFor(needNativeHero, color); + + if (newHero) + { + sah.hid = newHero->subID; + + if (giveArmy) + { + sah.roleID = TavernSlotRole::FULL_ARMY; + newHero->initArmy(getRandomGenerator(color), &sah.army); + } + else + { + sah.roleID = TavernSlotRole::SINGLE_UNIT; + sah.army.clear(); + sah.army.setCreature(SlotID(0), newHero->type->initialArmy[0].creature, 1); + } + } + else + { + sah.hid = -1; + } + gameHandler->sendAndApply(&sah); +} + +void HeroPoolProcessor::onNewWeek(const PlayerColor & color) +{ + const auto & heroesPool = gameHandler->gameState()->heroesPool; + const auto & heroes = heroesPool->getHeroesFor(color); + + const auto nativeSlotRole = heroes.size() < 1 ? TavernSlotRole::NONE : heroesPool->getSlotRole(heroes[0]->type->getId()); + const auto randomSlotRole = heroes.size() < 2 ? TavernSlotRole::NONE : heroesPool->getSlotRole(heroes[1]->type->getId()); + + bool resetNativeSlot = nativeSlotRole != TavernSlotRole::RETREATED_TODAY && nativeSlotRole != TavernSlotRole::SURRENDERED_TODAY; + bool resetRandomSlot = randomSlotRole != TavernSlotRole::RETREATED_TODAY && randomSlotRole != TavernSlotRole::SURRENDERED_TODAY; + + if (resetNativeSlot) + clearHeroFromSlot(color, TavernHeroSlot::NATIVE); + + if (resetRandomSlot) + clearHeroFromSlot(color, TavernHeroSlot::RANDOM); + + if (resetNativeSlot) + selectNewHeroForSlot(color, TavernHeroSlot::NATIVE, true, true); + + if (resetRandomSlot) + selectNewHeroForSlot(color, TavernHeroSlot::RANDOM, false, true); +} + +bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTypeID & heroToRecruit, const PlayerColor & player) +{ + const PlayerState * playerState = gameHandler->getPlayerState(player); + const CGObjectInstance * mapObject = gameHandler->getObj(objectID); + const CGTownInstance * town = gameHandler->getTown(objectID); + + if (!mapObject && gameHandler->complain("Invalid map object!")) + return false; + + if (!playerState && gameHandler->complain("Invalid player!")) + return false; + + if (playerState->resources[EGameResID::GOLD] < GameConstants::HERO_GOLD_COST && gameHandler->complain("Not enough gold for buying hero!")) + return false; + + if (gameHandler->getHeroCount(player, false) >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) && gameHandler->complain("Cannot hire hero, too many wandering heroes already!")) + return false; + + if (gameHandler->getHeroCount(player, true) >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP) && gameHandler->complain("Cannot hire hero, too many heroes garrizoned and wandering already!")) + return false; + + if(town) //tavern in town + { + if(gameHandler->getPlayerRelations(mapObject->tempOwner, player) == PlayerRelations::ENEMIES && gameHandler->complain("Can't buy hero in enemy town!")) + return false; + + if(!town->hasBuilt(BuildingID::TAVERN) && gameHandler->complain("No tavern!")) + return false; + + if(town->visitingHero && gameHandler->complain("There is visiting hero - no place!")) + return false; + } + + if(mapObject->ID == Obj::TAVERN) + { + if(gameHandler->getTile(mapObject->visitablePos())->visitableObjects.back() != mapObject && gameHandler->complain("Tavern entry must be unoccupied!")) + return false; + } + + auto recruitableHeroes = gameHandler->gameState()->heroesPool->getHeroesFor(player); + + const CGHeroInstance * recruitedHero = nullptr; + + for(const auto & hero : recruitableHeroes) + { + if(hero->subID == heroToRecruit) + recruitedHero = hero; + } + + if(!recruitedHero) + { + gameHandler->complain("Hero is not available for hiring!"); + return false; + } + + HeroRecruited hr; + hr.tid = mapObject->id; + hr.hid = recruitedHero->subID; + hr.player = player; + hr.tile = recruitedHero->convertFromVisitablePos(mapObject->visitablePos()); + if(gameHandler->getTile(hr.tile)->isWater()) + { + //Create a new boat for hero + gameHandler->createObject(mapObject->visitablePos(), Obj::BOAT, recruitedHero->getBoatType().getNum()); + + hr.boatId = gameHandler->getTopObj(hr.tile)->id; + } + + // apply netpack -> this will remove hired hero from pool + gameHandler->sendAndApply(&hr); + + if(recruitableHeroes[0] == recruitedHero) + selectNewHeroForSlot(player, TavernHeroSlot::NATIVE, false, false); + else + selectNewHeroForSlot(player, TavernHeroSlot::RANDOM, false, false); + + gameHandler->giveResource(player, EGameResID::GOLD, -GameConstants::HERO_GOLD_COST); + + if(town) + { + gameHandler->visitCastleObjects(town, recruitedHero); + gameHandler->giveSpells(town, recruitedHero); + } + return true; +} + +std::vector HeroPoolProcessor::findAvailableClassesFor(const PlayerColor & player) const +{ + std::vector result; + + const auto & heroesPool = gameHandler->gameState()->heroesPool; + FactionID factionID = gameHandler->getPlayerSettings(player)->castle; + + for(auto & elem : heroesPool->unusedHeroesFromPool()) + { + if (vstd::contains(result, elem.second->type->heroClass)) + continue; + + bool heroAvailable = heroesPool->isHeroAvailableFor(elem.first, player); + bool heroClassBanned = elem.second->type->heroClass->selectionProbability[factionID] == 0; + + if(heroAvailable && !heroClassBanned) + result.push_back(elem.second->type->heroClass); + } + + return result; +} + +std::vector HeroPoolProcessor::findAvailableHeroesFor(const PlayerColor & player, const CHeroClass * heroClass) const +{ + std::vector result; + + const auto & heroesPool = gameHandler->gameState()->heroesPool; + + for(auto & elem : heroesPool->unusedHeroesFromPool()) + { + assert(!vstd::contains(result, elem.second)); + + bool heroAvailable = heroesPool->isHeroAvailableFor(elem.first, player); + bool heroClassMatches = elem.second->type->heroClass == heroClass; + + if(heroAvailable && heroClassMatches) + result.push_back(elem.second); + } + + return result; +} + +const CHeroClass * HeroPoolProcessor::pickClassFor(bool isNative, const PlayerColor & player) +{ + if(player >= PlayerColor::PLAYER_LIMIT) + { + logGlobal->error("Cannot pick hero for player %d. Wrong owner!", player.getStr()); + return nullptr; + } + + FactionID factionID = gameHandler->getPlayerSettings(player)->castle; + const auto & heroesPool = gameHandler->gameState()->heroesPool; + const auto & currentTavern = heroesPool->getHeroesFor(player); + + std::vector potentialClasses = findAvailableClassesFor(player); + std::vector possibleClasses; + + if(potentialClasses.empty()) + { + logGlobal->error("There are no heroes available for player %s!", player.getStr()); + return nullptr; + } + + for(const auto & heroClass : potentialClasses) + { + if (isNative && heroClass->faction != factionID) + continue; + + bool hasSameClass = vstd::contains_if(currentTavern, [&](const CGHeroInstance * hero){ + return hero->type->heroClass == heroClass; + }); + + if (hasSameClass) + continue; + + possibleClasses.push_back(heroClass); + } + + if (possibleClasses.empty()) + { + logGlobal->error("Cannot pick native hero for %s. Picking any...", player.getStr()); + possibleClasses = potentialClasses; + } + + int totalWeight = 0; + for(const auto & heroClass : possibleClasses) + totalWeight += heroClass->selectionProbability.at(factionID); + + int roll = getRandomGenerator(player).nextInt(totalWeight - 1); + + for(const auto & heroClass : possibleClasses) + { + roll -= heroClass->selectionProbability.at(factionID); + if(roll < 0) + return heroClass; + } + + return *possibleClasses.rbegin(); +} + +CGHeroInstance * HeroPoolProcessor::pickHeroFor(bool isNative, const PlayerColor & player) +{ + const CHeroClass * heroClass = pickClassFor(isNative, player); + + if(!heroClass) + return nullptr; + + std::vector possibleHeroes = findAvailableHeroesFor(player, heroClass); + + assert(!possibleHeroes.empty()); + if(possibleHeroes.empty()) + return nullptr; + + return *RandomGeneratorUtil::nextItem(possibleHeroes, getRandomGenerator(player)); +} + +CRandomGenerator & HeroPoolProcessor::getRandomGenerator(const PlayerColor & player) +{ + if (playerSeed.count(player) == 0) + { + int seed = gameHandler->getRandomGenerator().nextInt(); + playerSeed.emplace(player, std::make_unique(seed)); + } + + return *playerSeed.at(player); +} diff --git a/server/HeroPoolProcessor.h b/server/HeroPoolProcessor.h new file mode 100644 index 000000000..dc69a7cf3 --- /dev/null +++ b/server/HeroPoolProcessor.h @@ -0,0 +1,66 @@ +/* + * HeroPoolProcessor.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 + +enum class TavernHeroSlot : int8_t; +enum class TavernSlotRole : int8_t; +class PlayerColor; +class CGHeroInstance; +class HeroTypeID; +class ObjectInstanceID; +class CRandomGenerator; +class CHeroClass; + +VCMI_LIB_NAMESPACE_END + +class CGameHandler; + +class HeroPoolProcessor : boost::noncopyable +{ + /// per-player random generators + std::map> playerSeed; + + void clearHeroFromSlot(const PlayerColor & color, TavernHeroSlot slot); + void selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveStartingArmy); + + std::vector findAvailableClassesFor(const PlayerColor & player) const; + std::vector findAvailableHeroesFor(const PlayerColor & player, const CHeroClass * heroClass) const; + + const CHeroClass * pickClassFor(bool isNative, const PlayerColor & player); + + CGHeroInstance * pickHeroFor(bool isNative, const PlayerColor & player); + + CRandomGenerator & getRandomGenerator(const PlayerColor & player); + + TavernHeroSlot selectSlotForRole(const PlayerColor & player, TavernSlotRole roleID); + + bool playerEndedTurn(const PlayerColor & player); +public: + CGameHandler * gameHandler; + + HeroPoolProcessor(); + HeroPoolProcessor(CGameHandler * gameHandler); + + void onHeroSurrendered(const PlayerColor & color, const CGHeroInstance * hero); + void onHeroEscaped(const PlayerColor & color, const CGHeroInstance * hero); + + void onNewWeek(const PlayerColor & color); + + /// Incoming net pack handling + bool hireHero(const ObjectInstanceID & objectID, const HeroTypeID & hid, const PlayerColor & player); + + template void serialize(Handler &h, const int version) + { + // h & gameHandler; // FIXME: make this work instead of using deserializationFix in gameHandler + h & playerSeed; + } +}; diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 892be093d..d6812f1ef 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -11,6 +11,9 @@ #include "ServerNetPackVisitors.h" #include "CGameHandler.h" +#include "HeroPoolProcessor.h" +#include "PlayerMessageProcessor.h" + #include "../lib/IGameCallback.h" #include "../lib/mapObjects/CGTownInstance.h" #include "../lib/gameState/CGameState.h" @@ -246,12 +249,10 @@ void ApplyGhNetPackVisitor::visitSetFormation(SetFormation & pack) void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack) { - const CGObjectInstance * obj = gh.getObj(pack.tid); - const CGTownInstance * town = dynamic_ptr_cast(obj); - if(town && PlayerRelations::ENEMIES == gh.getPlayerRelations(obj->tempOwner, gh.getPlayerAt(pack.c))) - gh.throwAndComplain(&pack, "Can't buy hero in enemy town!"); + if (!gh.hasPlayerAt(pack.player, pack.c)) + gh.throwAndComplain(&pack, "No such pack.player!"); - result = gh.hireHero(obj, pack.hid, pack.player); + result = gh.heroPool->hireHero(pack.tid, pack.hid, pack.player); } void ApplyGhNetPackVisitor::visitBuildBoat(BuildBoat & pack) @@ -352,6 +353,6 @@ void ApplyGhNetPackVisitor::visitPlayerMessage(PlayerMessage & pack) if(!pack.player.isSpectator()) // TODO: clearly not a great way to verify permissions gh.throwOnWrongPlayer(&pack, pack.player); - gh.playerMessage(pack.player, pack.text, pack.currObj); + gh.playerMessages->playerMessage(pack.player, pack.text, pack.currObj); result = true; } diff --git a/server/PlayerMessageProcessor.cpp b/server/PlayerMessageProcessor.cpp new file mode 100644 index 000000000..7ff537d88 --- /dev/null +++ b/server/PlayerMessageProcessor.cpp @@ -0,0 +1,523 @@ +/* + * CGameHandler.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 "PlayerMessageProcessor.h" + +#include "CGameHandler.h" +#include "CVCMIServer.h" + +#include "../lib/serializer/Connection.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/CModHandler.h" +#include "../lib/CPlayerState.h" +#include "../lib/GameConstants.h" +#include "../lib/NetPacks.h" +#include "../lib/StartInfo.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/mapObjects/CGTownInstance.h" + +PlayerMessageProcessor::PlayerMessageProcessor() + :gameHandler(nullptr) +{ +} + +PlayerMessageProcessor::PlayerMessageProcessor(CGameHandler * gameHandler) + :gameHandler(gameHandler) +{ +} + +void PlayerMessageProcessor::playerMessage(PlayerColor player, const std::string &message, ObjectInstanceID currObj) +{ + if (handleHostCommand(player, message)) + return; + + if (handleCheatCode(message, player, currObj)) + { + if(!gameHandler->getPlayerSettings(player)->isControlledByAI()) + broadcastSystemMessage(VLC->generaltexth->allTexts[260]); + + if(!player.isSpectator()) + gameHandler->checkVictoryLossConditionsForPlayer(player);//Player enter win code or got required art\creature + + return; + } + + broadcastMessage(player, message); +} + +bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::string &message) +{ + 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; + + if(!isHost || words.size() < 2 || words[0] != "game") + return false; + + if(words[1] == "exit" || words[1] == "quit" || words[1] == "end") + { + broadcastSystemMessage("game was terminated"); + gameHandler->gameLobby()->state = EServerState::SHUTDOWN; + + return true; + } + if(words.size() == 3 && words[1] == "save") + { + gameHandler->save("Saves/" + words[2]); + broadcastSystemMessage("game saved as " + words[2]); + + return true; + } + if(words.size() == 3 && words[1] == "kick") + { + auto playername = words[2]; + PlayerColor playerToKick(PlayerColor::CANNOT_DETERMINE); + if(std::all_of(playername.begin(), playername.end(), ::isdigit)) + playerToKick = PlayerColor(std::stoi(playername)); + else + { + for(auto & c : gameHandler->connections) + { + if(c.first.getStr(false) == playername) + playerToKick = c.first; + } + } + + if(playerToKick != PlayerColor::CANNOT_DETERMINE) + { + PlayerCheated pc; + pc.player = playerToKick; + pc.losingCheatCode = true; + gameHandler->sendAndApply(&pc); + gameHandler->checkVictoryLossConditionsForPlayer(playerToKick); + } + return true; + } + if(words.size() == 2 && words[1] == "cheaters") + { + if (cheaters.empty()) + broadcastSystemMessage("No cheaters registered!"); + + for (auto const & entry : cheaters) + broadcastSystemMessage("Player " + entry.getStr() + " is cheater!"); + + return true; + } + + return false; +} + +void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroInstance * hero) +{ + if (!hero) + return; + + ///Give hero spellbook + if (!hero->hasSpellbook()) + gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); + + ///Give all spells with bonus (to allow banned spells) + GiveBonus giveBonus(GiveBonus::ETarget::HERO); + giveBonus.id = hero->id.getNum(); + giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, 0); + //start with level 0 to skip abilities + for (int level = 1; level <= GameConstants::SPELL_LEVELS; level++) + { + giveBonus.bonus.subtype = level; + gameHandler->sendAndApply(&giveBonus); + } + + ///Give mana + SetMana sm; + sm.hid = hero->id; + sm.val = 999; + sm.absolute = true; + gameHandler->sendAndApply(&sm); +} + +void PlayerMessageProcessor::cheatBuildTown(PlayerColor player, const CGTownInstance * town) +{ + if (!town) + return; + + for (auto & build : town->town->buildings) + { + if (!town->hasBuilt(build.first) + && !build.second->getNameTranslated().empty() + && build.first != BuildingID::SHIP) + { + gameHandler->buildStructure(town->id, build.first, true); + } + } +} + +void PlayerMessageProcessor::cheatGiveArmy(PlayerColor player, const CGHeroInstance * hero, std::vector words) +{ + if (!hero) + return; + + std::string creatureIdentifier = words.empty() ? "archangel" : words[0]; + std::optional amountPerSlot; + + try + { + amountPerSlot = std::stol(words.at(1)); + } + catch(std::exception&) + { + } + + std::optional creatureId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "creature", creatureIdentifier, false); + + if(creatureId.has_value()) + { + const auto * creature = CreatureID(creatureId.value()).toCreature(); + + for (int i = 0; i < GameConstants::ARMY_SIZE; i++) + { + if (!hero->hasStackAtSlot(SlotID(i))) + { + if (amountPerSlot.has_value()) + gameHandler->insertNewStack(StackLocation(hero, SlotID(i)), creature, *amountPerSlot); + else + gameHandler->insertNewStack(StackLocation(hero, SlotID(i)), creature, 5 * std::pow(10, i)); + } + } + } +} + +void PlayerMessageProcessor::cheatGiveMachines(PlayerColor player, const CGHeroInstance * hero) +{ + if (!hero) + return; + + if (!hero->getArt(ArtifactPosition::MACH1)) + gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::BALLISTA], ArtifactPosition::MACH1); + if (!hero->getArt(ArtifactPosition::MACH2)) + gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::AMMO_CART], ArtifactPosition::MACH2); + if (!hero->getArt(ArtifactPosition::MACH3)) + gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::FIRST_AID_TENT], ArtifactPosition::MACH3); +} + +void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero) +{ + if (!hero) + return; + + for(int g = 7; g < VLC->arth->objects.size(); ++g) //including artifacts from mods + { + if(VLC->arth->objects[g]->canBePutAt(hero)) + gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[g], ArtifactPosition::FIRST_AVAILABLE); + } +} + +void PlayerMessageProcessor::cheatLevelup(PlayerColor player, const CGHeroInstance * hero, std::vector words) +{ + if (!hero) + return; + + int levelsToGain; + try + { + levelsToGain = std::stol(words.at(0)); + } + catch(std::exception&) + { + levelsToGain = 1; + } + + gameHandler->changePrimSkill(hero, PrimarySkill::EXPERIENCE, VLC->heroh->reqExp(hero->level + levelsToGain) - VLC->heroh->reqExp(hero->level)); +} + +void PlayerMessageProcessor::cheatExperience(PlayerColor player, const CGHeroInstance * hero, std::vector words) +{ + if (!hero) + return; + + int expAmountProcessed; + + try + { + expAmountProcessed = std::stol(words.at(0)); + } + catch(std::exception&) + { + expAmountProcessed = 10000; + } + + gameHandler->changePrimSkill(hero, PrimarySkill::EXPERIENCE, expAmountProcessed); +} + +void PlayerMessageProcessor::cheatMovement(PlayerColor player, const CGHeroInstance * hero, std::vector words) +{ + if (!hero) + return; + + SetMovePoints smp; + smp.hid = hero->id; + try + { + smp.val = std::stol(words.at(0));; + } + catch(std::exception&) + { + smp.val = 1000000; + } + + gameHandler->sendAndApply(&smp); + + GiveBonus gb(GiveBonus::ETarget::HERO); + gb.bonus.type = BonusType::FREE_SHIP_BOARDING; + gb.bonus.duration = BonusDuration::ONE_DAY; + gb.bonus.source = BonusSource::OTHER; + gb.id = hero->id.getNum(); + gameHandler->giveHeroBonus(&gb); +} + +void PlayerMessageProcessor::cheatResources(PlayerColor player, std::vector words) +{ + int baseResourceAmount; + try + { + baseResourceAmount = std::stol(words.at(0));; + } + catch(std::exception&) + { + baseResourceAmount = 100; + } + + TResources resources; + resources[EGameResID::GOLD] = baseResourceAmount * 100; + for (GameResID i = EGameResID::WOOD; i < EGameResID::GOLD; ++i) + resources[i] = baseResourceAmount; + + gameHandler->giveResources(player, resources); +} + +void PlayerMessageProcessor::cheatVictory(PlayerColor player) +{ + PlayerCheated pc; + pc.player = player; + pc.winningCheatCode = true; + gameHandler->sendAndApply(&pc); +} + +void PlayerMessageProcessor::cheatDefeat(PlayerColor player) +{ + PlayerCheated pc; + pc.player = player; + pc.losingCheatCode = true; + gameHandler->sendAndApply(&pc); +} + +void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) +{ + FoWChange fc; + fc.mode = reveal; + fc.player = player; + const auto & fowMap = gameHandler->gameState()->getPlayerTeam(player)->fogOfWarMap; + const auto & mapSize = gameHandler->gameState()->getMapSize(); + auto hlp_tab = new int3[mapSize.x * mapSize.y * mapSize.z]; + int lastUnc = 0; + + for(int z = 0; z < mapSize.z; z++) + for(int x = 0; x < mapSize.x; x++) + for(int y = 0; y < mapSize.y; y++) + if(!(*fowMap)[z][x][y] || !fc.mode) + hlp_tab[lastUnc++] = int3(x, y, z); + + fc.tiles.insert(hlp_tab, hlp_tab + lastUnc); + delete [] hlp_tab; + gameHandler->sendAndApply(&fc); +} + +bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerColor player, ObjectInstanceID currObj) +{ + std::vector words; + boost::split(words, cheat, boost::is_any_of("\t\r\n ")); + + if (words.empty()) + return false; + + //Make cheat name case-insensitive, but keep words/parameters (e.g. creature name) as it + std::string cheatName = boost::to_lower_copy(words[0]); + words.erase(words.begin()); + + std::vector townTargetedCheats = { "vcmiarmenelos", "vcmibuild", "nwczion" }; + std::vector playerTargetedCheats = { + "vcmiformenos", "vcmiresources", "nwctheconstruct", + "vcmimelkor", "vcmilose", "nwcbluepill", + "vcmisilmaril", "vcmiwin", "nwcredpill", + "vcmieagles", "vcmimap", "nwcwhatisthematrix", + "vcmiungoliant", "vcmihidemap", "nwcignoranceisbliss" + }; + std::vector heroTargetedCheats = { + "vcmiainur", "vcmiarchangel", "nwctrinity", + "vcmiangband", "vcmiblackknight", "nwcagents", + "vcmiglaurung", "vcmicrystal", "vcmiazure", + "vcmifaerie", "vcmiarmy", "vcminissi", + "vcmiistari", "vcmispells", "nwcthereisnospoon", + "vcminoldor", "vcmimachines", "nwclotsofguns", + "vcmiglorfindel", "vcmilevel", "nwcneo", + "vcminahar", "vcmimove", "nwcnebuchadnezzar", + "vcmiforgeofnoldorking", "vcmiartifacts", + "vcmiolorin", "vcmiexp", + }; + + if (!vstd::contains(townTargetedCheats, cheatName) && !vstd::contains(playerTargetedCheats, cheatName) && !vstd::contains(heroTargetedCheats, cheatName)) + return false; + + bool playerTargetedCheat = false; + + for (const auto & i : gameHandler->gameState()->players) + { + if (words.empty()) + break; + + if (i.first == PlayerColor::NEUTRAL) + continue; + + if (words.front() == "ai" && i.second.human) + continue; + + if (words.front() != "all" && words.front() != i.first.getStr()) + continue; + + std::vector parameters = words; + + cheaters.insert(i.first); + playerTargetedCheat = true; + parameters.erase(parameters.begin()); + + if (vstd::contains(playerTargetedCheats, cheatName)) + executeCheatCode(cheatName, i.first, ObjectInstanceID::NONE, parameters); + + if (vstd::contains(townTargetedCheats, cheatName)) + for (const auto & t : i.second.towns) + executeCheatCode(cheatName, i.first, t->id, parameters); + + if (vstd::contains(heroTargetedCheats, cheatName)) + for (const auto & h : i.second.heroes) + executeCheatCode(cheatName, i.first, h->id, parameters); + } + + if (!playerTargetedCheat) + executeCheatCode(cheatName, player, currObj, words); + + cheaters.insert(player); + return true; +} + +void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, PlayerColor player, ObjectInstanceID currObj, const std::vector & words) +{ + const CGHeroInstance * hero = gameHandler->getHero(currObj); + const CGTownInstance * town = gameHandler->getTown(currObj); + if (!town && hero) + town = hero->visitedTown; + + const auto & doCheatGiveSpells = [&]() { cheatGiveSpells(player, hero); }; + const auto & doCheatBuildTown = [&]() { cheatBuildTown(player, town); }; + const auto & doCheatGiveArmyCustom = [&]() { cheatGiveArmy(player, hero, words); }; + const auto & doCheatGiveArmyFixed = [&](std::vector customWords) { cheatGiveArmy(player, hero, customWords); }; + const auto & doCheatGiveMachines = [&]() { cheatGiveMachines(player, hero); }; + const auto & doCheatGiveArtifacts = [&]() { cheatGiveArtifacts(player, hero); }; + const auto & doCheatLevelup = [&]() { cheatLevelup(player, hero, words); }; + const auto & doCheatExperience = [&]() { cheatExperience(player, hero, words); }; + const auto & doCheatMovement = [&]() { cheatMovement(player, hero, words); }; + const auto & doCheatResources = [&]() { cheatResources(player, words); }; + const auto & doCheatVictory = [&]() { cheatVictory(player); }; + const auto & doCheatDefeat = [&]() { cheatDefeat(player); }; + const auto & doCheatMapReveal = [&]() { cheatMapReveal(player, true); }; + const auto & doCheatMapHide = [&]() { cheatMapReveal(player, false); }; + + // Unimplemented H3 cheats: + // nwcfollowthewhiterabbit - The currently selected hero permanently gains maximum luck. + // nwcmorpheus - The currently selected hero permanently gains maximum morale. + // nwcoracle - The puzzle map is permanently revealed. + // nwcphisherprice - Changes and brightens the game colors. + + std::map> callbacks = { + {"vcmiainur", [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} }, + {"nwctrinity", [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} }, + {"vcmiangband", [&] () {doCheatGiveArmyFixed({ "blackKnight", "10" });} }, + {"vcmiglaurung", [&] () {doCheatGiveArmyFixed({ "crystalDragon", "5000" });} }, + {"vcmiarchangel", [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} }, + {"nwcagents", [&] () {doCheatGiveArmyFixed({ "blackKnight", "10" });} }, + {"vcmiblackknight", [&] () {doCheatGiveArmyFixed({ "blackKnight", "10" });} }, + {"vcmicrystal", [&] () {doCheatGiveArmyFixed({ "crystalDragon", "5000" });} }, + {"vcmiazure", [&] () {doCheatGiveArmyFixed({ "azureDragon", "5000" });} }, + {"vcmifaerie", [&] () {doCheatGiveArmyFixed({ "fairieDragon", "5000" });} }, + {"vcmiarmy", doCheatGiveArmyCustom }, + {"vcminissi", doCheatGiveArmyCustom }, + {"vcmiistari", doCheatGiveSpells }, + {"vcmispells", doCheatGiveSpells }, + {"nwcthereisnospoon", doCheatGiveSpells }, + {"vcmiarmenelos", doCheatBuildTown }, + {"vcmibuild", doCheatBuildTown }, + {"nwczion", doCheatBuildTown }, + {"vcminoldor", doCheatGiveMachines }, + {"vcmimachines", doCheatGiveMachines }, + {"nwclotsofguns", doCheatGiveMachines }, + {"vcmiforgeofnoldorking", doCheatGiveArtifacts }, + {"vcmiartifacts", doCheatGiveArtifacts }, + {"vcmiglorfindel", doCheatLevelup }, + {"vcmilevel", doCheatLevelup }, + {"nwcneo", doCheatLevelup }, + {"vcmiolorin", doCheatExperience }, + {"vcmiexp", doCheatExperience }, + {"vcminahar", doCheatMovement }, + {"vcmimove", doCheatMovement }, + {"nwcnebuchadnezzar", doCheatMovement }, + {"vcmiformenos", doCheatResources }, + {"vcmiresources", doCheatResources }, + {"nwctheconstruct", doCheatResources }, + {"nwcbluepill", doCheatDefeat }, + {"vcmimelkor", doCheatDefeat }, + {"vcmilose", doCheatDefeat }, + {"nwcredpill", doCheatVictory }, + {"vcmisilmaril", doCheatVictory }, + {"vcmiwin", doCheatVictory }, + {"nwcwhatisthematrix", doCheatMapReveal }, + {"vcmieagles", doCheatMapReveal }, + {"vcmimap", doCheatMapReveal }, + {"vcmiungoliant", doCheatMapHide }, + {"vcmihidemap", doCheatMapHide }, + {"nwcignoranceisbliss", doCheatMapHide }, + }; + + assert(callbacks.count(cheatName)); + if (callbacks.count(cheatName)) + callbacks.at(cheatName)(); +} + +void PlayerMessageProcessor::sendSystemMessage(std::shared_ptr connection, const std::string & message) +{ + SystemMessage sm; + sm.text = message; + connection->sendPack(&sm); +} + +void PlayerMessageProcessor::broadcastSystemMessage(const std::string & message) +{ + SystemMessage sm; + sm.text = message; + gameHandler->sendToAllClients(&sm); +} + +void PlayerMessageProcessor::broadcastMessage(PlayerColor playerSender, const std::string & message) +{ + PlayerMessageClient temp_message(playerSender, message); + gameHandler->sendAndApply(&temp_message); +} diff --git a/server/PlayerMessageProcessor.h b/server/PlayerMessageProcessor.h new file mode 100644 index 000000000..2351140ca --- /dev/null +++ b/server/PlayerMessageProcessor.h @@ -0,0 +1,65 @@ +/* + * CGameHandler.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/GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CGHeroInstance; +class CGTownInstance; +class CConnection; +VCMI_LIB_NAMESPACE_END + +class CGameHandler; + +class PlayerMessageProcessor +{ + std::set cheaters; + + void executeCheatCode(const std::string & cheatName, PlayerColor player, ObjectInstanceID currObj, const std::vector & arguments ); + bool handleCheatCode(const std::string & cheatFullCommand, PlayerColor player, ObjectInstanceID currObj); + bool handleHostCommand(PlayerColor player, const std::string & message); + + void cheatGiveSpells(PlayerColor player, const CGHeroInstance * hero); + void cheatBuildTown(PlayerColor player, const CGTownInstance * town); + void cheatGiveArmy(PlayerColor player, const CGHeroInstance * hero, std::vector words); + void cheatGiveMachines(PlayerColor player, const CGHeroInstance * hero); + void cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero); + void cheatLevelup(PlayerColor player, const CGHeroInstance * hero, std::vector words); + void cheatExperience(PlayerColor player, const CGHeroInstance * hero, std::vector words); + void cheatMovement(PlayerColor player, const CGHeroInstance * hero, std::vector words); + void cheatResources(PlayerColor player, std::vector words); + void cheatVictory(PlayerColor player); + void cheatDefeat(PlayerColor player); + void cheatMapReveal(PlayerColor player, bool reveal); + +public: + CGameHandler * gameHandler; + + PlayerMessageProcessor(); + PlayerMessageProcessor(CGameHandler * gameHandler); + + /// incoming NetPack handling + void playerMessage(PlayerColor player, const std::string & message, ObjectInstanceID currObj); + + /// Send message to specific client with "System" as sender + void sendSystemMessage(std::shared_ptr connection, const std::string & message); + + /// Send message to all players with "System" as sender + void broadcastSystemMessage(const std::string & message); + + /// Send message from specific player to all other players + void broadcastMessage(PlayerColor playerSender, const std::string & message); + + template void serialize(Handler &h, const int version) + { + h & cheaters; + } +};