#include "char.hpp" // char.cpp - Character server. // // Copyright © ????-2004 Athena Dev Teams // Copyright © 2004-2011 The Mana World Development Team // Copyright © 2011-2014 Ben Longbons // Copyright © 2013-2014 MadCamel // // This file is part of The Mana World (Athena server) // // 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 3 of the License, or // (at your option) 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 #include #include #include #include #include #include #include #include #include #include #include "../ints/cmp.hpp" #include "../ints/udl.hpp" #include "../strings/mstring.hpp" #include "../strings/astring.hpp" #include "../strings/zstring.hpp" #include "../strings/xstring.hpp" #include "../generic/array.hpp" #include "../io/cxxstdio.hpp" #include "../io/lock.hpp" #include "../io/read.hpp" #include "../io/tty.hpp" #include "../io/write.hpp" #include "../net/packets.hpp" #include "../net/socket.hpp" #include "../net/timer.hpp" #include "../proto2/any-user.hpp" #include "../proto2/login-admin.hpp" #include "../proto2/login-char.hpp" #include "../proto2/char-map.hpp" #include "../proto2/char-user.hpp" #include "../mmo/config_parse.hpp" #include "../mmo/core.hpp" #include "../mmo/extract.hpp" #include "../mmo/human_time_diff.hpp" #include "../mmo/mmo.hpp" #include "../mmo/utils.hpp" #include "../mmo/version.hpp" #include "inter.hpp" #include "int_party.hpp" #include "int_storage.hpp" #include "../poison.hpp" namespace tmwa { static Array server; static Array server_session; static Array server_freezeflag; // Map-server anti-freeze system. Counter. 5 ok, 4...0 freezed static int anti_freeze_enable = 0; static std::chrono::seconds anti_freeze_interval = std::chrono::seconds(6); constexpr std::chrono::milliseconds DEFAULT_AUTOSAVE_INTERVAL = std::chrono::minutes(5); static Session *login_session, *char_session; static AccountName userid; static AccountPass passwd; static ServerName server_name; static CharName wisp_server_name = stringish("Server"_s); static IP4Address login_ip; static int login_port = 6900; static IP4Address char_ip; static int char_port = 6121; static AString char_txt; static CharName unknown_char_name = stringish("Unknown"_s); static AString char_log_filename = "log/char.log"_s; //Added for lan support static IP4Address lan_map_ip = IP4_LOCALHOST; static IP4Mask lan_subnet = IP4Mask(IP4_LOCALHOST, IP4_BROADCAST); static int char_name_option = 0; // Option to know which letters/symbols are authorised in the name of a character (0: all, 1: only those in char_name_letters, 2: all EXCEPT those in char_name_letters) by [Yor] static std::bitset<256> char_name_letters; // list of letters/symbols authorised (or not) in a character name. by [Yor] static constexpr GmLevel default_gm_level = GmLevel::from(0_u32); struct char_session_data : SessionData { AccountId account_id; int login_id1, login_id2; SEX sex; unsigned short packet_tmw_version; AccountEmail email; TimeT connect_until_time; }; void SessionDeleter::operator()(SessionData *sd) { really_delete1 static_cast(sd); } struct AuthFifoEntry { AccountId account_id; CharId char_id; int login_id1, login_id2; IP4Address ip; int delflag; SEX sex; unsigned short packet_tmw_version; TimeT connect_until_time; // # of seconds 1/1/1970 (timestamp): Validity limit of the account (0 = unlimited) }; static std::array auth_fifo; static auto auth_fifo_iter = auth_fifo.begin(); static int check_ip_flag = 1; // It's to check IP of a player between char-server and other servers (part of anti-hacking system) static CharId char_id_count = wrap(150000); static std::vector char_keys; static int max_connect_user = 0; static std::chrono::milliseconds autosave_time = DEFAULT_AUTOSAVE_INTERVAL; // Initial position (it's possible to set it in conf file) static Point start_point = { {"001-1.gat"_s}, 273, 354 }; static std::vector gm_accounts; // online players by [Yor] static AString online_txt_filename = "online.txt"_s; static AString online_html_filename = "online.html"_s; static int online_sorting_option = 0; // sorting option to display online players in online files static int online_refresh_html = 20; // refresh time (in sec) of the html file in the explorer static GmLevel online_gm_display_min_level = GmLevel::from(20_u32); // minimum GM level to display 'GM' when we want to display it static std::vector online_chars; // same size of char_keys, and id value of current server (or -1) static TimeT update_online; // to update online files when we receiving information from a server (not less than 8 seconds) static pid_t pid = 0; // For forked DB writes auto iter_map_sessions() -> decltype(filter_iterator(std::declval *>())) { return filter_iterator(&server_session); } static void create_online_files(void); static void delete_tologin(Session *sess) { assert (sess == login_session); PRINTF("Char-server can't connect to login-server (connection #%d).\n"_fmt, sess); login_session = nullptr; } static void delete_char(Session *sess) { (void)sess; } static void delete_frommap(Session *sess) { auto it = std::find(server_session.begin(), server_session.end(), sess); assert (it != server_session.end()); int id = it - server_session.begin(); PRINTF("Map-server %d (session #%d) has disconnected.\n"_fmt, id, sess); server[id] = mmo_map_server{}; server_session[id] = nullptr; for (Session *& oci : online_chars) if (oci == sess) oci = nullptr; create_online_files(); // update online players files (to remove all online players of this server) } //------------------------------ // Writing function of logs file //------------------------------ void char_log(XString line) { io::AppendFile logfp(char_log_filename, true); if (!logfp.is_open()) return; log_with_timestamp(logfp, line); } //---------------------------------------------------------------------- // Determine if an account (id) is a GM account // and returns its level (or 0 if it isn't a GM account or if not found) //---------------------------------------------------------------------- static GmLevel isGM(AccountId account_id) { for (GM_Account& gma : gm_accounts) if (gma.account_id == account_id) return gma.level; return default_gm_level; } //---------------------------------------------- // Search an character id // (return character pointer or nullptr (if not found)) // If exact character name is not found, // the function checks without case sensitive // and returns index if only 1 character is found // and similar to the searched name. //---------------------------------------------- const CharPair *search_character(CharName character_name) { for (const CharPair& cd : char_keys) { { // Strict comparison (if found, we finish the function immediatly with correct value) if (cd.key.name == character_name) return &cd; } } // Exact character name is not found and 0 or more than 1 similar characters have been found ==> we say not found return nullptr; } const CharPair *search_character_id(CharId char_id) { for (const CharPair& cd : char_keys) { if (cd.key.char_id == char_id) return &cd; } return nullptr; } // TODO make these DIE already Session *server_for(const CharPair *mcs) { if (!mcs) return nullptr; return online_chars[mcs - &char_keys.front()]; } static Session *& server_for_m(const CharPair *mcs) { return online_chars[mcs - &char_keys.front()]; } //------------------------------------------------- // Function to create the character line (for save) //------------------------------------------------- static AString mmo_char_tostr(struct CharPair *cp) { CharKey *k = &cp->key; CharData *p = cp->data.get(); // on multi-map server, sometimes it's posssible that last_point become void. (reason???) We check that to not lost character at restart. if (!p->last_point.map_) { p->last_point = start_point; } MString str_p; str_p += STRPRINTF( "%d\t" "%d,%d\t" "%s\t" "%d,%d,%d\t" "%d,%d,%d\t" "%d,%d,%d,%d\t" "%d,%d,%d,%d,%d,%d\t" "%d,%d\t" "%d,%d,%d\t" "%d,%d,%d\t" "%d,%d,%d\t" "%d,%d,%d,%d,%d\t" "%s,%d,%d\t" "%s,%d,%d,%d\t"_fmt, k->char_id, k->account_id, k->char_num, k->name, p->species, p->base_level, p->job_level, p->base_exp, p->job_exp, p->zeny, p->hp, p->max_hp, p->sp, p->max_sp, p->attrs[ATTR::STR], p->attrs[ATTR::AGI], p->attrs[ATTR::VIT], p->attrs[ATTR::INT], p->attrs[ATTR::DEX], p->attrs[ATTR::LUK], p->status_point, p->skill_point, p->option, p->karma, p->manner, p->party_id, 0/*guild_id*/, 0/*pet_id*/, p->hair, p->hair_color, p->clothes_color, p->weapon, p->shield, p->head_top, p->head_mid, p->head_bottom, p->last_point.map_, p->last_point.x, p->last_point.y, p->save_point.map_, p->save_point.x, p->save_point.y, p->partner_id); // memos were here (no longer supported) str_p += '\t'; for (IOff0 i : IOff0::iter()) { if (p->inventory[i].nameid) { str_p += STRPRINTF("%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d "_fmt, 0 /*id*/, p->inventory[i].nameid, p->inventory[i].amount, p->inventory[i].equip, 1 /*identify*/, 0 /*refine*/, 0 /*attribute*/, 0 /*card[0]*/, 0 /*card[1]*/, 0 /*card[2]*/, 0 /*card[3]*/, 0 /*broken*/); } } str_p += '\t'; // cart was here (no longer supported) str_p += '\t'; for (SkillID i : erange(SkillID(), MAX_SKILL)) { if (p->skill[i].lv) { str_p += STRPRINTF("%d,%d "_fmt, i, p->skill[i].lv | (static_cast(p->skill[i].flags) << 16)); } } str_p += '\t'; assert (p->global_reg_num < GLOBAL_REG_NUM); for (int i = 0; i < p->global_reg_num; i++) { if (p->global_reg[i].str) { str_p += STRPRINTF("%s,%d "_fmt, p->global_reg[i].str, p->global_reg[i].value); } } str_p += '\t'; return AString(str_p); } static bool extract(XString str, Point *p) { return extract(str, record<','>(&p->map_, &p->x, &p->y)); } struct skill_loader { SkillID id; uint16_t level; SkillFlags flags; }; static bool extract(XString str, struct skill_loader *s) { uint32_t flags_and_level; if (!extract(str, record<','>(&s->id, &flags_and_level))) return false; s->level = flags_and_level & 0xffff; s->flags = SkillFlags(flags_and_level >> 16); return true; } //------------------------------------------------------------------------- // Function to set the character from the line (at read of characters file) //------------------------------------------------------------------------- static bool extract(XString str, CharPair *cp) { CharKey *k = &cp->key; CharData *p = cp->data.get(); uint32_t unused_guild_id, unused_pet_id; XString unused_memos; std::vector inventory; XString unused_cart; std::vector skills; std::vector vars; if (!extract(str, record<'\t'>( &k->char_id, record<','>(&k->account_id, &k->char_num), &k->name, record<','>(&p->species, &p->base_level, &p->job_level), record<','>(&p->base_exp, &p->job_exp, &p->zeny), record<','>(&p->hp, &p->max_hp, &p->sp, &p->max_sp), record<','>(&p->attrs[ATTR::STR], &p->attrs[ATTR::AGI], &p->attrs[ATTR::VIT], &p->attrs[ATTR::INT], &p->attrs[ATTR::DEX], &p->attrs[ATTR::LUK]), record<','>(&p->status_point, &p->skill_point), record<','>(&p->option, &p->karma, &p->manner), record<','>(&p->party_id, &unused_guild_id, &unused_pet_id), record<','>(&p->hair, &p->hair_color, &p->clothes_color), record<','>(&p->weapon, &p->shield, &p->head_top, &p->head_mid, &p->head_bottom), &p->last_point, // somebody was silly and stuck partner id as a field // of this, instead of adding a new \t // or putting it elsewhere, like by pet/guild record<','>(&p->save_point.map_, &p->save_point.x, &p->save_point.y, &p->partner_id), &unused_memos, vrec<' '>(&inventory), &unused_cart, vrec<' '>(&skills), vrec<' '>(&vars)))) return false; if (wisp_server_name == k->name) return false; // TODO replace *every* lookup with a map lookup static std::set seen_ids; static std::set seen_names; // we don't have to worry about deleted characters, // this is only called during startup auto _seen_id = seen_ids.insert(k->char_id); if (!_seen_id.second) return false; auto _seen_name = seen_names.insert(k->name); if (!_seen_name.second) { seen_ids.erase(_seen_id.first); return false; } // memos were here - no longer supported if (inventory.size() > MAX_INVENTORY) return false; std::copy(inventory.begin(), inventory.end(), p->inventory.begin()); // number of inventory items is not saved - it just detects nameid 0 // cart was here - no longer supported for (struct skill_loader& sk : skills) { if (sk.id >= MAX_SKILL) return false; p->skill[sk.id].lv = sk.level; p->skill[sk.id].flags = sk.flags; } if (vars.size() > GLOBAL_REG_NUM) return false; std::copy(vars.begin(), vars.end(), p->global_reg.begin()); p->global_reg_num = vars.size(); return true; } //--------------------------------- // Function to read characters file //--------------------------------- static int mmo_char_init(void) { char_keys.clear(); online_chars.clear(); io::ReadFile in(char_txt); if (!in.is_open()) { PRINTF("Characters file not found: %s.\n"_fmt, char_txt); CHAR_LOG("Characters file not found: %s.\n"_fmt, char_txt); CHAR_LOG("Id for the next created character: %d.\n"_fmt, char_id_count); return 0; } int line_count = 0; AString line; while (in.getline(line)) { line_count++; if (is_comment(line)) continue; { CharId i; if (extract(line, record<'\t'>(&i, "%newid%"_s))) { if (char_id_count < i) char_id_count = i; continue; } } CharPair cd; if (!extract(line, &cd)) { CHAR_LOG("Char skipped\n%s"_fmt, line); continue; } if (char_id_count < next(cd.key.char_id)) char_id_count = next(cd.key.char_id); char_keys.push_back(std::move(cd)); online_chars.push_back(nullptr); } PRINTF("mmo_char_init: %zu characters read in %s.\n"_fmt, char_keys.size(), char_txt); CHAR_LOG("mmo_char_init: %zu characters read in %s.\n"_fmt, char_keys.size(), char_txt); CHAR_LOG("Id for the next created character: %d.\n"_fmt, char_id_count); return 0; } //--------------------------------------------------------- // Function to save characters in files (speed up by [Yor]) //--------------------------------------------------------- static void mmo_char_sync(void) { io::WriteLock fp(char_txt); if (!fp.is_open()) { PRINTF("WARNING: Server can't not save characters.\n"_fmt); CHAR_LOG("WARNING: Server can't not save characters.\n"_fmt); return; } { // yes, we need a mutable reference to do the saves ... for (CharPair& cd : char_keys) { AString line = mmo_char_tostr(&cd); fp.put_line(line); } FPRINTF(fp, "%d\t%%newid%%\n"_fmt, char_id_count); } } //---------------------------------------------------- // Function to save (in a periodic way) datas in files //---------------------------------------------------- static void mmo_char_sync_timer(TimerData *, tick_t) { if (pid != 0) { int status; pid_t temp = waitpid(pid, &status, WNOHANG); // Need to check status too? if (temp == 0) { return; } } // This can take a lot of time. Fork a child to handle the work and return at once // If we're unable to fork just continue running the function normally if ((pid = fork()) > 0) { return; } // If we're a child, run as a lower priority process if (pid == 0) setpriority(PRIO_PROCESS, getpid(), 10); mmo_char_sync(); inter_save(); // If we're a child we should suicide now. if (pid == 0) _exit(0); } //----------------------------------- // Function to create a new character //----------------------------------- static CharPair *make_new_char(Session *s, CharName name, const Stats6& stats, uint8_t slot, uint16_t hair_color, uint16_t hair_style) { // ugh char_session_data *sd = static_cast(s->session_data.get()); // remove control characters from the name if (!name.to__actual().is_print()) { CHAR_LOG("Make new char error (control char received in the name): (connection #%d, account: %d).\n"_fmt, s, sd->account_id); return nullptr; } // Eliminate whitespace if (name.to__actual() != name.to__actual().strip()) { CHAR_LOG("Make new char error (leading/trailing whitespace): (connection #%d, account: %d, name: '%s'.\n"_fmt, s, sd->account_id, name); return nullptr; } // check lenght of character name if (name.to__actual().size() < 4) { CHAR_LOG("Make new char error (character name too small): (connection #%d, account: %d, name: '%s').\n"_fmt, s, sd->account_id, name); return nullptr; } // Check Authorised letters/symbols in the name of the character if (char_name_option == 1) { // only letters/symbols in char_name_letters are authorised for (uint8_t c : name.to__actual()) if (!char_name_letters[c]) { CHAR_LOG("Make new char error (invalid letter in the name): (connection #%d, account: %d), name: %s, invalid letter: %c.\n"_fmt, s, sd->account_id, name, c); return nullptr; } } else if (char_name_option == 2) { // letters/symbols in char_name_letters are forbidden for (uint8_t c : name.to__actual()) if (char_name_letters[c]) { CHAR_LOG("Make new char error (invalid letter in the name): (connection #%d, account: %d), name: %s, invalid letter: %c.\n"_fmt, s, sd->account_id, name, c); return nullptr; } } // else, all letters/symbols are authorised (except control char removed before) // TODO this comment is obsolete // this is why it needs to be unsigned if (stats.str + stats.agi + stats.vit + stats.int_ + stats.dex + stats.luk != 5 * 6 || // stats slot >= 9 || hair_style >= 20 || hair_color >= 12) { CHAR_LOG("Make new char error (invalid values): (connection #%d, account: %d) slot %d, name: %s, stats: %d+%d+%d+%d+%d+%d=%d, hair: %d, hair color: %d\n"_fmt, s, sd->account_id, slot, name, stats.str, stats.agi, stats.vit, stats.int_, stats.dex, stats.luk, stats.str + stats.agi + stats.vit + stats.int_ + stats.dex + stats.luk, hair_style, hair_color); return nullptr; } // check individual stat value for (int i = 0; i < 6; i++) { uint8_t statsi = reinterpret_cast(&stats)[i]; if (statsi < 1 || statsi > 9) { CHAR_LOG("Make new char error (invalid stat value: not between 1 to 9): (connection #%d, account: %d) slot %d, name: %s, stats: %d+%d+%d+%d+%d+%d=%d, hair: %d, hair color: %d\n"_fmt, s, sd->account_id, slot, name, stats.str, stats.agi, stats.vit, stats.int_, stats.dex, stats.luk, stats.str + stats.agi + stats.vit + stats.int_ + stats.dex + stats.luk, hair_style, hair_color); return nullptr; } } for (const CharPair& cd : char_keys) { if (cd.key.name == name) { CHAR_LOG("Make new char error (name already exists): (connection #%d, account: %d) slot %d, name: %s (actual name of other char: %s), stats: %d+%d+%d+%d+%d+%d=%d, hair: %d, hair color: %d.\n"_fmt, s, sd->account_id, slot, name, cd.key.name, stats.str, stats.agi, stats.vit, stats.int_, stats.dex, stats.luk, stats.str + stats.agi + stats.vit + stats.int_ + stats.dex + stats.luk, hair_style, hair_color); return nullptr; } if (cd.key.account_id == sd->account_id && cd.key.char_num == slot) { CHAR_LOG("Make new char error (slot already used): (connection #%d, account: %d) slot %d, name: %s (actual name of other char: %s), stats: %d+%d+%d+%d+%d+%d=%d, hair: %d, hair color: %d.\n"_fmt, s, sd->account_id, slot, name, cd.key.name, stats.str, stats.agi, stats.vit, stats.int_, stats.dex, stats.luk, stats.str + stats.agi + stats.vit + stats.int_ + stats.dex + stats.luk, hair_style, hair_color); return nullptr; } } if (wisp_server_name == name) { CHAR_LOG("Make new char error (name used is wisp name for server): (connection #%d, account: %d) slot %d, name: %s (actual name whisper server: %s), stats: %d+%d+%d+%d+%d+%d=%d, hair: %d, hair color: %d.\n"_fmt, s, sd->account_id, slot, name, wisp_server_name, stats.str, stats.agi, stats.vit, stats.int_, stats.dex, stats.luk, stats.str + stats.agi + stats.vit + stats.int_ + stats.dex + stats.luk, hair_style, hair_color); return nullptr; } IP4Address ip = s->client_ip; CHAR_LOG("Creation of New Character: (connection #%d, account: %d) slot %d, character Name: %s, stats: %d+%d+%d+%d+%d+%d=%d, hair: %d, hair color: %d. [%s]\n"_fmt, s, sd->account_id, slot, name, stats.str, stats.agi, stats.vit, stats.int_, stats.dex, stats.luk, stats.str + stats.agi + stats.vit + stats.int_ + stats.dex + stats.luk, hair_style, hair_color, ip); CharPair cp; CharKey& ck = cp.key; CharData& cd = *cp.data; ck.char_id = char_id_count; char_id_count = next(char_id_count); ck.account_id = sd->account_id; ck.char_num = slot; ck.name = name; cd.species = Species(); cd.base_level = 1; cd.job_level = 1; cd.base_exp = 0; cd.job_exp = 0; cd.zeny = 0; cd.attrs[ATTR::STR] = stats.str; cd.attrs[ATTR::AGI] = stats.agi; cd.attrs[ATTR::VIT] = stats.vit; cd.attrs[ATTR::INT] = stats.int_; cd.attrs[ATTR::DEX] = stats.dex; cd.attrs[ATTR::LUK] = stats.luk; cd.max_hp = 40 * (100 + cd.attrs[ATTR::VIT]) / 100; cd.max_sp = 11 * (100 + cd.attrs[ATTR::INT]) / 100; cd.hp = cd.max_hp; cd.sp = cd.max_sp; cd.status_point = 0; cd.skill_point = 0; cd.option = static_cast