diff --git a/extras/videoDrivers/SDL2/VideoSDL2.cpp b/extras/videoDrivers/SDL2/VideoSDL2.cpp index 4fa0b49aaa..af2f2394f3 100644 --- a/extras/videoDrivers/SDL2/VideoSDL2.cpp +++ b/extras/videoDrivers/SDL2/VideoSDL2.cpp @@ -306,6 +306,12 @@ bool VideoSDL2::MessageLoop() // Die 12 F-Tasten if(ev.key.keysym.sym >= SDLK_F1 && ev.key.keysym.sym <= SDLK_F12) ke.kt = static_cast(rttr::enum_cast(KeyType::F1) + ev.key.keysym.sym - SDLK_F1); + + if((SDL_GetModState() & KMOD_ALT) && isdigit(ev.key.keysym.sym)) + { + ke.kt = KeyType::Char; + ke.c = ev.key.keysym.sym; + } } break; case SDLK_RETURN: ke.kt = KeyType::Return; break; diff --git a/libs/s25main/CheatCommandTracker.cpp b/libs/s25main/CheatCommandTracker.cpp new file mode 100644 index 0000000000..45b0896f78 --- /dev/null +++ b/libs/s25main/CheatCommandTracker.cpp @@ -0,0 +1,87 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "CheatCommandTracker.h" +#include "Cheats.h" +#include "driver/KeyEvent.h" + +namespace { +auto makeCircularBuffer(const std::string& str) +{ + return boost::circular_buffer{cbegin(str), cend(str)}; +} +const auto cheatStr = makeCircularBuffer("winter"); +} // namespace + +CheatCommandTracker::CheatCommandTracker(Cheats& cheats) : cheats_(cheats), lastChars_(cheatStr.size()) {} + +void CheatCommandTracker::trackKeyEvent(const KeyEvent& ke) +{ + if(trackSpecialKeyEvent(ke) || trackSpeedKeyEvent(ke)) + { + lastChars_.clear(); + return; + } + + trackCharKeyEvent(ke); +} + +void CheatCommandTracker::trackChatCommand(const std::string& cmd) +{ + if(cmd == "apocalypsis") + cheats_.armageddon(); +} + +bool CheatCommandTracker::trackSpecialKeyEvent(const KeyEvent& ke) +{ + if(ke.kt == KeyType::Char) + return false; + + if(ke.ctrl && ke.shift) + { + if(ke.kt >= KeyType::F1 && ke.kt <= KeyType::F8) + cheats_.destroyBuildings({static_cast(ke.kt) - static_cast(KeyType::F1)}); + else if(ke.kt == KeyType::F9) + cheats_.destroyAllAIBuildings(); + + return true; + } + + switch(ke.kt) + { + case KeyType::F7: + { + if(ke.alt) + cheats_.toggleResourceRevealMode(); + else + cheats_.toggleAllVisible(); + } + break; + case KeyType::F10: cheats_.toggleHumanAIPlayer(); break; + default: break; + } + + return true; +} + +bool CheatCommandTracker::trackSpeedKeyEvent(const KeyEvent& ke) +{ + const char c = ke.c; + if(ke.alt && c >= '1' && c <= '6') + { + cheats_.setGameSpeed(c - '1'); + return true; + } + return false; +} + +bool CheatCommandTracker::trackCharKeyEvent(const KeyEvent& ke) +{ + lastChars_.push_back(ke.c); + + if(lastChars_ == cheatStr) + cheats_.toggleCheatMode(); + + return true; +} diff --git a/libs/s25main/CheatCommandTracker.h b/libs/s25main/CheatCommandTracker.h new file mode 100644 index 0000000000..54215c7e34 --- /dev/null +++ b/libs/s25main/CheatCommandTracker.h @@ -0,0 +1,57 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +class Cheats; +struct KeyEvent; + +class CheatCommandTracker +{ +public: + CheatCommandTracker(Cheats& cheats); + + /** Tracks keyboard events related to cheats and triggers the actual cheats. + * Calls related private methods of this class in order but returns at the first success (return true). + * + * @param ke - The keyboard event encountered. + */ + void trackKeyEvent(const KeyEvent& ke); + + /** Tracks chat commands related to cheats and triggers the actual cheats. + * + * @param cmd - The chat command to track. + */ + void trackChatCommand(const std::string& cmd); + +private: + /** Tracks keyboard events related to cheats and triggers the actual cheats, but only tracks events of type + * different than KeyType::Char (e.g. F-keys). + * + * @param ke - The keyboard event encountered. + * @return true if keyboard event was NOT of type KeyType::Char, false otherwise + */ + bool trackSpecialKeyEvent(const KeyEvent& ke); + + /** Tracks keyboard events related to game speed cheats (ALT+1..ALT+6) and triggers the actual cheats. + * + * @param ke - The keyboard event encountered. + * @return true if keyboard event was related to game speed cheats, false otherwise + */ + bool trackSpeedKeyEvent(const KeyEvent& ke); + + /** Tracks keyboard events related to cheats and triggers the actual cheats, but only tracks events of type + * KeyType::Char (e.g. enabling cheat mode by typing "winter"). + * + * @param ke - The keyboard event encountered. + * @return always true + */ + bool trackCharKeyEvent(const KeyEvent& ke); + + Cheats& cheats_; + boost::circular_buffer lastChars_; +}; diff --git a/libs/s25main/Cheats.cpp b/libs/s25main/Cheats.cpp new file mode 100644 index 0000000000..5c1cae57d1 --- /dev/null +++ b/libs/s25main/Cheats.cpp @@ -0,0 +1,150 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Cheats.h" +#include "CheatCommandTracker.h" +#include "GameInterface.h" +#include "GamePlayer.h" +#include "RttrForeachPt.h" +#include "factories/BuildingFactory.h" +#include "network/GameClient.h" +#include "world/GameWorldBase.h" + +Cheats::Cheats(GameWorldBase& world) : cheatCmdTracker_(std::make_unique(*this)), world_(world) {} + +Cheats::~Cheats() = default; + +void Cheats::trackKeyEvent(const KeyEvent& ke) +{ + if(!canCheatModeBeOn()) + return; + + cheatCmdTracker_->trackKeyEvent(ke); +} + +void Cheats::trackChatCommand(const std::string& cmd) +{ + if(!canCheatModeBeOn()) + return; + + cheatCmdTracker_->trackChatCommand(cmd); +} + +void Cheats::toggleCheatMode() +{ + if(!canCheatModeBeOn()) + return; + + isCheatModeOn_ = !isCheatModeOn_; +} + +void Cheats::toggleAllVisible() +{ + // This is actually the behavior of the original game. + // If you enabled cheats, revealed the map and disabled cheats you would be unable to unreveal the map. + if(!isCheatModeOn()) + return; + + isAllVisible_ = !isAllVisible_; + + // The minimap in the original game is not updated immediately, but here this would cause complications. + if(GameInterface* gi = world_.GetGameInterface()) + gi->GI_UpdateMapVisibility(); +} + +bool Cheats::canPlaceCheatBuilding(const MapPoint& mp) const +{ + if(!isCheatModeOn()) + return false; + + // It seems that in the original game you can only build headquarters in unoccupied territory at least 2 nodes + // away from any border markers and that it doesn't need more bq than a hut. + const MapNode& node = world_.GetNode(mp); + return !node.owner && !world_.IsAnyNeighborOwned(mp) && node.bq >= BuildingQuality::Hut; +} + +void Cheats::placeCheatBuilding(const MapPoint& mp, const GamePlayer& player) +{ + if(!canPlaceCheatBuilding(mp)) + return; + + // The new HQ will have default resources. + // In the original game, new HQs created in the Roman campaign had no resources. + constexpr auto checkExists = false; + world_.DestroyNO(mp, checkExists); // if CanPlaceCheatBuilding is true then this must be safe to destroy + BuildingFactory::CreateBuilding(world_, BuildingType::Headquarters, mp, player.GetPlayerId(), player.nation, + player.IsHQTent()); +} + +void Cheats::setGameSpeed(uint8_t speedIndex) +{ + if(!isCheatModeOn()) + return; + + constexpr auto gfLengthInMs = 50; + GAMECLIENT.SetGFLengthReq(FramesInfo::milliseconds32_t{gfLengthInMs >> speedIndex}); + // 50 -> 25 -> 12 -> 6 -> 3 -> 1 +} + +void Cheats::toggleHumanAIPlayer() +{ + if(!isCheatModeOn()) + return; + + if(GAMECLIENT.IsReplayModeOn()) + return; + + GAMECLIENT.ToggleHumanAIPlayer(AI::Info{AI::Type::Default, AI::Level::Easy}); +} + +void Cheats::armageddon() +{ + if(!isCheatModeOn()) + return; + + GAMECLIENT.CheatArmageddon(); +} + +Cheats::ResourceRevealMode Cheats::getResourceRevealMode() const +{ + return isCheatModeOn() ? resourceRevealMode_ : ResourceRevealMode::Nothing; +} + +void Cheats::toggleResourceRevealMode() +{ + switch(resourceRevealMode_) + { + case ResourceRevealMode::Nothing: resourceRevealMode_ = ResourceRevealMode::Ores; break; + case ResourceRevealMode::Ores: resourceRevealMode_ = ResourceRevealMode::Fish; break; + case ResourceRevealMode::Fish: resourceRevealMode_ = ResourceRevealMode::Water; break; + default: resourceRevealMode_ = ResourceRevealMode::Nothing; break; + } +} + +void Cheats::destroyBuildings(const PlayerIDSet& playerIds) +{ + if(!isCheatModeOn()) + return; + + RTTR_FOREACH_PT(MapPoint, world_.GetSize()) + if(world_.GetNO(pt)->GetType() == NodalObjectType::Building && playerIds.count(world_.GetNode(pt).owner - 1)) + world_.DestroyNO(pt); +} + +void Cheats::destroyAllAIBuildings() +{ + if(!isCheatModeOn()) + return; + + PlayerIDSet ais; + for(auto i = 0u; i < world_.GetNumPlayers(); ++i) + if(!world_.GetPlayer(i).isHuman()) + ais.insert(i); + destroyBuildings(ais); +} + +bool Cheats::canCheatModeBeOn() const +{ + return world_.IsSinglePlayer(); +} diff --git a/libs/s25main/Cheats.h b/libs/s25main/Cheats.h new file mode 100644 index 0000000000..089f074ff0 --- /dev/null +++ b/libs/s25main/Cheats.h @@ -0,0 +1,130 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "gameTypes/MapCoordinates.h" +#include +#include +#include + +class CheatCommandTracker; +class GamePlayer; +class GameWorldBase; +struct KeyEvent; + +class Cheats +{ +public: + Cheats(GameWorldBase& world); + ~Cheats(); // = default - for unique_ptr + + /** In single player games, tracks keyboard events related to cheats. + * Delegates this responsibility to CheatCommandTracker, which triggers the actual cheats. + * In multiplayer games, does nothing. No cheats can be triggered in multiplayer. + * + * @param ke - The keyboard event encountered. + */ + void trackKeyEvent(const KeyEvent& ke); + + /** In single player games, tracks chat commands related to cheats. + * Delegates this responsibility to CheatCommandTracker, which triggers the actual cheats. + * In multiplayer games, does nothing. No cheats can be triggered in multiplayer. + * + * @param cmd - The chat command to track. + */ + void trackChatCommand(const std::string& cmd); + + /** Toggles cheat mode on and off. + * Cheat mode needs to be on for any cheats to trigger. + */ + void toggleCheatMode(); + /** Used by clients to check if cheat mode is on (e.g. to draw special sprites or enable or all buildings). + * Cheat mode needs to be on for any cheats to trigger. + * + * @return true if cheat mode is on, false otherwise + */ + bool isCheatModeOn() const { return isCheatModeOn_; } + + // Classic S2 cheats + + /** The classic F7 cheat. + * Does not modify game state, merely tricks clients into revealing the whole map. + * In the background, visibility is tracked as expected, i.e. if you reveal the map, send a scout and unreveal the + * map, you will see what was scouted. + */ + void toggleAllVisible(); + bool isAllVisible() const { return isAllVisible_; } + + /** The classic build headquarters cheat. + * This function is used to check if the cheat building can be placed at the chosen point when opening the activity + * window. + * + * @param mp - The map point where the user clicked to open the activity window. + * @return true if the building can be placed, false otherwise + */ + bool canPlaceCheatBuilding(const MapPoint& mp) const; + /** The classic build headquarters cheat. + * This function is used to place the cheat building at the chosen point. + * The building is immediately fully built, there is no need for a building site. + * + * @param mp - The map point at which to place the building. + * @param player - The player to whom the building should belong. + */ + void placeCheatBuilding(const MapPoint& mp, const GamePlayer& player); + + /** The classic ALT+1 through ALT+6 cheat which changes the game speed. + * + * @param speedIndex - 0 is normal, 1 is faster, 2 is even faster, etc. + */ + void setGameSpeed(uint8_t speedIndex); + + // RTTR cheats + + /** Shares control of the (human) user's country with the AI. Both the user and the AI retain full control of the + * country, so the user can observe what the AI does or "cooperate" with it. + */ + void toggleHumanAIPlayer(); + + void armageddon(); + + enum class ResourceRevealMode + { + Nothing, + Ores, + Fish, + Water + }; + /** Tells clients which resources to reveal: + * Nothing - reveal nothing + * Ores - reveal ores + * Fish - reveal ores and fish + * Water - reveal ores, fish and water + */ + ResourceRevealMode getResourceRevealMode() const; + void toggleResourceRevealMode(); + + using PlayerIDSet = std::unordered_set; + /** Destroys all buildings of given players, effectively defeating them. + * + * @param playerIds - Set of IDs of players. + */ + void destroyBuildings(const PlayerIDSet& playerIds); + /** Destroys all buildings of AI players. + */ + void destroyAllAIBuildings(); + +private: + /** Checks if cheats can be turned on at all. + * + * @return true if if cheats can be turned on, false otherwise + */ + bool canCheatModeBeOn() const; + + std::unique_ptr cheatCmdTracker_; + bool isCheatModeOn_ = false; + bool isAllVisible_ = false; + GameWorldBase& world_; + ResourceRevealMode resourceRevealMode_ = ResourceRevealMode::Nothing; +}; diff --git a/libs/s25main/GamePlayer.cpp b/libs/s25main/GamePlayer.cpp index d00de03b98..22dcf2ef0c 100644 --- a/libs/s25main/GamePlayer.cpp +++ b/libs/s25main/GamePlayer.cpp @@ -14,6 +14,7 @@ #include "WineLoader.h" #include "addons/const_addons.h" #include "buildings/noBuildingSite.h" +#include "buildings/nobHQ.h" #include "buildings/nobHarborBuilding.h" #include "buildings/nobMilitary.h" #include "buildings/nobUsual.h" @@ -411,6 +412,19 @@ void GamePlayer::RemoveBuildingSite(noBuildingSite* bldSite) buildings.Remove(bldSite); } +bool GamePlayer::IsHQTent() const +{ + if(const nobHQ* hq = GetHQ()) + return hq->IsTent(); + return false; +} + +void GamePlayer::SetHQIsTent(bool isTent) +{ + if(nobHQ* hq = GetHQ()) + hq->SetIsTent(isTent); +} + void GamePlayer::AddBuilding(noBuilding* bld, BuildingType bldType) { RTTR_Assert(bld->GetPlayer() == GetPlayerId()); @@ -430,8 +444,13 @@ void GamePlayer::AddBuilding(noBuilding* bld, BuildingType bldType) for(noShip* ship : ships) ship->NewHarborBuilt(static_cast(bld)); } else if(bldType == BuildingType::Headquarters) - hqPos = bld->GetPos(); - else if(BuildingProperties::IsMilitary(bldType)) + { + // If there is more than one HQ, keep the original position. + if(!hqPos.isValid()) + { + hqPos = bld->GetPos(); + } + } else if(BuildingProperties::IsMilitary(bldType)) { auto* milBld = static_cast(bld); // New built? -> Calculate frontier distance @@ -1400,6 +1419,12 @@ void GamePlayer::TestDefeat() Surrender(); } +nobHQ* GamePlayer::GetHQ() const +{ + const MapPoint& hqPos = GetHQPos(); + return const_cast(hqPos.isValid() ? GetGameWorld().GetSpecObj(hqPos) : nullptr); +} + void GamePlayer::Surrender() { if(isDefeated) @@ -2254,6 +2279,11 @@ void GamePlayer::Trade(nobBaseWarehouse* goalWh, const boost_variant2& GetRestrictedArea() { return restricted_area; } @@ -426,6 +430,7 @@ class GamePlayer : public GamePlayerInfo bool FindWarehouseForJob(Job job, noRoadNode* goal) const; /// Prüft, ob der Spieler besiegt wurde void TestDefeat(); + nobHQ* GetHQ() const; ////////////////////////////////////////////////////////////////////////// /// Unsynchronized state (e.g. lua, gui...) diff --git a/libs/s25main/buildings/noBaseBuilding.cpp b/libs/s25main/buildings/noBaseBuilding.cpp index ae7fd1e5c8..2617abec07 100644 --- a/libs/s25main/buildings/noBaseBuilding.cpp +++ b/libs/s25main/buildings/noBaseBuilding.cpp @@ -60,9 +60,21 @@ noBaseBuilding::noBaseBuilding(const NodalObjectType nop, const BuildingType typ { for(const Direction i : {Direction::West, Direction::NorthWest, Direction::NorthEast}) { - MapPoint pos2 = world->GetNeighbour(pos, i); - world->DestroyNO(pos2, false); - world->SetNO(pos2, new noExtension(this)); + const MapPoint neighbor = world->GetNeighbour(pos, i); + + if(type == BuildingType::Headquarters) + { + const NodalObjectType neighborNoType = world->GetNO(neighbor)->GetType(); + // Don't replace nearby static objects or trees. Needed for "build headquarters" cheat to work like in + // the original. This situation shouldn't happen any other way (can't normally build big buildings right + // next to static objects or trees). Trees which be remain because of this will be replaced by + // extensions instead of stumps if they are cut while still right next to the HQ. + if(neighborNoType == NodalObjectType::Object || neighborNoType == NodalObjectType::Tree) + continue; + } + + world->DestroyNO(neighbor, false); + world->SetNO(neighbor, new noExtension(this)); } } } @@ -220,7 +232,9 @@ void noBaseBuilding::DestroyBuildingExtensions() { for(const Direction i : {Direction::West, Direction::NorthWest, Direction::NorthEast}) { - world->DestroyNO(world->GetNeighbour(pos, i)); + const MapPoint neighbor = world->GetNeighbour(pos, i); + if(world->GetNO(neighbor)->GetType() == NodalObjectType::Extension) + world->DestroyNO(neighbor); } } } diff --git a/libs/s25main/desktops/dskGameInterface.cpp b/libs/s25main/desktops/dskGameInterface.cpp index c0a61f77fc..b26872c564 100644 --- a/libs/s25main/desktops/dskGameInterface.cpp +++ b/libs/s25main/desktops/dskGameInterface.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "dskGameInterface.h" +#include "Cheats.h" #include "CollisionDetection.h" #include "EventManager.h" #include "Game.h" @@ -104,8 +105,7 @@ dskGameInterface::dskGameInterface(std::shared_ptr game, std::shared_ptr(*game_).world_), gwv(worldViewer, Position(0, 0), VIDEODRIVER.GetRenderSize()), cbb(*LOADER.GetPaletteN("pal5")), - actionwindow(nullptr), roadwindow(nullptr), minimap(worldViewer), isScrolling(false), zoomLvl(ZOOM_DEFAULT_INDEX), - isCheatModeOn(false) + actionwindow(nullptr), roadwindow(nullptr), minimap(worldViewer), isScrolling(false), zoomLvl(ZOOM_DEFAULT_INDEX) { road.mode = RoadBuildMode::Disabled; road.point = MapPoint(0, 0); @@ -424,7 +424,7 @@ void dskGameInterface::Msg_PaintAfter() DrawPoint iconPos(VIDEODRIVER.GetRenderSize().x - 56, 32); // Draw cheating indicator icon (WINTER) - if(isCheatModeOn) + if(world.IsCheatModeOn()) { glArchivItem_Bitmap* cheatingImg = LOADER.GetImageN("io", 75); cheatingImg->DrawFull(iconPos); @@ -738,6 +738,8 @@ bool dskGameInterface::Msg_RightUp(const MouseCoords& /*mc*/) //-V524 */ bool dskGameInterface::Msg_KeyDown(const KeyEvent& ke) { + game_->world_.GetCheats().trackKeyEvent(ke); + switch(ke.kt) { default: break; @@ -779,17 +781,6 @@ bool dskGameInterface::Msg_KeyDown(const KeyEvent& ke) case KeyType::F9: // Readme WINDOWMANAGER.ToggleWindow(std::make_unique("readme.txt", _("Readme!"))); return true; - case KeyType::F10: - { -#ifdef NDEBUG - const bool allowHumanAI = isCheatModeOn; -#else - const bool allowHumanAI = true; -#endif // !NDEBUG - if(GAMECLIENT.GetState() == ClientState::Game && allowHumanAI && !GAMECLIENT.IsReplayModeOn()) - GAMECLIENT.ToggleHumanAIPlayer(AI::Info(AI::Type::Default, AI::Level::Easy)); - return true; - } case KeyType::F11: // Music player (midi files) WINDOWMANAGER.ToggleWindow(std::make_unique()); return true; @@ -798,28 +789,6 @@ bool dskGameInterface::Msg_KeyDown(const KeyEvent& ke) return true; } - static std::string winterCheat = "winter"; - switch(ke.c) - { - case 'w': - case 'i': - case 'n': - case 't': - case 'e': - case 'r': - curCheatTxt += char(ke.c); - if(winterCheat.find(curCheatTxt) == 0) - { - if(curCheatTxt == winterCheat) - { - isCheatModeOn = !isCheatModeOn; - curCheatTxt.clear(); - } - } else - curCheatTxt.clear(); - break; - } - switch(ke.c) { case '+': @@ -1127,9 +1096,9 @@ void dskGameInterface::ShowActionWindow(const iwAction::Tabs& action_tabs, MapPo void dskGameInterface::OnChatCommand(const std::string& cmd) { - if(cmd == "apocalypsis") - GAMECLIENT.CheatArmageddon(); - else if(cmd == "surrender") + game_->world_.GetCheats().trackChatCommand(cmd); + + if(cmd == "surrender") GAMECLIENT.Surrender(); else if(cmd == "async") (void)RANDOM.Rand(RANDOM_CONTEXT2(0), 255); diff --git a/libs/s25main/desktops/dskGameInterface.h b/libs/s25main/desktops/dskGameInterface.h index e7f3541121..ae2b6b2703 100644 --- a/libs/s25main/desktops/dskGameInterface.h +++ b/libs/s25main/desktops/dskGameInterface.h @@ -162,7 +162,5 @@ class dskGameInterface : bool isScrolling; Position startScrollPt; size_t zoomLvl; - bool isCheatModeOn; - std::string curCheatTxt; Subscription evBld; }; diff --git a/libs/s25main/factories/BuildingFactory.cpp b/libs/s25main/factories/BuildingFactory.cpp index b64e0c83bc..8f29d43373 100644 --- a/libs/s25main/factories/BuildingFactory.cpp +++ b/libs/s25main/factories/BuildingFactory.cpp @@ -14,12 +14,12 @@ #include "world/GameWorldBase.h" noBuilding* BuildingFactory::CreateBuilding(GameWorldBase& world, const BuildingType type, const MapPoint pt, - const unsigned char player, const Nation nation) + const unsigned char player, const Nation nation, bool isTent) { noBuilding* bld; switch(type) { - case BuildingType::Headquarters: bld = new nobHQ(pt, player, nation); break; + case BuildingType::Headquarters: bld = new nobHQ(pt, player, nation, isTent); break; case BuildingType::Storehouse: bld = new nobStorehouse(pt, player, nation); break; case BuildingType::HarborBuilding: bld = new nobHarborBuilding(pt, player, nation); break; case BuildingType::Barracks: diff --git a/libs/s25main/factories/BuildingFactory.h b/libs/s25main/factories/BuildingFactory.h index ad97916ec8..03c35c5f12 100644 --- a/libs/s25main/factories/BuildingFactory.h +++ b/libs/s25main/factories/BuildingFactory.h @@ -21,5 +21,5 @@ class BuildingFactory BuildingFactory() = delete; static noBuilding* CreateBuilding(GameWorldBase& world, BuildingType type, MapPoint pt, unsigned char player, - Nation nation); + Nation nation, bool isTent = false); }; diff --git a/libs/s25main/ingameWindows/iwAction.cpp b/libs/s25main/ingameWindows/iwAction.cpp index afb5c2be12..d1a343f241 100644 --- a/libs/s25main/ingameWindows/iwAction.cpp +++ b/libs/s25main/ingameWindows/iwAction.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "iwAction.h" +#include "Cheats.h" #include "GameInterface.h" #include "GamePlayer.h" #include "GlobalGameSettings.h" @@ -38,7 +39,8 @@ enum TabID TAB_FLAG, TAB_CUTROAD, TAB_ATTACK, - TAB_SEAATTACK + TAB_SEAATTACK, + TAB_CHEAT }; iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapPoint selectedPt, @@ -69,6 +71,8 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP TAB_ATTACK 3 = Option group: Better/Weaker TAB_ATTACK 4 = Angriff TAB_ATTACK 10-14 = Direktauswahl Anzahl + + TAB_CHEAT 1 = Place cheat building */ const GamePlayer& player = gwv.GetViewer().GetPlayer(); @@ -291,6 +295,15 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP // Beobachten-main_tab if(tabs.watch) { + if(gwv.GetWorld().GetCheats().canPlaceCheatBuilding(selectedPt)) + { + constexpr auto buildImgId = 18; + const auto buildImg = LOADER.GetImageN("io", buildImgId); + ctrlGroup* group = main_tab->AddTab(buildImg, _("Build headquarters"), TAB_CHEAT); + group->AddImageButton(1, DrawPoint{0, 45}, Extent{180, 36}, TextureColor::Grey, buildImg, + _("Build headquarters")); + } + ctrlGroup* group = main_tab->AddTab(LOADER.GetImageN("io", 36), _("Display options"), TAB_WATCH); const Extent btSize(45, 36); DrawPoint curPos(0, 45); @@ -438,6 +451,12 @@ void iwAction::Msg_Group_ButtonClick(const unsigned /*group_id*/, const unsigned Msg_ButtonClick_TabWatch(ctrl_id); } break; + + case TAB_CHEAT: + { + Msg_ButtonClick_TabCheat(ctrl_id); + } + break; } } @@ -453,6 +472,7 @@ void iwAction::Msg_TabChange(const unsigned ctrl_id, const unsigned short tab_id case TAB_FLAG: case TAB_CUTROAD: case TAB_SETFLAG: + case TAB_CHEAT: case TAB_WATCH: height = 138; break; case TAB_BUILD: { @@ -734,6 +754,17 @@ void iwAction::Msg_ButtonClick_TabWatch(const unsigned ctrl_id) } } +void iwAction::Msg_ButtonClick_TabCheat(const unsigned ctrl_id) +{ + switch(ctrl_id) + { + case 1: + gwv.GetWorld().GetCheats().placeCheatBuilding(selectedPt, gwv.GetViewer().GetPlayer()); + Close(); + break; + } +} + void iwAction::DisableMousePosResetOnClose() { mousePosAtOpen_ = DrawPoint::Invalid(); diff --git a/libs/s25main/ingameWindows/iwAction.h b/libs/s25main/ingameWindows/iwAction.h index 8544ee22c7..3007f4df09 100644 --- a/libs/s25main/ingameWindows/iwAction.h +++ b/libs/s25main/ingameWindows/iwAction.h @@ -91,6 +91,7 @@ class iwAction : public IngameWindow inline void Msg_ButtonClick_TabSeaAttack(unsigned ctrl_id); inline void Msg_ButtonClick_TabSetFlag(unsigned ctrl_id); inline void Msg_ButtonClick_TabWatch(unsigned ctrl_id); + inline void Msg_ButtonClick_TabCheat(unsigned ctrl_id); void DisableMousePosResetOnClose(); diff --git a/libs/s25main/lua/LuaPlayer.cpp b/libs/s25main/lua/LuaPlayer.cpp index 3dd5158883..850678d7eb 100644 --- a/libs/s25main/lua/LuaPlayer.cpp +++ b/libs/s25main/lua/LuaPlayer.cpp @@ -261,13 +261,7 @@ bool LuaPlayer::AIConstructionOrder(unsigned x, unsigned y, lua::SafeEnum(hqPos); - if(hq) - hq->SetIsTent(isTent); - } + player.SetHQIsTent(isTent); } bool LuaPlayer::IsDefeated() const diff --git a/libs/s25main/network/GameClient.cpp b/libs/s25main/network/GameClient.cpp index f6bf470891..765dda6ed4 100644 --- a/libs/s25main/network/GameClient.cpp +++ b/libs/s25main/network/GameClient.cpp @@ -1148,44 +1148,51 @@ bool GameClient::OnGameMessage(const GameMessage_GameCommand& msg) void GameClient::IncreaseSpeed() { - const bool debugMode = -#ifndef NDEBUG - true; -#else - false; -#endif - if(framesinfo.gfLengthReq > FramesInfo::milliseconds32_t(10)) - framesinfo.gfLengthReq -= FramesInfo::milliseconds32_t(10); - else if((replayMode || debugMode) && framesinfo.gfLengthReq == FramesInfo::milliseconds32_t(10)) - framesinfo.gfLengthReq = FramesInfo::milliseconds32_t(1); - else - framesinfo.gfLengthReq = FramesInfo::milliseconds32_t(70); - - if(replayMode) - framesinfo.gf_length = framesinfo.gfLengthReq; - else - mainPlayer.sendMsgAsync(new GameMessage_Speed(framesinfo.gfLengthReq.count())); + // 1..10 -> 0 + // 11..20 -> 10 + // 21..30 -> 20 etc. + SetGFLengthReq((framesinfo.gfLengthReq - 1ms) / 10 * 10); } void GameClient::DecreaseSpeed() { - const bool debugMode = + // 1.. 9 -> 10 + // 10..19 -> 20 + // 20..29 -> 30 etc. + SetGFLengthReq(framesinfo.gfLengthReq / 10 * 10 + 10ms); +} + +void GameClient::SetGFLengthReq(FramesInfo::milliseconds32_t gfLengthReq) +{ #ifndef NDEBUG - true; + constexpr auto minLength = 1ms; #else - false; + const auto minLength = IsReplayModeOn() ? 1ms : 10ms; #endif - FramesInfo::milliseconds32_t maxSpeed(replayMode ? 1000 : 70); + const auto maxLength = IsReplayModeOn() ? 1000ms : 70ms; - if(framesinfo.gfLengthReq == maxSpeed) - framesinfo.gfLengthReq = FramesInfo::milliseconds32_t(replayMode || debugMode ? 1 : 10); - else if(framesinfo.gfLengthReq == FramesInfo::milliseconds32_t(1)) - framesinfo.gfLengthReq = FramesInfo::milliseconds32_t(10); - else - framesinfo.gfLengthReq += FramesInfo::milliseconds32_t(10); + if(gfLengthReq == 0ms) + { + // already at minimum? wrap around from min to max + if(framesinfo.gfLengthReq == minLength) + framesinfo.gfLengthReq = maxLength; + else // treat set 0 as set minimum + framesinfo.gfLengthReq = minLength; + } else if(gfLengthReq < minLength) // clamp to min + { + framesinfo.gfLengthReq = minLength; + } else if(gfLengthReq > maxLength) + { + // already at maximum? wrap around from max to min + if(framesinfo.gfLengthReq == maxLength) + framesinfo.gfLengthReq = minLength; + else // clamp to max + framesinfo.gfLengthReq = maxLength; + } else + framesinfo.gfLengthReq = gfLengthReq; - if(replayMode) + if(IsReplayModeOn()) framesinfo.gf_length = framesinfo.gfLengthReq; else mainPlayer.sendMsgAsync(new GameMessage_Speed(framesinfo.gfLengthReq.count())); diff --git a/libs/s25main/network/GameClient.h b/libs/s25main/network/GameClient.h index c1638a5890..cf30e8e5d6 100644 --- a/libs/s25main/network/GameClient.h +++ b/libs/s25main/network/GameClient.h @@ -127,8 +127,14 @@ class GameClient final : /// And a 2nd time when the GUI is ready which actually starty the game void OnGameStart(); + // Used by + and v void IncreaseSpeed(); + // Used by - void DecreaseSpeed(); + // Used by ALT+1 through ALT+6 (cheats) + void SetGFLengthReq(FramesInfo::milliseconds32_t); + // Used by tests (stinks, but what to do?) + FramesInfo::milliseconds32_t GetGFLengthReq() { return framesinfo.gfLengthReq; } /// Lädt ein Replay und startet dementsprechend das Spiel bool StartReplay(const boost::filesystem::path& path); diff --git a/libs/s25main/nodeObjs/noTree.cpp b/libs/s25main/nodeObjs/noTree.cpp index c8413cf616..502150fb87 100644 --- a/libs/s25main/nodeObjs/noTree.cpp +++ b/libs/s25main/nodeObjs/noTree.cpp @@ -14,6 +14,7 @@ #include "network/GameClient.h" #include "noAnimal.h" #include "noDisappearingMapEnvObject.h" +#include "noExtension.h" #include "ogl/glSmartBitmap.h" #include "random/Random.h" #include "world/GameWorld.h" @@ -188,8 +189,28 @@ void noTree::HandleEvent(const unsigned id) // Baum verschwindet nun und es bleibt ein Baumstumpf zurück event = nullptr; GetEvMgr().AddToKillList(this); - world->SetNO(pos, new noDisappearingMapEnvObject(pos, 531), true); - world->RecalcBQAroundPoint(pos); + + // Due to the "build headquarters" cheat, there could be an HQ in these directions. Replace stump with + // extension which would have otherwise been placed in noBaseBuilding ctor. + bool shouldPlaceStump = true; + for(const Direction i : {Direction::East, Direction::SouthEast, Direction::SouthWest}) + { + const noBase* neighborNo = world->GetNO(world->GetNeighbour(pos, i)); + + if(neighborNo->GetType() == NodalObjectType::Building + && static_cast(neighborNo)->GetBuildingType() == BuildingType::Headquarters) + { + world->SetNO(pos, new noExtension(this), true); + shouldPlaceStump = false; + break; + } + } + + if(shouldPlaceStump) + { + world->SetNO(pos, new noDisappearingMapEnvObject(pos, 531), true); + world->RecalcBQAroundPoint(pos); + } // Minimap Bescheid geben (Baum gefallen) if(world->GetGameInterface()) diff --git a/libs/s25main/world/GameWorldBase.cpp b/libs/s25main/world/GameWorldBase.cpp index 568b6333b4..6be5a19eb5 100644 --- a/libs/s25main/world/GameWorldBase.cpp +++ b/libs/s25main/world/GameWorldBase.cpp @@ -4,6 +4,7 @@ #include "world/GameWorldBase.h" #include "BQCalculator.h" +#include "Cheats.h" #include "GamePlayer.h" #include "GlobalGameSettings.h" #include "MapGeometry.h" @@ -29,7 +30,8 @@ GameWorldBase::GameWorldBase(std::vector players, const GlobalGameSettings& gameSettings, EventManager& em) : roadPathFinder(new RoadPathFinder(*this)), freePathFinder(new FreePathFinder(*this)), players(std::move(players)), - gameSettings(gameSettings), em(em), soundManager(std::make_unique()), lua(nullptr), gi(nullptr) + gameSettings(gameSettings), em(em), soundManager(std::make_unique()), lua(nullptr), + cheats(std::make_unique(*this)), gi(nullptr) {} GameWorldBase::~GameWorldBase() = default; @@ -170,6 +172,14 @@ bool GameWorldBase::IsFlagAround(const MapPoint& pt) const return false; } +bool GameWorldBase::IsAnyNeighborOwned(const MapPoint& pt) const +{ + for(const MapPoint& nb : GetNeighbours(pt)) + if(GetNode(nb).owner) + return true; + return false; +} + void GameWorldBase::RecalcBQForRoad(const MapPoint pt) { RecalcBQ(pt); @@ -256,6 +266,11 @@ Position GameWorldBase::GetNodePos(const MapPoint pt) const return ::GetNodePos(pt, GetNode(pt).altitude); } +bool GameWorldBase::IsCheatModeOn() const +{ + return cheats->isCheatModeOn(); +} + void GameWorldBase::VisibilityChanged(const MapPoint pt, unsigned player, Visibility /*oldVis*/, Visibility /*newVis*/) { GetNotifications().publish(PlayerNodeNote(PlayerNodeNote::Visibility, pt, player)); diff --git a/libs/s25main/world/GameWorldBase.h b/libs/s25main/world/GameWorldBase.h index f47521e291..52b4954447 100644 --- a/libs/s25main/world/GameWorldBase.h +++ b/libs/s25main/world/GameWorldBase.h @@ -16,6 +16,7 @@ #include #include +class Cheats; class EventManager; class FreePathFinder; class GameInterface; @@ -61,6 +62,7 @@ class GameWorldBase : public World std::unique_ptr soundManager; std::set ptsInsideComputerBarriers; LuaInterfaceGame* lua; + std::unique_ptr cheats; protected: /// Interface zum GUI @@ -79,7 +81,7 @@ class GameWorldBase : public World // Remaining initialization after loading (BQ...) void InitAfterLoad(); - /// Setzt GameInterface + GameInterface* GetGameInterface() { return gi; } void SetGameInterface(GameInterface* const gi) { this->gi = gi; } /// Get the economy mode handler if set. @@ -95,6 +97,13 @@ class GameWorldBase : public World /// Check if a flag is at a neighbour node bool IsFlagAround(const MapPoint& pt) const; + /** Checks if any of the neighboring nodes of a given map point are owned by any player. + * + * @param pt - The map point whose neighbors should be checked. + * @return true if any neighbor has an owner, false otherwise + */ + bool IsAnyNeighborOwned(const MapPoint& pt) const; + /// Berechnet BQ bei einer gebauten Stra�e void RecalcBQForRoad(MapPoint pt); /// Pr�ft, ob sich in unmittelbarer N�he (im Radius von 4) Milit�rgeb�ude befinden @@ -218,6 +227,9 @@ class GameWorldBase : public World LuaInterfaceGame& GetLua() const { return *lua; } void SetLua(LuaInterfaceGame* newLua) { lua = newLua; } + Cheats& GetCheats() const { return *cheats; } + bool IsCheatModeOn() const; + protected: /// Called when the visibility of point changed for a player void VisibilityChanged(MapPoint pt, unsigned player, Visibility oldVis, Visibility newVis) override; diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index 5907cc91f6..9209226461 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -195,6 +195,9 @@ void GameWorldView::Draw(const RoadBuildState& rb, const MapPoint selected, bool for(IDrawNodeCallback* callback : drawNodeCallbacks) callback->onDraw(curPt, curPos); + + if(visibility == Visibility::Visible) + DrawResource(curPt, curPos, GetWorld().GetCheats().getResourceRevealMode()); } // Figuren zwischen den Zeilen zeichnen @@ -370,7 +373,9 @@ void GameWorldView::DrawNameProductivityOverlay(const TerrainRenderer& terrainRe auto* attackAidImage = LOADER.GetImageN("map_new", 20000); attackAidImage->DrawFull(curPos - DrawPoint(0, attackAidImage->getHeight())); } - continue; + // DO draw when object visible and cheat mode is on + if(gwv.GetVisibility(pt) != Visibility::Visible || !GetWorld().IsCheatModeOn()) + continue; } // Draw object name @@ -557,6 +562,54 @@ void GameWorldView::DrawBoundaryStone(const MapPoint& pt, const DrawPoint pos, V } } +void GameWorldView::DrawResource(const MapPoint& pt, DrawPoint curPos, Cheats::ResourceRevealMode resRevealMode) +{ + using RRM = Cheats::ResourceRevealMode; + + if(resRevealMode == RRM::Nothing) + return; + + const Resource res = gwv.GetNode(pt).resources; + const auto amount = res.getAmount(); + + if(!amount) + return; + + GoodType gt = GoodType::Nothing; + + switch(res.getType()) + { + case ResourceType::Iron: gt = GoodType::IronOre; break; + case ResourceType::Gold: gt = GoodType::Gold; break; + case ResourceType::Coal: gt = GoodType::Coal; break; + case ResourceType::Granite: gt = GoodType::Stones; break; + case ResourceType::Water: + if(resRevealMode >= RRM::Water) + { + gt = GoodType::Water; + break; + } else + return; + case ResourceType::Fish: + if(resRevealMode >= RRM::Fish) + { + gt = GoodType::Fish; + break; + } else + return; + default: return; + } + + if(auto bm = LOADER.GetWareTex(gt)) + { + for(auto i = 0u; i < amount; ++i) + { + bm->DrawFull(curPos); + curPos.y -= 4; + } + } +} + void GameWorldView::ToggleShowBQ() { show_bq = !show_bq; diff --git a/libs/s25main/world/GameWorldView.h b/libs/s25main/world/GameWorldView.h index b7d3302e41..88d7293831 100644 --- a/libs/s25main/world/GameWorldView.h +++ b/libs/s25main/world/GameWorldView.h @@ -4,6 +4,7 @@ #pragma once +#include "Cheats.h" #include "DrawPoint.h" #include "gameTypes/MapCoordinates.h" #include "gameTypes/MapTypes.h" @@ -127,6 +128,7 @@ class GameWorldView private: void CalcFxLx(); void DrawBoundaryStone(const MapPoint& pt, DrawPoint pos, Visibility vis); + void DrawResource(const MapPoint& pt, DrawPoint curPos, Cheats::ResourceRevealMode resRevealMode); void DrawObject(const MapPoint& pt, const DrawPoint& curPos) const; void DrawConstructionAid(const MapPoint& pt, const DrawPoint& curPos); void DrawFigures(const MapPoint& pt, const DrawPoint& curPos, std::vector& between_lines) const; diff --git a/libs/s25main/world/GameWorldViewer.cpp b/libs/s25main/world/GameWorldViewer.cpp index a51f6440be..1349d829ba 100644 --- a/libs/s25main/world/GameWorldViewer.cpp +++ b/libs/s25main/world/GameWorldViewer.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "world/GameWorldViewer.h" +#include "Cheats.h" #include "GamePlayer.h" #include "GlobalGameSettings.h" #include "RttrForeachPt.h" @@ -113,6 +114,9 @@ BuildingQuality GameWorldViewer::GetBQ(const MapPoint& pt) const Visibility GameWorldViewer::GetVisibility(const MapPoint pt) const { + if(GetWorld().GetCheats().isAllVisible()) + return Visibility::Visible; + /// Replaymodus und FoW aus? Dann alles sichtbar if(GAMECLIENT.IsReplayModeOn() && GAMECLIENT.IsReplayFOWDisabled()) return Visibility::Visible; diff --git a/tests/s25Main/integration/testCheatCommandTracker.cpp b/tests/s25Main/integration/testCheatCommandTracker.cpp new file mode 100644 index 0000000000..735a3f815b --- /dev/null +++ b/tests/s25Main/integration/testCheatCommandTracker.cpp @@ -0,0 +1,118 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "CheatCommandTracker.h" +#include "Cheats.h" +#include "driver/KeyEvent.h" +#include "worldFixtures/CreateEmptyWorld.h" +#include "worldFixtures/WorldFixture.h" + +BOOST_AUTO_TEST_SUITE(CheatsTests) + +namespace { +struct CheatCommandTrackerFixture : WorldFixture +{ + Cheats cheats_{world}; + CheatCommandTracker tracker_{cheats_}; + + KeyEvent makeKeyEvent(unsigned c) { return {KeyType::Char, c, 0, 0, 0}; } + KeyEvent makeKeyEvent(KeyType kt) { return {kt, 0, 0, 0, 0}; } + + void trackString(const std::string& str) + { + for(char c : str) + tracker_.trackKeyEvent(makeKeyEvent(c)); + } +}; + +} // namespace + +BOOST_FIXTURE_TEST_CASE(CheatModeIsOffByDefault, CheatCommandTrackerFixture) +{ + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == false); +} + +BOOST_FIXTURE_TEST_CASE(CheatModeCanBeTurnedOn, CheatCommandTrackerFixture) +{ + trackString("winter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == true); +} + +BOOST_FIXTURE_TEST_CASE(CheatModeCanBeTurnedOff, CheatCommandTrackerFixture) +{ + trackString("winter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == true); + trackString("winter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == false); +} + +BOOST_FIXTURE_TEST_CASE(CheatModeCanBeTurnedOnAndOffRepeatedly, CheatCommandTrackerFixture) +{ + trackString("winter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == true); + trackString("winter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == false); + trackString("winter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == true); + trackString("winter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == false); +} + +BOOST_FIXTURE_TEST_CASE(CheatModeIsNotTurnedOn_WhenIncomplete, CheatCommandTrackerFixture) +{ + trackString("winte"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == false); +} + +BOOST_FIXTURE_TEST_CASE(CheatModeIsNotTurnedOn_WhenInterruptedByAnotherKeyType, CheatCommandTrackerFixture) +{ + trackString("win"); + tracker_.trackKeyEvent(makeKeyEvent(KeyType::F10)); + trackString("ter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == false); +} + +BOOST_FIXTURE_TEST_CASE(CheatModeIsNotTurnedOn_WhenInterruptedByAnotherLetter, CheatCommandTrackerFixture) +{ + trackString("wainter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == false); +} + +BOOST_FIXTURE_TEST_CASE(CheatModeIsNotTurnedOn_WhenOrderOfCharactersIsWrong, CheatCommandTrackerFixture) +{ + trackString("winetr"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == false); +} + +BOOST_FIXTURE_TEST_CASE(CheatModeIsNotTurnedOn_WhenOrderOfCharactersIsWrong_Wraparound, CheatCommandTrackerFixture) +{ + trackString("rwinet"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == false); +} + +BOOST_FIXTURE_TEST_CASE(CheatModeIsNotTurnedOn_WhenACharacterIsRepeated, CheatCommandTrackerFixture) +{ + trackString("winnter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == false); +} + +BOOST_FIXTURE_TEST_CASE(CheatModeIsTurnedOn_WhenTheFirstCharacterIsRepeated, CheatCommandTrackerFixture) +{ + trackString("wwwinter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == true); +} + +BOOST_FIXTURE_TEST_CASE(CheatModeIsTurnedOn_EvenWhenWrongInputsWereProvidedBefore, CheatCommandTrackerFixture) +{ + trackString("www"); + auto ke = makeKeyEvent('1'); + ke.alt = true; + tracker_.trackKeyEvent(ke); + trackString("interwitter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == false); + trackString("winter"); + BOOST_TEST_REQUIRE(cheats_.isCheatModeOn() == true); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/s25Main/integration/testCheats.cpp b/tests/s25Main/integration/testCheats.cpp new file mode 100644 index 0000000000..467f8542fd --- /dev/null +++ b/tests/s25Main/integration/testCheats.cpp @@ -0,0 +1,311 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Cheats.h" +#include "GamePlayer.h" +#include "buildings/nobHQ.h" +#include "desktops/dskGameInterface.h" +#include "network/GameClient.h" +#include "worldFixtures/CreateEmptyWorld.h" +#include "worldFixtures/WorldFixture.h" +#include "gameData/MilitaryConsts.h" +#include + +namespace { +constexpr auto worldWidth = 64; +constexpr auto worldHeight = 64; +struct CheatWorldFixture : WorldFixture +{ + CheatWorldFixture() + { + p2.ps = PlayerState::AI; + p3.ps = PlayerState::AI; + } + + Cheats& cheats = world.GetCheats(); + + dskGameInterface gameDesktop{game, 0, 0, 0}; + const GameWorldViewer& viewer = gameDesktop.GetView().GetViewer(); + + GamePlayer& getPlayer(unsigned id) { return world.GetPlayer(id); } + GamePlayer& p1 = getPlayer(0); + GamePlayer& p2 = getPlayer(1); + GamePlayer& p3 = getPlayer(2); + + const MapPoint p1HQPos = p1.GetHQPos(); + const MapPoint p2HQPos = p2.GetHQPos(); + + MapPoint unownedPt = {static_cast(p1HQPos.x + HQ_RADIUS + 2), p1HQPos.y}; + + auto countHQs(const GamePlayer& player) + { + return player.GetBuildingRegister().GetBuildingNums().buildings[BuildingType::Headquarters]; + } + auto getHQs(const GamePlayer& player) + { + std::vector ret; + for(auto bld : player.GetBuildingRegister().GetStorehouses()) + if(bld->GetBuildingType() == BuildingType::Headquarters) + ret.push_back(static_cast(bld)); + return ret; + } + + auto countAllBuildings(const GamePlayer& player) + { + unsigned ret = 0; + for(auto b : player.GetBuildingRegister().GetBuildingNums().buildings) + ret += b; + return ret; + } +}; +} // namespace + +BOOST_FIXTURE_TEST_CASE(CheatModeIsOffByDefault, CheatWorldFixture) +{ + BOOST_TEST_REQUIRE(cheats.isCheatModeOn() == false); +} + +BOOST_FIXTURE_TEST_CASE(CanToggleCheatModeOn, CheatWorldFixture) +{ + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(cheats.isCheatModeOn() == true); +} + +BOOST_FIXTURE_TEST_CASE(CannotToggleCheatModeOn_IfMultiplayer, CheatWorldFixture) +{ + p2.ps = PlayerState::Occupied; + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(cheats.isCheatModeOn() == false); +} + +BOOST_FIXTURE_TEST_CASE(CanToggleCheatModeOnAndOff, CheatWorldFixture) +{ + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(cheats.isCheatModeOn() == true); + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(cheats.isCheatModeOn() == false); +} + +BOOST_FIXTURE_TEST_CASE(CanToggleCheatModeOnAndOffRepeatedly, CheatWorldFixture) +{ + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(cheats.isCheatModeOn() == true); + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(cheats.isCheatModeOn() == false); + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(cheats.isCheatModeOn() == true); + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(cheats.isCheatModeOn() == false); +} + +MOCK_BASE_CLASS(MockGameInterface, GameInterface) +{ + MOCK_METHOD(GI_PlayerDefeated, 1) + MOCK_METHOD(GI_UpdateMinimap, 1) + MOCK_METHOD(GI_FlagDestroyed, 1) + MOCK_METHOD(GI_TreatyOfAllianceChanged, 1) + MOCK_METHOD(GI_UpdateMapVisibility, 0) + MOCK_METHOD(GI_Winner, 1) + MOCK_METHOD(GI_TeamWinner, 1) + MOCK_METHOD(GI_StartRoadBuilding, 2) + MOCK_METHOD(GI_CancelRoadBuilding, 0) + MOCK_METHOD(GI_BuildRoad, 0) +}; + +BOOST_FIXTURE_TEST_CASE(CanToggleAllVisible_IfCheatModeIsOn, CheatWorldFixture) +{ + MockGameInterface mgi; + world.SetGameInterface(&mgi); + + MapPoint farawayPos = p1HQPos; + farawayPos.x += 20; + BOOST_TEST_REQUIRE((viewer.GetVisibility(p1HQPos) == Visibility::Visible) == true); + BOOST_TEST_REQUIRE((viewer.GetVisibility(farawayPos) == Visibility::Visible) == false); + cheats.toggleCheatMode(); + MOCK_EXPECT(mgi.GI_UpdateMapVisibility).once(); + cheats.toggleAllVisible(); + BOOST_TEST_REQUIRE((viewer.GetVisibility(p1HQPos) == Visibility::Visible) == true); + BOOST_TEST_REQUIRE((viewer.GetVisibility(farawayPos) == Visibility::Visible) == true); + MOCK_EXPECT(mgi.GI_UpdateMapVisibility).once(); + cheats.toggleAllVisible(); + BOOST_TEST_REQUIRE((viewer.GetVisibility(p1HQPos) == Visibility::Visible) == true); + BOOST_TEST_REQUIRE((viewer.GetVisibility(farawayPos) == Visibility::Visible) == false); +} + +BOOST_FIXTURE_TEST_CASE(CannotToggleAllVisible_IfCheatModeIsNotOn, CheatWorldFixture) +{ + MockGameInterface mgi; + world.SetGameInterface(&mgi); + + MOCK_EXPECT(mgi.GI_UpdateMapVisibility).never(); + MapPoint farawayPos = p1HQPos; + farawayPos.x += 20; + BOOST_TEST_REQUIRE((viewer.GetVisibility(p1HQPos) == Visibility::Visible) == true); + BOOST_TEST_REQUIRE((viewer.GetVisibility(farawayPos) == Visibility::Visible) == false); + cheats.toggleAllVisible(); + BOOST_TEST_REQUIRE((viewer.GetVisibility(p1HQPos) == Visibility::Visible) == true); + BOOST_TEST_REQUIRE((viewer.GetVisibility(farawayPos) == Visibility::Visible) == false); +} + +BOOST_FIXTURE_TEST_CASE(CannotPlaceCheatBuildingWithinOwnedTerritory, CheatWorldFixture) +{ + cheats.toggleCheatMode(); + + MapPoint p1territory = p1HQPos; + p1territory.x += 3; + p1territory.y += 3; + BOOST_TEST_REQUIRE(cheats.canPlaceCheatBuilding(p1territory) == false); + + MapPoint p2territory = p2HQPos; + p2territory.x += 3; + p2territory.y += 3; + BOOST_TEST_REQUIRE(cheats.canPlaceCheatBuilding(p2territory) == false); +} + +BOOST_FIXTURE_TEST_CASE(CannotPlaceCheatBuildingAtTerritoryBorderOrOneNodeFurther, CheatWorldFixture) +{ + cheats.toggleCheatMode(); + + MapPoint border = p1HQPos; + border.x += HQ_RADIUS; + BOOST_TEST_REQUIRE(cheats.canPlaceCheatBuilding(border) == false); + + MapPoint nodeBeyondBorder = border; + ++border.x; + BOOST_TEST_REQUIRE(cheats.canPlaceCheatBuilding(nodeBeyondBorder) == false); +} + +BOOST_FIXTURE_TEST_CASE(CanPlaceCheatBuildingOutsideOwnedTerritory, CheatWorldFixture) +{ + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(cheats.canPlaceCheatBuilding(unownedPt) == true); +} + +BOOST_FIXTURE_TEST_CASE(CannotPlaceCheatBuilding_IfCheatModeIsNotOn, CheatWorldFixture) +{ + BOOST_TEST_REQUIRE(cheats.canPlaceCheatBuilding(unownedPt) == false); +} + +BOOST_FIXTURE_TEST_CASE(PlacesHQAsACheatBuilding, CheatWorldFixture) +{ + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(countHQs(p1) == 1); + cheats.placeCheatBuilding(unownedPt, p1); + BOOST_TEST_REQUIRE(countHQs(p1) == 2); +} + +BOOST_FIXTURE_TEST_CASE(DoesNotPlaceHQAsACheatBuilding_IfCheatModeIsNotOn, CheatWorldFixture) +{ + BOOST_TEST_REQUIRE(countHQs(p1) == 1); + cheats.placeCheatBuilding(unownedPt, p1); + BOOST_TEST_REQUIRE(countHQs(p1) == 1); +} + +BOOST_FIXTURE_TEST_CASE(CanPlaceCheatBuildingForAnyPlayer, CheatWorldFixture) +{ + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(countHQs(p1) == 1); + BOOST_TEST_REQUIRE(countHQs(p2) == 1); + cheats.placeCheatBuilding(unownedPt, p2); + BOOST_TEST_REQUIRE(countHQs(p1) == 1); + BOOST_TEST_REQUIRE(countHQs(p2) == 2); + unownedPt.x -= 2 * (HQ_RADIUS + 2); + cheats.placeCheatBuilding(unownedPt, p1); + BOOST_TEST_REQUIRE(countHQs(p1) == 2); + BOOST_TEST_REQUIRE(countHQs(p2) == 2); +} + +BOOST_FIXTURE_TEST_CASE(CheatBuildingHasTheSameNation, CheatWorldFixture) +{ + cheats.toggleCheatMode(); + cheats.placeCheatBuilding(unownedPt, p1); + for(auto bld : getHQs(p1)) + BOOST_TEST_REQUIRE((bld->GetNation() == p1.nation) == true); +} + +BOOST_FIXTURE_TEST_CASE(CheatBuildingIsATent_IfPrimaryHQIsATent, CheatWorldFixture) +{ + p1.SetHQIsTent(true); + cheats.toggleCheatMode(); + cheats.placeCheatBuilding(unownedPt, p1); + for(auto bld : getHQs(p1)) + BOOST_TEST_REQUIRE(static_cast(bld)->IsTent() == true); +} + +BOOST_FIXTURE_TEST_CASE(CheatBuildingIsNotATent_IfPrimaryHQIsNotATent, CheatWorldFixture) +{ + p1.SetHQIsTent(false); + cheats.toggleCheatMode(); + cheats.placeCheatBuilding(unownedPt, p1); + for(auto bld : getHQs(p1)) + BOOST_TEST_REQUIRE(static_cast(bld)->IsTent() == false); +} + +BOOST_FIXTURE_TEST_CASE(CanToggleResourcesToRevealSuccessively, CheatWorldFixture) +{ + using RRM = Cheats::ResourceRevealMode; + + BOOST_CHECK(cheats.getResourceRevealMode() == RRM::Nothing); + cheats.toggleCheatMode(); + BOOST_CHECK(cheats.getResourceRevealMode() == RRM::Nothing); + cheats.toggleResourceRevealMode(); + BOOST_CHECK(cheats.getResourceRevealMode() == RRM::Ores); + cheats.toggleResourceRevealMode(); + BOOST_CHECK(cheats.getResourceRevealMode() == RRM::Fish); + cheats.toggleResourceRevealMode(); + BOOST_CHECK(cheats.getResourceRevealMode() == RRM::Water); + cheats.toggleResourceRevealMode(); + BOOST_CHECK(cheats.getResourceRevealMode() == RRM::Nothing); + cheats.toggleResourceRevealMode(); + BOOST_CHECK(cheats.getResourceRevealMode() == RRM::Ores); + cheats.toggleCheatMode(); + BOOST_CHECK(cheats.getResourceRevealMode() == RRM::Nothing); + cheats.toggleResourceRevealMode(); + cheats.toggleCheatMode(); + BOOST_CHECK(cheats.getResourceRevealMode() == RRM::Fish); +} + +BOOST_FIXTURE_TEST_CASE(DestroyBuildingsOfGivenPlayer, CheatWorldFixture) +{ + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(countAllBuildings(p1) == 1); + BOOST_TEST_REQUIRE(countAllBuildings(p2) == 1); + cheats.destroyBuildings({0}); + BOOST_TEST_REQUIRE(countAllBuildings(p1) == 0); + BOOST_TEST_REQUIRE(countAllBuildings(p2) == 1); +} + +BOOST_FIXTURE_TEST_CASE(DestroyBuildingsOfGivenPlayers, CheatWorldFixture) +{ + cheats.toggleCheatMode(); + BOOST_TEST_REQUIRE(countAllBuildings(p1) == 1); + BOOST_TEST_REQUIRE(countAllBuildings(p2) == 1); + cheats.destroyBuildings({0, 1}); + BOOST_TEST_REQUIRE(countAllBuildings(p1) == 0); + BOOST_TEST_REQUIRE(countAllBuildings(p2) == 0); +} + +BOOST_FIXTURE_TEST_CASE(DestroyBuildingsOfAIPlayers, CheatWorldFixture) +{ + cheats.toggleCheatMode(); + p2.ps = PlayerState::AI; + p3.ps = PlayerState::AI; + BOOST_TEST_REQUIRE(countAllBuildings(p1) == 1); + BOOST_TEST_REQUIRE(countAllBuildings(p2) == 1); + BOOST_TEST_REQUIRE(countAllBuildings(p3) == 1); + cheats.destroyAllAIBuildings(); + BOOST_TEST_REQUIRE(countAllBuildings(p1) == 1); + BOOST_TEST_REQUIRE(countAllBuildings(p2) == 0); + BOOST_TEST_REQUIRE(countAllBuildings(p3) == 0); +} + +BOOST_FIXTURE_TEST_CASE(CannotDestroyBuildings_IfCheatModeIsNotOn, CheatWorldFixture) +{ + p1.ps = PlayerState::AI; + BOOST_TEST_REQUIRE(countAllBuildings(p1) == 1); + cheats.destroyBuildings({0}); + BOOST_TEST_REQUIRE(countAllBuildings(p1) == 1); + cheats.destroyAllAIBuildings(); + BOOST_TEST_REQUIRE(countAllBuildings(p1) == 1); +} diff --git a/tests/s25Main/integration/testGamePlayer.cpp b/tests/s25Main/integration/testGamePlayer.cpp index 438e0131fe..76a6334380 100644 --- a/tests/s25Main/integration/testGamePlayer.cpp +++ b/tests/s25Main/integration/testGamePlayer.cpp @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: GPL-2.0-or-later +#include "Cheats.h" #include "GamePlayer.h" #include "buildings/nobBaseWarehouse.h" #include "buildings/nobMilitary.h" @@ -119,3 +120,40 @@ BOOST_FIXTURE_TEST_CASE(ProductivityStats, WorldFixtureEmpty1P) BOOST_TEST(buildingRegister.CalcProductivities() == expectedProductivity, per_element()); BOOST_TEST(buildingRegister.CalcAverageProductivity() == avgProd); } + +BOOST_FIXTURE_TEST_CASE(IsHQTent_ReturnsFalse_IfPrimaryHQIsNotTent, WorldFixtureEmpty1P) +{ + GamePlayer& p1 = world.GetPlayer(0); + + // place another HQ that is a tent + MapPoint newHqPos = p1.GetHQPos(); + newHqPos.x += 3; + BuildingFactory::CreateBuilding(world, BuildingType::Headquarters, newHqPos, 0, Nation::Babylonians, true); + + BOOST_TEST_REQUIRE(p1.IsHQTent() == false); +} + +BOOST_FIXTURE_TEST_CASE(IsHQTent_ReturnsTrue_IfPrimaryHQIsTent, WorldFixtureEmpty1P) +{ + GamePlayer& p1 = world.GetPlayer(0); + p1.SetHQIsTent(true); + + // place another HQ that is not a tent + MapPoint newHqPos = p1.GetHQPos(); + newHqPos.x += 3; + BuildingFactory::CreateBuilding(world, BuildingType::Headquarters, newHqPos, 0, Nation::Babylonians, false); + + BOOST_TEST_REQUIRE(p1.IsHQTent() == true); +} + +BOOST_FIXTURE_TEST_CASE(AllBuildingsAreEnabled_WhenCheatModeIsOn, WorldFixtureEmpty1P) +{ + GamePlayer& p1 = world.GetPlayer(0); + const auto bld = BuildingType::Brewery; + p1.DisableBuilding(bld); + BOOST_TEST_REQUIRE(p1.IsBuildingEnabled(bld) == false); + world.GetCheats().toggleCheatMode(); + BOOST_TEST_REQUIRE(p1.IsBuildingEnabled(bld) == true); + world.GetCheats().toggleCheatMode(); + BOOST_TEST_REQUIRE(p1.IsBuildingEnabled(bld) == false); +} diff --git a/tests/s25Main/network/testGameClient.cpp b/tests/s25Main/network/testGameClient.cpp index 711fffbc95..98f3778ec0 100644 --- a/tests/s25Main/network/testGameClient.cpp +++ b/tests/s25Main/network/testGameClient.cpp @@ -23,6 +23,8 @@ #include #include +using namespace std::literals; + namespace bfs = boost::filesystem; // LCOV_EXCL_START @@ -227,4 +229,158 @@ BOOST_AUTO_TEST_CASE(ClientDetectsMapBufferOverflow) BOOST_TEST(client.GetState() == ClientState::Stopped); } +BOOST_AUTO_TEST_CASE(CanSetGFLengthReq) +{ + GameClient client; + + client.SetGFLengthReq(50ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 50); +} + +BOOST_AUTO_TEST_CASE(CanSetGFLengthReq_Repeatedly) +{ + GameClient client; + + client.SetGFLengthReq(50ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 50); + client.SetGFLengthReq(40ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 40); +} + +BOOST_AUTO_TEST_CASE(IncreaseSpeed_DecreasesGF_WhenDivisibleBy10_By10) +{ + GameClient client; + + client.SetGFLengthReq(50ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 50); + client.IncreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 40); + client.IncreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 30); + client.IncreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 20); + client.IncreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 10); +} + +BOOST_AUTO_TEST_CASE(DecreaseSpeed_IncreasesGF_WhenDivisible10_By10) +{ + GameClient client; + + client.SetGFLengthReq(10ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 10); + client.DecreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 20); + client.DecreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 30); + client.DecreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 40); + client.DecreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 50); +} + +BOOST_AUTO_TEST_CASE(IncreaseSpeed_DecreasesGF_WhenNotDivisibleBy10_RoundsDownTo10) +{ + GameClient client; + + client.SetGFLengthReq(25ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 25); + client.IncreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 20); + + client.SetGFLengthReq(12ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 12); + client.IncreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 10); +} + +BOOST_AUTO_TEST_CASE(DecreaseSpeed_IncreasesGF_WhenNotDivisibleBy10_RoundsUpTo10) +{ + GameClient client; + + client.SetGFLengthReq(25ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 25); + client.DecreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 30); + + client.SetGFLengthReq(12ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 12); + client.DecreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 20); +} + +#ifndef NDEBUG +BOOST_AUTO_TEST_CASE(IncreaseSpeed_DecreasesGF_When10orLess_To1) +{ + GameClient client; + + client.SetGFLengthReq(10ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 10); + client.IncreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 1); + + client.SetGFLengthReq(3ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 3); + client.IncreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 1); +} + +BOOST_AUTO_TEST_CASE(DecreaseSpeed_IncreasesGF_WhenLessThan10_To10) +{ + GameClient client; + + client.SetGFLengthReq(3ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 3); + client.DecreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 10); + + client.SetGFLengthReq(1ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 1); + client.DecreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 10); +} + +BOOST_AUTO_TEST_CASE(IncreaseSpeed_SetsGF_When1_To70) +{ + GameClient client; + + client.SetGFLengthReq(1ms); + client.IncreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 70); +} + +BOOST_AUTO_TEST_CASE(DecreaseSpeed_SetsGF_WhenLimitReached_To1_ReplayModeOff) +{ + GameClient client; + + client.SetGFLengthReq(70ms); + client.DecreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 1); +} +#else +BOOST_AUTO_TEST_CASE(IncreaseSpeed_SetsGF_When10OrLess_To70) +{ + GameClient client; + + client.SetGFLengthReq(10ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 10); + client.IncreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 70); + + client.SetGFLengthReq(1ms); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 10); + client.IncreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 70); +} + +BOOST_AUTO_TEST_CASE(DecreaseSpeed_SetsGF_WhenLimitReached_To10_ReplayModeOff) +{ + GameClient client; + + client.SetGFLengthReq(70ms); + client.DecreaseSpeed(); + BOOST_TEST_REQUIRE(client.GetGFLengthReq().count() == 10); +} +#endif + BOOST_AUTO_TEST_SUITE_END()