/*
* The ManaPlus Client
* Copyright (C) 2008-2009 The Mana World Development Team
* Copyright (C) 2009-2010 The Mana Developers
* Copyright (C) 2011-2020 The ManaPlus Developers
* Copyright (C) 2020-2023 The ManaVerse Developers
*
* This file is part of The ManaPlus Client.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "being/playerrelations.h"
#include "actormanager.h"
#include "configuration.h"
#include "logger.h"
#include "being/localplayer.h"
#include "being/playerignorestrategy.h"
#include "being/playerrelation.h"
#include "utils/dtor.h"
#include "utils/foreach.h"
#include "utils/gettext.h"
#include "listeners/playerrelationslistener.h"
#include "debug.h"
static const unsigned int FIRST_IGNORE_EMOTE = 14;
typedef std::map PlayerRelations;
typedef PlayerRelations::const_iterator PlayerRelationsCIter;
typedef std::list PlayerRelationListeners;
typedef PlayerRelationListeners::const_iterator PlayerRelationListenersCIter;
static const char *const PLAYER_IGNORE_STRATEGY_NOP = "nop";
static const char *const PLAYER_IGNORE_STRATEGY_EMOTE0 = "emote0";
static const char *const DEFAULT_IGNORE_STRATEGY =
PLAYER_IGNORE_STRATEGY_EMOTE0;
static const char *const NAME = "name";
static const char *const RELATION = "relation";
static const unsigned int IGNORE_EMOTE_TIME = 100;
namespace
{
class SortPlayersFunctor final
{
public:
SortPlayersFunctor()
{ }
A_DEFAULT_COPY(SortPlayersFunctor)
bool operator() (const std::string &str1,
const std::string &str2) const
{
std::string s1 = str1;
std::string s2 = str2;
toLower(s1);
toLower(s2);
if (s1 == s2)
return str1 < str2;
return s1 < s2;
}
} playersRelSorter;
// (De)serialisation class
class PlayerConfSerialiser final :
public ConfigurationListManager, std::map *>
{
public:
PlayerConfSerialiser()
{ }
A_DELETE_COPY(PlayerConfSerialiser)
ConfigurationObject *writeConfigItem(
const std::pair &value,
ConfigurationObject *const cobj) const override final
{
if (cobj == nullptr ||
value.second == nullptr)
{
return nullptr;
}
cobj->setValue(NAME, value.first);
cobj->setValue(RELATION, toString(
CAST_S32(value.second->mRelation)));
return cobj;
}
std::map *
readConfigItem(const ConfigurationObject *const cobj,
std::map
*const container) const override final
{
if (cobj == nullptr ||
container == nullptr)
{
return container;
}
const std::string name = cobj->getValue(NAME, "");
if (name.empty())
return container;
if ((*container)[name] == nullptr)
{
const int v = cobj->getValueInt(RELATION,
CAST_S32(Relation::NEUTRAL));
(*container)[name] = new PlayerRelation(
static_cast(v));
}
// otherwise ignore the duplicate entry
return container;
}
};
} // namespace
static PlayerConfSerialiser player_conf_serialiser; // stateless singleton
const unsigned int PlayerRelation::RELATION_PERMISSIONS[RELATIONS_NR] =
{
/* NEUTRAL */ 0, // we always fall back to the defaults anyway
/* FRIEND */ EMOTE | SPEECH_FLOAT | SPEECH_LOG | WHISPER | TRADE,
/* DISREGARDED*/ EMOTE | SPEECH_FLOAT,
/* IGNORED */ 0,
/* ERASED */ INVISIBLE,
/* BLACKLISTED */ SPEECH_LOG | WHISPER,
/* ENEMY2 */ EMOTE | SPEECH_FLOAT | SPEECH_LOG | WHISPER | TRADE
};
PlayerRelationsManager::PlayerRelationsManager() :
mPersistIgnores(false),
mDefaultPermissions(PlayerRelation::DEFAULT),
mIgnoreStrategy(nullptr),
mRelations(),
mListeners(),
mIgnoreStrategies()
{
}
PlayerRelationsManager::~PlayerRelationsManager()
{
delete_all(mIgnoreStrategies);
FOR_EACH (PlayerRelationsCIter, it, mRelations)
delete it->second;
mRelations.clear();
}
void PlayerRelationsManager::clear()
{
StringVect *const names = getPlayers();
FOR_EACHP (StringVectCIter, it, names)
removePlayer(*it);
delete names;
}
static const char *const PERSIST_IGNORE_LIST = "persistent-player-list";
static const char *const PLAYER_IGNORE_STRATEGY = "player-ignore-strategy";
static const char *const DEFAULT_PERMISSIONS = "default-player-permissions";
int PlayerRelationsManager::getPlayerIgnoreStrategyIndex(
const std::string &name)
{
const STD_VECTOR *const strategies
= getPlayerIgnoreStrategies();
if (strategies == nullptr)
return -1;
const size_t sz = strategies->size();
for (size_t i = 0; i < sz; i++)
{
if ((*strategies)[i]->mShortName == name)
return CAST_S32(i);
}
return -1;
}
void PlayerRelationsManager::load()
{
Configuration *const cfg = &serverConfig;
clear();
mPersistIgnores = (cfg->getValue(PERSIST_IGNORE_LIST, 1) != 0);
mDefaultPermissions = CAST_S32(cfg->getValue(DEFAULT_PERMISSIONS,
mDefaultPermissions));
const std::string ignore_strategy_name = cfg->getValue(
PLAYER_IGNORE_STRATEGY, DEFAULT_IGNORE_STRATEGY);
const int ignore_strategy_index = getPlayerIgnoreStrategyIndex(
ignore_strategy_name);
if (ignore_strategy_index >= 0)
{
setPlayerIgnoreStrategy((*getPlayerIgnoreStrategies())
[ignore_strategy_index]);
}
cfg->getList,
std::map *>
("player", &(mRelations), &player_conf_serialiser);
}
void PlayerRelationsManager::init()
{
load();
if (!mPersistIgnores)
{
clear(); // Yes, we still keep them around in the config file
// until the next update.
}
FOR_EACH (PlayerRelationListenersCIter, it, mListeners)
(*it)->updateAll();
}
void PlayerRelationsManager::store() const
{
serverConfig.setList::const_iterator,
std::pair,
std::map *>
("player", mRelations.begin(), mRelations.end(),
&player_conf_serialiser);
serverConfig.setValue(DEFAULT_PERMISSIONS, mDefaultPermissions);
serverConfig.setValue(PERSIST_IGNORE_LIST, mPersistIgnores);
serverConfig.setValue(PLAYER_IGNORE_STRATEGY,
mIgnoreStrategy != nullptr ? mIgnoreStrategy->mShortName :
DEFAULT_IGNORE_STRATEGY);
serverConfig.write();
}
void PlayerRelationsManager::signalUpdate(const std::string &name)
{
FOR_EACH (PlayerRelationListenersCIter, it, mListeners)
(*it)->updatedPlayer(name);
if (actorManager != nullptr)
{
Being *const being = actorManager->findBeingByName(
name, ActorType::Player);
if (being != nullptr &&
being->getType() == ActorType::Player)
{
being->updateColors();
}
}
}
unsigned int PlayerRelationsManager::checkPermissionSilently(
const std::string &player_name, const unsigned int flags) const
{
const std::map::const_iterator
it = mRelations.find(player_name);
if (it == mRelations.end())
{
return mDefaultPermissions & flags;
}
const PlayerRelation *const r = (*it).second;
unsigned int permissions = PlayerRelation::RELATION_PERMISSIONS[
CAST_S32(r->mRelation)];
switch (r->mRelation)
{
case Relation::NEUTRAL:
permissions = mDefaultPermissions;
break;
case Relation::FRIEND:
permissions |= mDefaultPermissions; // widen
break;
case Relation::DISREGARDED:
case Relation::IGNORED:
case Relation::ERASED:
case Relation::BLACKLISTED:
case Relation::ENEMY2:
default:
permissions &= mDefaultPermissions; // narrow
break;
}
return permissions & flags;
}
bool PlayerRelationsManager::hasPermission(const Being *const being,
const unsigned int flags) const
{
if (being == nullptr)
return false;
if (being->getType() == ActorType::Player)
{
return static_cast(hasPermission(
being->getName(), flags)) == flags;
}
return true;
}
bool PlayerRelationsManager::hasPermission(const std::string &name,
const unsigned int flags) const
{
if (actorManager == nullptr)
return false;
const unsigned int rejections = flags
& ~checkPermissionSilently(name, flags);
const bool permitted = (rejections == 0);
if (!permitted)
{
// execute `ignore' strategy, if possible
if (mIgnoreStrategy != nullptr)
{
Being *const b = actorManager->findBeingByName(
name, ActorType::Player);
if ((b != nullptr) && b->getType() == ActorType::Player)
mIgnoreStrategy->ignore(b, rejections);
}
}
return permitted;
}
void PlayerRelationsManager::setRelation(const std::string &player_name,
const RelationT relation)
{
if (localPlayer == nullptr ||
(relation != Relation::NEUTRAL &&
localPlayer->getName() == player_name))
{
return;
}
PlayerRelation *const r = mRelations[player_name];
if (r == nullptr)
mRelations[player_name] = new PlayerRelation(relation);
else
r->mRelation = relation;
signalUpdate(player_name);
store();
}
StringVect *PlayerRelationsManager::getPlayers() const
{
StringVect *const retval = new StringVect;
FOR_EACH (PlayerRelationsCIter, it, mRelations)
{
if (it->second != nullptr)
retval->push_back(it->first);
}
std::sort(retval->begin(), retval->end(), playersRelSorter);
return retval;
}
StringVect *PlayerRelationsManager::getPlayersByRelation(
const RelationT rel) const
{
StringVect *const retval = new StringVect;
FOR_EACH (PlayerRelationsCIter, it, mRelations)
{
if ((it->second != nullptr) &&
it->second->mRelation == rel)
{
retval->push_back(it->first);
}
}
std::sort(retval->begin(), retval->end(), playersRelSorter);
return retval;
}
void PlayerRelationsManager::removePlayer(const std::string &name)
{
delete mRelations[name];
mRelations.erase(name);
signalUpdate(name);
}
RelationT PlayerRelationsManager::getRelation(
const std::string &name) const
{
const std::map::const_iterator
it = mRelations.find(name);
if (it != mRelations.end())
return (*it).second->mRelation;
return Relation::NEUTRAL;
}
////////////////////////////////////////
// defaults
unsigned int PlayerRelationsManager::getDefault() const
{
return mDefaultPermissions;
}
void PlayerRelationsManager::setDefault(const unsigned int permissions)
{
mDefaultPermissions = permissions;
store();
signalUpdate("");
}
void PlayerRelationsManager::ignoreTrade(const std::string &name) const
{
if (name.empty())
return;
const RelationT relation = getRelation(name);
if (relation == Relation::IGNORED ||
relation == Relation::DISREGARDED ||
relation == Relation::BLACKLISTED ||
relation == Relation::ERASED)
{
return;
}
playerRelations.setRelation(name, Relation::BLACKLISTED);
}
bool PlayerRelationsManager::checkBadRelation(const std::string &name) const
{
if (name.empty())
return true;
const RelationT relation = getRelation(name);
if (relation == Relation::IGNORED ||
relation == Relation::DISREGARDED ||
relation == Relation::BLACKLISTED ||
relation == Relation::ERASED ||
relation == Relation::ENEMY2)
{
return true;
}
return false;
}
////////////////////////////////////////
// ignore strategies
class PIS_nothing final : public PlayerIgnoreStrategy
{
public:
PIS_nothing() :
PlayerIgnoreStrategy()
{
// TRANSLATORS: ignore/unignore action
mDescription = _("Completely ignore");
mShortName = PLAYER_IGNORE_STRATEGY_NOP;
}
A_DELETE_COPY(PIS_nothing)
void ignore(Being *const being A_UNUSED,
const unsigned int flags A_UNUSED) const override final
{
}
};
class PIS_dotdotdot final : public PlayerIgnoreStrategy
{
public:
PIS_dotdotdot() :
PlayerIgnoreStrategy()
{
// TRANSLATORS: ignore/unignore action
mDescription = _("Print '...'");
mShortName = "dotdotdot";
}
A_DELETE_COPY(PIS_dotdotdot)
void ignore(Being *const being,
const unsigned int flags A_UNUSED) const override final
{
if (being == nullptr)
return;
logger->log("ignoring: " + being->getName());
being->setSpeech("...");
}
};
class PIS_blinkname final : public PlayerIgnoreStrategy
{
public:
PIS_blinkname() :
PlayerIgnoreStrategy()
{
// TRANSLATORS: ignore/unignore action
mDescription = _("Blink name");
mShortName = "blinkname";
}
A_DELETE_COPY(PIS_blinkname)
void ignore(Being *const being,
const unsigned int flags A_UNUSED) const override final
{
if (being == nullptr)
return;
logger->log("ignoring: " + being->getName());
being->flashName(200);
}
};
class PIS_emote final : public PlayerIgnoreStrategy
{
public:
PIS_emote(const uint8_t emote_nr,
const std::string &description,
const std::string &shortname) :
PlayerIgnoreStrategy(),
mEmotion(emote_nr)
{
mDescription = description;
mShortName = shortname;
}
A_DELETE_COPY(PIS_emote)
void ignore(Being *const being,
const unsigned int flags A_UNUSED) const override final
{
if (being == nullptr)
return;
being->setEmote(mEmotion, IGNORE_EMOTE_TIME);
}
uint8_t mEmotion;
};
STD_VECTOR *
PlayerRelationsManager::getPlayerIgnoreStrategies()
{
if (mIgnoreStrategies.empty())
{
// not initialised yet?
mIgnoreStrategies.push_back(new PIS_emote(FIRST_IGNORE_EMOTE,
// TRANSLATORS: ignore strategi
_("Floating '...' bubble"),
PLAYER_IGNORE_STRATEGY_EMOTE0));
mIgnoreStrategies.push_back(new PIS_emote(FIRST_IGNORE_EMOTE + 1,
// TRANSLATORS: ignore strategi
_("Floating bubble"),
"emote1"));
mIgnoreStrategies.push_back(new PIS_nothing);
mIgnoreStrategies.push_back(new PIS_dotdotdot);
mIgnoreStrategies.push_back(new PIS_blinkname);
}
return &mIgnoreStrategies;
}
bool PlayerRelationsManager::isGoodName(const std::string &name) const
{
const size_t size = name.size();
if (size < 3)
return true;
const std::map::const_iterator
it = mRelations.find(name);
if (it != mRelations.end())
return true;
return checkName(name);
}
bool PlayerRelationsManager::isGoodName(Being *const being) const
{
if (being == nullptr)
return false;
if (being->getGoodStatus() != -1)
return being->getGoodStatus() == 1;
const std::string &name = being->getName();
const size_t size = name.size();
if (size < 3)
return true;
const std::map::const_iterator
it = mRelations.find(name);
if (it != mRelations.end())
return true;
const bool status = checkName(name);
being->setGoodStatus(status ? 1 : 0);
return status;
}
bool PlayerRelationsManager::checkName(const std::string &name)
{
const size_t size = name.size();
const std::string check = config.getStringValue("unsecureChars");
const std::string lastChar = name.substr(size - 1, 1);
if (name.substr(0, 1) == " " ||
lastChar == " " ||
lastChar == "." ||
name.find(" ") != std::string::npos)
{
return false;
}
else if (check.empty())
{
return true;
}
else if (name.find_first_of(check) != std::string::npos)
{
return false;
}
else
{
return true;
}
}
PlayerRelationsManager playerRelations;