mirror of
https://github.com/vcmi/vcmi.git
synced 2025-06-23 00:28:08 +02:00
Fix regressions
This commit is contained in:
@ -2012,6 +2012,9 @@ void NewTurn::applyGs(CGameState *gs)
|
|||||||
logGlobal->error("Hero %d not found in NewTurn::applyGs", h.id.getNum());
|
logGlobal->error("Hero %d not found in NewTurn::applyGs", h.id.getNum());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hero->setMovementPoints(h.move);
|
||||||
|
hero->mana = h.mana;
|
||||||
}
|
}
|
||||||
|
|
||||||
gs->hpool->onNewDay();
|
gs->hpool->onNewDay();
|
||||||
|
@ -385,7 +385,7 @@ int CGameState::getDate(Date::EDateType mode) const
|
|||||||
CGameState::CGameState()
|
CGameState::CGameState()
|
||||||
{
|
{
|
||||||
gs = this;
|
gs = this;
|
||||||
hpool = std::make_unique<TavernHeroesPool>(this);
|
hpool = std::make_unique<TavernHeroesPool>();
|
||||||
applier = std::make_shared<CApplier<CBaseForGSApply>>();
|
applier = std::make_shared<CApplier<CBaseForGSApply>>();
|
||||||
registerTypesClientPacks1(*applier);
|
registerTypesClientPacks1(*applier);
|
||||||
registerTypesClientPacks2(*applier);
|
registerTypesClientPacks2(*applier);
|
||||||
|
@ -10,18 +10,9 @@
|
|||||||
#include "StdInc.h"
|
#include "StdInc.h"
|
||||||
#include "TavernHeroesPool.h"
|
#include "TavernHeroesPool.h"
|
||||||
|
|
||||||
#include "CGameState.h"
|
|
||||||
#include "CPlayerState.h"
|
|
||||||
|
|
||||||
#include "../mapObjects/CGHeroInstance.h"
|
#include "../mapObjects/CGHeroInstance.h"
|
||||||
#include "../CHeroHandler.h"
|
|
||||||
|
|
||||||
TavernHeroesPool::TavernHeroesPool() = default;
|
VCMI_LIB_NAMESPACE_BEGIN
|
||||||
|
|
||||||
TavernHeroesPool::TavernHeroesPool(CGameState * gameState)
|
|
||||||
: gameState(gameState)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
TavernHeroesPool::~TavernHeroesPool()
|
TavernHeroesPool::~TavernHeroesPool()
|
||||||
{
|
{
|
||||||
@ -29,7 +20,7 @@ TavernHeroesPool::~TavernHeroesPool()
|
|||||||
delete ptr.second;
|
delete ptr.second;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::map<HeroTypeID, CGHeroInstance*> TavernHeroesPool::unusedHeroesFromPool()
|
std::map<HeroTypeID, CGHeroInstance*> TavernHeroesPool::unusedHeroesFromPool() const
|
||||||
{
|
{
|
||||||
std::map<HeroTypeID, CGHeroInstance*> pool = heroesPool;
|
std::map<HeroTypeID, CGHeroInstance*> pool = heroesPool;
|
||||||
for(const auto & player : currentTavern)
|
for(const auto & player : currentTavern)
|
||||||
@ -63,71 +54,6 @@ bool TavernHeroesPool::isHeroAvailableFor(HeroTypeID hero, PlayerColor color) co
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
CGHeroInstance * TavernHeroesPool::pickHeroFor(TavernHeroSlot slot,
|
|
||||||
const PlayerColor & player,
|
|
||||||
const FactionID & factionID,
|
|
||||||
CRandomGenerator & rand,
|
|
||||||
const CHeroClass * bannedClass) const
|
|
||||||
{
|
|
||||||
if(player>=PlayerColor::PLAYER_LIMIT)
|
|
||||||
{
|
|
||||||
logGlobal->error("Cannot pick hero for player %d. Wrong owner!", player.getStr());
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(slot == TavernHeroSlot::NATIVE)
|
|
||||||
{
|
|
||||||
std::vector<CGHeroInstance *> pool;
|
|
||||||
|
|
||||||
for(auto & elem : heroesPool)
|
|
||||||
{
|
|
||||||
//get all available heroes
|
|
||||||
bool heroAvailable = isHeroAvailableFor(elem.first, player);
|
|
||||||
bool heroClassNative = elem.second->type->heroClass->faction == factionID;
|
|
||||||
|
|
||||||
if(heroAvailable && heroClassNative)
|
|
||||||
pool.push_back(elem.second);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!pool.empty())
|
|
||||||
return *RandomGeneratorUtil::nextItem(pool, rand);
|
|
||||||
|
|
||||||
logGlobal->error("Cannot pick native hero for %s. Picking any...", player.getStr());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<CGHeroInstance *> pool;
|
|
||||||
int totalWeight = 0;
|
|
||||||
|
|
||||||
for(auto & elem : heroesPool)
|
|
||||||
{
|
|
||||||
bool heroAvailable = isHeroAvailableFor(elem.first, player);
|
|
||||||
bool heroClassBanned = bannedClass && elem.second->type->heroClass == bannedClass;
|
|
||||||
|
|
||||||
if ( heroAvailable && !heroClassBanned)
|
|
||||||
{
|
|
||||||
pool.push_back(elem.second);
|
|
||||||
totalWeight += elem.second->type->heroClass->selectionProbability[factionID]; //total weight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(pool.empty() || totalWeight == 0)
|
|
||||||
{
|
|
||||||
logGlobal->error("There are no heroes available for player %s!", player.getStr());
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
int roll = rand.nextInt(totalWeight - 1);
|
|
||||||
for (auto & elem : pool)
|
|
||||||
{
|
|
||||||
roll -= elem->type->heroClass->selectionProbability[factionID];
|
|
||||||
if(roll < 0)
|
|
||||||
{
|
|
||||||
return elem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pool.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<const CGHeroInstance *> TavernHeroesPool::getHeroesFor(PlayerColor color) const
|
std::vector<const CGHeroInstance *> TavernHeroesPool::getHeroesFor(PlayerColor color) const
|
||||||
{
|
{
|
||||||
std::vector<const CGHeroInstance *> result;
|
std::vector<const CGHeroInstance *> result;
|
||||||
@ -174,3 +100,5 @@ void TavernHeroesPool::setAvailability(HeroTypeID hero, PlayerColor::Mask mask)
|
|||||||
{
|
{
|
||||||
pavailable[hero] = mask;
|
pavailable[hero] = mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VCMI_LIB_NAMESPACE_END
|
||||||
|
@ -23,38 +23,34 @@ class CSimpleArmy;
|
|||||||
|
|
||||||
enum class TavernHeroSlot
|
enum class TavernHeroSlot
|
||||||
{
|
{
|
||||||
NATIVE,
|
NATIVE, // 1st / left slot in tavern, contains hero native to player's faction on new week
|
||||||
RANDOM
|
RANDOM // 2nd / right slot in tavern, contains hero of random class
|
||||||
};
|
};
|
||||||
|
|
||||||
class DLL_LINKAGE TavernHeroesPool
|
class DLL_LINKAGE TavernHeroesPool
|
||||||
{
|
{
|
||||||
CGameState * gameState;
|
/// list of all heroes in pool, including those currently present in taverns
|
||||||
|
|
||||||
//[subID] - heroes available to buy; nullptr if not available
|
|
||||||
std::map<HeroTypeID, CGHeroInstance* > heroesPool;
|
std::map<HeroTypeID, CGHeroInstance* > heroesPool;
|
||||||
|
|
||||||
// [subid] -> which players can recruit hero (binary flags)
|
/// list of which players are able to purchase specific hero
|
||||||
|
/// if hero is not present in list, he is available for everyone
|
||||||
std::map<HeroTypeID, PlayerColor::Mask> pavailable;
|
std::map<HeroTypeID, PlayerColor::Mask> pavailable;
|
||||||
|
|
||||||
std::map<HeroTypeID, CGHeroInstance* > unusedHeroesFromPool(); //heroes pool without heroes that are available in taverns
|
/// list of heroes currently available in a tavern of a specific player
|
||||||
|
|
||||||
std::map<PlayerColor, std::map<TavernHeroSlot, CGHeroInstance*> > currentTavern;
|
std::map<PlayerColor, std::map<TavernHeroSlot, CGHeroInstance*> > currentTavern;
|
||||||
|
|
||||||
bool isHeroAvailableFor(HeroTypeID hero, PlayerColor color) const;
|
|
||||||
public:
|
public:
|
||||||
TavernHeroesPool();
|
|
||||||
TavernHeroesPool(CGameState * gameState);
|
|
||||||
~TavernHeroesPool();
|
~TavernHeroesPool();
|
||||||
|
|
||||||
CGHeroInstance * pickHeroFor(TavernHeroSlot slot,
|
/// Returns heroes currently availabe in tavern of a specific player
|
||||||
const PlayerColor & player,
|
|
||||||
const FactionID & faction,
|
|
||||||
CRandomGenerator & rand,
|
|
||||||
const CHeroClass * bannedClass = nullptr) const;
|
|
||||||
|
|
||||||
std::vector<const CGHeroInstance *> getHeroesFor(PlayerColor color) const;
|
std::vector<const CGHeroInstance *> getHeroesFor(PlayerColor color) const;
|
||||||
|
|
||||||
|
/// returns heroes in pool without heroes that are available in taverns
|
||||||
|
std::map<HeroTypeID, CGHeroInstance* > unusedHeroesFromPool() const;
|
||||||
|
|
||||||
|
/// Returns true if hero is available to a specific player
|
||||||
|
bool isHeroAvailableFor(HeroTypeID hero, PlayerColor color) const;
|
||||||
|
|
||||||
CGHeroInstance * takeHero(HeroTypeID hero);
|
CGHeroInstance * takeHero(HeroTypeID hero);
|
||||||
|
|
||||||
/// reset mana and movement points for all heroes in pool
|
/// reset mana and movement points for all heroes in pool
|
||||||
@ -66,7 +62,6 @@ public:
|
|||||||
|
|
||||||
template <typename Handler> void serialize(Handler &h, const int version)
|
template <typename Handler> void serialize(Handler &h, const int version)
|
||||||
{
|
{
|
||||||
h & gameState;
|
|
||||||
h & heroesPool;
|
h & heroesPool;
|
||||||
h & pavailable;
|
h & pavailable;
|
||||||
}
|
}
|
||||||
|
@ -58,19 +58,28 @@ void HeroPoolProcessor::clearHeroFromSlot(const PlayerColor & color, TavernHeroS
|
|||||||
gameHandler->sendAndApply(&sah);
|
gameHandler->sendAndApply(&sah);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot)
|
void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveArmy)
|
||||||
{
|
{
|
||||||
SetAvailableHero sah;
|
SetAvailableHero sah;
|
||||||
sah.player = color;
|
sah.player = color;
|
||||||
sah.slotID = static_cast<int>(slot);
|
sah.slotID = static_cast<int>(slot);
|
||||||
|
|
||||||
//first hero - native if possible, second hero -> any other class
|
//first hero - native if possible, second hero -> any other class
|
||||||
CGHeroInstance *h = gameHandler->gameState()->hpool->pickHeroFor(slot, color, gameHandler->getPlayerSettings(color)->castle, gameHandler->getRandomGenerator());
|
CGHeroInstance *h = pickHeroFor(needNativeHero, color, gameHandler->getPlayerSettings(color)->castle, gameHandler->getRandomGenerator(), nullptr);
|
||||||
|
|
||||||
if (h)
|
if (h)
|
||||||
{
|
{
|
||||||
sah.hid = h->subID;
|
sah.hid = h->subID;
|
||||||
h->initArmy(gameHandler->getRandomGenerator(), &sah.army);
|
|
||||||
|
if (giveArmy)
|
||||||
|
{
|
||||||
|
h->initArmy(gameHandler->getRandomGenerator(), &sah.army);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sah.army.clear();
|
||||||
|
sah.army.setCreature(SlotID(0), h->type->initialArmy[0].creature, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -83,8 +92,8 @@ void HeroPoolProcessor::onNewWeek(const PlayerColor & color)
|
|||||||
{
|
{
|
||||||
clearHeroFromSlot(color, TavernHeroSlot::NATIVE);
|
clearHeroFromSlot(color, TavernHeroSlot::NATIVE);
|
||||||
clearHeroFromSlot(color, TavernHeroSlot::RANDOM);
|
clearHeroFromSlot(color, TavernHeroSlot::RANDOM);
|
||||||
selectNewHeroForSlot(color, TavernHeroSlot::NATIVE);
|
selectNewHeroForSlot(color, TavernHeroSlot::NATIVE, true, true);
|
||||||
selectNewHeroForSlot(color, TavernHeroSlot::RANDOM);
|
selectNewHeroForSlot(color, TavernHeroSlot::RANDOM, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HeroPoolProcessor::hireHero(const CGObjectInstance *obj, const HeroTypeID & heroToRecruit, const PlayerColor & player)
|
bool HeroPoolProcessor::hireHero(const CGObjectInstance *obj, const HeroTypeID & heroToRecruit, const PlayerColor & player)
|
||||||
@ -101,34 +110,34 @@ bool HeroPoolProcessor::hireHero(const CGObjectInstance *obj, const HeroTypeID &
|
|||||||
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!"))
|
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;
|
return false;
|
||||||
|
|
||||||
if (town) //tavern in town
|
if(town) //tavern in town
|
||||||
{
|
{
|
||||||
if (!town->hasBuilt(BuildingID::TAVERN) && gameHandler->complain("No tavern!"))
|
if(!town->hasBuilt(BuildingID::TAVERN) && gameHandler->complain("No tavern!"))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (town->visitingHero && gameHandler->complain("There is visiting hero - no place!"))
|
if(town->visitingHero && gameHandler->complain("There is visiting hero - no place!"))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj->ID == Obj::TAVERN)
|
if(obj->ID == Obj::TAVERN)
|
||||||
{
|
{
|
||||||
if (gameHandler->getTile(obj->visitablePos())->visitableObjects.back() != obj && gameHandler->complain("Tavern entry must be unoccupied!"))
|
if(gameHandler->getTile(obj->visitablePos())->visitableObjects.back() != obj && gameHandler->complain("Tavern entry must be unoccupied!"))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto recruitableHeroes = gameHandler->gameState()->hpool->getHeroesFor(player);
|
auto recruitableHeroes = gameHandler->gameState()->hpool->getHeroesFor(player);
|
||||||
|
|
||||||
const CGHeroInstance *recruitedHero = nullptr;;
|
const CGHeroInstance * recruitedHero = nullptr;
|
||||||
|
|
||||||
for(const auto & hero : recruitableHeroes)
|
for(const auto & hero : recruitableHeroes)
|
||||||
{
|
{
|
||||||
if (hero->subID == heroToRecruit)
|
if(hero->subID == heroToRecruit)
|
||||||
recruitedHero = hero;
|
recruitedHero = hero;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!recruitedHero)
|
if(!recruitedHero)
|
||||||
{
|
{
|
||||||
gameHandler->complain ("Hero is not available for hiring!");
|
gameHandler->complain("Hero is not available for hiring!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,19 +146,21 @@ bool HeroPoolProcessor::hireHero(const CGObjectInstance *obj, const HeroTypeID &
|
|||||||
hr.hid = recruitedHero->subID;
|
hr.hid = recruitedHero->subID;
|
||||||
hr.player = player;
|
hr.player = player;
|
||||||
hr.tile = recruitedHero->convertFromVisitablePos(obj->visitablePos());
|
hr.tile = recruitedHero->convertFromVisitablePos(obj->visitablePos());
|
||||||
if (gameHandler->getTile(hr.tile)->isWater())
|
if(gameHandler->getTile(hr.tile)->isWater())
|
||||||
{
|
{
|
||||||
//Create a new boat for hero
|
//Create a new boat for hero
|
||||||
gameHandler->createObject(obj->visitablePos(), Obj::BOAT, recruitedHero->getBoatType().getNum());
|
gameHandler->createObject(obj->visitablePos(), Obj::BOAT, recruitedHero->getBoatType().getNum());
|
||||||
|
|
||||||
hr.boatId = gameHandler->getTopObj(hr.tile)->id;
|
hr.boatId = gameHandler->getTopObj(hr.tile)->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply netpack -> this will remove hired hero from tavern slot
|
||||||
gameHandler->sendAndApply(&hr);
|
gameHandler->sendAndApply(&hr);
|
||||||
|
|
||||||
if (recruitableHeroes[0] == recruitedHero)
|
if(recruitableHeroes[0] == recruitedHero)
|
||||||
selectNewHeroForSlot(player, TavernHeroSlot::NATIVE);
|
selectNewHeroForSlot(player, TavernHeroSlot::NATIVE, false, false);
|
||||||
else
|
else
|
||||||
selectNewHeroForSlot(player, TavernHeroSlot::RANDOM);
|
selectNewHeroForSlot(player, TavernHeroSlot::RANDOM, false, false);
|
||||||
|
|
||||||
gameHandler->giveResource(player, EGameResID::GOLD, -GameConstants::HERO_GOLD_COST);
|
gameHandler->giveResource(player, EGameResID::GOLD, -GameConstants::HERO_GOLD_COST);
|
||||||
|
|
||||||
@ -160,3 +171,70 @@ bool HeroPoolProcessor::hireHero(const CGObjectInstance *obj, const HeroTypeID &
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CGHeroInstance * HeroPoolProcessor::pickHeroFor(bool isNative,
|
||||||
|
const PlayerColor & player,
|
||||||
|
const FactionID & factionID,
|
||||||
|
CRandomGenerator & rand,
|
||||||
|
const CHeroClass * bannedClass) const
|
||||||
|
{
|
||||||
|
if(player >= PlayerColor::PLAYER_LIMIT)
|
||||||
|
{
|
||||||
|
logGlobal->error("Cannot pick hero for player %d. Wrong owner!", player.getStr());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto & hpool = gameHandler->gameState()->hpool;
|
||||||
|
|
||||||
|
if(isNative)
|
||||||
|
{
|
||||||
|
std::vector<CGHeroInstance *> pool;
|
||||||
|
|
||||||
|
for(auto & elem : hpool->unusedHeroesFromPool())
|
||||||
|
{
|
||||||
|
//get all available heroes
|
||||||
|
bool heroAvailable = hpool->isHeroAvailableFor(elem.first, player);
|
||||||
|
bool heroClassNative = elem.second->type->heroClass->faction == factionID;
|
||||||
|
|
||||||
|
if(heroAvailable && heroClassNative)
|
||||||
|
pool.push_back(elem.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!pool.empty())
|
||||||
|
return *RandomGeneratorUtil::nextItem(pool, rand);
|
||||||
|
|
||||||
|
logGlobal->error("Cannot pick native hero for %s. Picking any...", player.getStr());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<CGHeroInstance *> pool;
|
||||||
|
int totalWeight = 0;
|
||||||
|
|
||||||
|
for(auto & elem : hpool->unusedHeroesFromPool())
|
||||||
|
{
|
||||||
|
bool heroAvailable = hpool->isHeroAvailableFor(elem.first, player);
|
||||||
|
bool heroClassBanned = bannedClass && elem.second->type->heroClass == bannedClass;
|
||||||
|
|
||||||
|
if(heroAvailable && !heroClassBanned)
|
||||||
|
{
|
||||||
|
pool.push_back(elem.second);
|
||||||
|
totalWeight += elem.second->type->heroClass->selectionProbability[factionID]; //total weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(pool.empty() || totalWeight == 0)
|
||||||
|
{
|
||||||
|
logGlobal->error("There are no heroes available for player %s!", player.getStr());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int roll = rand.nextInt(totalWeight - 1);
|
||||||
|
for(auto & elem : pool)
|
||||||
|
{
|
||||||
|
roll -= elem->type->heroClass->selectionProbability[factionID];
|
||||||
|
if(roll < 0)
|
||||||
|
{
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool.back();
|
||||||
|
}
|
||||||
|
@ -16,6 +16,9 @@ class PlayerColor;
|
|||||||
class CGHeroInstance;
|
class CGHeroInstance;
|
||||||
class HeroTypeID;
|
class HeroTypeID;
|
||||||
class CGObjectInstance;
|
class CGObjectInstance;
|
||||||
|
class FactionID;
|
||||||
|
class CRandomGenerator;
|
||||||
|
class CHeroClass;
|
||||||
|
|
||||||
VCMI_LIB_NAMESPACE_END
|
VCMI_LIB_NAMESPACE_END
|
||||||
|
|
||||||
@ -26,7 +29,9 @@ class HeroPoolProcessor : boost::noncopyable
|
|||||||
CGameHandler * gameHandler;
|
CGameHandler * gameHandler;
|
||||||
|
|
||||||
void clearHeroFromSlot(const PlayerColor & color, TavernHeroSlot slot);
|
void clearHeroFromSlot(const PlayerColor & color, TavernHeroSlot slot);
|
||||||
void selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot);
|
void selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveStartingArmy);
|
||||||
|
|
||||||
|
CGHeroInstance * pickHeroFor(bool isNative, const PlayerColor & player, const FactionID & faction, CRandomGenerator & rand, const CHeroClass * bannedClass) const;
|
||||||
public:
|
public:
|
||||||
HeroPoolProcessor();
|
HeroPoolProcessor();
|
||||||
HeroPoolProcessor(CGameHandler * gameHandler);
|
HeroPoolProcessor(CGameHandler * gameHandler);
|
||||||
|
Reference in New Issue
Block a user