diff --git a/CMakeLists.txt b/CMakeLists.txt index 0950ccf4..ba917d8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ set(COMMON_HEADER_FILES include/mudObjects/rooms.hpp include/mudObjects/uniqueRooms.hpp + include/account.hpp include/alchemy.hpp include/anchor.hpp include/area.hpp @@ -204,6 +205,8 @@ set(COMMON_HEADER_FILES ) set(COMMON_SOURCE_FILES + accounts/account.cpp + areas/area.cpp areas/catRef.cpp areas/catRefInfo.cpp @@ -405,6 +408,7 @@ set(COMMON_SOURCE_FILES util/misc.cpp util/timer.cpp + json/accounts-json.cpp json/catref-json.cpp json/effects-json.cpp json/hooks-json.cpp diff --git a/accounts/account.cpp b/accounts/account.cpp new file mode 100644 index 00000000..d7615372 --- /dev/null +++ b/accounts/account.cpp @@ -0,0 +1,435 @@ +/* + * account.cpp + * Account system implementation + * ____ _ + * | _ \ ___ __ _| |_ __ ___ ___ + * | |_) / _ \/ _` | | '_ ` _ \/ __| + * | _ < __/ (_| | | | | | | \__ \ + * |_| \_\___|\__,_|_|_| |_| |_|___/ + * + * Permission to use, modify and distribute is granted via the + * GNU Affero General Public License v3 or later + * + * Copyright (C) 2007-2021 Jason Mitchell, Randi Mitchell + * Contributions by Tim Callahan, Jonathan Hseu + * Based on Mordor (C) Brooke Paul, Brett J. Vickers, John P. Freeman + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "account.hpp" +#include "json.hpp" +#include "paths.hpp" +#include "mudObjects/players.hpp" +#include "socket.hpp" +#include "server.hpp" +#include "xml.hpp" +#include "config.hpp" + +namespace fs = std::filesystem; + +//********************************************************************* +// Constructors & Destructor +//********************************************************************* + +Account::Account() { + reset(); +} + +Account::Account(const std::string& name) { + reset(); + accountName = name; + created = time(nullptr); + lastLogin = created; +} + +Account::Account(const Account& other) { + copyFrom(other); +} + +Account& Account::operator=(const Account& other) { + if (this != &other) { + copyFrom(other); + } + return *this; +} + +Account::~Account() = default; + +//********************************************************************* +// Helper Functions +//********************************************************************* + +void Account::reset() { + accountName.clear(); + password.clear(); + email.clear(); + created = 0; + lastLogin = 0; + characterLimit = 60; // Default character limit + characterNames.clear(); + banned = false; + banReason.clear(); + expEarned = 0; + expSpent = 0; + version.clear(); +} + +void Account::copyFrom(const Account& other) { + accountName = other.accountName; + password = other.password; + email = other.email; + created = other.created; + lastLogin = other.lastLogin; + characterLimit = other.characterLimit; + characterNames = other.characterNames; + banned = other.banned; + banReason = other.banReason; + expEarned = other.expEarned; + expSpent = other.expSpent; + version = other.version; +} + +//********************************************************************* +// File Operations +//********************************************************************* + +bool Account::save() const { + if (accountName.empty()) { + std::clog << "Account::save() - Cannot save account with empty name\n"; + return false; + } + + const auto filename = (Path::Account / accountName).replace_extension("json"); + + // Ensure directory exists + fs::create_directories(Path::Account); + + try { + nlohmann::json j = *this; + std::ofstream file(filename); + if (!file.is_open()) { + std::clog << "Account::save() - Cannot open file: " << filename << "\n"; + return false; + } + file << j.dump(4); // Pretty print with 4 space indentation + return true; + } catch (const std::exception& e) { + std::clog << "Account::save() - Error saving account " << accountName << ": " << e.what() << "\n"; + return false; + } +} + +bool Account::load(const std::string& accountName, std::shared_ptr& account) { + if (accountName.empty()) { + return false; + } + + const auto filename = (Path::Account / accountName).replace_extension("json"); + + if (!fs::exists(filename)) { + return false; + } + + try { + std::ifstream file(filename); + if (!file.is_open()) { + std::clog << "Account::load() - Cannot open file: " << filename << "\n"; + return false; + } + + nlohmann::json j; + file >> j; + + account = std::make_shared(); + j.get_to(*account); + + return true; + } catch (const std::exception& e) { + std::clog << "Account::load() - Error loading account " << accountName << ": " << e.what() << "\n"; + return false; + } +} + +bool Account::exists(const std::string& accountName) { + if (accountName.empty()) { + return false; + } + const auto filename = (Path::Account / accountName).replace_extension("json"); + return fs::exists(filename); +} + +//********************************************************************* +// Password Functions +//********************************************************************* + +std::string Account::hashPassword(const std::string& password) { + // Use the same password hashing as Player class + return Player::hashPassword(password); +} + +bool Account::isPassword(const std::string& pass) const { + return password == hashPassword(pass); +} + +void Account::updateLastLogin() { + lastLogin = time(nullptr); + // Record the last game version this account logged in with + if(gConfig) { + setVersion(gConfig->getVersion()); + } +} + +//********************************************************************* +// Getters +//********************************************************************* + +const std::string& Account::getName() const { return accountName; } +const std::string& Account::getPassword() const { return password; } +const std::string& Account::getEmail() const { return email; } +time_t Account::getCreated() const { return created; } +time_t Account::getLastLogin() const { return lastLogin; } +int Account::getCharacterLimit() const { return characterLimit; } +const std::vector& Account::getCharacterNames() const { return characterNames; } +bool Account::isBanned() const { return banned; } +const std::string& Account::getBanReason() const { return banReason; } +unsigned long Account::getAvailableExp() const { + unsigned long long net = 0; + if(expEarned >= expSpent) { + net = expEarned - expSpent; + } + const unsigned long long cap = std::numeric_limits::max(); + if(net > cap) + net = cap; + return static_cast(net); +} + +unsigned long long Account::getExpEarned() const { return expEarned; } +unsigned long long Account::getExpSpent() const { return expSpent; } +const std::string& Account::getVersion() const { return version; } + +//********************************************************************* +// Setters +//********************************************************************* + +void Account::setName(const std::string& name) { + if(name == accountName) + return; + + std::string oldName = accountName; + accountName = name; + + // Update server caches and connected players' account mapping + if(gServer) { + // Move accountConnections entry + auto itConn = gServer->accountConnections.find(oldName); + if(itConn != gServer->accountConnections.end()) { + gServer->accountConnections[name] = itConn->second; + gServer->accountConnections.erase(itConn); + } + + // Fix accountCache key if present + for(auto it = gServer->accountCache.begin(); it != gServer->accountCache.end(); ++it) { + if(it->second.get() == this) { + if(it->first != name) { + auto accPtr = it->second; + gServer->accountCache.erase(it); + gServer->accountCache.emplace(name, accPtr); + } + break; + } + } + } + + // Update all characters linked to this account (online and offline) + for(const auto& charName : characterNames) { + std::shared_ptr player = nullptr; + if(gServer) player = gServer->findPlayer(charName); + if(player) { + player->setAccountName(accountName); + player->save(true); + } else { + // Load from disk, update, and save + std::shared_ptr diskPlayer; + if(loadPlayer(charName, diskPlayer)) { + diskPlayer->setAccountName(accountName); + diskPlayer->save(true); + } + } + } +} + +void Account::setPassword(const std::string& pass) { password = hashPassword(pass); } +void Account::setEmail(const std::string& mail) { email = mail; } +void Account::setCreated(time_t time) { created = time; } +void Account::setLastLogin(time_t time) { lastLogin = time; } +void Account::setBanned(bool ban) { banned = ban; } +void Account::setBanReason(const std::string& reason) { banReason = reason; } + +void Account::addExp(unsigned long amount) { + expEarned += amount; +} + +bool Account::spendExp(unsigned long amount) { + if(getAvailableExp() < amount) + return false; + expSpent += amount; + return true; +} + +void Account::setExpEarned(unsigned long long earned) { + expEarned = earned; + if(expSpent > expEarned) { + expSpent = expEarned; + } +} + +void Account::setExpSpent(unsigned long long spent) { + expSpent = std::min(spent, expEarned); +} +void Account::setVersion(const std::string& v) { version = v; } + +//********************************************************************* +// Character Management +//********************************************************************* + +bool Account::addCharacter(const std::string& characterName) { + if (characterName.empty()) { + return false; + } + + // Check if we already have this character + if (hasCharacter(characterName)) { + return true; // Already have it, consider success + } + + // Check character limit + if (!canCreateCharacter()) { + return false; + } + + characterNames.push_back(characterName); + // Keep list sorted alphabetically (case-insensitive) + std::sort(characterNames.begin(), characterNames.end(), [](const std::string& a, const std::string& b) { + return std::lexicographical_compare( + a.begin(), a.end(), b.begin(), b.end(), + [](unsigned char ac, unsigned char bc) { + return std::tolower(ac) < std::tolower(bc); + } + ); + }); + return true; +} + +bool Account::removeCharacter(const std::string& characterName) { + auto it = std::find(characterNames.begin(), characterNames.end(), characterName); + if (it != characterNames.end()) { + characterNames.erase(it); + return true; + } + return false; +} + +bool Account::hasCharacter(const std::string& characterName) const { + return std::find(characterNames.begin(), characterNames.end(), characterName) != characterNames.end(); +} + +bool Account::canCreateCharacter() const { + return static_cast(characterNames.size()) < characterLimit; +} + +int Account::getCharacterCount() const { + return static_cast(characterNames.size()); +} + +//********************************************************************* +// Validation Functions +//********************************************************************* + + +bool Account::isValidPassword(const std::string& password) { + // Same validation as used elsewhere in the codebase + return password.length() >= 5 && password.length() <= 35; +} + +//********************************************************************* +// UI Helpers +//********************************************************************* + +void Account::printInfoFields(const std::shared_ptr& player) const { + if(!player) return; + player->print("^W%-12s^C%s^x\n", "Account:", getName().c_str()); + if(!getEmail().empty()) { + player->print("^W%-12s^x%s\n", "Email:", getEmail().c_str()); + } + player->print("^W%-12s^x%d/%d\n", "Characters:", getCharacterCount(), getCharacterLimit()); + player->print("^W%-12s^G%llu^x\n", "Exp Earned:", getExpEarned()); + player->print("^W%-12s^G%llu^x\n\n", "Exp Spent:", getExpSpent()); +} + +void Account::printInfoFields(const std::shared_ptr& sock) const { + if(!sock) return; + sock->print("^W%-12s^C%s^x\n", "Account:", getName().c_str()); + if(!getEmail().empty()) { + sock->print("^W%-12s^x%s\n", "Email:", getEmail().c_str()); + } + sock->print("^W%-12s^x%d/%d\n", "Characters:", getCharacterCount(), getCharacterLimit()); + sock->print("^W%-12s^G%llu^x\n", "Exp Earned:", getExpEarned()); + sock->print("^W%-12s^G%llu^x\n\n", "Exp Spent:", getExpSpent()); +} + +void Account::printCharacterList(const std::shared_ptr& player) const { + if(!player) return; + const auto& chars = getCharacterNames(); + if(chars.empty()) { + player->print("No characters.\n"); + return; + } + player->print("^WYour Characters:^x\n"); + for(const auto& name : chars) { + player->print(" ^C%s^x\n", name.c_str()); + } +} + +void Account::printCharacterList(const std::shared_ptr& sock) const { + if(!sock) return; + const auto& chars = getCharacterNames(); + if(chars.empty()) { + sock->print("^KYou have no characters.^x\n"); + return; + } + sock->print("^WYour Characters:^x\n"); + for(const auto& name : chars) { + sock->print(" ^C%s^x\n", name.c_str()); + } +} + +//********************************************************************* +// Player Account Functions +//********************************************************************* + +bool Player::hasAccount() const { + return(!accountName.empty()); +} + +std::shared_ptr Player::getAccount() const { + auto sock = getSock(); + if (sock) { + return sock->getAccount(); + } + return nullptr; +} + +void Player::setAccountName(const std::string& name) { + accountName = name; +} + +std::string Player::getAccountName() const { return(accountName); } + \ No newline at end of file diff --git a/commands/cmd.cpp b/commands/cmd.cpp index 50001f8f..6736e213 100644 --- a/commands/cmd.cpp +++ b/commands/cmd.cpp @@ -292,6 +292,7 @@ bool Config::initCommands() { staffCommands.emplace("*log", 100, dmLog, isCt, "View login log file"); staffCommands.emplace("*list", 100, dmList, isCt, ""); staffCommands.emplace("*info", 100, dmInfo, isCt, "Show game info (includes some memory)"); + staffCommands.emplace("*account", 100, dmAccount, isDm, "Manage accounts"); staffCommands.emplace("*md5", 100, dmMd5, isCt, "Show md5 of input string"); staffCommands.emplace("*ids", 100, dmIds, isDm, "Shows registered ids"); staffCommands.emplace("*status", 80, dmStat, nullptr, "Show info about a room/player/object/monster"); @@ -547,6 +548,7 @@ bool Config::initCommands() { playerCommands.emplace("statistics", 100, cmdStatistics, nullptr, "Show character-related statistics"); playerCommands.emplace("information", 50, cmdInfo, nullptr, "Show extended information about your character"); playerCommands.emplace("attributes", 100, cmdInfo, nullptr, "Show extended information about your character"); + playerCommands.emplace("account", 100, cmdAccount, nullptr, "Display account information and perform account-related commands"); playerCommands.emplace("skills", 100, cmdSkills, nullptr, "Show what skills your character knows"); playerCommands.emplace("version", 100, cmdVersion, nullptr, "View RoH current version"); playerCommands.emplace("age", 100, cmdAge, nullptr, "Show your character's age and time played"); diff --git a/commands/command4.cpp b/commands/command4.cpp index 139eaaed..d0a0f78f 100644 --- a/commands/command4.cpp +++ b/commands/command4.cpp @@ -15,12 +15,16 @@ * Based on Mordor (C) Brooke Paul, Brett J. Vickers, John P. Freeman * */ +#include // for std::min #include // for sprintf #include // for abs -#include // for strchr +#include // for strchr, strncasecmp #include // for operator<<, basic_ostream, ostring... #include // for allocator, string, char_traits +#include // for to_lower + +#include "account.hpp" // for Account #include "calendar.hpp" // for cDay, Calendar, cMonth #include "cmd.hpp" // for cmd #include "commands.hpp" // for cmdAge, cmdHelp, cmdInfo, cmdVersion @@ -215,3 +219,53 @@ int cmdInfo(const std::shared_ptr& player, cmd* cmnd) { return(0); } +//********************************************************************* +// cmdAccount +//********************************************************************* + +int cmdAccount(const std::shared_ptr& player, cmd* cmnd) { + if(!player->hasAccount()) { + player->print("You do not have an account.\n"); + return(0); + } + + if(cmnd->num < 2) { + player->print("Account command options:\n"); + player->print(" ^Waccount (i)nfo^x - Display account information\n"); + player->print(" ^Waccount (c)haracters^x - List your characters\n"); + return(0); + } + + std::string subcommand = cmnd->str[1]; + boost::to_lower(subcommand); + + // Handle "info" command with partial matching + if(partialMatch(subcommand, "info", 4)) { + auto account = gServer->getOrLoadAccount(player->getAccountName()); + if(!account) { + player->print("Unable to load your account information.\n"); + return(0); + } + + player->print("\n^W~~~~~~~ Account Information ~~~~~~~^x\n\n"); + account->printInfoFields(player); + + return(0); + } + + // Handle "characters" with partial matching + if(partialMatch(subcommand, "characters", 10)) { + auto account = gServer->getOrLoadAccount(player->getAccountName()); + if(!account) { + player->print("Unable to load your account information.\n"); + return(0); + } + account->printCharacterList(player); + return(0); + } + + player->print("Unknown account option '%s'.\n", subcommand.c_str()); + player->print("Type '^Waccount^x' for a list of available options.\n"); + return(0); +} + diff --git a/commands/command5.cpp b/commands/command5.cpp index 345e2bee..e498e047 100644 --- a/commands/command5.cpp +++ b/commands/command5.cpp @@ -56,6 +56,7 @@ #include "stats.hpp" // for Stat #include "structs.hpp" // for StatsContainer #include "web.hpp" // for updateRecentActivity, webUnass... +#include "account.hpp" // for Account //********************************************************************* // who @@ -586,6 +587,24 @@ void Player::deletePlayer() { } } + // Remove player from account if they belong to one + if(hasAccount()) { + auto account = getAccount(); + if(account) { + if(account->removeCharacter(name)) { + account->save(); + } + } else { + // Fallback: load from disk if socket account not available + std::shared_ptr diskAccount; + if(Account::load(getAccountName(), diskAccount)) { + if(diskAccount->removeCharacter(name)) { + diskAccount->save(); + } + } + } + } + // this deletes the player object std::shared_ptr sock = getSock(); uninit(); diff --git a/creatures/creatureAttr.cpp b/creatures/creatureAttr.cpp index bcbcc26a..2c9a9261 100644 --- a/creatures/creatureAttr.cpp +++ b/creatures/creatureAttr.cpp @@ -69,6 +69,7 @@ #include "version.hpp" // for VERSION #include "xml.hpp" // for copyPropToString #include "server.hpp" +#include "account.hpp" // for Account //********************************************************************* // getClass @@ -146,16 +147,33 @@ bool Creature::inJail() const { void Creature::addExperience(unsigned long e) { setExperience(experience + e); - if(isPlayer()) - getAsPlayer()->checkLevel(); + if(isPlayer()) { + auto player = getAsPlayer(); + player->checkLevel(); + + // Add account experience if player has an account + if(player->hasAccount()) { + auto account = gServer->getOrLoadAccount(player->getAccountName()); + if(account) { + // Calculate 1% of player experience gained (rounded to nearest integer) + // This means players must earn at least 50 exp to gain account exp + double accountExpDouble = e * 0.01; + unsigned long accountExp = static_cast(accountExpDouble + 0.5); + if(accountExp > 0) { + account->addExp(e); + } + } + } + } } void Creature::subExperience(unsigned long e) { setExperience(e > experience ? 0 : experience - e); unsigned long lost = (e > experience ? 0 : e); if(isPlayer()) { - getAsPlayer()->checkLevel(); - getAsPlayer()->statistics.experienceLost(lost); + auto player = getAsPlayer(); + player->checkLevel(); + player->statistics.experienceLost(lost); } } @@ -617,7 +635,7 @@ void Player::plyReset() { bank.zero(); created = 0; - oldCreated = surname = lastCommand = lastCommunicate = password = title = tempTitle = ""; + accountName = oldCreated = surname = lastCommand = lastCommunicate = password = title = tempTitle = ""; lastPassword = afflictedBy = forum = ""; tickDmg = pkwon = pkin = lastLogin = lastInterest = uniqueObjId = 0; @@ -818,6 +836,7 @@ void Player::plyCopy(const Player& cr, bool assign) { wrap = cr.wrap; + accountName = cr.accountName; title = cr.title; password = cr.getPassword(); surname = cr.surname; diff --git a/creatures/creatures.cpp b/creatures/creatures.cpp index a3883f11..b1380b87 100644 --- a/creatures/creatures.cpp +++ b/creatures/creatures.cpp @@ -59,7 +59,7 @@ #include "size.hpp" // for getSizeName, SIZE_COLOSSAL #include "stats.hpp" // for Stat #include "structs.hpp" // for Command, SEX_FEMALE, SEX_MALE - +#include "account.hpp" // for Account //******************************************************************** // canSee @@ -988,6 +988,18 @@ int Player::save(bool updateTime, LoadType saveType) { if(saveToFile(saveType) < 0) std::clog << "*** ERROR: saveXml!\n"; + // Save account if player has one + if(hasAccount()) { + auto account = getAccount(); + if(account) { + if(!account->save()) { + std::clog << "*** ERROR: Failed to save account " << account->getName() << " for player " << getName() << "\n"; + } + } else { + std::clog << "*** ERROR: Player has account name but no active account loaded for player " << getName() << "\n"; + } + } + return(0); } diff --git a/include/account.hpp b/include/account.hpp new file mode 100644 index 00000000..cc1e8e3f --- /dev/null +++ b/include/account.hpp @@ -0,0 +1,115 @@ +/* + * account.hpp + * Account system for multiple characters per user + * ____ _ + * | _ \ ___ __ _| |_ __ ___ ___ + * | |_) / _ \/ _` | | '_ ` _ \/ __| + * | _ < __/ (_| | | | | | | \__ \ + * |_| \_\___|\__,_|_|_| |_| |_|___/ + * + * Permission to use, modify and distribute is granted via the + * GNU Affero General Public License v3 or later + * + * Copyright (C) 2007-2021 Jason Mitchell, Randi Mitchell + * Contributions by Tim Callahan, Jonathan Hseu + * Based on Mordor (C) Brooke Paul, Brett J. Vickers, John P. Freeman + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +class Player; +class Socket; + +class Account { +public: + // Constructors & Destructor + Account(); + Account(const std::string& name); + Account(const Account& other); + Account& operator=(const Account& other); + ~Account(); + + // JSON serialization friends + friend void to_json(nlohmann::json &j, const Account &account); + friend void from_json(const nlohmann::json &j, Account &account); + + // Core account functions + bool save() const; + static bool load(const std::string& accountName, std::shared_ptr& account); + static bool exists(const std::string& accountName); + static std::string hashPassword(const std::string& password); + + // Getters + const std::string& getName() const; + const std::string& getPassword() const; + const std::string& getEmail() const; + time_t getCreated() const; + time_t getLastLogin() const; + int getCharacterLimit() const; + const std::vector& getCharacterNames() const; + bool isBanned() const; + const std::string& getBanReason() const; + unsigned long getAvailableExp() const; + unsigned long long getExpEarned() const; + unsigned long long getExpSpent() const; + const std::string& getVersion() const; + + // Setters + void setName(const std::string& name); + void setPassword(const std::string& password); + void setEmail(const std::string& email); + void setCreated(time_t created); + void setLastLogin(time_t lastLogin); + void setBanned(bool banned); + void setBanReason(const std::string& reason); + void addExp(unsigned long amount); + bool spendExp(unsigned long amount); + void setExpEarned(unsigned long long earned); + void setExpSpent(unsigned long long spent); + void setVersion(const std::string& v); + + // Character management + bool addCharacter(const std::string& characterName); + bool removeCharacter(const std::string& characterName); + bool hasCharacter(const std::string& characterName) const; + bool canCreateCharacter() const; + int getCharacterCount() const; + + // Password verification + bool isPassword(const std::string& password) const; + void updateLastLogin(); + + static bool isValidPassword(const std::string& password); + + // UI helpers + void printInfoFields(const std::shared_ptr& player) const; + void printInfoFields(const std::shared_ptr& sock) const; + void printCharacterList(const std::shared_ptr& player) const; + void printCharacterList(const std::shared_ptr& sock) const; + + +private: + std::string accountName; // Unique account identifier + std::string password; // Hashed password + std::string email; // Optional email address + time_t created; // Account creation time + time_t lastLogin; // Last login time + int characterLimit; // Maximum characters allowed + std::vector characterNames; // List of character names + bool banned; // Is account banned + std::string banReason; // Reason for ban if applicable + unsigned long long expEarned; // Total account experience earned over lifetime + unsigned long long expSpent; // Total account experience spent on upgrades, etc. + std::string version; // Last game version this account logged in with + + // Helper functions + void copyFrom(const Account& other); + void reset(); +}; \ No newline at end of file diff --git a/include/commands.hpp b/include/commands.hpp index aaf8b0fe..bed55060 100644 --- a/include/commands.hpp +++ b/include/commands.hpp @@ -39,6 +39,7 @@ int orderPet(const std::shared_ptr& player, cmd* cmnd); // Effects.cpp int dmEffectList(const std::shared_ptr& player, cmd* cmnd); int dmShowEffectsIndex(const std::shared_ptr& player, cmd* cmnd); +int dmAccount(const std::shared_ptr& player, cmd* cmnd); // songs.cpp int cmdPlay(const std::shared_ptr& player, cmd* cmnd); @@ -107,6 +108,7 @@ int cmdVersion(const std::shared_ptr& player, cmd* cmnd); int cmdLevelHistory(const std::shared_ptr& player, cmd* cmnd); int cmdStatistics(const std::shared_ptr& player, cmd* cmnd); int cmdInfo(const std::shared_ptr& player, cmd* cmnd); +int cmdAccount(const std::shared_ptr& player, cmd* cmnd); int cmdSpells(const std::shared_ptr& player, cmd* cmnd); void spellsUnder(const std::shared_ptr& viewer, const std::shared_ptr & target, bool notSelf); diff --git a/include/login.hpp b/include/login.hpp index 9c755237..93a032c5 100644 --- a/include/login.hpp +++ b/include/login.hpp @@ -67,15 +67,23 @@ typedef enum { LOGIN_GET_COLOR, LOGIN_PAUSE_SCREEN, LOGIN_GET_LOCKOUT_PASSWORD, - LOGIN_GET_NAME, - LOGIN_CHECK_CREATE_NEW, - LOGIN_GET_PASSWORD, - LOGIN_GET_PROXY_PASSWORD, + LOGIN_ENTRY_CHOICE, + LOGIN_GET_ACCOUNT_NAME, + LOGIN_CHECK_CREATE_ACCOUNT, + LOGIN_GET_ACCOUNT_PASSWORD, + LOGIN_GET_ACCOUNT_CREATE_PASSWORD, + LOGIN_GET_LEGACY_NAME, + LOGIN_ACCOUNT_MENU, + LOGIN_CLAIM_CHARACTER, + LOGIN_CLAIM_PASSWORD, + LOGIN_SET_EMAIL, + LOGIN_SET_EMAIL_CONFIRM, + LOGIN_LEGACY_PASSWORD, LOGIN_END, // Creation States CREATE_START, - CREATE_NEW, + CREATE_NEW_CHARACTER, CREATE_GET_DM_PASSWORD, CREATE_CHECK_LOCKED_OUT, CREATE_GET_SEX, @@ -89,7 +97,7 @@ typedef enum { CREATE_GET_STATS, CREATE_GET_PROF, CREATE_GET_ALIGNMENT, - CREATE_GET_PASSWORD, + CREATE_GET_NAME, CREATE_BONUS_STAT, CREATE_PENALTY_STAT, CREATE_SECOND_PROF, @@ -158,7 +166,7 @@ namespace Create { bool handleWeapon(const std::shared_ptr& sock, int mode, char ch); bool getProf(const std::shared_ptr& sock, std::string str, int mode); bool getSecondProf(const std::shared_ptr& sock, std::string str, int mode); - bool getPassword(const std::shared_ptr& sock, const std::string &str, int mode); + bool getName(const std::shared_ptr& sock, const std::string &str, int mode); void done(const std::shared_ptr& sock, const std::string &str, int mode); // character customization functions diff --git a/include/mudObjects/players.hpp b/include/mudObjects/players.hpp index 0e9ee1d8..6a12c3d8 100644 --- a/include/mudObjects/players.hpp +++ b/include/mudObjects/players.hpp @@ -19,9 +19,11 @@ #pragma once #include +#include #include "mudObjects/creatures.hpp" +class Account; class Blackjack; class Fishing; @@ -61,6 +63,7 @@ class Player : public Creature { protected: // Data + std::string accountName; // Account this character belongs to std::string proxyName; std::string proxyId; @@ -119,6 +122,7 @@ class Player : public Creature { public: std::string getFlagList(std::string_view sep=", ") const override; + void setName(std::string_view newName); void hardcoreDeath(); void deletePlayer(); @@ -154,13 +158,19 @@ class Player : public Creature { bool checkProxyAccess(const std::shared_ptr& proxy); + void setAccountName(const std::string& name); void setProxy(std::shared_ptr proxy); void setProxy(std::string_view pProxyName, std::string_view pProxyId); void setProxyName(std::string_view pProxyName); void setProxyId(std::string_view pProxyId); + std::string getAccountName() const; std::string getProxyName() const; std::string getProxyId() const; + + // Account utilities + bool hasAccount() const; + std::shared_ptr getAccount() const; // Combat & Death int computeAttackPower(); diff --git a/include/paths.hpp b/include/paths.hpp index 7d66b64a..1255f49f 100644 --- a/include/paths.hpp +++ b/include/paths.hpp @@ -39,6 +39,7 @@ struct Path { static inline const fs::path Object = BasePath / "objects"; static inline const fs::path Player = BasePath / "player"; static inline const fs::path PlayerBackup = BasePath / "player/backup"; + static inline const fs::path Account = BasePath / "accounts"; static inline const fs::path Config = BasePath / "config"; diff --git a/include/proto.hpp b/include/proto.hpp index 21eb7d23..3a20ad92 100644 --- a/include/proto.hpp +++ b/include/proto.hpp @@ -333,6 +333,7 @@ bool parse_name(std::string_view name); int dmIson(void); int strPrefix(const char *haystack, const char *needle); int strSuffix(const char *haystack, const char *needle); +bool partialMatch(const std::string& got, const char* full, size_t maxLen); int pkillPercent(int pkillsWon, int pkillsIn); int getLastDigit(int n, int digits); diff --git a/include/server.hpp b/include/server.hpp index ac4d4165..c2a75993 100644 --- a/include/server.hpp +++ b/include/server.hpp @@ -28,6 +28,7 @@ namespace odbc { #include #include +#include #include // C Includes @@ -57,6 +58,7 @@ class Monster; class MsdpVariable; class Object; class Player; +class Account; class PythonHandler; class ReportedMsdpVariable; class Socket; @@ -171,6 +173,10 @@ class Server { RoomCache roomCache; MonsterCache monsterCache; ObjectCache objectCache; + + // Account management + std::map> accountCache; // Shared account instances + std::map> accountConnections; // Account -> Set of character names // ****************** // Internal Variables @@ -220,6 +226,7 @@ class Server { long lastRoomPulseUpdate; long lastRandomUpdate; long lastActiveUpdate; + long lastAccountSave; public: std::list > areas; @@ -403,6 +410,14 @@ class Server { void saveAllPly(); int getNumPlayers(); + // Account management + std::shared_ptr getOrLoadAccount(const std::string& accountName); + void trackAccountConnection(const std::string& accountName, const std::string& characterName); + void untrackAccountConnection(const std::string& accountName, const std::string& characterName); + std::vector getAccountCharacters(const std::string& accountName) const; + void releaseAccount(const std::string& accountName, const std::string& characterName); + void saveAllCachedAccounts(); + void disconnectAll(); int processOutput(); // Send any buffered output @@ -429,7 +444,7 @@ class Server { // Queries bool checkDuplicateName(std::shared_ptr sock, bool dis); - bool checkDouble(std::shared_ptr sock); + bool checkDouble(std::shared_ptr sock, bool disconnectOnLimit = true); // Bans void checkBans(); diff --git a/include/socket.hpp b/include/socket.hpp index a1d8c7be..af17cc6f 100644 --- a/include/socket.hpp +++ b/include/socket.hpp @@ -44,6 +44,7 @@ extern long UnCompressedBytes; extern long OutBytes; class Player; +class Account; typedef struct _xmlNode xmlNode; typedef xmlNode *xmlNodePtr; @@ -274,6 +275,12 @@ class Socket : public std::enable_shared_from_this { void setPlayer(std::shared_ptr ply); void clearPlayer(); + // Account methods + [[nodiscard]] bool hasAccount() const; + [[nodiscard]] std::shared_ptr getAccount() const; + [[nodiscard]] std::string getAccountName() const; + void setAccount(std::shared_ptr acc); + void clearAccount(); void clearSpying(); void clearSpiedOn(); @@ -344,6 +351,7 @@ class Socket : public std::enable_shared_from_this { bool registered{}; std::shared_ptr myPlayer{}; + std::string currentAccountName{}; // Account name for this socket // For MCCP diff --git a/include/version.hpp b/include/version.hpp index 69756cfc..87a9dbc2 100644 --- a/include/version.hpp +++ b/include/version.hpp @@ -21,7 +21,7 @@ #define VERSION_MINOR "6" -#define VERSION_SUB "2b" +#define VERSION_SUB "3" #define VERSION VERSION_MAJOR "." VERSION_MINOR VERSION_SUB diff --git a/io/socket.cpp b/io/socket.cpp index c75d9719..fa6df960 100644 --- a/io/socket.cpp +++ b/io/socket.cpp @@ -73,6 +73,7 @@ #include "version.hpp" // for VERSION #include "xml.hpp" // for copyToBool, newBo... #include "blackjack.hpp" // for interactive gambling +#include "account.hpp" // for Account const int MIN_PAGES = 10; @@ -217,6 +218,7 @@ void Socket::reset() { outCompressBuf = nullptr; outCompress = nullptr; myPlayer = nullptr; + currentAccountName.clear(); tState = NEG_NONE; oneIAC = watchBrokenClient = false; @@ -289,6 +291,18 @@ void Socket::cleanUp() { clearSpiedOn(); msdpClearReporting(); + // Ensure account connection is untracked before player/account state is cleared + std::string accountName = getAccountName(); + if (myPlayer) { + std::string characterName = myPlayer->getName(); + if(!accountName.empty() && !characterName.empty() && gServer) { + gServer->untrackAccountConnection(accountName, characterName); + } + } else if(!accountName.empty() && gServer) { + // If we are at the account menu (no player), release the cached account + gServer->releaseAccount(accountName, ""); + } + if (myPlayer) { if (myPlayer->fd > -1) { myPlayer->save(true); @@ -300,6 +314,7 @@ void Socket::cleanUp() { } myPlayer = nullptr; } + currentAccountName.clear(); endCompress(); if(fd > -1) { close(fd); @@ -524,8 +539,8 @@ std::string Socket::stripTelnet(std::string_view inStr) { void Socket::checkLockOut() { int lockStatus = gConfig->isLockedOut(shared_from_this()); if (lockStatus == 0) { - askFor("\n\nPlease enter name: "); - setState(LOGIN_GET_NAME); + askFor("\n\nLogin Options:\n ^Wa^x) Enter account name to create or login\n ^Wb^x) Skip accounts and login with a character name\n\nEnter choice (a/b): "); + setState(LOGIN_ENTRY_CHOICE); } else if (lockStatus == 2) { print("\n\nA password is required to play from your site: "); setState(LOGIN_GET_LOCKOUT_PASSWORD); @@ -1149,14 +1164,15 @@ void Socket::reconnect(bool pauseScreen) { gServer->clearPlayer(myPlayer->getName()); myPlayer = nullptr; } + currentAccountName.clear(); if (pauseScreen) { setState(LOGIN_PAUSE_SCREEN); printColor("\nPress ^W[RETURN]^x to reconnect or type ^Wquit^x to disconnect.\n: "); } else { - setState(LOGIN_GET_NAME); showLoginScreen(); - askFor("\n\nPlease enter name: "); + askFor("\n\nPlease enter account name: "); + setState(LOGIN_GET_ACCOUNT_NAME); } } @@ -1885,11 +1901,11 @@ const char EOR_STR[] = {(char) IAC, (char) EOR, '\0' }; const char GA_STR[] = {(char) IAC, (char) GA, '\0' }; void Socket::askFor(const char *str) { - if (eorEnabled()) { printColor(str); + + if (eorEnabled()) { print(EOR_STR); } else { - printColor(str); print(GA_STR); } } @@ -2259,9 +2275,84 @@ void Socket::registerPlayer() { if(myPlayer) { registered = true; gServer->addPlayer(myPlayer); + + // Track account connection when player logs in + std::string accountName = getAccountName(); + std::string characterName = myPlayer->getName(); + if(!accountName.empty() && !characterName.empty()) { + gServer->trackAccountConnection(accountName, characterName); + } } else { registered = false; } } +//******************************************************************** +// Account Methods +//******************************************************************** + +bool Socket::hasAccount() const { + // First try to get account name from player if available + if (myPlayer && !myPlayer->getAccountName().empty()) { + auto account = gServer->getOrLoadAccount(myPlayer->getAccountName()); + return account != nullptr; + } + + // Fall back to temporary account name during login + if (!currentAccountName.empty()) { + auto account = gServer->getOrLoadAccount(currentAccountName); + return account != nullptr; + } + + return false; +} + +std::shared_ptr Socket::getAccount() const { + // First try to get account name from player if available + if (myPlayer && !myPlayer->getAccountName().empty()) { + return gServer->getOrLoadAccount(myPlayer->getAccountName()); + } + + // Fall back to temporary account name during login + if (!currentAccountName.empty()) { + return gServer->getOrLoadAccount(currentAccountName); + } + + return nullptr; +} + +std::string Socket::getAccountName() const { + // First try to get account name from player if available + if (myPlayer && !myPlayer->getAccountName().empty()) { + return myPlayer->getAccountName(); + } + + // Fall back to temporary account name during login + return currentAccountName; +} + +void Socket::setAccount(std::shared_ptr acc) { + if (acc) { + currentAccountName = acc->getName(); + // If we have a player, also set the account name there + if (myPlayer) { + myPlayer->setAccountName(acc->getName()); + } + } else { + currentAccountName.clear(); + if (myPlayer) { + myPlayer->setAccountName(""); + } + } +} + +void Socket::clearAccount() { + currentAccountName.clear(); + if (myPlayer) { + myPlayer->setAccountName(""); + } +} + + + diff --git a/json/accounts-json.cpp b/json/accounts-json.cpp new file mode 100644 index 00000000..d5d5cb2c --- /dev/null +++ b/json/accounts-json.cpp @@ -0,0 +1,90 @@ +/* + * accounts-json.cpp + * Account JSON serialization + * ____ _ + * | _ \ ___ __ _| |_ __ ___ ___ + * | |_) / _ \/ _` | | '_ ` _ \/ __| + * | _ < __/ (_| | | | | | | \__ \ + * |_| \_\___|\__,_|_|_| |_| |_|___/ + * + * Permission to use, modify and distribute is granted via the + * GNU Affero General Public License v3 or later + * + * Copyright (C) 2007-2021 Jason Mitchell, Randi Mitchell + * Contributions by Tim Callahan, Jonathan Hseu + * Based on Mordor (C) Brooke Paul, Brett J. Vickers, John P. Freeman + * + */ + +#include "json.hpp" +#include "account.hpp" + +void to_json(nlohmann::json &j, const Account &account) { + j = nlohmann::json{ + {"accountName", account.getName()}, + {"password", account.getPassword()}, + {"email", account.getEmail()}, + {"created", account.getCreated()}, + {"lastLogin", account.getLastLogin()}, + {"version", account.getVersion()}, + {"characterNames", account.getCharacterNames()}, + {"banned", account.isBanned()}, + {"banReason", account.getBanReason()}, + {"expEarned", account.getExpEarned()}, + {"expSpent", account.getExpSpent()} + }; +} + +void from_json(const nlohmann::json &j, Account &account) { + // Required fields + account.setName(j.at("accountName").get()); + + // Password is already hashed when loaded from JSON, so set directly + // Note: We need a way to set the raw hashed password without re-hashing + account.setPassword(j.at("password").get()); + + // Optional fields with defaults + if (j.contains("email")) { + account.setEmail(j.at("email").get()); + } + + if (j.contains("created")) { + account.setCreated(j.at("created").get()); + } + + if (j.contains("lastLogin")) { + account.setLastLogin(j.at("lastLogin").get()); + } + + if (j.contains("version")) { + account.setVersion(j.at("version").get()); + } + + if (j.contains("characterNames")) { + // Add each character from JSON + auto jsonCharNames = j.at("characterNames").get>(); + for (const auto& charName : jsonCharNames) { + account.addCharacter(charName); + } + } + + if (j.contains("banned")) { + account.setBanned(j.at("banned").get()); + } + + if (j.contains("banReason")) { + account.setBanReason(j.at("banReason").get()); + } + + if(j.contains("expEarned")) { + account.setExpEarned(j.at("expEarned").get()); + } else { + account.setExpEarned(0); + } + + if(j.contains("expSpent")) { + account.setExpSpent(j.at("expSpent").get()); + } else { + account.setExpSpent(0); + } +} \ No newline at end of file diff --git a/players/player.cpp b/players/player.cpp index cf5da782..2c712201 100644 --- a/players/player.cpp +++ b/players/player.cpp @@ -79,7 +79,7 @@ #include "unique.hpp" // for remove, deleteOwner #include "xml.hpp" // for loadRoom #include "toNum.hpp" - +#include "account.hpp" //******************************************************************** // init @@ -569,6 +569,27 @@ void Player::uninit() { } } +void Player::setName(std::string_view newName) { + std::string oldName = getName(); + MudObject::setName(newName); + + auto account = getAccount(); + if(account) { + if(account->removeCharacter(oldName)) { + account->addCharacter(std::string(newName)); + account->save(); + } + } else { + std::shared_ptr diskAccount; + if(Account::load(getAccountName(), diskAccount)) { + if(diskAccount->removeCharacter(oldName)) { + diskAccount->addCharacter(std::string(newName)); + diskAccount->save(); + } + } + } +} + //********************************************************************* // courageous //********************************************************************* diff --git a/server/config.cpp b/server/config.cpp index 57d63a38..38a73092 100644 --- a/server/config.cpp +++ b/server/config.cpp @@ -459,6 +459,7 @@ bool Path::checkPaths() { fs::create_directory(Path::Object); fs::create_directory(Path::Player); fs::create_directory(Path::PlayerBackup); + fs::create_directory(Path::Account); fs::create_directory(Path::Config); diff --git a/server/login.cpp b/server/login.cpp index e9429d4b..aad658be 100644 --- a/server/login.cpp +++ b/server/login.cpp @@ -18,6 +18,7 @@ #include // for open, O_RDONLY #include // for format +#include // for strncasecmp #include // for close, read #include // for contains #include // for trim @@ -66,22 +67,16 @@ #include "structs.hpp" // for SEX_FEMALE, SEX_MALE #include "xml.hpp" // for loadPlayer, loadObject #include "deityData.hpp" // for Deity names +#include "account.hpp" // for Account class StartLoc; -/* - * Generic get function, copy for future use - * -bool Create::get(std::shared_ptr sock, std::string str, int mode) { - if(mode == Create::doPrint) { - - } else if(mode == Create::doWork) { - - } - return(true); -} - * - */ +// Forward declarations for account login functions +std::shared_ptr validateAndGetAccount(std::shared_ptr sock); +void showAccountMenu(std::shared_ptr sock, std::shared_ptr account); +void handleAccountMenuCommand(std::shared_ptr sock, std::shared_ptr account, const std::string& str); +void handleCharacterClaim(std::shared_ptr sock, std::shared_ptr account, const std::string& str); +bool loadCharacterForPlay(std::shared_ptr sock, std::shared_ptr account, const std::string& charName); char allowedClassesStr[static_cast(CreatureClass::CLASS_COUNT) + 4][16] = { "Assassin", "Berserker", "Cleric", "Fighter", @@ -90,8 +85,6 @@ char allowedClassesStr[static_cast(CreatureClass::CLASS_COUNT) + 4][16] = "Cler/Ass", "Mage/Thief", "Thief/Mage", "Cler/Figh", "Mage/Ass" }; - - //********************************************************************* // cmdReconnect //********************************************************************* @@ -109,23 +102,6 @@ int cmdReconnect(const std::shared_ptr& player, cmd* cmnd) { return(0); } -std::string::size_type checkProxyLogin(const std::string &str) { - std::string::size_type x = std::string::npos; - std::string::size_type n = str.find(" as "); - if(n == x) - n = str.find(" for "); - return(n); -} -// Character used for access -std::string getProxyChar(const std::string &str, int n) { - return(str.substr(0,n)); -} -// Character being logged in -std::string getProxiedChar(const std::string &str, int n) { - int m = str.find_first_of(' ', n+1); - return(str.substr(m+1, str.length() - m - 1)); -} - bool Player::checkProxyAccess(const std::shared_ptr& proxy) { if(!proxy) return(false); @@ -151,7 +127,7 @@ unsigned const char echo_on[] = {255, 252, 1, 0}; void login(std::shared_ptr sock, const std::string& inStr) { std::shared_ptr player=nullptr; - std::string::size_type proxyCheck = 0; + std::shared_ptr account=nullptr; if(!sock) { std::clog << "**** ERORR: Null socket in login.\n"; return; @@ -159,168 +135,564 @@ void login(std::shared_ptr sock, const std::string& inStr) { std::string str = inStr; - switch(sock->getState()) { - case LOGIN_DNS_LOOKUP: - sock->print("Still performing DNS lookup, please be patient!\n"); - return; - case LOGIN_GET_LOCKOUT_PASSWORD: - if(str != sock->tempstr[0]) { - sock->disconnect(); + case LOGIN_DNS_LOOKUP: { + sock->print("Still performing DNS lookup, please be patient!\n"); return; } - sock->askFor("Please enter name: "); - - sock->setState(LOGIN_GET_NAME); - - return; - // End LOGIN_GET_LOCKOUT_PASSWORD - case LOGIN_GET_NAME: - - proxyCheck = checkProxyLogin(str); - if(proxyCheck != std::string::npos) { - std::string proxyChar = getProxyChar(str, proxyCheck); - std::string proxiedChar = getProxiedChar(str, proxyCheck); - lowercize(proxyChar, 1); - lowercize(proxiedChar, 1); - if(proxyChar == proxiedChar) { - sock->askFor("That's just silly.\nPlease enter name: "); + case LOGIN_GET_LOCKOUT_PASSWORD: { + if(str != sock->tempstr[0]) { + sock->disconnect(); return; } - if(!nameIsAllowed(proxyChar, sock) || !nameIsAllowed(proxiedChar, sock)) { - sock->askFor("Please enter name: "); + sock->askFor("\n\nLogin Options:\n ^Wa^x) Enter account name to create or login\n ^Wb^x) Skip accounts and login with a legacy character name\n\nEnter choice (a/b): "); + sock->setState(LOGIN_ENTRY_CHOICE); + return; + // End LOGIN_GET_LOCKOUT_PASSWORD + } + case LOGIN_ENTRY_CHOICE: { + std::string choice = str; + boost::trim(choice); + if(choice.empty()) { + sock->askFor("Enter choice (a/b): "); return; } - if(!Player::exists(proxyChar)) { - sock->println(proxyChar + " doesn't exist."); - sock->askFor("Please enter name: "); + char c = std::tolower(choice[0]); + if(c == 'a') { + sock->askFor("Please enter account name: "); + sock->setState(LOGIN_GET_ACCOUNT_NAME); return; - } - if(!Player::exists(proxiedChar)) { - sock->println(proxiedChar + " doesn't exist."); - sock->askFor("Please enter name: "); + } else if(c == 'b') { + sock->askFor("Please enter character name for legacy login: "); + sock->setState(LOGIN_GET_LEGACY_NAME); + return; + } else { + sock->askFor("Please enter A or B: "); return; } - if(!loadPlayer(proxiedChar, player)) { - sock->println(std::string("Error loading ") + proxiedChar + "\n"); - sock->askFor("Please enter name: "); + } + case LOGIN_GET_ACCOUNT_NAME: { + lowercize(str, 1); + if(str.length() >= 25) + str[25] = 0; + + if(!nameIsAllowed(str, sock)) { + sock->askFor("Please enter account name: "); return; } - player->fd = -1; - std::shared_ptr proxy = nullptr; - proxy = gServer->findPlayer(proxyChar); - if(!proxy) { - if(!loadPlayer(proxyChar, proxy)) { - sock->println(std::string("Error loading ") + proxyChar + "\n"); - sock->askFor("Please enter name: "); + + strcpy(sock->tempstr[0], str.c_str()); // Store account name + + if(!Account::load(str, account)) { + sock->print("\n%s? ", str.c_str()); + sock->askFor("Did I get that right? (yes/no): "); + sock->setState(LOGIN_CHECK_CREATE_ACCOUNT); + return; + } else { + if(account->isBanned()) { + sock->print("Account is banned: %s\n", account->getBanReason().c_str()); + sock->disconnect(); return; } + sock->print("%s", echo_off); + sock->askFor("Please enter account password: "); + sock->setState(LOGIN_GET_ACCOUNT_PASSWORD); + return; } - if(!player->checkProxyAccess(proxy)) { - sock->println(std::string(proxy->getName()) + " does not have access to " + player->getName()); - sock->askFor("Please enter name: "); + // End LOGIN_GET_ACCOUNT_NAME + } + case LOGIN_GET_LEGACY_NAME: { + std::string charName = str; + boost::trim(charName); + if(charName.empty()) { + sock->askFor("Please enter character name for legacy login: "); return; } + lowercize(charName, 1); + if(charName.length() >= 25) charName[25] = 0; - player->fd = -1; - sock->setPlayer(player); - - if(gServer->checkDuplicateName(sock, false)) { - // Don't free player here or ask for name again because checkDuplicateName does that - // We only need to worry about freeing proxy + std::shared_ptr player; + if(!loadPlayer(charName, player)) { + sock->print("Character '%s' does not exist.\n", charName.c_str()); + sock->askFor("Please enter character name for legacy login: "); return; } - sock->println(std::string("Trying to log in ") + player->getName() + " using " + proxy->getName() + " as proxy."); - - - + if(!player->getAccountName().empty()) { + sock->print("Character '%s' is already claimed by an account.\n", charName.c_str()); + sock->print("Please login using the account system instead of legacy login.\n"); + sock->askFor("Please enter account name: "); + sock->setState(LOGIN_GET_ACCOUNT_NAME); + return; + } + strcpy(sock->tempstr[1], charName.c_str()); + sock->print("Legacy login for character '%s'.\n", charName.c_str()); sock->print("%s", echo_off); - //sock->print("%c%c%c", 255, 251, 1); - std::string passwordPrompt = std::string("Please enter password for ") + proxy->getName() + ": "; - sock->askFor(passwordPrompt.c_str()); - sock->tempbstr = proxy->getPassword(); - - - player->setProxy(proxy); - - sock->setState(LOGIN_GET_PROXY_PASSWORD); - + sock->askFor("Please enter character password: "); + sock->setState(LOGIN_LEGACY_PASSWORD); return; } - lowercize(str, 1); - if(str.length() >= 25) - str[25]=0; - - if(!nameIsAllowed(str, sock)) { - sock->askFor("Please enter name: "); + case LOGIN_CHECK_CREATE_ACCOUNT: { + if(str[0] != 'y' && str[0] != 'Y') { + sock->tempstr[0][0] = 0; + sock->askFor("Please enter account name: "); + sock->setState(LOGIN_GET_ACCOUNT_NAME); + return; + } else { + sock->print("\nCreating new account...\n"); + sock->print("Please set a password for new account '%s': \n", sock->tempstr[0]); + sock->setState(LOGIN_GET_ACCOUNT_CREATE_PASSWORD); + return; + } + // End LOGIN_CHECK_CREATE_ACCOUNT + } + case LOGIN_GET_ACCOUNT_PASSWORD: { + if(!Account::load(sock->tempstr[0], account) || !account->isPassword(str)) { + sock->write("\255\252\1\n\rIncorrect.\n\r"); + logn("log.incorrect", fmt::format("Invalid account password({}) for {} from {}\n", str, sock->tempstr[0], sock->getHostname()).c_str()); + sock->disconnect(); + return; + } else { + account->updateLastLogin(); + account->save(); + sock->setAccount(account); + // Show character selection + showAccountMenu(sock, account); + return; + } + // End LOGIN_GET_ACCOUNT_PASSWORD + } + case LOGIN_GET_ACCOUNT_CREATE_PASSWORD: { + sock->print("%s", echo_on); + + if(!Account::isValidPassword(str)) { + sock->print("\nPassword must be between 5 and 35 characters.\n"); + sock->print("Please set a password for new account '%s': \n", sock->tempstr[0]); + sock->print("%s", echo_off); + sock->setState(LOGIN_GET_ACCOUNT_CREATE_PASSWORD); + return; + } + + // Create the account + std::string accountName = sock->tempstr[0]; + account = std::make_shared(accountName); + account->setPassword(str); + account->setCreated(time(nullptr)); + account->updateLastLogin(); + + if(!account->save()) { + sock->print("Error creating account. Please try again.\n"); + sock->askFor("Please enter account name: "); + sock->setState(LOGIN_GET_ACCOUNT_NAME); + return; + } + + sock->print("\n^GAccount '%s' created successfully!^x\n", accountName.c_str()); + sock->setAccount(account); + showAccountMenu(sock, account); return; + // End LOGIN_GET_ACCOUNT_CREATE_PASSWORD } + case LOGIN_ACCOUNT_MENU: { + account = validateAndGetAccount(sock); + if(!account) return; - if(!loadPlayer(str, player)) { - strcpy(sock->tempstr[0], str.c_str()); - sock->print("\n%s? Did I get that right? ", str.c_str()); - sock->setState(LOGIN_CHECK_CREATE_NEW); + handleAccountMenuCommand(sock, account, str); return; - } else { - player->fd = -1; - sock->setPlayer(player); - sock->print("%s", echo_off); - //sock->print("%c%c%c", 255, 251, 1); - sock->askFor("Please enter password: ");//, 255, 251, 1); - sock->setState(LOGIN_GET_PASSWORD); - player = nullptr; + // End LOGIN_ACCOUNT_MENU + } + case LOGIN_CLAIM_CHARACTER: { + account = validateAndGetAccount(sock); + if(!account) return; + + handleCharacterClaim(sock, account, str); return; + // End LOGIN_CLAIM_CHARACTER } - // End LOGIN_GET_NAME - case LOGIN_CHECK_CREATE_NEW: - if(str[0] != 'y' && str[0] != 'Y') { - sock->tempstr[0][0] = 0; - sock->askFor("Please enter name: "); - sock->setState(LOGIN_GET_NAME); + case LOGIN_CLAIM_PASSWORD: { + account = validateAndGetAccount(sock); + if(!account) return; + + std::string charName = sock->tempstr[1]; + + // Load the character to verify password + std::shared_ptr player; + if(!loadPlayer(charName, player)) { + sock->print("Error: Character no longer exists!\n"); + showAccountMenu(sock, account); + return; + } + + // Check password + if(!player->isPassword(str)) { + sock->print("\n^RIncorrect password for character '%s'.^x\n", charName.c_str()); + sock->print("Character claim failed.\n"); + showAccountMenu(sock, account); + return; + } + + // Success! Claim the character + if(!account->addCharacter(charName)) { + sock->print("Error: Could not add character to account.\n"); + showAccountMenu(sock, account); + return; + } + + // Update the character's account name + player->setAccountName(account->getName()); + player->save(); + + sock->print("\n^GCharacter '%s' has been successfully claimed!^x\n", charName.c_str()); + sock->print("The character is now linked to your account.\n"); + showAccountMenu(sock, account); return; - } else { + // End LOGIN_CLAIM_PASSWORD + } + case LOGIN_SET_EMAIL: { + account = validateAndGetAccount(sock); + if(!account) return; + + std::string email = str; + boost::trim(email); + + // Empty string clears the email + if(email.empty()) { + account->setEmail(""); + account->save(); + sock->print("^GEmail address cleared.^x\n"); + showAccountMenu(sock, account); + return; + } + + // Basic email validation + if(email.find('@') == std::string::npos || email.find('.') == std::string::npos) { + sock->print("^RInvalid email format. Please enter a valid email address.^x\n"); + sock->askFor("Enter new email address (or press enter to clear): "); + return; + } + + if(email.length() > 255) { + sock->print("^REmail address too long (maximum 255 characters).^x\n"); + sock->askFor("Enter new email address (or press enter to clear): "); + return; + } + + // Store the email temporarily for confirmation + strcpy(sock->tempstr[2], email.c_str()); + sock->print("Confirm email address: ^C%s^x\n", email.c_str()); + sock->askFor("Is this correct? (y/n): "); + sock->setState(LOGIN_SET_EMAIL_CONFIRM); + return; + // End LOGIN_SET_EMAIL + } + case LOGIN_SET_EMAIL_CONFIRM: { + account = validateAndGetAccount(sock); + if(!account) return; + + if(str.empty() || (str[0] != 'y' && str[0] != 'Y')) { + sock->print("Email not set.\n"); + showAccountMenu(sock, account); + return; + } + + // Set and save the email + std::string email = sock->tempstr[2]; + account->setEmail(email); + account->save(); + + sock->print("^GEmail address set to: ^C%s^x^G.^x\n", email.c_str()); + showAccountMenu(sock, account); + return; + // End LOGIN_SET_EMAIL_CONFIRM + } + case LOGIN_LEGACY_PASSWORD: { + sock->print("%s", echo_on); + std::string charName = sock->tempstr[1]; + + // Load the character + std::shared_ptr player; + if(!loadPlayer(charName, player)) { + sock->print("\n^RCharacter '%s' does not exist.^x\n", charName.c_str()); + sock->askFor("Please enter character name for legacy login: "); + sock->setState(LOGIN_GET_LEGACY_NAME); + return; + } + + // Check password + if(!player->isPassword(str)) { + sock->print("\n^RIncorrect.^x\n"); + logn("log.incorrect", fmt::format("Invalid legacy password({}) for {} from {}\n", str, charName, sock->getHostname()).c_str()); + sock->disconnect(); + return; + } + + // Success! Log the player in directly + sock->print("\n^GLegacy login successful!^x\n"); + + // Update last login time + player->setLastLogin(time(nullptr)); + + // Set up the socket and player connection + sock->setPlayer(player); + player->fd = -1; + + // Check for duplicate names + if(gServer->checkDuplicateName(sock, false)) { + return; + } + + // Complete the login process + sock->finishLogin(); + + return; + // End LOGIN_LEGACY_PASSWORD + } + } +} +//********************************************************************* +// showAccountMenu +//********************************************************************* - sock->print("\nTo get help at any time during creation use the \"^Whelp^x\" command. \n"); +void showAccountMenu(std::shared_ptr sock, std::shared_ptr account) { + sock->print("\n\n^W~~~~~~~ Account Menu ~~~~~~~^x\n\n"); + account->printInfoFields(sock); + + // Show command options + sock->print("^WCommands:^x\n"); + sock->print(" ^W%-15s^x - Create a new character", "^c(c)^Wreate^x"); + if(account->canCreateCharacter()) { + sock->print("\n"); + } else { + sock->print(" ^R(limit reached)^x\n"); + } + + sock->print(" ^W%-15s^x - List your characters\n", "^c(l)^Wist^x"); + sock->print(" ^W%-15s^x - Claim a legacy character\n", "^c(cl)^Waim^x"); + sock->print(" ^W%-15s^x - Set email address\n", "^c(e)^Wmail^x"); + sock->print(" ^W%-15s^x - Disconnect\n", "^c(q)^Wuit^x"); + + const auto& characters = account->getCharacterNames(); + if (characters.empty()) { + sock->askFor("\nEnter a command: "); + } else { + sock->askFor("\nEnter a command, or a character name to play: "); + } - sock->print("\nHit return: "); - sock->setState(CREATE_NEW); + sock->setState(LOGIN_ACCOUNT_MENU); +} + +//********************************************************************* +// handleAccountMenuCommand +//********************************************************************* + +void handleAccountMenuCommand(std::shared_ptr sock, std::shared_ptr account, const std::string& str) { + if(str.empty()) { + showAccountMenu(sock, account); + return; + } + + std::string input = str; + std::transform(input.begin(), input.end(), input.begin(), ::tolower); + boost::trim(input); + + const auto& characters = account->getCharacterNames(); + + // Extract just the command word (first word) for proper matching + std::string command; + size_t spacePos = input.find(' '); + if(spacePos != std::string::npos) { + command = input.substr(0, spacePos); + } else { + command = input; + } + + // Handle "create" command with partial matching + if(command.length() >= 1 && !strncasecmp(command.c_str(), "create", std::min(command.length(), 6UL))) { + if(!account->canCreateCharacter()) { + sock->print("^RYou have reached your character limit (%d/%d).^x\n", + account->getCharacterCount(), account->getCharacterLimit()); + sock->print("You must delete a character before creating a new one.\n\n"); + showAccountMenu(sock, account); return; } - // End LOGIN_CHECK_CREATE_NEW - case LOGIN_GET_PASSWORD: - player = sock->getPlayer(); - if(!player || !player->isPassword(str)) { - sock->write("\255\252\1\n\rIncorrect.\n\r"); - logn("log.incorrect", fmt::format("Invalid password({}) for {} from {}\n", str, player ? player->getName() : "", sock->getHostname()).c_str()); - sock->disconnect(); + + // Check if IP has reached max connections + if(gServer->checkDouble(sock, false)) { + sock->print("^RMaximum number of characters already connected.\n"); + sock->print("Please disconnect one of your other characters first.\n\n"); + showAccountMenu(sock, account); return; - } else { - player = nullptr; - sock->finishLogin(); + } + sock->print("\nCreating new character..."); + sock->print("\nTo get help at any time during creation use the \"^Whelp^x\" command."); + sock->askFor("\nHit return to continue: "); + sock->setState(CREATE_NEW_CHARACTER); + return; + } + + // Handle "list" command with partial matching + if(command.length() >= 1 && !strncasecmp(command.c_str(), "list", std::min(command.length(), 4UL))) { + account->printCharacterList(sock); + if(account->getCharacterNames().empty()) + sock->askFor("\nEnter a command: "); + else + sock->askFor("\nEnter a command, or a character name to play: "); + // Stay in LOGIN_ACCOUNT_MENU state to return to menu + return; + } + + // Handle "claim" command with partial matching + if(command.length() >= 2 && !strncasecmp(command.c_str(), "claim", std::min(command.length(), 5UL))) { + if(!account->canCreateCharacter()) { + sock->print("^RYou have reached your character limit (%d/%d).^x\n", + account->getCharacterCount(), account->getCharacterLimit()); + sock->print("You cannot claim additional characters.\n"); + sock->askFor("Press ^W^x to continue: "); + showAccountMenu(sock, account); return; } - break; + sock->print("\n^WClaim Legacy Character^x\n"); + sock->print("Enter the name of the character you wish to claim.\n"); + sock->print("This character must have existed before the account system was implemented.\n"); + sock->askFor("Character name: "); + sock->setState(LOGIN_CLAIM_CHARACTER); + return; + } + + // Handle "email" command with partial matching + if(command.length() >= 1 && !strncasecmp(command.c_str(), "email", std::min(command.length(), 5UL))) { + if(!account->getEmail().empty()) { + sock->print("Current email: %s\n", account->getEmail().c_str()); + } + sock->askFor("Enter new email address (or press enter to clear): "); + sock->setState(LOGIN_SET_EMAIL); + return; + } + + // Handle "quit" command with partial matching + if(command.length() >= 1 && !strncasecmp(command.c_str(), "quit", std::min(command.length(), 4UL))) { + sock->print("Goodbye!\n"); + sock->disconnect(); + return; + } + + // Handle character name login + // First check if IP has reached max connections + if(gServer->checkDouble(sock, false)) { + sock->print("^RMaximum number of characters already connected.\n"); + sock->print("Please disconnect one of your other characters first.\n\n"); + showAccountMenu(sock, account); + return; + } - case LOGIN_GET_PROXY_PASSWORD: - if(Player::hashPassword(str) != sock->tempbstr) { - sock->write("\255\252\1\n\rIncorrect.\n\r"); - logn("log.incorrect", fmt::format("Invalid password({}) for {} from {}\n", str, sock->getPlayer()->getName(), sock->getHostname()).c_str()); - sock->disconnect(); - return; - } else { - sock->tempbstr.clear(); + std::string foundChar; + for(const auto& accountChar : characters) { + if(strcasecmp(accountChar.c_str(), input.c_str()) == 0) { + foundChar = accountChar; + break; + } + } + + if(!foundChar.empty()) { + // Load and play the character from our account + if(loadCharacterForPlay(sock, account, foundChar)) { + sock->finishLogin(); + } + return; + } + + // Character not in our account - check if it exists and if we have proxy access + // Format the character name properly (capitalize first letter) + std::string formattedInput = input; + lowercize(formattedInput, 1); + + std::shared_ptr player = nullptr; + if(loadPlayer(formattedInput, player)) { + // Check if any of our account's characters has proxy access to this character + std::shared_ptr proxyGranter = nullptr; + + for(const auto& accountCharName : characters) { + std::shared_ptr accountChar = nullptr; + if(loadPlayer(accountCharName, accountChar)) { + if(player->checkProxyAccess(accountChar)) { + proxyGranter = accountChar; + break; + } + } + } + + if(proxyGranter) { + // Set up the proxy relationship + player->setProxy(proxyGranter); + + // Set up the socket for the player + player->fd = -1; + sock->setPlayer(player); + + // Check for duplicate names + if(gServer->checkDuplicateName(sock, false)) { + return; + } + + sock->print("Loading %s (using %s as proxy)...\n", player->getName().c_str(), proxyGranter->getName().c_str()); sock->finishLogin(); return; } - break; } + + // Invalid command + sock->print("Invalid command or character name '%s'.\n", input.c_str()); + showAccountMenu(sock, account); +} + +//********************************************************************* +// loadCharacterForPlay +//********************************************************************* + +bool loadCharacterForPlay(std::shared_ptr sock, std::shared_ptr account, const std::string& charName) { + // Verify character still exists and belongs to this account + std::shared_ptr player; + if(!loadPlayer(charName, player)) { + sock->print("Character '%s' no longer exists!\n", charName.c_str()); + // Remove from account + account->removeCharacter(charName); + account->save(); + showAccountMenu(sock, account); + return false; + } + + // Check if character belongs to this account (migration support) + if(player->getAccountName() != account->getName()) { + // Update player's account name + player->setAccountName(account->getName()); + player->save(); + } + + // Load character for login + sock->setPlayer(player); + player->fd = -1; + + if(gServer->checkDuplicateName(sock, false)) { + return false; + } + + return true; +} + +// Helper function to validate account exists and is loaded, returns account or nullptr if invalid (handles error) +std::shared_ptr validateAndGetAccount(std::shared_ptr sock) { + std::shared_ptr account = sock->getAccount(); + if(!account) { + sock->print("Error: No account found!\n"); + sock->disconnect(); + return nullptr; + } + return account; } void Socket::finishLogin() { char charName[25]; - print("%s", echo_on); auto player = getPlayer(); strcpy(charName, player->getCName()); @@ -337,8 +709,8 @@ void Socket::finishLogin() { myPlayer = nullptr; if(!loadPlayer(charName, player)) { - askFor("Player no longer exists!\n\nPlease enter name: "); - setState(LOGIN_GET_NAME); + askFor("Player no longer exists!\n\nPlease enter account name: "); + setState(LOGIN_GET_ACCOUNT_NAME); return; } player->setProxy(proxyName, proxyId); @@ -499,7 +871,7 @@ void doCreateHelp(const std::shared_ptr& sock, std::string_view str) { void createPlayer(std::shared_ptr sock, const std::string& str) { switch(sock->getState()) { - case CREATE_NEW: + case CREATE_NEW_CHARACTER: case CREATE_GET_DM_PASSWORD: break; default: @@ -510,7 +882,7 @@ void createPlayer(std::shared_ptr sock, const std::string& str) { break; } switch(sock->getState()) { - case CREATE_NEW: + case CREATE_NEW_CHARACTER: { std::shared_ptr target = sock->getPlayer(); sock->print("\n"); @@ -534,7 +906,7 @@ void createPlayer(std::shared_ptr sock, const std::string& str) { return; } else goto no_pass; - // End CREATE_NEW + // End CREATE_NEW_CHARACTER case CREATE_GET_DM_PASSWORD: if(str != gConfig->getDmPass()) { sock->disconnect(); @@ -650,19 +1022,19 @@ void createPlayer(std::shared_ptr sock, const std::string& str) { if(gConfig->classes[get_class_string(static_cast(sock->getPlayer()->getClass()))]->numProfs()==2) Create::getSecondProf(sock, str, Create::doPrint); else - Create::getPassword(sock, str, Create::doPrint); + Create::getName(sock, str, Create::doPrint); return; case CREATE_SECOND_PROF: if(!Create::getSecondProf(sock, str, Create::doWork)) return; - Create::getPassword(sock, str, Create::doPrint); + Create::getName(sock, str, Create::doPrint); return; - case CREATE_GET_PASSWORD: + case CREATE_GET_NAME: - if(!Create::getPassword(sock, str, Create::doWork)) + if(!Create::getName(sock, str, Create::doWork)) return; Create::done(sock, str, Create::doPrint); return; @@ -1727,35 +2099,63 @@ bool Create::getSecondProf(const std::shared_ptr& sock, std::string str, } //********************************************************************* -// getPassword +// getName //********************************************************************* -bool Create::getPassword(const std::shared_ptr& sock, const std::string &str, int mode) { +bool Create::getName(const std::shared_ptr& sock, const std::string &str, int mode) { if(mode == Create::doPrint) { - sock->print("\nYou must now choose a password. Remember that it\n"); - sock->print("is YOUR responsibility to remember this password. The staff\n"); - sock->print("at Realms will not give out password information to anyone\n"); - sock->print("at any time. Please write it down someplace, because if you\n"); - sock->print("forget it, you will no longer be able to play this character.\n\n"); - sock->print("Please choose a password (up to 14 chars): "); + sock->print("\nYou must now choose a name for your character.\n"); + sock->print("Your character name should be appropriate for a fantasy setting.\n"); + sock->print("Names that are offensive, reference real-world people/places,\n"); + sock->print("or are otherwise inappropriate may be changed by staff.\n\n"); + sock->print("Please choose a character name: "); - sock->setState(CREATE_GET_PASSWORD); + sock->setState(CREATE_GET_NAME); } else if(mode == Create::doWork) { - if(!isValidPassword(sock, str)) { - sock->print("\nChoose a password: "); - sock->setState(CREATE_GET_PASSWORD); + // Store the name in tempstr[0] for validation and use + std::string charName = str; + boost::trim(charName); + + if(charName.empty()) { + sock->print("\nName cannot be empty. Choose a character name: "); + sock->setState(CREATE_GET_NAME); return(false); } - - sock->getPlayer()->setFlag(P_PASSWORD_CURRENT); - - //t = time(0); - //strcpy(sock->getPlayer()->last_mod, ctime(&t)); - - sock->getPlayer()->setPassword(str); + + if(charName.length() > 20) { + sock->print("\nName too long (max 20 characters). Choose a character name: "); + sock->setState(CREATE_GET_NAME); + return(false); + } + + // Capitalize the name properly (first letter uppercase, rest lowercase) + lowercize(charName, 1); + + if(!nameIsAllowed(charName, sock)) { + sock->setState(CREATE_GET_NAME); + return(false); + } + + // Check if character already exists + if(Player::exists(charName)) { + sock->print("\nThat character name is already taken. Choose a character name: "); + sock->setState(CREATE_GET_NAME); + return(false); + } + + // Check if character is already in the account + std::shared_ptr account = sock->getAccount(); + if(account && account->hasCharacter(charName)) { + sock->print("\nYou already have a character with that name. Choose a character name: "); + sock->setState(CREATE_GET_NAME); + return(false); + } + + // Store the properly capitalized name for later use + strcpy(sock->tempstr[0], charName.c_str()); } return(true); @@ -1784,17 +2184,35 @@ void Create::done(const std::shared_ptr& sock, const std::string &str, i player->setBirthday(); player->setCreated(); - player->setName( sock->tempstr[0]); + // Set character name from what user entered + player->setName(sock->tempstr[0]); if(gServer->checkDuplicateName(sock, false)) return; if(gServer->checkDouble(sock)) return; + + // Additional check (name validation was already done in getName()) if(Player::exists(player->getName())) { sock->printColor("\n\n^ySorry, that player already exists.^x\n\n\n"); sock->reconnect(); return; } + + // Link character to account system + std::shared_ptr account = sock->getAccount(); + if(account) { + // Set the character's account name + player->setAccountName(account->getName()); + // Characters don't need individual passwords in account system + // Keep existing password field empty or use a placeholder + player->setPassword(""); // Clear any password - account handles auth + player->setFlag(P_PASSWORD_CURRENT); + } else { + sock->print("Error: No account found during character creation!\n"); + sock->reconnect(); + return; + } player->lasttime[LT_AGE].interval = 0; @@ -1932,6 +2350,10 @@ void Create::done(const std::shared_ptr& sock, const std::string &str, i if(player->getClass() == CreatureClass::BARD) player->learnSong(SONG_HEAL); + + // Add character to account (account was already validated above) + account->addCharacter(player->getName()); + account->save(); player->save(true); sock->registerPlayer(); @@ -2348,8 +2770,10 @@ bool Create::getWeight(const std::shared_ptr& sock, std::string str, int bool nameIsAllowed(std::string str, const std::shared_ptr& sock) { int i=0, nonalpha=0, len = str.length(); - if(!isalpha(str[0])) + if(!isalpha(str[0])) { + sock->print("The first character of your name must be a letter.\n"); return(false); + } if(len < 3) { sock->print("Name must be at least 3 characters.\n"); @@ -2374,16 +2798,75 @@ bool nameIsAllowed(std::string str, const std::shared_ptr& sock) { return(false); } + // Check each character - allow only letters, apostrophes, and hyphens for(i=0; iprint("Name must be alphabetic.\n"); + sock->print("It may only contain the non-alpha characters ' and -.\n"); return(false); } } + // First character must be a letter (already checked above) + // Last two characters must be letters + if( str[len-1] == '\'' || str[len-1] == '-' || + (len > 1 && (str[len-2] == '\'' || str[len-2] == '-'))) { + sock->print("The last two characters of your name must be letters.\n"); + return(false); + } + if(!parse_name(str)) { sock->print("That name is not allowed.\n"); return(false); } return(true); } + +//********************************************************************* +// handleCharacterClaim +//********************************************************************* + +void handleCharacterClaim(std::shared_ptr sock, std::shared_ptr account, const std::string& str) { + std::string charName = str; + boost::trim(charName); + + if(charName.empty()) { + sock->print("You must enter a character name.\n"); + showAccountMenu(sock, account); + return; + } + + // Format the character name properly (capitalize first letter) + lowercize(charName, 1); + + // Check if character exists + std::shared_ptr player; + if(!loadPlayer(charName, player)) { + sock->print("Character '%s' does not exist.\n", charName.c_str()); + showAccountMenu(sock, account); + return; + } + + // Check if character is already claimed by an account + if(!player->getAccountName().empty()) { + sock->print("Character '%s' is already claimed by an account.\n", charName.c_str()); + sock->print("Only unclaimed legacy characters can be claimed.\n"); + showAccountMenu(sock, account); + return; + } + + // Check if we already have this character in our account somehow + if(account->hasCharacter(charName)) { + sock->print("Character '%s' is already in your account.\n", charName.c_str()); + showAccountMenu(sock, account); + return; + } + + // Store the character name for the password verification step + strcpy(sock->tempstr[1], charName.c_str()); + + sock->print("\nTo claim character '%s', you must verify ownership by entering the character's password.\n", charName.c_str()); + sock->print("%s", echo_off); + sock->askFor("Character password: "); + sock->setState(LOGIN_CLAIM_PASSWORD); +} diff --git a/server/server.cpp b/server/server.cpp index 3b461878..afc3708a 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -56,6 +56,7 @@ #include "catRef.hpp" // for CatRef #include "color.hpp" // for stripColor #include "config.hpp" // for Config, gConfig +#include "account.hpp" // for Account #include "delayedAction.hpp" // for DelayedAction #include "factions.hpp" // for Faction #include "flags.hpp" // for M_PERMANENT_MONSTER @@ -96,6 +97,9 @@ extern int Numplayers; extern long last_time_update; extern long last_weather_update; +// Forward declaration +void showAccountMenu(std::shared_ptr sock, std::shared_ptr account); + // Function prototypes bool init_spelling(); // TODO: Move spelling stuff into server void initSpellList(); @@ -130,7 +134,7 @@ Server::Server(): roomCache(RQMAX, true), monsterCache(MQMAX, false), objectCach running = false; pulse = 0; webInterface = nullptr; - lastDnsPrune = lastUserUpdate = lastRoomPulseUpdate = lastRandomUpdate = lastActiveUpdate = 0; + lastDnsPrune = lastUserUpdate = lastRoomPulseUpdate = lastRandomUpdate = lastActiveUpdate = lastAccountSave = 0; maxPlayerId = maxObjectId = maxMonsterId = 0; loadDnsCache(); pythonHandler = nullptr; @@ -635,6 +639,15 @@ void Server::disconnectAll() { bool isDisconnecting(const std::shared_ptr& sock) { if(sock->getState() == CON_DISCONNECTING) { + // Handle account cleanup before disconnecting + if(sock->hasPlayer()) { + std::string accountName = sock->getAccountName(); + std::string characterName = sock->getPlayer()->getName(); + if(!accountName.empty() && !characterName.empty()) { + gServer->untrackAccountConnection(accountName, characterName); + } + } + // Flush any residual data sock->flush(); return true; @@ -1882,7 +1895,14 @@ bool Server::checkDuplicateName(std::shared_ptr sock, bool dis) { if(sock != s && s->hasPlayer() && s->getPlayer()->getName() == sock->getPlayer()->getName()) { if(!dis) { sock->printColor("\n\n^ySorry, that character is already logged in.^x\n\n\n"); - sock->reconnect(); + // Return to account menu instead of reconnecting + auto account = sock->getAccount(); + if(account) { + sock->clearPlayer(); + showAccountMenu(sock, account); + } else { + sock->reconnect(); + } } else { s->disconnect(); } @@ -1895,9 +1915,10 @@ bool Server::checkDuplicateName(std::shared_ptr sock, bool dis) { //********************************************************************* // checkDouble //********************************************************************* -// returning true will disconnect the connecting socket (sock) +// returning true indicates the limit has been exceeded +// if disconnectOnLimit is true, will disconnect the connecting socket -bool Server::checkDouble(std::shared_ptr sock) { +bool Server::checkDouble(std::shared_ptr sock, bool disconnectOnLimit) { if(!gConfig->getCheckDouble()) return(false); @@ -1924,8 +1945,10 @@ bool Server::checkDouble(std::shared_ptr sock) { cnt++; if(cnt >= gConfig->getMaxDouble()) { - sock->write("\nMaximum number of connections has been exceeded!\n\n"); - sock->disconnect(); + if(disconnectOnLimit) { + sock->write("\nMaximum number of connections has been exceeded!\n\n"); + sock->disconnect(); + } return true; } } @@ -2420,3 +2443,86 @@ void Server::stop() { if(httpServer) httpServer->stop(); } + +//********************************************************************* +// Account Management Methods +//********************************************************************* + +std::shared_ptr Server::getOrLoadAccount(const std::string& accountName) { + auto it = accountCache.find(accountName); + if(it != accountCache.end()) { + return it->second; // Return existing shared instance + } + + // Load from disk + std::shared_ptr account; + if(Account::load(accountName, account)) { + accountCache[accountName] = account; + return account; + } + + return nullptr; +} + +void Server::trackAccountConnection(const std::string& accountName, const std::string& characterName) { + accountConnections[accountName].insert(characterName); +} + +void Server::untrackAccountConnection(const std::string& accountName, const std::string& characterName) { + auto it = accountConnections.find(accountName); + if(it != accountConnections.end()) { + it->second.erase(characterName); + if(it->second.empty()) { + // No more connections, can remove from cache + accountCache.erase(accountName); + accountConnections.erase(it); + } + } +} + +std::vector Server::getAccountCharacters(const std::string& accountName) const { + auto it = accountConnections.find(accountName); + if(it != accountConnections.end()) { + return std::vector(it->second.begin(), it->second.end()); + } + return {}; +} + +void Server::releaseAccount(const std::string& accountName, const std::string& characterName) { + // If a specific character is provided, untrack it first + if(!characterName.empty()) { + untrackAccountConnection(accountName, characterName); + return; + } + // No character provided: if there are no active character connections + // for this account, evict the account from cache. + auto it = accountConnections.find(accountName); + if(it == accountConnections.end() || it->second.empty()) { + accountCache.erase(accountName); + if(it != accountConnections.end()) { + accountConnections.erase(it); + } + } +} + +void Server::saveAllCachedAccounts() { + // Only save accounts that have at least one active character connection. + // Also prune any accounts that linger in cache without connections. + std::vector toErase; + for(const auto& [accountName, account] : accountCache) { + auto it = accountConnections.find(accountName); + bool hasConnections = (it != accountConnections.end() && !it->second.empty()); + if(!hasConnections) { + toErase.push_back(accountName); + continue; + } + if(account) { + account->save(); + } + } + // Erase after iterating to avoid invalidating iterators + for(const auto& name : toErase) { + accountCache.erase(name); + accountConnections.erase(name); + } +} diff --git a/server/update.cpp b/server/update.cpp index 48f68451..797a6de0 100644 --- a/server/update.cpp +++ b/server/update.cpp @@ -135,6 +135,12 @@ void Server::updateGame() { if(t > gConfig->getLotteryRunTime()) gConfig->runLottery(); + // Save all cached accounts every 10 seconds + if(t - gServer->lastAccountSave >= 10) { + gServer->saveAllCachedAccounts(); + gServer->lastAccountSave = t; + } + if(Shutdown.ltime && t - last_shutdown_update >= 30) if(Shutdown.ltime + Shutdown.interval <= t+500) update_shutdown(t); diff --git a/staff/dmcrt.cpp b/staff/dmcrt.cpp index d1490d00..a7e27e49 100644 --- a/staff/dmcrt.cpp +++ b/staff/dmcrt.cpp @@ -206,6 +206,13 @@ std::string Creature::statCrt(int statFlags) { crtStr << " size: " << sock->getTermCols() << " x " << sock->getTermRows() << "\n"; crtStr << "Host: " << sock->getHostname() << " Ip: " << sock->getIp() << "\n"; + // Show the owning account name if this player is linked to an account + if(!pTarget->getAccountName().empty()) { + crtStr << "Account: " << pTarget->getAccountName() << "\n"; + } else { + crtStr << "Account: N/A\n"; + } + if(statFlags & ISDM) { crtStr << "Password: " << pTarget->getPassword(); if(!pTarget->getLastPassword().empty()) diff --git a/staff/dmply.cpp b/staff/dmply.cpp index 4261d5ab..74506b1d 100644 --- a/staff/dmply.cpp +++ b/staff/dmply.cpp @@ -64,6 +64,7 @@ #include "unique.hpp" // for addOwner, deleteOwner #include "web.hpp" // for callWebserver #include "xml.hpp" // for loadPlayer, loadRoom +#include "account.hpp" // for Account #include "toNum.hpp" class UniqueRoom; @@ -1596,6 +1597,162 @@ int dmBugPlayer(const std::shared_ptr& player, cmd* cmnd) { return(dmGeneric(player, cmnd, "*bug", DM_GEN_BUG)); } +//********************************************************************* +// dmAccount helpers +//********************************************************************* + +static int dmAccountAddPlayer(const std::shared_ptr& invoker, const std::string& accountName, const std::string& targetName) { + std::shared_ptr account = gServer->getOrLoadAccount(accountName); + if(!account) { + invoker->print("Account '%s' does not exist.\n", accountName.c_str()); + return(0); + } + + std::shared_ptr target = gServer->findPlayer(targetName); + bool online = true; + if(!target) { + online = false; + if(!loadPlayer(targetName, target)) { + invoker->print("Player '%s' does not exist.\n", targetName.c_str()); + return(0); + } + } + + if(!target->getAccountName().empty() && target->getAccountName() != account->getName()) { + invoker->print("Player '%s' already belongs to account '%s'.\n", target->getCName(), target->getAccountName().c_str()); + return(0); + } + + if(account->hasCharacter(target->getName())) { + invoker->print("Account '%s' already contains '%s'.\n", account->getName().c_str(), target->getCName()); + return(0); + } + + target->setAccountName(account->getName()); + if(account->addCharacter(target->getName())) + account->save(); + target->save(online); + invoker->print("Added '%s' to account '%s'.\n", target->getCName(), account->getName().c_str()); + return(0); +} + +static int dmAccountRemovePlayer(const std::shared_ptr& invoker, const std::string& accountName, const std::string& targetName) { + std::shared_ptr account = gServer->getOrLoadAccount(accountName); + if(!account) { + invoker->print("Account '%s' does not exist.\n", accountName.c_str()); + return(0); + } + + std::shared_ptr target = gServer->findPlayer(targetName); + bool online = true; + if(!target) { + online = false; + if(!loadPlayer(targetName, target)) { + invoker->print("Player '%s' does not exist.\n", targetName.c_str()); + return(0); + } + } + + if(!account->hasCharacter(target->getName())) { + invoker->print("Account '%s' does not contain '%s'.\n", account->getName().c_str(), target->getCName()); + return(0); + } + if(account->removeCharacter(target->getName())) + account->save(); + target->setAccountName(""); + target->save(online); + if(online) { + auto sock = target->getSock(); + if(sock && sock->hasAccount()) + sock->clearAccount(); + } + invoker->print("Removed '%s' from account '%s'.\n", target->getCName(), account->getName().c_str()); + return(0); +} + +static int dmAccountInfo(const std::shared_ptr& invoker, const std::string& accountName) { + std::shared_ptr account = gServer->getOrLoadAccount(accountName); + if(!account) { + invoker->print("Account '%s' does not exist.\n", accountName.c_str()); + return(0); + } + invoker->print("\n^W~~~~~~~ Account Information ~~~~~~~^x\n\n"); + account->printInfoFields(invoker); + return(0); +} + +static int dmAccountCharacters(const std::shared_ptr& invoker, const std::string& accountName) { + std::shared_ptr account = gServer->getOrLoadAccount(accountName); + if(!account) { + invoker->print("Account '%s' does not exist.\n", accountName.c_str()); + return(0); + } + invoker->print("\n^W~~~~~~~ Account Characters ~~~~~~~^x\n\n"); + invoker->print("^WTotal:^x %d/%d\n", account->getCharacterCount(), account->getCharacterLimit()); + account->printCharacterList(invoker); + return(0); +} + +//********************************************************************* +// dmAccount (dispatcher) +//********************************************************************* +// *account + +int dmAccount(const std::shared_ptr& player, cmd* cmnd) { + if(!player->isDm()) { + return(cmdNoAuth(player)); + } + + const char* syntax = "\nSyntax:\n" + " *account info\n" + " *account characters\n" + " *account characters add \n" + " *account characters remove \n"; + + if(cmnd->num < 3) { + player->print("%s", syntax); + return(0); + } + + std::string accountName = cmnd->str[1]; + std::string action = cmnd->str[2]; + lowercize(action, 0); + lowercize(accountName, 1); + + // Partial matching for subcommands + if(partialMatch(action, "info", 4)) { + return dmAccountInfo(player, accountName); + } + if(partialMatch(action, "characters", 10)) { + // Bare "characters" -> just list characters + if(cmnd->num == 3) { + return dmAccountCharacters(player, accountName); + } + + // Expect: *account characters + if(cmnd->num < 5) { + player->print("%s", syntax); + return(0); + } + + std::string subAction = cmnd->str[3]; + std::string targetName = cmnd->str[4]; + lowercize(subAction, 0); + lowercize(targetName, 1); + + if(partialMatch(subAction, "add", 3)) + return dmAccountAddPlayer(player, accountName, targetName); + if(partialMatch(subAction, "remove", 6)) + return dmAccountRemovePlayer(player, accountName, targetName); + + player->print("Unknown subcommand '%s'.%s", subAction.c_str(), syntax); + return(0); + } + + player->print("Unknown subcommand '%s'.%s", action.c_str(), syntax); + return(0); +} + // dmKill function decides which of these are player-kill, diff --git a/util/misc.cpp b/util/misc.cpp index 97b0c84e..90afb21e 100644 --- a/util/misc.cpp +++ b/util/misc.cpp @@ -66,6 +66,11 @@ class MudObject; bool isClass(std::string_view str); bool isTitle(std::string_view str); +bool partialMatch(const std::string& got, const char* full, size_t maxLen) { + size_t n = std::min(got.size(), maxLen); + if(n < 1) return false; + return ::strncasecmp(got.c_str(), full, n) == 0; +} //********************************************************************* // validId functions diff --git a/xml/players-xml.cpp b/xml/players-xml.cpp index fcf8bc14..3ff0b88f 100644 --- a/xml/players-xml.cpp +++ b/xml/players-xml.cpp @@ -120,6 +120,7 @@ void Player::readXml(xmlNodePtr curNode, bool offline) { else if(NODE_NAME(curNode, "Statistics")) statistics.load(curNode); else if(NODE_NAME(curNode, "Bank")) bank.load(curNode); + else if(NODE_NAME(curNode, "AccountName")) xml::copyToString(accountName, curNode); else if(NODE_NAME(curNode, "Surname")) xml::copyToString(surname, curNode); else if(NODE_NAME(curNode, "Wrap")) xml::copyToNum(wrap, curNode); else if(NODE_NAME(curNode, "Forum")) xml::copyToString(forum, curNode); @@ -400,6 +401,7 @@ void Player::saveXml(xmlNodePtr curNode) const { bank.save("Bank", curNode); xml::saveNonZeroNum(curNode, "WeaponTrains", weaponTrains); + xml::saveNonNullString(curNode, "AccountName", accountName); xml::saveNonNullString(curNode, "Surname", surname); xml::saveNonNullString(curNode, "Forum", forum); xml::newNumChild(curNode, "Wrap", wrap);