You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
251 lines
6.2 KiB
251 lines
6.2 KiB
/** |
|
* @file ui_test.hpp |
|
* |
|
* Shared test fixture for UI domain tests. |
|
* |
|
* Provides a fully-initialised single-player game state with a level-25 Warrior |
|
* who has plenty of gold. All panels are closed before and after each test. |
|
* |
|
* Tests that need MPQ assets skip gracefully when they are not available. |
|
*/ |
|
#pragma once |
|
|
|
#include <algorithm> |
|
|
|
#include <gtest/gtest.h> |
|
|
|
#include "control/control.hpp" |
|
#include "controls/control_mode.hpp" |
|
#include "cursor.h" |
|
#include "engine/assets.hpp" |
|
#include "inv.h" |
|
#include "items.h" |
|
#include "minitext.h" |
|
#include "options.h" |
|
#include "player.h" |
|
#include "qol/stash.h" |
|
#include "qol/visual_store.h" |
|
#include "quests.h" |
|
#include "stores.h" |
|
#include "storm/storm_net.hpp" |
|
#include "tables/itemdat.h" |
|
#include "tables/playerdat.hpp" |
|
#include "tables/spelldat.h" |
|
|
|
namespace devilution { |
|
|
|
constexpr const char UITestMissingMpqMsg[] = "MPQ assets (spawn.mpq or DIABDAT.MPQ) not found - skipping test suite"; |
|
|
|
/** |
|
* @brief Base test fixture that boots enough of the engine for UI-level tests. |
|
* |
|
* Usage: |
|
* class MyTest : public UITest { ... }; |
|
* TEST_F(MyTest, SomeScenario) { ... } |
|
* |
|
* The fixture guarantees: |
|
* - A single-player game with one Warrior at character level 25 and 100 000 gold. |
|
* - ControlMode pinned to KeyboardAndMouse (tests are deterministic regardless of host input). |
|
* - All panels / stores / overlays closed before and after each test. |
|
* - visualStoreUI option disabled so we always exercise the text-based store path. |
|
* - A loopback network provider (needed by functions that send net commands). |
|
*/ |
|
class UITest : public ::testing::Test { |
|
protected: |
|
/* ---- once per test suite ---- */ |
|
|
|
static void SetUpTestSuite() |
|
{ |
|
LoadCoreArchives(); |
|
LoadGameArchives(); |
|
|
|
missingAssets_ = !HaveMainData(); |
|
if (missingAssets_) |
|
return; |
|
|
|
LoadPlayerDataFiles(); |
|
LoadItemData(); |
|
LoadSpellData(); |
|
LoadQuestData(); |
|
// Note: do NOT call InitCursor() here. CreatePlayer() calls |
|
// CreatePlrItems() which does its own InitCursor()/FreeCursor() cycle. |
|
} |
|
|
|
/* ---- every test ---- */ |
|
|
|
void SetUp() override |
|
{ |
|
if (missingAssets_) { |
|
GTEST_SKIP() << UITestMissingMpqMsg; |
|
} |
|
|
|
Players.resize(1); |
|
MyPlayer = &Players[0]; |
|
|
|
// Ensure we are in single-player Diablo mode. |
|
gbIsHellfire = false; |
|
gbIsMultiplayer = false; |
|
|
|
CreatePlayer(*MyPlayer, HeroClass::Warrior); |
|
MyPlayer->setCharacterLevel(25); |
|
|
|
// CreatePlayer() calls CreatePlrItems() which does InitCursor()/FreeCursor(). |
|
// Re-initialise cursors because store operations (StoreAutoPlace, etc.) |
|
// need cursor sprite data to determine item sizes. |
|
InitCursor(); |
|
|
|
// Give the player a generous amount of gold, distributed as inventory gold piles. |
|
SetPlayerGold(100000); |
|
|
|
// Initialise stash with some gold too so stash-related paths work. |
|
Stash = {}; |
|
Stash.gold = 0; |
|
|
|
// Pin the control mode so behaviour is deterministic. |
|
ControlMode = ControlTypes::KeyboardAndMouse; |
|
|
|
// Always use the text-based store path so we can drive its state machine. |
|
GetOptions().Gameplay.visualStoreUI.SetValue(false); |
|
GetOptions().Gameplay.showItemGraphicsInStores.SetValue(false); |
|
|
|
// Close everything that might be open. |
|
CloseAllPanels(); |
|
|
|
// Set up loopback networking (needed for inventory mutations that send net commands). |
|
SNetInitializeProvider(SELCONN_LOOPBACK, nullptr); |
|
|
|
// Clear store state. |
|
InitStores(); |
|
|
|
// Make sure quest-text overlay is off. |
|
qtextflag = false; |
|
} |
|
|
|
void TearDown() override |
|
{ |
|
CloseAllPanels(); |
|
ActiveStore = TalkID::None; |
|
FreeCursor(); |
|
} |
|
|
|
/* ---- helpers ---- */ |
|
|
|
/** |
|
* @brief Close every panel / overlay without relying on ClosePanels() |
|
* (which has no header declaration and does cursor-position side-effects |
|
* that are undesirable in headless tests). |
|
*/ |
|
static void CloseAllPanels() |
|
{ |
|
invflag = false; |
|
SpellbookFlag = false; |
|
CharFlag = false; |
|
QuestLogIsOpen = false; |
|
SpellSelectFlag = false; |
|
IsStashOpen = false; |
|
if (IsVisualStoreOpen) { |
|
IsVisualStoreOpen = false; |
|
} |
|
ActiveStore = TalkID::None; |
|
} |
|
|
|
/** |
|
* @brief Give the player exactly @p amount gold, placed as inventory gold piles. |
|
*/ |
|
static void SetPlayerGold(int amount) |
|
{ |
|
// Clear existing gold piles. |
|
for (int i = 0; i < MyPlayer->_pNumInv; ++i) { |
|
if (MyPlayer->InvList[i]._itype == ItemType::Gold) { |
|
MyPlayer->InvList[i].clear(); |
|
} |
|
} |
|
|
|
MyPlayer->_pGold = 0; |
|
|
|
if (amount <= 0) |
|
return; |
|
|
|
// Place gold in a single pile (up to GOLD_MAX_LIMIT per pile). |
|
int remaining = amount; |
|
int slot = 0; |
|
while (remaining > 0 && slot < InventoryGridCells) { |
|
int pileSize = std::min(remaining, static_cast<int>(GOLD_MAX_LIMIT)); |
|
MyPlayer->InvList[slot]._itype = ItemType::Gold; |
|
MyPlayer->InvList[slot]._ivalue = pileSize; |
|
MyPlayer->InvGrid[slot] = static_cast<int8_t>(slot + 1); |
|
remaining -= pileSize; |
|
slot++; |
|
} |
|
MyPlayer->_pNumInv = slot; |
|
MyPlayer->_pGold = amount; |
|
} |
|
|
|
/** |
|
* @brief Clear the player's inventory completely (items + grid + gold). |
|
*/ |
|
static void ClearInventory() |
|
{ |
|
for (int i = 0; i < InventoryGridCells; i++) { |
|
MyPlayer->InvList[i] = {}; |
|
MyPlayer->InvGrid[i] = 0; |
|
} |
|
MyPlayer->_pNumInv = 0; |
|
MyPlayer->_pGold = 0; |
|
} |
|
|
|
/** |
|
* @brief Clear the player's belt. |
|
*/ |
|
static void ClearBelt() |
|
{ |
|
for (int i = 0; i < MaxBeltItems; i++) { |
|
MyPlayer->SpdList[i].clear(); |
|
} |
|
} |
|
|
|
/** |
|
* @brief Clear all equipment slots. |
|
*/ |
|
static void ClearEquipment() |
|
{ |
|
for (auto &bodyItem : MyPlayer->InvBody) { |
|
bodyItem = {}; |
|
} |
|
} |
|
|
|
/** |
|
* @brief Completely strip the player of all items and gold. |
|
*/ |
|
void StripPlayer() |
|
{ |
|
ClearInventory(); |
|
ClearBelt(); |
|
ClearEquipment(); |
|
MyPlayer->_pGold = 0; |
|
} |
|
|
|
/** |
|
* @brief Place a simple item in the player's inventory at the first free slot. |
|
* @return The inventory index where the item was placed, or -1 on failure. |
|
*/ |
|
int PlaceItemInInventory(const Item &item) |
|
{ |
|
int idx = MyPlayer->_pNumInv; |
|
if (idx >= InventoryGridCells) |
|
return -1; |
|
MyPlayer->InvList[idx] = item; |
|
MyPlayer->InvGrid[idx] = static_cast<int8_t>(idx + 1); |
|
MyPlayer->_pNumInv = idx + 1; |
|
return idx; |
|
} |
|
|
|
private: |
|
static bool missingAssets_; |
|
}; |
|
|
|
// Static member definition — must appear in exactly one translation unit. |
|
// Since this is a header, we use `inline` (C++17). |
|
inline bool UITest::missingAssets_ = false; |
|
|
|
} // namespace devilution
|