| @@ -0,0 +1,1216 @@ | |||
| 1 | + | /** | |
| 2 | + | * The Forgotten Server - a free and open-source MMORPG server emulator | |
| 3 | + | * Copyright (C) 2019 Mark Samman <mark.samman@gmail.com> | |
| 4 | + | * | |
| 5 | + | * This program is free software; you can redistribute it and/or modify | |
| 6 | + | * it under the terms of the GNU General Public License as published by | |
| 7 | + | * the Free Software Foundation; either version 2 of the License, or | |
| 8 | + | * (at your option) any later version. | |
| 9 | + | * | |
| 10 | + | * This program is distributed in the hope that it will be useful, | |
| 11 | + | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 12 | + | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 13 | + | * GNU General Public License for more details. | |
| 14 | + | * | |
| 15 | + | * You should have received a copy of the GNU General Public License along | |
| 16 | + | * with this program; if not, write to the Free Software Foundation, Inc., | |
| 17 | + | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| 18 | + | */ | |
| 19 | + | ||
| 20 | + | #include "otpch.h" | |
| 21 | + | ||
| 22 | + | #include "configmanager.h" | |
| 23 | + | #include "game.h" | |
| 24 | + | #include "iologindata.h" | |
| 25 | + | ||
| 26 | + | extern ConfigManager g_config; | |
| 27 | + | extern Game g_game; | |
| 28 | + | ||
| 29 | + | Account IOLoginData::loadAccount(uint32_t accno) { | |
| 30 | + | Account account; | |
| 31 | + | ||
| 32 | + | std::ostringstream query; | |
| 33 | + | query << "SELECT `id`, `name`, `password`, `type`, `premdays`, `lastday` " | |
| 34 | + | "FROM `accounts` WHERE `id` = " | |
| 35 | + | << accno; | |
| 36 | + | DBResult_ptr result = Database::getInstance().storeQuery(query.str()); | |
| 37 | + | if (!result) { | |
| 38 | + | return account; | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | account.id = result->getNumber<uint32_t>("id"); | |
| 42 | + | account.name = result->getString("name"); | |
| 43 | + | account.accountType = | |
| 44 | + | static_cast<AccountType_t>(result->getNumber<int32_t>("type")); | |
| 45 | + | account.premiumDays = result->getNumber<uint16_t>("premdays"); | |
| 46 | + | account.lastDay = result->getNumber<time_t>("lastday"); | |
| 47 | + | return account; | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | bool IOLoginData::saveAccount(const Account &acc) { | |
| 51 | + | std::ostringstream query; | |
| 52 | + | query << "UPDATE `accounts` SET `premdays` = " << acc.premiumDays | |
| 53 | + | << ", `lastday` = " << acc.lastDay << " WHERE `id` = " << acc.id; | |
| 54 | + | return Database::getInstance().executeQuery(query.str()); | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | std::string decodeSecret(const std::string &secret) { | |
| 58 | + | // simple base32 decoding | |
| 59 | + | std::string key; | |
| 60 | + | key.reserve(10); | |
| 61 | + | ||
| 62 | + | uint32_t buffer = 0, left = 0; | |
| 63 | + | for (const auto &ch : secret) { | |
| 64 | + | buffer <<= 5; | |
| 65 | + | if (ch >= 'A' && ch <= 'Z') { | |
| 66 | + | buffer |= (ch & 0x1F) - 1; | |
| 67 | + | } else if (ch >= '2' && ch <= '7') { | |
| 68 | + | buffer |= ch - 24; | |
| 69 | + | } else { | |
| 70 | + | // if a key is broken, return empty and the comparison | |
| 71 | + | // will always be false since the token must not be empty | |
| 72 | + | return {}; | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | left += 5; | |
| 76 | + | if (left >= 8) { | |
| 77 | + | left -= 8; | |
| 78 | + | key.push_back(static_cast<char>(buffer >> left)); | |
| 79 | + | } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | return key; | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | bool IOLoginData::loginserverAuthentication(const std::string &name, | |
| 86 | + | const std::string &password, | |
| 87 | + | Account &account) { | |
| 88 | + | Database &db = Database::getInstance(); | |
| 89 | + | ||
| 90 | + | std::ostringstream query; | |
| 91 | + | query << "SELECT `id`, `name`, `password`, `secret`, `type`, `premdays`, " | |
| 92 | + | "`lastday` FROM `accounts` WHERE `name` = " | |
| 93 | + | << db.escapeString(name); | |
| 94 | + | DBResult_ptr result = db.storeQuery(query.str()); | |
| 95 | + | if (!result) { | |
| 96 | + | return false; | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | if (transformToSHA1(password) != result->getString("password")) { | |
| 100 | + | return false; | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | account.id = result->getNumber<uint32_t>("id"); | |
| 104 | + | account.name = result->getString("name"); | |
| 105 | + | account.key = decodeSecret(result->getString("secret")); | |
| 106 | + | account.accountType = | |
| 107 | + | static_cast<AccountType_t>(result->getNumber<int32_t>("type")); | |
| 108 | + | account.premiumDays = result->getNumber<uint16_t>("premdays"); | |
| 109 | + | account.lastDay = result->getNumber<time_t>("lastday"); | |
| 110 | + | ||
| 111 | + | query.str(std::string()); | |
| 112 | + | query << "SELECT `name`, `level`, `vocation`, " | |
| 113 | + | << "`lookwings`, `lookaura`, `lookshader`, `lookruneeffect`, `lookbody`, `lookfeet`, " | |
| 114 | + | << "`lookhead`, `looklegs`, `looktype`, `lookaddons` " | |
| 115 | + | << "FROM `players` WHERE `account_id` = " << account.id | |
| 116 | + | << " AND `deletion` = 0 " | |
| 117 | + | << "ORDER BY `name` ASC"; | |
| 118 | + | result = db.storeQuery(query.str()); | |
| 119 | + | if (result) { | |
| 120 | + | do { | |
| 121 | + | Character character; | |
| 122 | + | ||
| 123 | + | // Fill the character object with retrieved data | |
| 124 | + | character.name = result->getString("name"); | |
| 125 | + | character.level = result->getNumber<uint32_t>("level"); | |
| 126 | + | character.vocation = result->getNumber<uint16_t>("vocation"); | |
| 127 | + | ||
| 128 | + | // Outfit details | |
| 129 | + | character.outfit.lookType = result->getNumber<uint16_t>("looktype"); | |
| 130 | + | character.outfit.lookHead = result->getNumber<uint16_t>("lookhead"); | |
| 131 | + | character.outfit.lookBody = result->getNumber<uint16_t>("lookbody"); | |
| 132 | + | character.outfit.lookLegs = result->getNumber<uint16_t>("looklegs"); | |
| 133 | + | character.outfit.lookFeet = result->getNumber<uint16_t>("lookfeet"); | |
| 134 | + | character.outfit.lookAddons = result->getNumber<uint16_t>("lookaddons"); | |
| 135 | + | character.outfit.lookWings = result->getNumber<uint16_t>("lookwings"); | |
| 136 | + | character.outfit.lookAura = result->getNumber<uint16_t>("lookaura"); | |
| 137 | + | character.outfit.lookShader = result->getNumber<uint16_t>("lookshader"); | |
| 138 | + | character.outfit.lookRuneEffect = result->getNumber<uint16_t>("lookruneeffect"); | |
| 139 | + | ||
| 140 | + | // Add the character to the account's character list | |
| 141 | + | account.characters.push_back(character); | |
| 142 | + | } while (result->next()); | |
| 143 | + | } | |
| 144 | + | return true; | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | uint32_t IOLoginData::gameworldAuthentication(const std::string &accountName, | |
| 148 | + | const std::string &password, | |
| 149 | + | std::string &characterName, | |
| 150 | + | std::string &token, | |
| 151 | + | uint32_t tokenTime) { | |
| 152 | + | Database &db = Database::getInstance(); | |
| 153 | + | ||
| 154 | + | std::ostringstream query; | |
| 155 | + | query << "SELECT `id`, `password`, `secret` FROM `accounts` WHERE `name` = " | |
| 156 | + | << db.escapeString(accountName); | |
| 157 | + | DBResult_ptr result = db.storeQuery(query.str()); | |
| 158 | + | if (!result) { | |
| 159 | + | return 0; | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | std::string secret = decodeSecret(result->getString("secret")); | |
| 163 | + | if (!secret.empty()) { | |
| 164 | + | if (token.empty()) { | |
| 165 | + | return 0; | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | bool tokenValid = token == generateToken(secret, tokenTime) || | |
| 169 | + | token == generateToken(secret, tokenTime - 1) || | |
| 170 | + | token == generateToken(secret, tokenTime + 1); | |
| 171 | + | if (!tokenValid) { | |
| 172 | + | return 0; | |
| 173 | + | } | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | if (transformToSHA1(password) != result->getString("password")) { | |
| 177 | + | return 0; | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | uint32_t accountId = result->getNumber<uint32_t>("id"); | |
| 181 | + | ||
| 182 | + | query.str(std::string()); | |
| 183 | + | query << "SELECT `account_id`, `name`, `deletion` FROM `players` WHERE " | |
| 184 | + | "`name` = " | |
| 185 | + | << db.escapeString(characterName); | |
| 186 | + | result = db.storeQuery(query.str()); | |
| 187 | + | if (!result) { | |
| 188 | + | return 0; | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | if (result->getNumber<uint32_t>("account_id") != accountId || | |
| 192 | + | result->getNumber<uint64_t>("deletion") != 0) { | |
| 193 | + | return 0; | |
| 194 | + | } | |
| 195 | + | characterName = result->getString("name"); | |
| 196 | + | return accountId; | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | uint32_t IOLoginData::getAccountIdByPlayerName(const std::string &playerName) { | |
| 200 | + | Database &db = Database::getInstance(); | |
| 201 | + | ||
| 202 | + | std::ostringstream query; | |
| 203 | + | query << "SELECT `account_id` FROM `players` WHERE `name` = " | |
| 204 | + | << db.escapeString(playerName); | |
| 205 | + | DBResult_ptr result = db.storeQuery(query.str()); | |
| 206 | + | if (!result) { | |
| 207 | + | return 0; | |
| 208 | + | } | |
| 209 | + | return result->getNumber<uint32_t>("account_id"); | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | AccountType_t IOLoginData::getAccountType(uint32_t accountId) { | |
| 213 | + | std::ostringstream query; | |
| 214 | + | query << "SELECT `type` FROM `accounts` WHERE `id` = " << accountId; | |
| 215 | + | DBResult_ptr result = Database::getInstance().storeQuery(query.str()); | |
| 216 | + | if (!result) { | |
| 217 | + | return ACCOUNT_TYPE_NORMAL; | |
| 218 | + | } | |
| 219 | + | return static_cast<AccountType_t>(result->getNumber<uint16_t>("type")); | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | void IOLoginData::setAccountType(uint32_t accountId, | |
| 223 | + | AccountType_t accountType) { | |
| 224 | + | std::ostringstream query; | |
| 225 | + | query << "UPDATE `accounts` SET `type` = " | |
| 226 | + | << static_cast<uint16_t>(accountType) << " WHERE `id` = " << accountId; | |
| 227 | + | Database::getInstance().executeQuery(query.str()); | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | void IOLoginData::updateOnlineStatus(uint32_t guid, bool login) { | |
| 231 | + | if (g_config.getBoolean(ConfigManager::ALLOW_CLONES)) { | |
| 232 | + | return; | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | std::ostringstream query; | |
| 236 | + | if (login) { | |
| 237 | + | query << "INSERT INTO `players_online` VALUES (" << guid << ')'; | |
| 238 | + | } else { | |
| 239 | + | query << "DELETE FROM `players_online` WHERE `player_id` = " << guid; | |
| 240 | + | } | |
| 241 | + | Database::getInstance().executeQuery(query.str()); | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | bool IOLoginData::preloadPlayer(Player *player, const std::string &name) { | |
| 245 | + | Database &db = Database::getInstance(); | |
| 246 | + | ||
| 247 | + | std::ostringstream query; | |
| 248 | + | query << "SELECT `id`, `account_id`, `group_id`, `deletion`, (SELECT `type` " | |
| 249 | + | "FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS " | |
| 250 | + | "`account_type`"; | |
| 251 | + | if (!g_config.getBoolean(ConfigManager::FREE_PREMIUM)) { | |
| 252 | + | query << ", (SELECT `premdays` FROM `accounts` WHERE `accounts`.`id` = " | |
| 253 | + | "`account_id`) AS `premium_days`"; | |
| 254 | + | } | |
| 255 | + | query << " FROM `players` WHERE `name` = " << db.escapeString(name); | |
| 256 | + | DBResult_ptr result = db.storeQuery(query.str()); | |
| 257 | + | if (!result) { | |
| 258 | + | return false; | |
| 259 | + | } | |
| 260 | + | ||
| 261 | + | if (result->getNumber<uint64_t>("deletion") != 0) { | |
| 262 | + | return false; | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | player->setGUID(result->getNumber<uint32_t>("id")); | |
| 266 | + | Group *group = | |
| 267 | + | g_game.groups.getGroup(result->getNumber<uint16_t>("group_id")); | |
| 268 | + | if (!group) { | |
| 269 | + | std::cout << "[Error - IOLoginData::preloadPlayer] " << player->name | |
| 270 | + | << " has Group ID " << result->getNumber<uint16_t>("group_id") | |
| 271 | + | << " which doesn't exist." << std::endl; | |
| 272 | + | return false; | |
| 273 | + | } | |
| 274 | + | player->setGroup(group); | |
| 275 | + | player->accountNumber = result->getNumber<uint32_t>("account_id"); | |
| 276 | + | player->accountType = | |
| 277 | + | static_cast<AccountType_t>(result->getNumber<uint16_t>("account_type")); | |
| 278 | + | if (!g_config.getBoolean(ConfigManager::FREE_PREMIUM)) { | |
| 279 | + | player->premiumDays = result->getNumber<uint16_t>("premium_days"); | |
| 280 | + | } else { | |
| 281 | + | player->premiumDays = std::numeric_limits<uint16_t>::max(); | |
| 282 | + | } | |
| 283 | + | return true; | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | bool IOLoginData::loadPlayerById(Player *player, uint32_t id) { | |
| 287 | + | Database &db = Database::getInstance(); | |
| 288 | + | std::ostringstream query; | |
| 289 | + | query | |
| 290 | + | << "SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, " | |
| 291 | + | "`experience`, `level`, `maglevel`, `health`, `healthmax`, " | |
| 292 | + | "`blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, " | |
| 293 | + | "`lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookattacktype`, " | |
| 294 | + | "`lookaddons`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, " | |
| 295 | + | "`lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, " | |
| 296 | + | "`town_id`, `balance`, `offlinetraining_time`, " | |
| 297 | + | "`offlinetraining_skill`, `stamina`, `skill_fist`, " | |
| 298 | + | "`skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, " | |
| 299 | + | "`skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, " | |
| 300 | + | "`skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, " | |
| 301 | + | "`skill_fishing`, `skill_fishing_tries`,`lookshader`, `lookwings`, " | |
| 302 | + | "`lookaura`, `lookruneeffect`, `direction` FROM `players` WHERE `id` = " | |
| 303 | + | << id; | |
| 304 | + | return loadPlayer(player, db.storeQuery(query.str())); | |
| 305 | + | } | |
| 306 | + | ||
| 307 | + | bool IOLoginData::loadPlayerByName(Player *player, const std::string &name) { | |
| 308 | + | Database &db = Database::getInstance(); | |
| 309 | + | std::ostringstream query; | |
| 310 | + | query | |
| 311 | + | << "SELECT `id`, `name`, `account_id`, `group_id`, `sex`, `vocation`, " | |
| 312 | + | "`experience`, `level`, `maglevel`, `health`, `healthmax`, " | |
| 313 | + | "`blessings`, `mana`, `manamax`, `manaspent`, `soul`, `lookbody`, " | |
| 314 | + | "`lookfeet`, `lookhead`, `looklegs`, `looktype`, `lookattacktype`, " | |
| 315 | + | "`lookaddons`, `posx`, `posy`, `posz`, `cap`, `lastlogin`, " | |
| 316 | + | "`lastlogout`, `lastip`, `conditions`, `skulltime`, `skull`, " | |
| 317 | + | "`town_id`, `balance`, `offlinetraining_time`, " | |
| 318 | + | "`offlinetraining_skill`, `stamina`, `skill_fist`, " | |
| 319 | + | "`skill_fist_tries`, `skill_club`, `skill_club_tries`, `skill_sword`, " | |
| 320 | + | "`skill_sword_tries`, `skill_axe`, `skill_axe_tries`, `skill_dist`, " | |
| 321 | + | "`skill_dist_tries`, `skill_shielding`, `skill_shielding_tries`, " | |
| 322 | + | "`skill_fishing`, `skill_fishing_tries`,`lookshader`, `lookwings`, " | |
| 323 | + | "`lookaura`, `lookruneeffect`, `direction` FROM `players` WHERE `name` = " | |
| 324 | + | << db.escapeString(name); | |
| 325 | + | return loadPlayer(player, db.storeQuery(query.str())); | |
| 326 | + | } | |
| 327 | + | ||
| 328 | + | bool IOLoginData::loadPlayer(Player *player, DBResult_ptr result) { | |
| 329 | + | if (!result) { | |
| 330 | + | return false; | |
| 331 | + | } | |
| 332 | + | ||
| 333 | + | Database &db = Database::getInstance(); | |
| 334 | + | ||
| 335 | + | uint32_t accno = result->getNumber<uint32_t>("account_id"); | |
| 336 | + | Account acc = loadAccount(accno); | |
| 337 | + | ||
| 338 | + | player->setGUID(result->getNumber<uint32_t>("id")); | |
| 339 | + | player->name = result->getString("name"); | |
| 340 | + | player->accountNumber = accno; | |
| 341 | + | ||
| 342 | + | player->accountType = acc.accountType; | |
| 343 | + | ||
| 344 | + | if (g_config.getBoolean(ConfigManager::FREE_PREMIUM)) { | |
| 345 | + | player->premiumDays = std::numeric_limits<uint16_t>::max(); | |
| 346 | + | } else { | |
| 347 | + | player->premiumDays = acc.premiumDays; | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | Group *group = | |
| 351 | + | g_game.groups.getGroup(result->getNumber<uint16_t>("group_id")); | |
| 352 | + | if (!group) { | |
| 353 | + | std::cout << "[Error - IOLoginData::loadPlayer] " << player->name | |
| 354 | + | << " has Group ID " << result->getNumber<uint16_t>("group_id") | |
| 355 | + | << " which doesn't exist" << std::endl; | |
| 356 | + | return false; | |
| 357 | + | } | |
| 358 | + | player->setGroup(group); | |
| 359 | + | ||
| 360 | + | player->bankBalance = result->getNumber<uint64_t>("balance"); | |
| 361 | + | ||
| 362 | + | player->setSex(static_cast<PlayerSex_t>(result->getNumber<uint16_t>("sex"))); | |
| 363 | + | player->level = std::max<uint32_t>(1, result->getNumber<uint32_t>("level")); | |
| 364 | + | ||
| 365 | + | uint64_t experience = result->getNumber<uint64_t>("experience"); | |
| 366 | + | ||
| 367 | + | uint64_t currExpCount = Player::getExpForLevel(player->level); | |
| 368 | + | uint64_t nextExpCount = Player::getExpForLevel(player->level + 1); | |
| 369 | + | if (experience < currExpCount || experience > nextExpCount) { | |
| 370 | + | experience = currExpCount; | |
| 371 | + | } | |
| 372 | + | ||
| 373 | + | player->experience = experience; | |
| 374 | + | ||
| 375 | + | if (currExpCount < nextExpCount) { | |
| 376 | + | player->levelPercent = Player::getPercentLevel( | |
| 377 | + | player->experience - currExpCount, nextExpCount - currExpCount); | |
| 378 | + | } else { | |
| 379 | + | player->levelPercent = 0; | |
| 380 | + | } | |
| 381 | + | ||
| 382 | + | player->soul = result->getNumber<uint16_t>("soul"); | |
| 383 | + | player->capacity = result->getNumber<uint32_t>("cap") * 100; | |
| 384 | + | player->blessings = result->getNumber<uint16_t>("blessings"); | |
| 385 | + | ||
| 386 | + | unsigned long conditionsSize; | |
| 387 | + | const char *conditions = result->getStream("conditions", conditionsSize); | |
| 388 | + | PropStream propStream; | |
| 389 | + | propStream.init(conditions, conditionsSize); | |
| 390 | + | ||
| 391 | + | Condition *condition = Condition::createCondition(propStream); | |
| 392 | + | while (condition) { | |
| 393 | + | if (condition->unserialize(propStream)) { | |
| 394 | + | player->storedConditionList.push_front(condition); | |
| 395 | + | } else { | |
| 396 | + | delete condition; | |
| 397 | + | } | |
| 398 | + | condition = Condition::createCondition(propStream); | |
| 399 | + | } | |
| 400 | + | ||
| 401 | + | if (!player->setVocation(result->getNumber<uint16_t>("vocation"))) { | |
| 402 | + | std::cout << "[Error - IOLoginData::loadPlayer] " << player->name | |
| 403 | + | << " has Vocation ID " << result->getNumber<uint16_t>("vocation") | |
| 404 | + | << " which doesn't exist" << std::endl; | |
| 405 | + | return false; | |
| 406 | + | } | |
| 407 | + | ||
| 408 | + | player->mana = result->getNumber<uint32_t>("mana"); | |
| 409 | + | player->manaMax = result->getNumber<uint32_t>("manamax"); | |
| 410 | + | player->magLevel = result->getNumber<uint32_t>("maglevel"); | |
| 411 | + | ||
| 412 | + | uint64_t nextManaCount = player->vocation->getReqMana(player->magLevel + 1); | |
| 413 | + | uint64_t manaSpent = result->getNumber<uint64_t>("manaspent"); | |
| 414 | + | if (manaSpent > nextManaCount) { | |
| 415 | + | manaSpent = 0; | |
| 416 | + | } | |
| 417 | + | ||
| 418 | + | player->manaSpent = manaSpent; | |
| 419 | + | player->magLevelPercent = | |
| 420 | + | Player::getPercentLevel(player->manaSpent, nextManaCount); | |
| 421 | + | ||
| 422 | + | player->health = result->getNumber<int32_t>("health"); | |
| 423 | + | player->healthMax = result->getNumber<int32_t>("healthmax"); | |
| 424 | + | ||
| 425 | + | player->defaultOutfit.lookType = result->getNumber<uint16_t>("looktype"); | |
| 426 | + | player->defaultOutfit.lookHead = result->getNumber<uint16_t>("lookhead"); | |
| 427 | + | player->defaultOutfit.lookBody = result->getNumber<uint16_t>("lookbody"); | |
| 428 | + | player->defaultOutfit.lookLegs = result->getNumber<uint16_t>("looklegs"); | |
| 429 | + | player->defaultOutfit.lookFeet = result->getNumber<uint16_t>("lookfeet"); | |
| 430 | + | player->defaultOutfit.lookAddons = result->getNumber<uint16_t>("lookaddons"); | |
| 431 | + | player->defaultOutfit.lookWings = result->getNumber<uint16_t>("lookwings"); | |
| 432 | + | player->defaultOutfit.lookShader = result->getNumber<uint16_t>("lookshader"); | |
| 433 | + | player->defaultOutfit.lookAura = result->getNumber<uint16_t>("lookaura"); | |
| 434 | + | player->defaultOutfit.lookRuneEffect = result->getNumber<uint16_t>("lookruneeffect"); | |
| 435 | + | // Cargar el lookType del outfit de ataque (colores se toman del outfit actual | |
| 436 | + | // en tiempo de combate) | |
| 437 | + | uint16_t lookAttackType = result->getNumber<uint16_t>("lookattacktype"); | |
| 438 | + | player->setAttackOutfitLookType(lookAttackType); | |
| 439 | + | player->currentOutfit = player->defaultOutfit; | |
| 440 | + | player->direction = | |
| 441 | + | static_cast<Direction>(result->getNumber<uint16_t>("direction")); | |
| 442 | + | ||
| 443 | + | if (g_game.getWorldType() != WORLD_TYPE_PVP_ENFORCED) { | |
| 444 | + | const time_t skullSeconds = | |
| 445 | + | result->getNumber<time_t>("skulltime") - time(nullptr); | |
| 446 | + | if (skullSeconds > 0) { | |
| 447 | + | // ensure that we round up the number of ticks | |
| 448 | + | player->skullTicks = (skullSeconds + 2); | |
| 449 | + | ||
| 450 | + | uint16_t skull = result->getNumber<uint16_t>("skull"); | |
| 451 | + | if (skull == SKULL_RED) { | |
| 452 | + | player->skull = SKULL_RED; | |
| 453 | + | } else if (skull == SKULL_BLACK) { | |
| 454 | + | player->skull = SKULL_BLACK; | |
| 455 | + | } | |
| 456 | + | } | |
| 457 | + | } | |
| 458 | + | ||
| 459 | + | player->loginPosition.x = result->getNumber<uint16_t>("posx"); | |
| 460 | + | player->loginPosition.y = result->getNumber<uint16_t>("posy"); | |
| 461 | + | player->loginPosition.z = result->getNumber<uint16_t>("posz"); | |
| 462 | + | ||
| 463 | + | player->lastLoginSaved = result->getNumber<time_t>("lastlogin"); | |
| 464 | + | player->lastLogout = result->getNumber<time_t>("lastlogout"); | |
| 465 | + | ||
| 466 | + | player->offlineTrainingTime = | |
| 467 | + | result->getNumber<int32_t>("offlinetraining_time") * 1000; | |
| 468 | + | player->offlineTrainingSkill = | |
| 469 | + | result->getNumber<int32_t>("offlinetraining_skill"); | |
| 470 | + | ||
| 471 | + | Town *town = g_game.map.towns.getTown(result->getNumber<uint32_t>("town_id")); | |
| 472 | + | if (!town) { | |
| 473 | + | std::cout << "[Error - IOLoginData::loadPlayer] " << player->name | |
| 474 | + | << " has Town ID " << result->getNumber<uint32_t>("town_id") | |
| 475 | + | << " which doesn't exist" << std::endl; | |
| 476 | + | return false; | |
| 477 | + | } | |
| 478 | + | ||
| 479 | + | player->town = town; | |
| 480 | + | ||
| 481 | + | const Position &loginPos = player->loginPosition; | |
| 482 | + | if (loginPos.x == 0 && loginPos.y == 0 && loginPos.z == 0) { | |
| 483 | + | player->loginPosition = player->getTemplePosition(); | |
| 484 | + | } | |
| 485 | + | ||
| 486 | + | player->staminaMinutes = result->getNumber<uint16_t>("stamina"); | |
| 487 | + | ||
| 488 | + | static const std::string skillNames[] = { | |
| 489 | + | "skill_fist", "skill_club", "skill_sword", "skill_axe", | |
| 490 | + | "skill_dist", "skill_shielding", "skill_fishing"}; | |
| 491 | + | static const std::string skillNameTries[] = { | |
| 492 | + | "skill_fist_tries", "skill_club_tries", "skill_sword_tries", | |
| 493 | + | "skill_axe_tries", "skill_dist_tries", "skill_shielding_tries", | |
| 494 | + | "skill_fishing_tries"}; | |
| 495 | + | static constexpr size_t size = sizeof(skillNames) / sizeof(std::string); | |
| 496 | + | for (uint8_t i = 0; i < size; ++i) { | |
| 497 | + | uint16_t skillLevel = result->getNumber<uint16_t>(skillNames[i]); | |
| 498 | + | uint64_t skillTries = result->getNumber<uint64_t>(skillNameTries[i]); | |
| 499 | + | uint64_t nextSkillTries = | |
| 500 | + | player->vocation->getReqSkillTries(i, skillLevel + 1); | |
| 501 | + | if (skillTries > nextSkillTries) { | |
| 502 | + | skillTries = 0; | |
| 503 | + | } | |
| 504 | + | ||
| 505 | + | player->skills[i].level = skillLevel; | |
| 506 | + | player->skills[i].tries = skillTries; | |
| 507 | + | player->skills[i].percent = | |
| 508 | + | Player::getPercentLevel(skillTries, nextSkillTries); | |
| 509 | + | } | |
| 510 | + | ||
| 511 | + | std::ostringstream query; | |
| 512 | + | query << "SELECT `guild_id`, `rank_id`, `nick` FROM `guild_membership` WHERE " | |
| 513 | + | "`player_id` = " | |
| 514 | + | << player->getGUID(); | |
| 515 | + | if ((result = db.storeQuery(query.str()))) { | |
| 516 | + | uint32_t guildId = result->getNumber<uint32_t>("guild_id"); | |
| 517 | + | uint32_t playerRankId = result->getNumber<uint32_t>("rank_id"); | |
| 518 | + | player->guildNick = result->getString("nick"); | |
| 519 | + | ||
| 520 | + | Guild *guild = g_game.getGuild(guildId); | |
| 521 | + | if (!guild) { | |
| 522 | + | guild = IOGuild::loadGuild(guildId); | |
| 523 | + | if (guild) { | |
| 524 | + | g_game.addGuild(guild); | |
| 525 | + | } else { | |
| 526 | + | std::cout << "[Warning - IOLoginData::loadPlayer] " << player->name | |
| 527 | + | << " has Guild ID " << guildId << " which doesn't exist" | |
| 528 | + | << std::endl; | |
| 529 | + | } | |
| 530 | + | } | |
| 531 | + | ||
| 532 | + | if (guild) { | |
| 533 | + | player->guild = guild; | |
| 534 | + | GuildRank_ptr rank = guild->getRankById(playerRankId); | |
| 535 | + | if (!rank) { | |
| 536 | + | query.str(std::string()); | |
| 537 | + | query << "SELECT `id`, `name`, `level` FROM `guild_ranks` WHERE `id` = " | |
| 538 | + | << playerRankId; | |
| 539 | + | ||
| 540 | + | if ((result = db.storeQuery(query.str()))) { | |
| 541 | + | guild->addRank(result->getNumber<uint32_t>("id"), | |
| 542 | + | result->getString("name"), | |
| 543 | + | result->getNumber<uint16_t>("level")); | |
| 544 | + | } | |
| 545 | + | ||
| 546 | + | rank = guild->getRankById(playerRankId); | |
| 547 | + | if (!rank) { | |
| 548 | + | player->guild = nullptr; | |
| 549 | + | } | |
| 550 | + | } | |
| 551 | + | ||
| 552 | + | player->guildRank = rank; | |
| 553 | + | ||
| 554 | + | IOGuild::getWarList(guildId, player->guildWarVector); | |
| 555 | + | ||
| 556 | + | query.str(std::string()); | |
| 557 | + | query << "SELECT COUNT(*) AS `members` FROM `guild_membership` WHERE " | |
| 558 | + | "`guild_id` = " | |
| 559 | + | << guildId; | |
| 560 | + | if ((result = db.storeQuery(query.str()))) { | |
| 561 | + | guild->setMemberCount(result->getNumber<uint32_t>("members")); | |
| 562 | + | } | |
| 563 | + | } | |
| 564 | + | } | |
| 565 | + | ||
| 566 | + | query.str(std::string()); | |
| 567 | + | query | |
| 568 | + | << "SELECT `player_id`, `name` FROM `player_spells` WHERE `player_id` = " | |
| 569 | + | << player->getGUID(); | |
| 570 | + | if ((result = db.storeQuery(query.str()))) { | |
| 571 | + | do { | |
| 572 | + | player->learnedInstantSpellList.emplace_front(result->getString("name")); | |
| 573 | + | } while (result->next()); | |
| 574 | + | } | |
| 575 | + | ||
| 576 | + | // load inventory items | |
| 577 | + | ItemMap itemMap; | |
| 578 | + | ||
| 579 | + | query.str(std::string()); | |
| 580 | + | query << "SELECT `pid`, `sid`, `itemtype`, `count`, `attributes` FROM " | |
| 581 | + | "`player_items` WHERE `player_id` = " | |
| 582 | + | << player->getGUID() << " ORDER BY `sid` DESC"; | |
| 583 | + | if ((result = db.storeQuery(query.str()))) { | |
| 584 | + | loadItems(itemMap, result); | |
| 585 | + | ||
| 586 | + | for (ItemMap::const_reverse_iterator it = itemMap.rbegin(), | |
| 587 | + | end = itemMap.rend(); | |
| 588 | + | it != end; ++it) { | |
| 589 | + | const std::pair<Item *, int32_t> &pair = it->second; | |
| 590 | + | Item *item = pair.first; | |
| 591 | + | int32_t pid = pair.second; | |
| 592 | + | if (pid >= CONST_SLOT_FIRST && pid <= CONST_SLOT_LAST) { | |
| 593 | + | player->internalAddThing(pid, item); | |
| 594 | + | } else { | |
| 595 | + | ItemMap::const_iterator it2 = itemMap.find(pid); | |
| 596 | + | if (it2 == itemMap.end()) { | |
| 597 | + | continue; | |
| 598 | + | } | |
| 599 | + | ||
| 600 | + | Container *container = it2->second.first->getContainer(); | |
| 601 | + | if (container) { | |
| 602 | + | container->internalAddThing(item); | |
| 603 | + | } | |
| 604 | + | } | |
| 605 | + | } | |
| 606 | + | } | |
| 607 | + | ||
| 608 | + | // load depot items | |
| 609 | + | itemMap.clear(); | |
| 610 | + | ||
| 611 | + | query.str(std::string()); | |
| 612 | + | query << "SELECT `pid`, `sid`, `itemtype`, `count`, `attributes` FROM " | |
| 613 | + | "`player_depotitems` WHERE `player_id` = " | |
| 614 | + | << player->getGUID() << " ORDER BY `sid` DESC"; | |
| 615 | + | if ((result = db.storeQuery(query.str()))) { | |
| 616 | + | loadItems(itemMap, result); | |
| 617 | + | ||
| 618 | + | for (ItemMap::const_reverse_iterator it = itemMap.rbegin(), | |
| 619 | + | end = itemMap.rend(); | |
| 620 | + | it != end; ++it) { | |
| 621 | + | const std::pair<Item *, int32_t> &pair = it->second; | |
| 622 | + | Item *item = pair.first; | |
| 623 | + | ||
| 624 | + | int32_t pid = pair.second; | |
| 625 | + | if (pid >= 0 && pid < 100) { | |
| 626 | + | DepotChest *depotChest = player->getDepotChest(pid, true); | |
| 627 | + | if (depotChest) { | |
| 628 | + | depotChest->internalAddThing(item); | |
| 629 | + | } | |
| 630 | + | } else { | |
| 631 | + | ItemMap::const_iterator it2 = itemMap.find(pid); | |
| 632 | + | if (it2 == itemMap.end()) { | |
| 633 | + | continue; | |
| 634 | + | } | |
| 635 | + | ||
| 636 | + | Container *container = it2->second.first->getContainer(); | |
| 637 | + | if (container) { | |
| 638 | + | container->internalAddThing(item); | |
| 639 | + | } | |
| 640 | + | } | |
| 641 | + | } | |
| 642 | + | } | |
| 643 | + | ||
| 644 | + | // load inbox items | |
| 645 | + | itemMap.clear(); | |
| 646 | + | ||
| 647 | + | query.str(std::string()); | |
| 648 | + | query << "SELECT `pid`, `sid`, `itemtype`, `count`, `attributes` FROM " | |
| 649 | + | "`player_inboxitems` WHERE `player_id` = " | |
| 650 | + | << player->getGUID() << " ORDER BY `sid` DESC"; | |
| 651 | + | if ((result = db.storeQuery(query.str()))) { | |
| 652 | + | loadItems(itemMap, result); | |
| 653 | + | ||
| 654 | + | for (ItemMap::const_reverse_iterator it = itemMap.rbegin(), | |
| 655 | + | end = itemMap.rend(); | |
| 656 | + | it != end; ++it) { | |
| 657 | + | const std::pair<Item *, int32_t> &pair = it->second; | |
| 658 | + | Item *item = pair.first; | |
| 659 | + | int32_t pid = pair.second; | |
| 660 | + | ||
| 661 | + | if (pid >= 0 && pid < 100) { | |
| 662 | + | player->getInbox()->internalAddThing(item); | |
| 663 | + | } else { | |
| 664 | + | ItemMap::const_iterator it2 = itemMap.find(pid); | |
| 665 | + | ||
| 666 | + | if (it2 == itemMap.end()) { | |
| 667 | + | continue; | |
| 668 | + | } | |
| 669 | + | ||
| 670 | + | Container *container = it2->second.first->getContainer(); | |
| 671 | + | if (container) { | |
| 672 | + | container->internalAddThing(item); | |
| 673 | + | } | |
| 674 | + | } | |
| 675 | + | } | |
| 676 | + | } | |
| 677 | + | ||
| 678 | + | // load storage map | |
| 679 | + | query.str(std::string()); | |
| 680 | + | query << "SELECT `key`, `value` FROM `player_storage` WHERE `player_id` = " | |
| 681 | + | << player->getGUID(); | |
| 682 | + | if ((result = db.storeQuery(query.str()))) { | |
| 683 | + | do { | |
| 684 | + | player->addStorageValue(result->getNumber<uint32_t>("key"), | |
| 685 | + | result->getNumber<int32_t>("value"), true); | |
| 686 | + | } while (result->next()); | |
| 687 | + | } | |
| 688 | + | ||
| 689 | + | // load vip | |
| 690 | + | query.str(std::string()); | |
| 691 | + | query << "SELECT `player_id` FROM `account_viplist` WHERE `account_id` = " | |
| 692 | + | << player->getAccount(); | |
| 693 | + | if ((result = db.storeQuery(query.str()))) { | |
| 694 | + | do { | |
| 695 | + | player->addVIPInternal(result->getNumber<uint32_t>("player_id")); | |
| 696 | + | } while (result->next()); | |
| 697 | + | } | |
| 698 | + | ||
| 699 | + | player->updateBaseSpeed(); | |
| 700 | + | player->updateInventoryWeight(); | |
| 701 | + | player->updateItemsLight(true); | |
| 702 | + | return true; | |
| 703 | + | } | |
| 704 | + | ||
| 705 | + | bool IOLoginData::saveItems(const Player *player, const ItemBlockList &itemList, | |
| 706 | + | DBInsert &query_insert, | |
| 707 | + | PropWriteStream &propWriteStream) { | |
| 708 | + | std::ostringstream ss; | |
| 709 | + | ||
| 710 | + | using ContainerBlock = std::pair<Container *, int32_t>; | |
| 711 | + | std::list<ContainerBlock> queue; | |
| 712 | + | ||
| 713 | + | int32_t runningId = 100; | |
| 714 | + | ||
| 715 | + | Database &db = Database::getInstance(); | |
| 716 | + | for (const auto &it : itemList) { | |
| 717 | + | int32_t pid = it.first; | |
| 718 | + | Item *item = it.second; | |
| 719 | + | ++runningId; | |
| 720 | + | ||
| 721 | + | propWriteStream.clear(); | |
| 722 | + | item->serializeAttr(propWriteStream); | |
| 723 | + | ||
| 724 | + | size_t attributesSize; | |
| 725 | + | const char *attributes = propWriteStream.getStream(attributesSize); | |
| 726 | + | ||
| 727 | + | ss << player->getGUID() << ',' << pid << ',' << runningId << ',' | |
| 728 | + | << item->getID() << ',' << item->getSubType() << ',' | |
| 729 | + | << db.escapeBlob(attributes, attributesSize); | |
| 730 | + | if (!query_insert.addRow(ss)) { | |
| 731 | + | return false; | |
| 732 | + | } | |
| 733 | + | ||
| 734 | + | if (Container *container = item->getContainer()) { | |
| 735 | + | queue.emplace_back(container, runningId); | |
| 736 | + | } | |
| 737 | + | } | |
| 738 | + | ||
| 739 | + | while (!queue.empty()) { | |
| 740 | + | const ContainerBlock &cb = queue.front(); | |
| 741 | + | Container *container = cb.first; | |
| 742 | + | int32_t parentId = cb.second; | |
| 743 | + | queue.pop_front(); | |
| 744 | + | ||
| 745 | + | for (Item *item : container->getItemList()) { | |
| 746 | + | ++runningId; | |
| 747 | + | ||
| 748 | + | Container *subContainer = item->getContainer(); | |
| 749 | + | if (subContainer) { | |
| 750 | + | queue.emplace_back(subContainer, runningId); | |
| 751 | + | } | |
| 752 | + | ||
| 753 | + | propWriteStream.clear(); | |
| 754 | + | item->serializeAttr(propWriteStream); | |
| 755 | + | ||
| 756 | + | size_t attributesSize; | |
| 757 | + | const char *attributes = propWriteStream.getStream(attributesSize); | |
| 758 | + | ||
| 759 | + | ss << player->getGUID() << ',' << parentId << ',' << runningId << ',' | |
| 760 | + | << item->getID() << ',' << item->getSubType() << ',' | |
| 761 | + | << db.escapeBlob(attributes, attributesSize); | |
| 762 | + | if (!query_insert.addRow(ss)) { | |
| 763 | + | return false; | |
| 764 | + | } | |
| 765 | + | } | |
| 766 | + | } | |
| 767 | + | return query_insert.execute(); | |
| 768 | + | } | |
| 769 | + | ||
| 770 | + | bool IOLoginData::savePlayer(Player *player) { | |
| 771 | + | if (player->getHealth() <= 0) { | |
| 772 | + | player->changeHealth(1); | |
| 773 | + | } | |
| 774 | + | ||
| 775 | + | Database &db = Database::getInstance(); | |
| 776 | + | ||
| 777 | + | std::ostringstream query; | |
| 778 | + | query << "SELECT `save` FROM `players` WHERE `id` = " << player->getGUID(); | |
| 779 | + | DBResult_ptr result = db.storeQuery(query.str()); | |
| 780 | + | if (!result) { | |
| 781 | + | return false; | |
| 782 | + | } | |
| 783 | + | ||
| 784 | + | if (result->getNumber<uint16_t>("save") == 0) { | |
| 785 | + | query.str(std::string()); | |
| 786 | + | query << "UPDATE `players` SET `lastlogin` = " << player->lastLoginSaved | |
| 787 | + | << ", `lastip` = " << player->lastIP | |
| 788 | + | << " WHERE `id` = " << player->getGUID(); | |
| 789 | + | return db.executeQuery(query.str()); | |
| 790 | + | } | |
| 791 | + | ||
| 792 | + | // serialize conditions | |
| 793 | + | PropWriteStream propWriteStream; | |
| 794 | + | for (Condition *condition : player->conditions) { | |
| 795 | + | if (condition->isPersistent()) { | |
| 796 | + | condition->serialize(propWriteStream); | |
| 797 | + | propWriteStream.write<uint8_t>(CONDITIONATTR_END); | |
| 798 | + | } | |
| 799 | + | } | |
| 800 | + | ||
| 801 | + | size_t conditionsSize; | |
| 802 | + | const char *conditions = propWriteStream.getStream(conditionsSize); | |
| 803 | + | ||
| 804 | + | // First, an UPDATE query to write the player itself | |
| 805 | + | query.str(std::string()); | |
| 806 | + | query << "UPDATE `players` SET "; | |
| 807 | + | query << "`level` = " << player->level << ','; | |
| 808 | + | query << "`group_id` = " << player->group->id << ','; | |
| 809 | + | query << "`vocation` = " << player->getVocationId() << ','; | |
| 810 | + | query << "`health` = " << player->health << ','; | |
| 811 | + | query << "`healthmax` = " << player->healthMax << ','; | |
| 812 | + | query << "`experience` = " << player->experience << ','; | |
| 813 | + | query << "`lookbody` = " | |
| 814 | + | << static_cast<uint32_t>(player->defaultOutfit.lookBody) << ','; | |
| 815 | + | query << "`lookfeet` = " | |
| 816 | + | << static_cast<uint32_t>(player->defaultOutfit.lookFeet) << ','; | |
| 817 | + | query << "`lookhead` = " | |
| 818 | + | << static_cast<uint32_t>(player->defaultOutfit.lookHead) << ','; | |
| 819 | + | query << "`looklegs` = " | |
| 820 | + | << static_cast<uint32_t>(player->defaultOutfit.lookLegs) << ','; | |
| 821 | + | query << "`looktype` = " << player->defaultOutfit.lookType << ','; | |
| 822 | + | query << "`lookattacktype` = " | |
| 823 | + | << static_cast<uint32_t>(player->getAttackOutfitLookType()) << ','; | |
| 824 | + | query << "`lookaddons` = " | |
| 825 | + | << static_cast<uint32_t>(player->defaultOutfit.lookAddons) << ','; | |
| 826 | + | query << "`lookwings` = " | |
| 827 | + | << static_cast<uint32_t>(player->defaultOutfit.lookWings) << ','; | |
| 828 | + | query << "`lookshader` = " | |
| 829 | + | << static_cast<uint32_t>(player->defaultOutfit.lookShader) << ','; | |
| 830 | + | query << "`lookaura` = " | |
| 831 | + | << static_cast<uint32_t>(player->defaultOutfit.lookAura) << ','; | |
| 832 | + | query << "`lookruneeffect` = " | |
| 833 | + | << static_cast<uint32_t>(player->defaultOutfit.lookRuneEffect) << ','; | |
| 834 | + | query << "`maglevel` = " << player->magLevel << ','; | |
| 835 | + | query << "`mana` = " << player->mana << ','; | |
| 836 | + | query << "`manamax` = " << player->manaMax << ','; | |
| 837 | + | query << "`manaspent` = " << player->manaSpent << ','; | |
| 838 | + | query << "`soul` = " << static_cast<uint16_t>(player->soul) << ','; | |
| 839 | + | query << "`town_id` = " << player->town->getID() << ','; | |
| 840 | + | ||
| 841 | + | const Position &loginPosition = player->getLoginPosition(); | |
| 842 | + | query << "`posx` = " << loginPosition.getX() << ','; | |
| 843 | + | query << "`posy` = " << loginPosition.getY() << ','; | |
| 844 | + | query << "`posz` = " << loginPosition.getZ() << ','; | |
| 845 | + | ||
| 846 | + | query << "`cap` = " << (player->capacity / 100) << ','; | |
| 847 | + | query << "`sex` = " << static_cast<uint16_t>(player->sex) << ','; | |
| 848 | + | ||
| 849 | + | if (player->lastLoginSaved != 0) { | |
| 850 | + | query << "`lastlogin` = " << player->lastLoginSaved << ','; | |
| 851 | + | } | |
| 852 | + | ||
| 853 | + | if (player->lastIP != 0) { | |
| 854 | + | query << "`lastip` = " << player->lastIP << ','; | |
| 855 | + | } | |
| 856 | + | ||
| 857 | + | query << "`conditions` = " << db.escapeBlob(conditions, conditionsSize) | |
| 858 | + | << ','; | |
| 859 | + | ||
| 860 | + | if (g_game.getWorldType() != WORLD_TYPE_PVP_ENFORCED) { | |
| 861 | + | int64_t skullTime = 0; | |
| 862 | + | ||
| 863 | + | if (player->skullTicks > 0) { | |
| 864 | + | skullTime = time(nullptr) + player->skullTicks; | |
| 865 | + | } | |
| 866 | + | query << "`skulltime` = " << skullTime << ','; | |
| 867 | + | ||
| 868 | + | Skulls_t skull = SKULL_NONE; | |
| 869 | + | if (player->skull == SKULL_RED) { | |
| 870 | + | skull = SKULL_RED; | |
| 871 | + | } else if (player->skull == SKULL_BLACK) { | |
| 872 | + | skull = SKULL_BLACK; | |
| 873 | + | } | |
| 874 | + | query << "`skull` = " << static_cast<int64_t>(skull) << ','; | |
| 875 | + | } | |
| 876 | + | ||
| 877 | + | query << "`lastlogout` = " << player->getLastLogout() << ','; | |
| 878 | + | query << "`balance` = " << player->bankBalance << ','; | |
| 879 | + | query << "`offlinetraining_time` = " | |
| 880 | + | << player->getOfflineTrainingTime() / 1000 << ','; | |
| 881 | + | query << "`offlinetraining_skill` = " << player->getOfflineTrainingSkill() | |
| 882 | + | << ','; | |
| 883 | + | query << "`stamina` = " << player->getStaminaMinutes() << ','; | |
| 884 | + | ||
| 885 | + | query << "`skill_fist` = " << player->skills[SKILL_FIST].level << ','; | |
| 886 | + | query << "`skill_fist_tries` = " << player->skills[SKILL_FIST].tries << ','; | |
| 887 | + | query << "`skill_club` = " << player->skills[SKILL_CLUB].level << ','; | |
| 888 | + | query << "`skill_club_tries` = " << player->skills[SKILL_CLUB].tries << ','; | |
| 889 | + | query << "`skill_sword` = " << player->skills[SKILL_SWORD].level << ','; | |
| 890 | + | query << "`skill_sword_tries` = " << player->skills[SKILL_SWORD].tries << ','; | |
| 891 | + | query << "`skill_axe` = " << player->skills[SKILL_AXE].level << ','; | |
| 892 | + | query << "`skill_axe_tries` = " << player->skills[SKILL_AXE].tries << ','; | |
| 893 | + | query << "`skill_dist` = " << player->skills[SKILL_DISTANCE].level << ','; | |
| 894 | + | query << "`skill_dist_tries` = " << player->skills[SKILL_DISTANCE].tries | |
| 895 | + | << ','; | |
| 896 | + | query << "`skill_shielding` = " << player->skills[SKILL_SHIELD].level << ','; | |
| 897 | + | query << "`skill_shielding_tries` = " << player->skills[SKILL_SHIELD].tries | |
| 898 | + | << ','; | |
| 899 | + | query << "`skill_fishing` = " << player->skills[SKILL_FISHING].level << ','; | |
| 900 | + | query << "`skill_fishing_tries` = " << player->skills[SKILL_FISHING].tries | |
| 901 | + | << ','; | |
| 902 | + | query << "`direction` = " << static_cast<uint16_t>(player->getDirection()) | |
| 903 | + | << ','; | |
| 904 | + | ||
| 905 | + | if (!player->isOffline()) { | |
| 906 | + | query << "`onlinetime` = `onlinetime` + " | |
| 907 | + | << (time(nullptr) - player->lastLoginSaved) << ','; | |
| 908 | + | } | |
| 909 | + | query << "`blessings` = " << static_cast<uint32_t>(player->blessings); | |
| 910 | + | query << " WHERE `id` = " << player->getGUID(); | |
| 911 | + | ||
| 912 | + | DBTransaction transaction; | |
| 913 | + | if (!transaction.begin()) { | |
| 914 | + | return false; | |
| 915 | + | } | |
| 916 | + | ||
| 917 | + | if (!db.executeQuery(query.str())) { | |
| 918 | + | return false; | |
| 919 | + | } | |
| 920 | + | ||
| 921 | + | // learned spells | |
| 922 | + | query.str(std::string()); | |
| 923 | + | query << "DELETE FROM `player_spells` WHERE `player_id` = " | |
| 924 | + | << player->getGUID(); | |
| 925 | + | if (!db.executeQuery(query.str())) { | |
| 926 | + | return false; | |
| 927 | + | } | |
| 928 | + | ||
| 929 | + | query.str(std::string()); | |
| 930 | + | ||
| 931 | + | DBInsert spellsQuery( | |
| 932 | + | "INSERT INTO `player_spells` (`player_id`, `name` ) VALUES "); | |
| 933 | + | for (const std::string &spellName : player->learnedInstantSpellList) { | |
| 934 | + | query << player->getGUID() << ',' << db.escapeString(spellName); | |
| 935 | + | if (!spellsQuery.addRow(query)) { | |
| 936 | + | return false; | |
| 937 | + | } | |
| 938 | + | } | |
| 939 | + | ||
| 940 | + | if (!spellsQuery.execute()) { | |
| 941 | + | return false; | |
| 942 | + | } | |
| 943 | + | ||
| 944 | + | // item saving | |
| 945 | + | query << "DELETE FROM `player_items` WHERE `player_id` = " | |
| 946 | + | << player->getGUID(); | |
| 947 | + | if (!db.executeQuery(query.str())) { | |
| 948 | + | return false; | |
| 949 | + | } | |
| 950 | + | ||
| 951 | + | DBInsert itemsQuery("INSERT INTO `player_items` (`player_id`, `pid`, `sid`, " | |
| 952 | + | "`itemtype`, `count`, `attributes`) VALUES "); | |
| 953 | + | ||
| 954 | + | ItemBlockList itemList; | |
| 955 | + | for (int32_t slotId = CONST_SLOT_FIRST; slotId <= CONST_SLOT_LAST; ++slotId) { | |
| 956 | + | Item *item = player->inventory[slotId]; | |
| 957 | + | if (item) { | |
| 958 | + | itemList.emplace_back(slotId, item); | |
| 959 | + | } | |
| 960 | + | } | |
| 961 | + | ||
| 962 | + | if (!saveItems(player, itemList, itemsQuery, propWriteStream)) { | |
| 963 | + | return false; | |
| 964 | + | } | |
| 965 | + | ||
| 966 | + | if (player->lastDepotId != -1) { | |
| 967 | + | // save depot items | |
| 968 | + | query.str(std::string()); | |
| 969 | + | query << "DELETE FROM `player_depotitems` WHERE `player_id` = " | |
| 970 | + | << player->getGUID(); | |
| 971 | + | ||
| 972 | + | if (!db.executeQuery(query.str())) { | |
| 973 | + | return false; | |
| 974 | + | } | |
| 975 | + | ||
| 976 | + | DBInsert depotQuery("INSERT INTO `player_depotitems` (`player_id`, `pid`, " | |
| 977 | + | "`sid`, `itemtype`, `count`, `attributes`) VALUES "); | |
| 978 | + | itemList.clear(); | |
| 979 | + | ||
| 980 | + | for (const auto &it : player->depotChests) { | |
| 981 | + | DepotChest *depotChest = it.second; | |
| 982 | + | for (Item *item : depotChest->getItemList()) { | |
| 983 | + | itemList.emplace_back(it.first, item); | |
| 984 | + | } | |
| 985 | + | } | |
| 986 | + | ||
| 987 | + | if (!saveItems(player, itemList, depotQuery, propWriteStream)) { | |
| 988 | + | return false; | |
| 989 | + | } | |
| 990 | + | } | |
| 991 | + | ||
| 992 | + | // save inbox items | |
| 993 | + | query.str(std::string()); | |
| 994 | + | query << "DELETE FROM `player_inboxitems` WHERE `player_id` = " | |
| 995 | + | << player->getGUID(); | |
| 996 | + | if (!db.executeQuery(query.str())) { | |
| 997 | + | return false; | |
| 998 | + | } | |
| 999 | + | ||
| 1000 | + | DBInsert inboxQuery("INSERT INTO `player_inboxitems` (`player_id`, `pid`, " | |
| 1001 | + | "`sid`, `itemtype`, `count`, `attributes`) VALUES "); | |
| 1002 | + | itemList.clear(); | |
| 1003 | + | ||
| 1004 | + | for (Item *item : player->getInbox()->getItemList()) { | |
| 1005 | + | itemList.emplace_back(0, item); | |
| 1006 | + | } | |
| 1007 | + | ||
| 1008 | + | if (!saveItems(player, itemList, inboxQuery, propWriteStream)) { | |
| 1009 | + | return false; | |
| 1010 | + | } | |
| 1011 | + | ||
| 1012 | + | query.str(std::string()); | |
| 1013 | + | query << "DELETE FROM `player_storage` WHERE `player_id` = " | |
| 1014 | + | << player->getGUID(); | |
| 1015 | + | if (!db.executeQuery(query.str())) { | |
| 1016 | + | return false; | |
| 1017 | + | } | |
| 1018 | + | ||
| 1019 | + | query.str(std::string()); | |
| 1020 | + | ||
| 1021 | + | DBInsert storageQuery( | |
| 1022 | + | "INSERT INTO `player_storage` (`player_id`, `key`, `value`) VALUES "); | |
| 1023 | + | player->genReservedStorageRange(); | |
| 1024 | + | ||
| 1025 | + | for (const auto &it : player->storageMap) { | |
| 1026 | + | query << player->getGUID() << ',' << it.first << ',' << it.second; | |
| 1027 | + | if (!storageQuery.addRow(query)) { | |
| 1028 | + | return false; | |
| 1029 | + | } | |
| 1030 | + | } | |
| 1031 | + | ||
| 1032 | + | if (!storageQuery.execute()) { | |
| 1033 | + | return false; | |
| 1034 | + | } | |
| 1035 | + | ||
| 1036 | + | // End the transaction | |
| 1037 | + | return transaction.commit(); | |
| 1038 | + | } | |
| 1039 | + | ||
| 1040 | + | std::string IOLoginData::getNameByGuid(uint32_t guid) { | |
| 1041 | + | std::ostringstream query; | |
| 1042 | + | query << "SELECT `name` FROM `players` WHERE `id` = " << guid; | |
| 1043 | + | DBResult_ptr result = Database::getInstance().storeQuery(query.str()); | |
| 1044 | + | if (!result) { | |
| 1045 | + | return std::string(); | |
| 1046 | + | } | |
| 1047 | + | return result->getString("name"); | |
| 1048 | + | } | |
| 1049 | + | ||
| 1050 | + | uint32_t IOLoginData::getGuidByName(const std::string &name) { | |
| 1051 | + | Database &db = Database::getInstance(); | |
| 1052 | + | ||
| 1053 | + | std::ostringstream query; | |
| 1054 | + | query << "SELECT `id` FROM `players` WHERE `name` = " | |
| 1055 | + | << db.escapeString(name); | |
| 1056 | + | DBResult_ptr result = db.storeQuery(query.str()); | |
| 1057 | + | if (!result) { | |
| 1058 | + | return 0; | |
| 1059 | + | } | |
| 1060 | + | return result->getNumber<uint32_t>("id"); | |
| 1061 | + | } | |
| 1062 | + | ||
| 1063 | + | bool IOLoginData::getGuidByNameEx(uint32_t &guid, bool &specialVip, | |
| 1064 | + | std::string &name) { | |
| 1065 | + | Database &db = Database::getInstance(); | |
| 1066 | + | ||
| 1067 | + | std::ostringstream query; | |
| 1068 | + | query << "SELECT `name`, `id`, `group_id`, `account_id` FROM `players` WHERE " | |
| 1069 | + | "`name` = " | |
| 1070 | + | << db.escapeString(name); | |
| 1071 | + | DBResult_ptr result = db.storeQuery(query.str()); | |
| 1072 | + | if (!result) { | |
| 1073 | + | return false; | |
| 1074 | + | } | |
| 1075 | + | ||
| 1076 | + | name = result->getString("name"); | |
| 1077 | + | guid = result->getNumber<uint32_t>("id"); | |
| 1078 | + | Group *group = | |
| 1079 | + | g_game.groups.getGroup(result->getNumber<uint16_t>("group_id")); | |
| 1080 | + | ||
| 1081 | + | uint64_t flags; | |
| 1082 | + | if (group) { | |
| 1083 | + | flags = group->flags; | |
| 1084 | + | } else { | |
| 1085 | + | flags = 0; | |
| 1086 | + | } | |
| 1087 | + | ||
| 1088 | + | specialVip = (flags & PlayerFlag_SpecialVIP) != 0; | |
| 1089 | + | return true; | |
| 1090 | + | } | |
| 1091 | + | ||
| 1092 | + | bool IOLoginData::formatPlayerName(std::string &name) { | |
| 1093 | + | Database &db = Database::getInstance(); | |
| 1094 | + | ||
| 1095 | + | std::ostringstream query; | |
| 1096 | + | query << "SELECT `name` FROM `players` WHERE `name` = " | |
| 1097 | + | << db.escapeString(name); | |
| 1098 | + | ||
| 1099 | + | DBResult_ptr result = db.storeQuery(query.str()); | |
| 1100 | + | if (!result) { | |
| 1101 | + | return false; | |
| 1102 | + | } | |
| 1103 | + | ||
| 1104 | + | name = result->getString("name"); | |
| 1105 | + | return true; | |
| 1106 | + | } | |
| 1107 | + | ||
| 1108 | + | void IOLoginData::loadItems(ItemMap &itemMap, DBResult_ptr result) { | |
| 1109 | + | do { | |
| 1110 | + | uint32_t sid = result->getNumber<uint32_t>("sid"); | |
| 1111 | + | uint32_t pid = result->getNumber<uint32_t>("pid"); | |
| 1112 | + | uint16_t type = result->getNumber<uint16_t>("itemtype"); | |
| 1113 | + | uint16_t count = result->getNumber<uint16_t>("count"); | |
| 1114 | + | ||
| 1115 | + | unsigned long attrSize; | |
| 1116 | + | const char *attr = result->getStream("attributes", attrSize); | |
| 1117 | + | ||
| 1118 | + | PropStream propStream; | |
| 1119 | + | propStream.init(attr, attrSize); | |
| 1120 | + | ||
| 1121 | + | Item *item = Item::CreateItem(type, count); | |
| 1122 | + | if (item) { | |
| 1123 | + | if (!item->unserializeAttr(propStream)) { | |
| 1124 | + | std::cout << "WARNING: Serialize error in IOLoginData::loadItems" | |
| 1125 | + | << std::endl; | |
| 1126 | + | } | |
| 1127 | + | ||
| 1128 | + | std::pair<Item *, uint32_t> pair(item, pid); | |
| 1129 | + | itemMap[sid] = pair; | |
| 1130 | + | } | |
| 1131 | + | } while (result->next()); | |
| 1132 | + | } | |
| 1133 | + | ||
| 1134 | + | void IOLoginData::increaseBankBalance(uint32_t guid, uint64_t bankBalance) { | |
| 1135 | + | std::ostringstream query; | |
| 1136 | + | query << "UPDATE `players` SET `balance` = `balance` + " << bankBalance | |
| 1137 | + | << " WHERE `id` = " << guid; | |
| 1138 | + | Database::getInstance().executeQuery(query.str()); | |
| 1139 | + | } | |
| 1140 | + | ||
| 1141 | + | bool IOLoginData::hasBiddedOnHouse(uint32_t guid) { | |
| 1142 | + | Database &db = Database::getInstance(); | |
| 1143 | + | ||
| 1144 | + | std::ostringstream query; | |
| 1145 | + | query << "SELECT `id` FROM `houses` WHERE `highest_bidder` = " << guid | |
| 1146 | + | << " LIMIT 1"; | |
| 1147 | + | return db.storeQuery(query.str()).get() != nullptr; | |
| 1148 | + | } | |
| 1149 | + | ||
| 1150 | + | std::forward_list<VIPEntry> IOLoginData::getVIPEntries(uint32_t accountId) { | |
| 1151 | + | std::forward_list<VIPEntry> entries; | |
| 1152 | + | ||
| 1153 | + | std::ostringstream query; | |
| 1154 | + | query << "SELECT `player_id`, (SELECT `name` FROM `players` WHERE `id` = " | |
| 1155 | + | "`player_id`) AS `name`, `description`, `icon`, `notify` FROM " | |
| 1156 | + | "`account_viplist` WHERE `account_id` = " | |
| 1157 | + | << accountId; | |
| 1158 | + | ||
| 1159 | + | DBResult_ptr result = Database::getInstance().storeQuery(query.str()); | |
| 1160 | + | if (result) { | |
| 1161 | + | do { | |
| 1162 | + | entries.emplace_front( | |
| 1163 | + | result->getNumber<uint32_t>("player_id"), result->getString("name"), | |
| 1164 | + | result->getString("description"), result->getNumber<uint32_t>("icon"), | |
| 1165 | + | result->getNumber<uint16_t>("notify") != 0); | |
| 1166 | + | } while (result->next()); | |
| 1167 | + | } | |
| 1168 | + | return entries; | |
| 1169 | + | } | |
| 1170 | + | ||
| 1171 | + | void IOLoginData::addVIPEntry(uint32_t accountId, uint32_t guid, | |
| 1172 | + | const std::string &description, uint32_t icon, | |
| 1173 | + | bool notify) { | |
| 1174 | + | Database &db = Database::getInstance(); | |
| 1175 | + | ||
| 1176 | + | std::ostringstream query; | |
| 1177 | + | query << "INSERT INTO `account_viplist` (`account_id`, `player_id`, " | |
| 1178 | + | "`description`, `icon`, `notify`) VALUES (" | |
| 1179 | + | << accountId << ',' << guid << ',' << db.escapeString(description) | |
| 1180 | + | << ',' << icon << ',' << notify << ')'; | |
| 1181 | + | db.executeQuery(query.str()); | |
| 1182 | + | } | |
| 1183 | + | ||
| 1184 | + | void IOLoginData::editVIPEntry(uint32_t accountId, uint32_t guid, | |
| 1185 | + | const std::string &description, uint32_t icon, | |
| 1186 | + | bool notify) { | |
| 1187 | + | Database &db = Database::getInstance(); | |
| 1188 | + | ||
| 1189 | + | std::ostringstream query; | |
| 1190 | + | query << "UPDATE `account_viplist` SET `description` = " | |
| 1191 | + | << db.escapeString(description) << ", `icon` = " << icon | |
| 1192 | + | << ", `notify` = " << notify << " WHERE `account_id` = " << accountId | |
| 1193 | + | << " AND `player_id` = " << guid; | |
| 1194 | + | db.executeQuery(query.str()); | |
| 1195 | + | } | |
| 1196 | + | ||
| 1197 | + | void IOLoginData::removeVIPEntry(uint32_t accountId, uint32_t guid) { | |
| 1198 | + | std::ostringstream query; | |
| 1199 | + | query << "DELETE FROM `account_viplist` WHERE `account_id` = " << accountId | |
| 1200 | + | << " AND `player_id` = " << guid; | |
| 1201 | + | Database::getInstance().executeQuery(query.str()); | |
| 1202 | + | } | |
| 1203 | + | ||
| 1204 | + | void IOLoginData::addPremiumDays(uint32_t accountId, int32_t addDays) { | |
| 1205 | + | std::ostringstream query; | |
| 1206 | + | query << "UPDATE `accounts` SET `premdays` = `premdays` + " << addDays | |
| 1207 | + | << " WHERE `id` = " << accountId; | |
| 1208 | + | Database::getInstance().executeQuery(query.str()); | |
| 1209 | + | } | |
| 1210 | + | ||
| 1211 | + | void IOLoginData::removePremiumDays(uint32_t accountId, int32_t removeDays) { | |
| 1212 | + | std::ostringstream query; | |
| 1213 | + | query << "UPDATE `accounts` SET `premdays` = `premdays` - " << removeDays | |
| 1214 | + | << " WHERE `id` = " << accountId; | |
| 1215 | + | Database::getInstance().executeQuery(query.str()); | |
| 1216 | + | } |
| @@ -0,0 +1,70 @@ | |||
| 1 | + | /** | |
| 2 | + | * The Forgotten Server - a free and open-source MMORPG server emulator | |
| 3 | + | * Copyright (C) 2019 Mark Samman <mark.samman@gmail.com> | |
| 4 | + | * | |
| 5 | + | * This program is free software; you can redistribute it and/or modify | |
| 6 | + | * it under the terms of the GNU General Public License as published by | |
| 7 | + | * the Free Software Foundation; either version 2 of the License, or | |
| 8 | + | * (at your option) any later version. | |
| 9 | + | * | |
| 10 | + | * This program is distributed in the hope that it will be useful, | |
| 11 | + | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 12 | + | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 13 | + | * GNU General Public License for more details. | |
| 14 | + | * | |
| 15 | + | * You should have received a copy of the GNU General Public License along | |
| 16 | + | * with this program; if not, write to the Free Software Foundation, Inc., | |
| 17 | + | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| 18 | + | */ | |
| 19 | + | ||
| 20 | + | #ifndef FS_IOLOGINDATA_H_28B0440BEC594654AC0F4E1A5E42B2EF | |
| 21 | + | #define FS_IOLOGINDATA_H_28B0440BEC594654AC0F4E1A5E42B2EF | |
| 22 | + | ||
| 23 | + | #include "account.h" | |
| 24 | + | #include "player.h" | |
| 25 | + | #include "database.h" | |
| 26 | + | ||
| 27 | + | using ItemBlockList = std::list<std::pair<int32_t, Item*>>; | |
| 28 | + | ||
| 29 | + | class IOLoginData | |
| 30 | + | { | |
| 31 | + | public: | |
| 32 | + | static Account loadAccount(uint32_t accno); | |
| 33 | + | static bool saveAccount(const Account& acc); | |
| 34 | + | ||
| 35 | + | static bool loginserverAuthentication(const std::string& name, const std::string& password, Account& account); | |
| 36 | + | static uint32_t gameworldAuthentication(const std::string& accountName, const std::string& password, std::string& characterName, std::string& token, uint32_t tokenTime); | |
| 37 | + | static uint32_t getAccountIdByPlayerName(const std::string& playerName); | |
| 38 | + | ||
| 39 | + | static AccountType_t getAccountType(uint32_t accountId); | |
| 40 | + | static void setAccountType(uint32_t accountId, AccountType_t accountType); | |
| 41 | + | static void updateOnlineStatus(uint32_t guid, bool login); | |
| 42 | + | static bool preloadPlayer(Player* player, const std::string& name); | |
| 43 | + | ||
| 44 | + | static bool loadPlayerById(Player* player, uint32_t id); | |
| 45 | + | static bool loadPlayerByName(Player* player, const std::string& name); | |
| 46 | + | static bool loadPlayer(Player* player, DBResult_ptr result); | |
| 47 | + | static bool savePlayer(Player* player); | |
| 48 | + | static uint32_t getGuidByName(const std::string& name); | |
| 49 | + | static bool getGuidByNameEx(uint32_t& guid, bool& specialVip, std::string& name); | |
| 50 | + | static std::string getNameByGuid(uint32_t guid); | |
| 51 | + | static bool formatPlayerName(std::string& name); | |
| 52 | + | static void increaseBankBalance(uint32_t guid, uint64_t bankBalance); | |
| 53 | + | static bool hasBiddedOnHouse(uint32_t guid); | |
| 54 | + | ||
| 55 | + | static std::forward_list<VIPEntry> getVIPEntries(uint32_t accountId); | |
| 56 | + | static void addVIPEntry(uint32_t accountId, uint32_t guid, const std::string& description, uint32_t icon, bool notify); | |
| 57 | + | static void editVIPEntry(uint32_t accountId, uint32_t guid, const std::string& description, uint32_t icon, bool notify); | |
| 58 | + | static void removeVIPEntry(uint32_t accountId, uint32_t guid); | |
| 59 | + | ||
| 60 | + | static void addPremiumDays(uint32_t accountId, int32_t addDays); | |
| 61 | + | static void removePremiumDays(uint32_t accountId, int32_t removeDays); | |
| 62 | + | ||
| 63 | + | private: | |
| 64 | + | using ItemMap = std::map<uint32_t, std::pair<Item*, uint32_t>>; | |
| 65 | + | ||
| 66 | + | static void loadItems(ItemMap& itemMap, DBResult_ptr result); | |
| 67 | + | static bool saveItems(const Player* player, const ItemBlockList& itemList, DBInsert& query_insert, PropWriteStream& propWriteStream); | |
| 68 | + | }; | |
| 69 | + | ||
| 70 | + | #endif |
| @@ -0,0 +1,482 @@ | |||
| 1 | + | /** | |
| 2 | + | * The Forgotten Server - a free and open-source MMORPG server emulator | |
| 3 | + | * Copyright (C) 2019 Mark Samman <mark.samman@gmail.com> | |
| 4 | + | * | |
| 5 | + | * This program is free software; you can redistribute it and/or modify | |
| 6 | + | * it under the terms of the GNU General Public License as published by | |
| 7 | + | * the Free Software Foundation; either version 2 of the License, or | |
| 8 | + | * (at your option) any later version. | |
| 9 | + | * | |
| 10 | + | * This program is distributed in the hope that it will be useful, | |
| 11 | + | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 12 | + | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 13 | + | * GNU General Public License for more details. | |
| 14 | + | * | |
| 15 | + | * You should have received a copy of the GNU General Public License along | |
| 16 | + | * with this program; if not, write to the Free Software Foundation, Inc., | |
| 17 | + | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| 18 | + | */ | |
| 19 | + | ||
| 20 | + | #include "otpch.h" | |
| 21 | + | ||
| 22 | + | #include "iomap.h" | |
| 23 | + | ||
| 24 | + | #include "bed.h" | |
| 25 | + | ||
| 26 | + | /* | |
| 27 | + | OTBM_ROOTV1 | |
| 28 | + | | | |
| 29 | + | |--- OTBM_MAP_DATA | |
| 30 | + | | | | |
| 31 | + | | |--- OTBM_TILE_AREA | |
| 32 | + | | | |--- OTBM_TILE | |
| 33 | + | | | |--- OTBM_TILE_SQUARE (not implemented) | |
| 34 | + | | | |--- OTBM_TILE_REF (not implemented) | |
| 35 | + | | | |--- OTBM_HOUSETILE | |
| 36 | + | | | | |
| 37 | + | | |--- OTBM_SPAWNS (not implemented) | |
| 38 | + | | | |--- OTBM_SPAWN_AREA (not implemented) | |
| 39 | + | | | |--- OTBM_MONSTER (not implemented) | |
| 40 | + | | | | |
| 41 | + | | |--- OTBM_TOWNS | |
| 42 | + | | | |--- OTBM_TOWN | |
| 43 | + | | | | |
| 44 | + | | |--- OTBM_WAYPOINTS | |
| 45 | + | | |--- OTBM_WAYPOINT | |
| 46 | + | | | |
| 47 | + | |--- OTBM_ITEM_DEF (not implemented) | |
| 48 | + | */ | |
| 49 | + | ||
| 50 | + | Tile* IOMap::createTile(Item*& ground, Item* item, uint16_t x, uint16_t y, uint8_t z) | |
| 51 | + | { | |
| 52 | + | if (!ground) { | |
| 53 | + | return new StaticTile(x, y, z); | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | Tile* tile; | |
| 57 | + | if ((item && item->isBlocking()) || ground->isBlocking()) { | |
| 58 | + | tile = new StaticTile(x, y, z); | |
| 59 | + | } else { | |
| 60 | + | tile = new DynamicTile(x, y, z); | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | tile->internalAddThing(ground); | |
| 64 | + | ground->startDecaying(); | |
| 65 | + | ground = nullptr; | |
| 66 | + | return tile; | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | bool IOMap::loadMap(Map* map, const std::string& fileName) | |
| 70 | + | { | |
| 71 | + | int64_t start = OTSYS_TIME(); | |
| 72 | + | OTB::Loader loader{fileName, OTB::Identifier{{'O', 'T', 'B', 'M'}}}; | |
| 73 | + | auto& root = loader.parseTree(); | |
| 74 | + | ||
| 75 | + | PropStream propStream; | |
| 76 | + | if (!loader.getProps(root, propStream)) { | |
| 77 | + | setLastErrorString("Could not read root property."); | |
| 78 | + | return false; | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | OTBM_root_header root_header; | |
| 82 | + | if (!propStream.read(root_header)) { | |
| 83 | + | setLastErrorString("Could not read header."); | |
| 84 | + | return false; | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | uint32_t headerVersion = root_header.version; | |
| 88 | + | if (headerVersion == 0) { | |
| 89 | + | //In otbm version 1 the count variable after splashes/fluidcontainers and stackables | |
| 90 | + | //are saved as attributes instead, this solves alot of problems with items | |
| 91 | + | //that is changed (stackable/charges/fluidcontainer/splash) during an update. | |
| 92 | + | setLastErrorString("This map need to be upgraded by using the latest map editor version to be able to load correctly."); | |
| 93 | + | return false; | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | if (headerVersion > 2) { | |
| 97 | + | setLastErrorString("Unknown OTBM version detected."); | |
| 98 | + | return false; | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | if (root_header.majorVersionItems < 3) { | |
| 102 | + | setLastErrorString("This map need to be upgraded by using the latest map editor version to be able to load correctly."); | |
| 103 | + | return false; | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | if (root_header.majorVersionItems > Item::items.majorVersion) { | |
| 107 | + | setLastErrorString("The map was saved with a different items.otb version, an upgraded items.otb is required."); | |
| 108 | + | return false; | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | if (root_header.minorVersionItems < CLIENT_VERSION_810) { | |
| 112 | + | setLastErrorString("This map needs to be updated."); | |
| 113 | + | return false; | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | if (root_header.minorVersionItems > Item::items.minorVersion) { | |
| 117 | + | std::cout << "[Warning - IOMap::loadMap] This map needs an updated items.otb." << std::endl; | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | std::cout << "> Map size: " << root_header.width << "x" << root_header.height << '.' << std::endl; | |
| 121 | + | map->width = root_header.width; | |
| 122 | + | map->height = root_header.height; | |
| 123 | + | ||
| 124 | + | if (root.children.size() != 1 || root.children[0].type != OTBM_MAP_DATA) { | |
| 125 | + | setLastErrorString("Could not read data node."); | |
| 126 | + | return false; | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | auto& mapNode = root.children[0]; | |
| 130 | + | if (!parseMapDataAttributes(loader, mapNode, *map, fileName)) { | |
| 131 | + | return false; | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | for (auto& mapDataNode : mapNode.children) { | |
| 135 | + | if (mapDataNode.type == OTBM_TILE_AREA) { | |
| 136 | + | if (!parseTileArea(loader, mapDataNode, *map)) { | |
| 137 | + | return false; | |
| 138 | + | } | |
| 139 | + | } else if (mapDataNode.type == OTBM_TOWNS) { | |
| 140 | + | if (!parseTowns(loader, mapDataNode, *map)) { | |
| 141 | + | return false; | |
| 142 | + | } | |
| 143 | + | } else if (mapDataNode.type == OTBM_WAYPOINTS && headerVersion > 1) { | |
| 144 | + | if (!parseWaypoints(loader, mapDataNode, *map)) { | |
| 145 | + | return false; | |
| 146 | + | } | |
| 147 | + | } else { | |
| 148 | + | setLastErrorString("Unknown map node."); | |
| 149 | + | return false; | |
| 150 | + | } | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | std::cout << "> Map loading time: " << (OTSYS_TIME() - start) / (1000.) << " seconds." << std::endl; | |
| 154 | + | return true; | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | bool IOMap::parseMapDataAttributes(OTB::Loader& loader, const OTB::Node& mapNode, Map& map, const std::string& fileName) | |
| 158 | + | { | |
| 159 | + | PropStream propStream; | |
| 160 | + | if (!loader.getProps(mapNode, propStream)) { | |
| 161 | + | setLastErrorString("Could not read map data attributes."); | |
| 162 | + | return false; | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | std::string mapDescription; | |
| 166 | + | std::string tmp; | |
| 167 | + | ||
| 168 | + | uint8_t attribute; | |
| 169 | + | while (propStream.read<uint8_t>(attribute)) { | |
| 170 | + | switch (attribute) { | |
| 171 | + | case OTBM_ATTR_DESCRIPTION: | |
| 172 | + | if (!propStream.readString(mapDescription)) { | |
| 173 | + | setLastErrorString("Invalid description tag."); | |
| 174 | + | return false; | |
| 175 | + | } | |
| 176 | + | break; | |
| 177 | + | ||
| 178 | + | case OTBM_ATTR_EXT_SPAWN_FILE: | |
| 179 | + | if (!propStream.readString(tmp)) { | |
| 180 | + | setLastErrorString("Invalid spawn tag."); | |
| 181 | + | return false; | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | map.spawnfile = fileName.substr(0, fileName.rfind('/') + 1); | |
| 185 | + | map.spawnfile += tmp; | |
| 186 | + | break; | |
| 187 | + | ||
| 188 | + | case OTBM_ATTR_EXT_HOUSE_FILE: | |
| 189 | + | if (!propStream.readString(tmp)) { | |
| 190 | + | setLastErrorString("Invalid house tag."); | |
| 191 | + | return false; | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | map.housefile = fileName.substr(0, fileName.rfind('/') + 1); | |
| 195 | + | map.housefile += tmp; | |
| 196 | + | break; | |
| 197 | + | ||
| 198 | + | default: | |
| 199 | + | setLastErrorString("Unknown header node."); | |
| 200 | + | return false; | |
| 201 | + | } | |
| 202 | + | } | |
| 203 | + | return true; | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | bool IOMap::parseTileArea(OTB::Loader& loader, const OTB::Node& tileAreaNode, Map& map) | |
| 207 | + | { | |
| 208 | + | PropStream propStream; | |
| 209 | + | if (!loader.getProps(tileAreaNode, propStream)) { | |
| 210 | + | setLastErrorString("Invalid map node."); | |
| 211 | + | return false; | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | OTBM_Destination_coords area_coord; | |
| 215 | + | if (!propStream.read(area_coord)) { | |
| 216 | + | setLastErrorString("Invalid map node."); | |
| 217 | + | return false; | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | uint16_t base_x = area_coord.x; | |
| 221 | + | uint16_t base_y = area_coord.y; | |
| 222 | + | uint16_t z = area_coord.z; | |
| 223 | + | ||
| 224 | + | for (auto& tileNode : tileAreaNode.children) { | |
| 225 | + | if (tileNode.type != OTBM_TILE && tileNode.type != OTBM_HOUSETILE) { | |
| 226 | + | setLastErrorString("Unknown tile node."); | |
| 227 | + | return false; | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | if (!loader.getProps(tileNode, propStream)) { | |
| 231 | + | setLastErrorString("Could not read node data."); | |
| 232 | + | return false; | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | OTBM_Tile_coords tile_coord; | |
| 236 | + | if (!propStream.read(tile_coord)) { | |
| 237 | + | setLastErrorString("Could not read tile position."); | |
| 238 | + | return false; | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | uint16_t x = base_x + tile_coord.x; | |
| 242 | + | uint16_t y = base_y + tile_coord.y; | |
| 243 | + | ||
| 244 | + | bool isHouseTile = false; | |
| 245 | + | House* house = nullptr; | |
| 246 | + | Tile* tile = nullptr; | |
| 247 | + | Item* ground_item = nullptr; | |
| 248 | + | uint32_t tileflags = TILESTATE_NONE; | |
| 249 | + | ||
| 250 | + | if (tileNode.type == OTBM_HOUSETILE) { | |
| 251 | + | uint32_t houseId; | |
| 252 | + | if (!propStream.read<uint32_t>(houseId)) { | |
| 253 | + | std::ostringstream ss; | |
| 254 | + | ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Could not read house id."; | |
| 255 | + | setLastErrorString(ss.str()); | |
| 256 | + | return false; | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | house = map.houses.addHouse(houseId); | |
| 260 | + | if (!house) { | |
| 261 | + | std::ostringstream ss; | |
| 262 | + | ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Could not create house id: " << houseId; | |
| 263 | + | setLastErrorString(ss.str()); | |
| 264 | + | return false; | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | tile = new HouseTile(x, y, z, house); | |
| 268 | + | house->addTile(static_cast<HouseTile*>(tile)); | |
| 269 | + | isHouseTile = true; | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | uint8_t attribute; | |
| 273 | + | //read tile attributes | |
| 274 | + | while (propStream.read<uint8_t>(attribute)) { | |
| 275 | + | switch (attribute) { | |
| 276 | + | case OTBM_ATTR_TILE_FLAGS: { | |
| 277 | + | uint32_t flags; | |
| 278 | + | if (!propStream.read<uint32_t>(flags)) { | |
| 279 | + | std::ostringstream ss; | |
| 280 | + | ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Failed to read tile flags."; | |
| 281 | + | setLastErrorString(ss.str()); | |
| 282 | + | return false; | |
| 283 | + | } | |
| 284 | + | ||
| 285 | + | if ((flags & OTBM_TILEFLAG_PROTECTIONZONE) != 0) { | |
| 286 | + | tileflags |= TILESTATE_PROTECTIONZONE; | |
| 287 | + | } else if ((flags & OTBM_TILEFLAG_NOPVPZONE) != 0) { | |
| 288 | + | tileflags |= TILESTATE_NOPVPZONE; | |
| 289 | + | } else if ((flags & OTBM_TILEFLAG_PVPZONE) != 0) { | |
| 290 | + | tileflags |= TILESTATE_PVPZONE; | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | if ((flags & OTBM_TILEFLAG_NOLOGOUT) != 0) { | |
| 294 | + | tileflags |= TILESTATE_NOLOGOUT; | |
| 295 | + | } | |
| 296 | + | break; | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | case OTBM_ATTR_ITEM: { | |
| 300 | + | Item* item = Item::CreateItem(propStream); | |
| 301 | + | if (!item) { | |
| 302 | + | std::ostringstream ss; | |
| 303 | + | ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Failed to create item."; | |
| 304 | + | setLastErrorString(ss.str()); | |
| 305 | + | return false; | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | if (isHouseTile && item->isMoveable()) { | |
| 309 | + | std::cout << "[Warning - IOMap::loadMap] Moveable item with ID: " << item->getID() << ", in house: " << house->getId() << ", at position [x: " << x << ", y: " << y << ", z: " << z << "]." << std::endl; | |
| 310 | + | delete item; | |
| 311 | + | } else { | |
| 312 | + | if (item->getItemCount() == 0) { | |
| 313 | + | item->setItemCount(1); | |
| 314 | + | } | |
| 315 | + | ||
| 316 | + | if (tile) { | |
| 317 | + | tile->internalAddThing(item); | |
| 318 | + | item->startDecaying(); | |
| 319 | + | item->setLoadedFromMap(true); | |
| 320 | + | } else if (item->isGroundTile()) { | |
| 321 | + | delete ground_item; | |
| 322 | + | ground_item = item; | |
| 323 | + | } else { | |
| 324 | + | tile = createTile(ground_item, item, x, y, z); | |
| 325 | + | tile->internalAddThing(item); | |
| 326 | + | item->startDecaying(); | |
| 327 | + | item->setLoadedFromMap(true); | |
| 328 | + | } | |
| 329 | + | } | |
| 330 | + | break; | |
| 331 | + | } | |
| 332 | + | ||
| 333 | + | default: | |
| 334 | + | std::ostringstream ss; | |
| 335 | + | ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Unknown tile attribute."; | |
| 336 | + | setLastErrorString(ss.str()); | |
| 337 | + | return false; | |
| 338 | + | } | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | for (auto& itemNode : tileNode.children) { | |
| 342 | + | if (itemNode.type != OTBM_ITEM) { | |
| 343 | + | std::ostringstream ss; | |
| 344 | + | ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Unknown node type."; | |
| 345 | + | setLastErrorString(ss.str()); | |
| 346 | + | return false; | |
| 347 | + | } | |
| 348 | + | ||
| 349 | + | PropStream stream; | |
| 350 | + | if (!loader.getProps(itemNode, stream)) { | |
| 351 | + | setLastErrorString("Invalid item node."); | |
| 352 | + | return false; | |
| 353 | + | } | |
| 354 | + | ||
| 355 | + | Item* item = Item::CreateItem(stream); | |
| 356 | + | if (!item) { | |
| 357 | + | std::ostringstream ss; | |
| 358 | + | ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Failed to create item."; | |
| 359 | + | setLastErrorString(ss.str()); | |
| 360 | + | return false; | |
| 361 | + | } | |
| 362 | + | ||
| 363 | + | if (!item->unserializeItemNode(loader, itemNode, stream)) { | |
| 364 | + | std::ostringstream ss; | |
| 365 | + | ss << "[x:" << x << ", y:" << y << ", z:" << z << "] Failed to load item " << item->getID() << '.'; | |
| 366 | + | setLastErrorString(ss.str()); | |
| 367 | + | delete item; | |
| 368 | + | return false; | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | if (isHouseTile && item->isMoveable()) { | |
| 372 | + | std::cout << "[Warning - IOMap::loadMap] Moveable item with ID: " << item->getID() << ", in house: " << house->getId() << ", at position [x: " << x << ", y: " << y << ", z: " << z << "]." << std::endl; | |
| 373 | + | delete item; | |
| 374 | + | } else { | |
| 375 | + | if (item->getItemCount() == 0) { | |
| 376 | + | item->setItemCount(1); | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | if (tile) { | |
| 380 | + | tile->internalAddThing(item); | |
| 381 | + | item->startDecaying(); | |
| 382 | + | item->setLoadedFromMap(true); | |
| 383 | + | } else if (item->isGroundTile()) { | |
| 384 | + | delete ground_item; | |
| 385 | + | ground_item = item; | |
| 386 | + | } else { | |
| 387 | + | tile = createTile(ground_item, item, x, y, z); | |
| 388 | + | tile->internalAddThing(item); | |
| 389 | + | item->startDecaying(); | |
| 390 | + | item->setLoadedFromMap(true); | |
| 391 | + | } | |
| 392 | + | } | |
| 393 | + | } | |
| 394 | + | ||
| 395 | + | if (!tile) { | |
| 396 | + | tile = createTile(ground_item, nullptr, x, y, z); | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | tile->setFlag(static_cast<tileflags_t>(tileflags)); | |
| 400 | + | ||
| 401 | + | map.setTile(x, y, z, tile); | |
| 402 | + | } | |
| 403 | + | return true; | |
| 404 | + | } | |
| 405 | + | ||
| 406 | + | bool IOMap::parseTowns(OTB::Loader& loader, const OTB::Node& townsNode, Map& map) | |
| 407 | + | { | |
| 408 | + | for (auto& townNode : townsNode.children) { | |
| 409 | + | PropStream propStream; | |
| 410 | + | if (townNode.type != OTBM_TOWN) { | |
| 411 | + | setLastErrorString("Unknown town node."); | |
| 412 | + | return false; | |
| 413 | + | } | |
| 414 | + | ||
| 415 | + | if (!loader.getProps(townNode, propStream)) { | |
| 416 | + | setLastErrorString("Could not read town data."); | |
| 417 | + | return false; | |
| 418 | + | } | |
| 419 | + | ||
| 420 | + | uint32_t townId; | |
| 421 | + | if (!propStream.read<uint32_t>(townId)) { | |
| 422 | + | setLastErrorString("Could not read town id."); | |
| 423 | + | return false; | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | Town* town = map.towns.getTown(townId); | |
| 427 | + | if (!town) { | |
| 428 | + | town = new Town(townId); | |
| 429 | + | map.towns.addTown(townId, town); | |
| 430 | + | } | |
| 431 | + | ||
| 432 | + | std::string townName; | |
| 433 | + | if (!propStream.readString(townName)) { | |
| 434 | + | setLastErrorString("Could not read town name."); | |
| 435 | + | return false; | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | town->setName(townName); | |
| 439 | + | ||
| 440 | + | OTBM_Destination_coords town_coords; | |
| 441 | + | if (!propStream.read(town_coords)) { | |
| 442 | + | setLastErrorString("Could not read town coordinates."); | |
| 443 | + | return false; | |
| 444 | + | } | |
| 445 | + | ||
| 446 | + | town->setTemplePos(Position(town_coords.x, town_coords.y, town_coords.z)); | |
| 447 | + | } | |
| 448 | + | return true; | |
| 449 | + | } | |
| 450 | + | ||
| 451 | + | ||
| 452 | + | bool IOMap::parseWaypoints(OTB::Loader& loader, const OTB::Node& waypointsNode, Map& map) | |
| 453 | + | { | |
| 454 | + | PropStream propStream; | |
| 455 | + | for (auto& node : waypointsNode.children) { | |
| 456 | + | if (node.type != OTBM_WAYPOINT) { | |
| 457 | + | setLastErrorString("Unknown waypoint node."); | |
| 458 | + | return false; | |
| 459 | + | } | |
| 460 | + | ||
| 461 | + | if (!loader.getProps(node, propStream)) { | |
| 462 | + | setLastErrorString("Could not read waypoint data."); | |
| 463 | + | return false; | |
| 464 | + | } | |
| 465 | + | ||
| 466 | + | std::string name; | |
| 467 | + | if (!propStream.readString(name)) { | |
| 468 | + | setLastErrorString("Could not read waypoint name."); | |
| 469 | + | return false; | |
| 470 | + | } | |
| 471 | + | ||
| 472 | + | OTBM_Destination_coords waypoint_coords; | |
| 473 | + | if (!propStream.read(waypoint_coords)) { | |
| 474 | + | setLastErrorString("Could not read waypoint coordinates."); | |
| 475 | + | return false; | |
| 476 | + | } | |
| 477 | + | ||
| 478 | + | map.waypoints[name] = Position(waypoint_coords.x, waypoint_coords.y, waypoint_coords.z); | |
| 479 | + | } | |
| 480 | + | return true; | |
| 481 | + | } | |
| 482 | + |
| @@ -0,0 +1,158 @@ | |||
| 1 | + | /** | |
| 2 | + | * The Forgotten Server - a free and open-source MMORPG server emulator | |
| 3 | + | * Copyright (C) 2019 Mark Samman <mark.samman@gmail.com> | |
| 4 | + | * | |
| 5 | + | * This program is free software; you can redistribute it and/or modify | |
| 6 | + | * it under the terms of the GNU General Public License as published by | |
| 7 | + | * the Free Software Foundation; either version 2 of the License, or | |
| 8 | + | * (at your option) any later version. | |
| 9 | + | * | |
| 10 | + | * This program is distributed in the hope that it will be useful, | |
| 11 | + | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 12 | + | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 13 | + | * GNU General Public License for more details. | |
| 14 | + | * | |
| 15 | + | * You should have received a copy of the GNU General Public License along | |
| 16 | + | * with this program; if not, write to the Free Software Foundation, Inc., | |
| 17 | + | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| 18 | + | */ | |
| 19 | + | ||
| 20 | + | #ifndef FS_IOMAP_H_8085D4B1037A44288494A52FDBB775E4 | |
| 21 | + | #define FS_IOMAP_H_8085D4B1037A44288494A52FDBB775E4 | |
| 22 | + | ||
| 23 | + | #include "item.h" | |
| 24 | + | #include "map.h" | |
| 25 | + | #include "house.h" | |
| 26 | + | #include "spawn.h" | |
| 27 | + | #include "configmanager.h" | |
| 28 | + | ||
| 29 | + | extern ConfigManager g_config; | |
| 30 | + | ||
| 31 | + | enum OTBM_AttrTypes_t { | |
| 32 | + | OTBM_ATTR_DESCRIPTION = 1, | |
| 33 | + | OTBM_ATTR_EXT_FILE = 2, | |
| 34 | + | OTBM_ATTR_TILE_FLAGS = 3, | |
| 35 | + | OTBM_ATTR_ACTION_ID = 4, | |
| 36 | + | OTBM_ATTR_UNIQUE_ID = 5, | |
| 37 | + | OTBM_ATTR_TEXT = 6, | |
| 38 | + | OTBM_ATTR_DESC = 7, | |
| 39 | + | OTBM_ATTR_TELE_DEST = 8, | |
| 40 | + | OTBM_ATTR_ITEM = 9, | |
| 41 | + | OTBM_ATTR_DEPOT_ID = 10, | |
| 42 | + | OTBM_ATTR_EXT_SPAWN_FILE = 11, | |
| 43 | + | OTBM_ATTR_RUNE_CHARGES = 12, | |
| 44 | + | OTBM_ATTR_EXT_HOUSE_FILE = 13, | |
| 45 | + | OTBM_ATTR_HOUSEDOORID = 14, | |
| 46 | + | OTBM_ATTR_COUNT = 15, | |
| 47 | + | OTBM_ATTR_DURATION = 16, | |
| 48 | + | OTBM_ATTR_DECAYING_STATE = 17, | |
| 49 | + | OTBM_ATTR_WRITTENDATE = 18, | |
| 50 | + | OTBM_ATTR_WRITTENBY = 19, | |
| 51 | + | OTBM_ATTR_SLEEPERGUID = 20, | |
| 52 | + | OTBM_ATTR_SLEEPSTART = 21, | |
| 53 | + | OTBM_ATTR_CHARGES = 22, | |
| 54 | + | }; | |
| 55 | + | ||
| 56 | + | enum OTBM_NodeTypes_t { | |
| 57 | + | OTBM_ROOTV1 = 1, | |
| 58 | + | OTBM_MAP_DATA = 2, | |
| 59 | + | OTBM_ITEM_DEF = 3, | |
| 60 | + | OTBM_TILE_AREA = 4, | |
| 61 | + | OTBM_TILE = 5, | |
| 62 | + | OTBM_ITEM = 6, | |
| 63 | + | OTBM_TILE_SQUARE = 7, | |
| 64 | + | OTBM_TILE_REF = 8, | |
| 65 | + | OTBM_SPAWNS = 9, | |
| 66 | + | OTBM_SPAWN_AREA = 10, | |
| 67 | + | OTBM_MONSTER = 11, | |
| 68 | + | OTBM_TOWNS = 12, | |
| 69 | + | OTBM_TOWN = 13, | |
| 70 | + | OTBM_HOUSETILE = 14, | |
| 71 | + | OTBM_WAYPOINTS = 15, | |
| 72 | + | OTBM_WAYPOINT = 16, | |
| 73 | + | }; | |
| 74 | + | ||
| 75 | + | enum OTBM_TileFlag_t : uint32_t { | |
| 76 | + | OTBM_TILEFLAG_PROTECTIONZONE = 1 << 0, | |
| 77 | + | OTBM_TILEFLAG_NOPVPZONE = 1 << 2, | |
| 78 | + | OTBM_TILEFLAG_NOLOGOUT = 1 << 3, | |
| 79 | + | OTBM_TILEFLAG_PVPZONE = 1 << 4 | |
| 80 | + | }; | |
| 81 | + | ||
| 82 | + | #pragma pack(1) | |
| 83 | + | ||
| 84 | + | struct OTBM_root_header { | |
| 85 | + | uint32_t version; | |
| 86 | + | uint16_t width; | |
| 87 | + | uint16_t height; | |
| 88 | + | uint32_t majorVersionItems; | |
| 89 | + | uint32_t minorVersionItems; | |
| 90 | + | }; | |
| 91 | + | ||
| 92 | + | struct OTBM_Destination_coords { | |
| 93 | + | uint16_t x; | |
| 94 | + | uint16_t y; | |
| 95 | + | uint8_t z; | |
| 96 | + | }; | |
| 97 | + | ||
| 98 | + | struct OTBM_Tile_coords { | |
| 99 | + | uint8_t x; | |
| 100 | + | uint8_t y; | |
| 101 | + | }; | |
| 102 | + | ||
| 103 | + | #pragma pack() | |
| 104 | + | ||
| 105 | + | class IOMap | |
| 106 | + | { | |
| 107 | + | static Tile* createTile(Item*& ground, Item* item, uint16_t x, uint16_t y, uint8_t z); | |
| 108 | + | ||
| 109 | + | public: | |
| 110 | + | bool loadMap(Map* map, const std::string& fileName); | |
| 111 | + | ||
| 112 | + | /* Load the spawns | |
| 113 | + | * \param map pointer to the Map class | |
| 114 | + | * \returns Returns true if the spawns were loaded successfully | |
| 115 | + | */ | |
| 116 | + | static bool loadSpawns(Map* map) { | |
| 117 | + | if (map->spawnfile.empty()) { | |
| 118 | + | //OTBM file doesn't tell us about the spawnfile, | |
| 119 | + | //lets guess it is mapname-spawn.xml. | |
| 120 | + | map->spawnfile = g_config.getString(ConfigManager::MAP_NAME); | |
| 121 | + | map->spawnfile += "-spawn.xml"; | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | return map->spawns.loadFromXml(map->spawnfile); | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | /* Load the houses (not house tile-data) | |
| 128 | + | * \param map pointer to the Map class | |
| 129 | + | * \returns Returns true if the houses were loaded successfully | |
| 130 | + | */ | |
| 131 | + | static bool loadHouses(Map* map) { | |
| 132 | + | if (map->housefile.empty()) { | |
| 133 | + | //OTBM file doesn't tell us about the housefile, | |
| 134 | + | //lets guess it is mapname-house.xml. | |
| 135 | + | map->housefile = g_config.getString(ConfigManager::MAP_NAME); | |
| 136 | + | map->housefile += "-house.xml"; | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | return map->houses.loadHousesXML(map->housefile); | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | const std::string& getLastErrorString() const { | |
| 143 | + | return errorString; | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | void setLastErrorString(std::string error) { | |
| 147 | + | errorString = error; | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | private: | |
| 151 | + | bool parseMapDataAttributes(OTB::Loader& loader, const OTB::Node& mapNode, Map& map, const std::string& fileName); | |
| 152 | + | bool parseWaypoints(OTB::Loader& loader, const OTB::Node& waypointsNode, Map& map); | |
| 153 | + | bool parseTowns(OTB::Loader& loader, const OTB::Node& townsNode, Map& map); | |
| 154 | + | bool parseTileArea(OTB::Loader& loader, const OTB::Node& tileAreaNode, Map& map); | |
| 155 | + | std::string errorString; | |
| 156 | + | }; | |
| 157 | + | ||
| 158 | + | #endif |
| @@ -0,0 +1,372 @@ | |||
| 1 | + | /** | |
| 2 | + | * The Forgotten Server - a free and open-source MMORPG server emulator | |
| 3 | + | * Copyright (C) 2019 Mark Samman <mark.samman@gmail.com> | |
| 4 | + | * | |
| 5 | + | * This program is free software; you can redistribute it and/or modify | |
| 6 | + | * it under the terms of the GNU General Public License as published by | |
| 7 | + | * the Free Software Foundation; either version 2 of the License, or | |
| 8 | + | * (at your option) any later version. | |
| 9 | + | * | |
| 10 | + | * This program is distributed in the hope that it will be useful, | |
| 11 | + | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 12 | + | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 13 | + | * GNU General Public License for more details. | |
| 14 | + | * | |
| 15 | + | * You should have received a copy of the GNU General Public License along | |
| 16 | + | * with this program; if not, write to the Free Software Foundation, Inc., | |
| 17 | + | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| 18 | + | */ | |
| 19 | + | ||
| 20 | + | #include "otpch.h" | |
| 21 | + | ||
| 22 | + | #include "iomapserialize.h" | |
| 23 | + | #include "game.h" | |
| 24 | + | #include "bed.h" | |
| 25 | + | ||
| 26 | + | extern Game g_game; | |
| 27 | + | ||
| 28 | + | void IOMapSerialize::loadHouseItems(Map* map) | |
| 29 | + | { | |
| 30 | + | int64_t start = OTSYS_TIME(); | |
| 31 | + | ||
| 32 | + | DBResult_ptr result = Database::getInstance().storeQuery("SELECT `data` FROM `tile_store`"); | |
| 33 | + | if (!result) { | |
| 34 | + | return; | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | do { | |
| 38 | + | unsigned long attrSize; | |
| 39 | + | const char* attr = result->getStream("data", attrSize); | |
| 40 | + | ||
| 41 | + | PropStream propStream; | |
| 42 | + | propStream.init(attr, attrSize); | |
| 43 | + | ||
| 44 | + | uint16_t x, y; | |
| 45 | + | uint8_t z; | |
| 46 | + | if (!propStream.read<uint16_t>(x) || !propStream.read<uint16_t>(y) || !propStream.read<uint8_t>(z)) { | |
| 47 | + | continue; | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | Tile* tile = map->getTile(x, y, z); | |
| 51 | + | if (!tile) { | |
| 52 | + | continue; | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | uint32_t item_count; | |
| 56 | + | if (!propStream.read<uint32_t>(item_count)) { | |
| 57 | + | continue; | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | while (item_count--) { | |
| 61 | + | loadItem(propStream, tile); | |
| 62 | + | } | |
| 63 | + | } while (result->next()); | |
| 64 | + | std::cout << "> Loaded house items in: " << (OTSYS_TIME() - start) / (1000.) << " s" << std::endl; | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | bool IOMapSerialize::saveHouseItems() | |
| 68 | + | { | |
| 69 | + | int64_t start = OTSYS_TIME(); | |
| 70 | + | Database& db = Database::getInstance(); | |
| 71 | + | std::ostringstream query; | |
| 72 | + | ||
| 73 | + | //Start the transaction | |
| 74 | + | DBTransaction transaction; | |
| 75 | + | if (!transaction.begin()) { | |
| 76 | + | return false; | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | //clear old tile data | |
| 80 | + | if (!db.executeQuery("DELETE FROM `tile_store`")) { | |
| 81 | + | return false; | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | DBInsert stmt("INSERT INTO `tile_store` (`house_id`, `data`) VALUES "); | |
| 85 | + | ||
| 86 | + | PropWriteStream stream; | |
| 87 | + | for (const auto& it : g_game.map.houses.getHouses()) { | |
| 88 | + | //save house items | |
| 89 | + | House* house = it.second; | |
| 90 | + | for (HouseTile* tile : house->getTiles()) { | |
| 91 | + | saveTile(stream, tile); | |
| 92 | + | ||
| 93 | + | size_t attributesSize; | |
| 94 | + | const char* attributes = stream.getStream(attributesSize); | |
| 95 | + | if (attributesSize > 0) { | |
| 96 | + | query << house->getId() << ',' << db.escapeBlob(attributes, attributesSize); | |
| 97 | + | if (!stmt.addRow(query)) { | |
| 98 | + | return false; | |
| 99 | + | } | |
| 100 | + | stream.clear(); | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | if (!stmt.execute()) { | |
| 106 | + | return false; | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | //End the transaction | |
| 110 | + | bool success = transaction.commit(); | |
| 111 | + | std::cout << "> Saved house items in: " << | |
| 112 | + | (OTSYS_TIME() - start) / (1000.) << " s" << std::endl; | |
| 113 | + | return success; | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | bool IOMapSerialize::loadContainer(PropStream& propStream, Container* container) | |
| 117 | + | { | |
| 118 | + | while (container->serializationCount > 0) { | |
| 119 | + | if (!loadItem(propStream, container)) { | |
| 120 | + | std::cout << "[Warning - IOMapSerialize::loadContainer] Unserialization error for container item: " << container->getID() << std::endl; | |
| 121 | + | return false; | |
| 122 | + | } | |
| 123 | + | container->serializationCount--; | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | uint8_t endAttr; | |
| 127 | + | if (!propStream.read<uint8_t>(endAttr) || endAttr != 0) { | |
| 128 | + | std::cout << "[Warning - IOMapSerialize::loadContainer] Unserialization error for container item: " << container->getID() << std::endl; | |
| 129 | + | return false; | |
| 130 | + | } | |
| 131 | + | return true; | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | bool IOMapSerialize::loadItem(PropStream& propStream, Cylinder* parent) | |
| 135 | + | { | |
| 136 | + | uint16_t id; | |
| 137 | + | if (!propStream.read<uint16_t>(id)) { | |
| 138 | + | return false; | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | Tile* tile = nullptr; | |
| 142 | + | if (parent->getParent() == nullptr) { | |
| 143 | + | tile = parent->getTile(); | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | const ItemType& iType = Item::items[id]; | |
| 147 | + | if (iType.moveable || iType.forceSerialize || !tile) { | |
| 148 | + | //create a new item | |
| 149 | + | Item* item = Item::CreateItem(id); | |
| 150 | + | if (item) { | |
| 151 | + | if (item->unserializeAttr(propStream)) { | |
| 152 | + | Container* container = item->getContainer(); | |
| 153 | + | if (container && !loadContainer(propStream, container)) { | |
| 154 | + | delete item; | |
| 155 | + | return false; | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | parent->internalAddThing(item); | |
| 159 | + | item->startDecaying(); | |
| 160 | + | } else { | |
| 161 | + | std::cout << "WARNING: Unserialization error in IOMapSerialize::loadItem()" << id << std::endl; | |
| 162 | + | delete item; | |
| 163 | + | return false; | |
| 164 | + | } | |
| 165 | + | } | |
| 166 | + | } else { | |
| 167 | + | // Stationary items like doors/beds/blackboards/bookcases | |
| 168 | + | Item* item = nullptr; | |
| 169 | + | if (const TileItemVector* items = tile->getItemList()) { | |
| 170 | + | for (Item* findItem : *items) { | |
| 171 | + | if (findItem->getID() == id) { | |
| 172 | + | item = findItem; | |
| 173 | + | break; | |
| 174 | + | } else if (iType.isDoor() && findItem->getDoor()) { | |
| 175 | + | item = findItem; | |
| 176 | + | break; | |
| 177 | + | } else if (iType.isBed() && findItem->getBed()) { | |
| 178 | + | item = findItem; | |
| 179 | + | break; | |
| 180 | + | } | |
| 181 | + | } | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | if (item) { | |
| 185 | + | if (item->unserializeAttr(propStream)) { | |
| 186 | + | Container* container = item->getContainer(); | |
| 187 | + | if (container && !loadContainer(propStream, container)) { | |
| 188 | + | return false; | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | g_game.transformItem(item, id); | |
| 192 | + | } else { | |
| 193 | + | std::cout << "WARNING: Unserialization error in IOMapSerialize::loadItem()" << id << std::endl; | |
| 194 | + | } | |
| 195 | + | } else { | |
| 196 | + | //The map changed since the last save, just read the attributes | |
| 197 | + | std::unique_ptr<Item> dummy(Item::CreateItem(id)); | |
| 198 | + | if (dummy) { | |
| 199 | + | dummy->unserializeAttr(propStream); | |
| 200 | + | Container* container = dummy->getContainer(); | |
| 201 | + | if (container) { | |
| 202 | + | if (!loadContainer(propStream, container)) { | |
| 203 | + | return false; | |
| 204 | + | } | |
| 205 | + | } else if (BedItem* bedItem = dynamic_cast<BedItem*>(dummy.get())) { | |
| 206 | + | uint32_t sleeperGUID = bedItem->getSleeper(); | |
| 207 | + | if (sleeperGUID != 0) { | |
| 208 | + | g_game.removeBedSleeper(sleeperGUID); | |
| 209 | + | } | |
| 210 | + | } | |
| 211 | + | } | |
| 212 | + | } | |
| 213 | + | } | |
| 214 | + | return true; | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | void IOMapSerialize::saveItem(PropWriteStream& stream, const Item* item) | |
| 218 | + | { | |
| 219 | + | const Container* container = item->getContainer(); | |
| 220 | + | ||
| 221 | + | // Write ID & props | |
| 222 | + | stream.write<uint16_t>(item->getID()); | |
| 223 | + | item->serializeAttr(stream); | |
| 224 | + | ||
| 225 | + | if (container) { | |
| 226 | + | // Hack our way into the attributes | |
| 227 | + | stream.write<uint8_t>(ATTR_CONTAINER_ITEMS); | |
| 228 | + | stream.write<uint32_t>(container->size()); | |
| 229 | + | for (auto it = container->getReversedItems(), end = container->getReversedEnd(); it != end; ++it) { | |
| 230 | + | saveItem(stream, *it); | |
| 231 | + | } | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | stream.write<uint8_t>(0x00); // attr end | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | void IOMapSerialize::saveTile(PropWriteStream& stream, const Tile* tile) | |
| 238 | + | { | |
| 239 | + | const TileItemVector* tileItems = tile->getItemList(); | |
| 240 | + | if (!tileItems) { | |
| 241 | + | return; | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | std::forward_list<Item*> items; | |
| 245 | + | uint16_t count = 0; | |
| 246 | + | for (Item* item : *tileItems) { | |
| 247 | + | const ItemType& it = Item::items[item->getID()]; | |
| 248 | + | ||
| 249 | + | // Note that these are NEGATED, ie. these are the items that will be saved. | |
| 250 | + | if (!(it.moveable || it.forceSerialize || item->getDoor() || (item->getContainer() && !item->getContainer()->empty()) || it.canWriteText || item->getBed())) { | |
| 251 | + | continue; | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | items.push_front(item); | |
| 255 | + | ++count; | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | if (!items.empty()) { | |
| 259 | + | const Position& tilePosition = tile->getPosition(); | |
| 260 | + | stream.write<uint16_t>(tilePosition.x); | |
| 261 | + | stream.write<uint16_t>(tilePosition.y); | |
| 262 | + | stream.write<uint8_t>(tilePosition.z); | |
| 263 | + | ||
| 264 | + | stream.write<uint32_t>(count); | |
| 265 | + | for (const Item* item : items) { | |
| 266 | + | saveItem(stream, item); | |
| 267 | + | } | |
| 268 | + | } | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | bool IOMapSerialize::loadHouseInfo() | |
| 272 | + | { | |
| 273 | + | Database& db = Database::getInstance(); | |
| 274 | + | ||
| 275 | + | DBResult_ptr result = db.storeQuery("SELECT `id`, `owner`, `paid`, `warnings` FROM `houses`"); | |
| 276 | + | if (!result) { | |
| 277 | + | return false; | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | do { | |
| 281 | + | House* house = g_game.map.houses.getHouse(result->getNumber<uint32_t>("id")); | |
| 282 | + | if (house) { | |
| 283 | + | house->setOwner(result->getNumber<uint32_t>("owner"), false); | |
| 284 | + | house->setPaidUntil(result->getNumber<time_t>("paid")); | |
| 285 | + | house->setPayRentWarnings(result->getNumber<uint32_t>("warnings")); | |
| 286 | + | } | |
| 287 | + | } while (result->next()); | |
| 288 | + | ||
| 289 | + | result = db.storeQuery("SELECT `house_id`, `listid`, `list` FROM `house_lists`"); | |
| 290 | + | if (result) { | |
| 291 | + | do { | |
| 292 | + | House* house = g_game.map.houses.getHouse(result->getNumber<uint32_t>("house_id")); | |
| 293 | + | if (house) { | |
| 294 | + | house->setAccessList(result->getNumber<uint32_t>("listid"), result->getString("list")); | |
| 295 | + | } | |
| 296 | + | } while (result->next()); | |
| 297 | + | } | |
| 298 | + | return true; | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | bool IOMapSerialize::saveHouseInfo() | |
| 302 | + | { | |
| 303 | + | Database& db = Database::getInstance(); | |
| 304 | + | ||
| 305 | + | DBTransaction transaction; | |
| 306 | + | if (!transaction.begin()) { | |
| 307 | + | return false; | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | if (!db.executeQuery("DELETE FROM `house_lists`")) { | |
| 311 | + | return false; | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | std::ostringstream query; | |
| 315 | + | for (const auto& it : g_game.map.houses.getHouses()) { | |
| 316 | + | House* house = it.second; | |
| 317 | + | query << "SELECT `id` FROM `houses` WHERE `id` = " << house->getId(); | |
| 318 | + | DBResult_ptr result = db.storeQuery(query.str()); | |
| 319 | + | if (result) { | |
| 320 | + | query.str(std::string()); | |
| 321 | + | query << "UPDATE `houses` SET `owner` = " << house->getOwner() << ", `paid` = " << house->getPaidUntil() << ", `warnings` = " << house->getPayRentWarnings() << ", `name` = " << db.escapeString(house->getName()) << ", `town_id` = " << house->getTownId() << ", `rent` = " << house->getRent() << ", `size` = " << house->getTiles().size() << ", `beds` = " << house->getBedCount() << " WHERE `id` = " << house->getId(); | |
| 322 | + | } else { | |
| 323 | + | query.str(std::string()); | |
| 324 | + | query << "INSERT INTO `houses` (`id`, `owner`, `paid`, `warnings`, `name`, `town_id`, `rent`, `size`, `beds`) VALUES (" << house->getId() << ',' << house->getOwner() << ',' << house->getPaidUntil() << ',' << house->getPayRentWarnings() << ',' << db.escapeString(house->getName()) << ',' << house->getTownId() << ',' << house->getRent() << ',' << house->getTiles().size() << ',' << house->getBedCount() << ')'; | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | db.executeQuery(query.str()); | |
| 328 | + | query.str(std::string()); | |
| 329 | + | } | |
| 330 | + | ||
| 331 | + | DBInsert stmt("INSERT INTO `house_lists` (`house_id` , `listid` , `list`) VALUES "); | |
| 332 | + | ||
| 333 | + | for (const auto& it : g_game.map.houses.getHouses()) { | |
| 334 | + | House* house = it.second; | |
| 335 | + | ||
| 336 | + | std::string listText; | |
| 337 | + | if (house->getAccessList(GUEST_LIST, listText) && !listText.empty()) { | |
| 338 | + | query << house->getId() << ',' << GUEST_LIST << ',' << db.escapeString(listText); | |
| 339 | + | if (!stmt.addRow(query)) { | |
| 340 | + | return false; | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | listText.clear(); | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | if (house->getAccessList(SUBOWNER_LIST, listText) && !listText.empty()) { | |
| 347 | + | query << house->getId() << ',' << SUBOWNER_LIST << ',' << db.escapeString(listText); | |
| 348 | + | if (!stmt.addRow(query)) { | |
| 349 | + | return false; | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | listText.clear(); | |
| 353 | + | } | |
| 354 | + | ||
| 355 | + | for (Door* door : house->getDoors()) { | |
| 356 | + | if (door->getAccessList(listText) && !listText.empty()) { | |
| 357 | + | query << house->getId() << ',' << door->getDoorId() << ',' << db.escapeString(listText); | |
| 358 | + | if (!stmt.addRow(query)) { | |
| 359 | + | return false; | |
| 360 | + | } | |
| 361 | + | ||
| 362 | + | listText.clear(); | |
| 363 | + | } | |
| 364 | + | } | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | if (!stmt.execute()) { | |
| 368 | + | return false; | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | return transaction.commit(); | |
| 372 | + | } |
| @@ -0,0 +1,42 @@ | |||
| 1 | + | /** | |
| 2 | + | * The Forgotten Server - a free and open-source MMORPG server emulator | |
| 3 | + | * Copyright (C) 2019 Mark Samman <mark.samman@gmail.com> | |
| 4 | + | * | |
| 5 | + | * This program is free software; you can redistribute it and/or modify | |
| 6 | + | * it under the terms of the GNU General Public License as published by | |
| 7 | + | * the Free Software Foundation; either version 2 of the License, or | |
| 8 | + | * (at your option) any later version. | |
| 9 | + | * | |
| 10 | + | * This program is distributed in the hope that it will be useful, | |
| 11 | + | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 12 | + | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 13 | + | * GNU General Public License for more details. | |
| 14 | + | * | |
| 15 | + | * You should have received a copy of the GNU General Public License along | |
| 16 | + | * with this program; if not, write to the Free Software Foundation, Inc., | |
| 17 | + | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| 18 | + | */ | |
| 19 | + | ||
| 20 | + | #ifndef FS_IOMAPSERIALIZE_H_7E903658F34E44F9BE03A713B55A3D6D | |
| 21 | + | #define FS_IOMAPSERIALIZE_H_7E903658F34E44F9BE03A713B55A3D6D | |
| 22 | + | ||
| 23 | + | #include "database.h" | |
| 24 | + | #include "map.h" | |
| 25 | + | ||
| 26 | + | class IOMapSerialize | |
| 27 | + | { | |
| 28 | + | public: | |
| 29 | + | static void loadHouseItems(Map* map); | |
| 30 | + | static bool saveHouseItems(); | |
| 31 | + | static bool loadHouseInfo(); | |
| 32 | + | static bool saveHouseInfo(); | |
| 33 | + | ||
| 34 | + | private: | |
| 35 | + | static void saveItem(PropWriteStream& stream, const Item* item); | |
| 36 | + | static void saveTile(PropWriteStream& stream, const Tile* tile); | |
| 37 | + | ||
| 38 | + | static bool loadContainer(PropStream& propStream, Container* container); | |
| 39 | + | static bool loadItem(PropStream& propStream, Cylinder* parent); | |
| 40 | + | }; | |
| 41 | + | ||
| 42 | + | #endif |
| @@ -0,0 +1,348 @@ | |||
| 1 | + | /** | |
| 2 | + | * The Forgotten Server - a free and open-source MMORPG server emulator | |
| 3 | + | * Copyright (C) 2019 Mark Samman <mark.samman@gmail.com> | |
| 4 | + | * | |
| 5 | + | * This program is free software; you can redistribute it and/or modify | |
| 6 | + | * it under the terms of the GNU General Public License as published by | |
| 7 | + | * the Free Software Foundation; either version 2 of the License, or | |
| 8 | + | * (at your option) any later version. | |
| 9 | + | * | |
| 10 | + | * This program is distributed in the hope that it will be useful, | |
| 11 | + | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 12 | + | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 13 | + | * GNU General Public License for more details. | |
| 14 | + | * | |
| 15 | + | * You should have received a copy of the GNU General Public License along | |
| 16 | + | * with this program; if not, write to the Free Software Foundation, Inc., | |
| 17 | + | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| 18 | + | */ | |
| 19 | + | ||
| 20 | + | #include "otpch.h" | |
| 21 | + | ||
| 22 | + | #include "iomarket.h" | |
| 23 | + | ||
| 24 | + | #include "configmanager.h" | |
| 25 | + | #include "databasetasks.h" | |
| 26 | + | #include "iologindata.h" | |
| 27 | + | #include "game.h" | |
| 28 | + | #include "scheduler.h" | |
| 29 | + | ||
| 30 | + | extern ConfigManager g_config; | |
| 31 | + | extern Game g_game; | |
| 32 | + | ||
| 33 | + | MarketOfferList IOMarket::getActiveOffers(MarketAction_t action, uint16_t itemId) | |
| 34 | + | { | |
| 35 | + | MarketOfferList offerList; | |
| 36 | + | ||
| 37 | + | std::ostringstream query; | |
| 38 | + | query << "SELECT `id`, `amount`, `price`, `created`, `anonymous`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `player_name` FROM `market_offers` WHERE `sale` = " << action << " AND `itemtype` = " << itemId; | |
| 39 | + | ||
| 40 | + | DBResult_ptr result = Database::getInstance().storeQuery(query.str()); | |
| 41 | + | if (!result) { | |
| 42 | + | return offerList; | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | const int32_t marketOfferDuration = g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION); | |
| 46 | + | ||
| 47 | + | do { | |
| 48 | + | MarketOffer offer; | |
| 49 | + | offer.amount = result->getNumber<uint16_t>("amount"); | |
| 50 | + | offer.price = result->getNumber<uint32_t>("price"); | |
| 51 | + | offer.timestamp = result->getNumber<uint32_t>("created") + marketOfferDuration; | |
| 52 | + | offer.counter = result->getNumber<uint32_t>("id") & 0xFFFF; | |
| 53 | + | if (result->getNumber<uint16_t>("anonymous") == 0) { | |
| 54 | + | offer.playerName = result->getString("player_name"); | |
| 55 | + | } else { | |
| 56 | + | offer.playerName = "Anonymous"; | |
| 57 | + | } | |
| 58 | + | offerList.push_back(offer); | |
| 59 | + | } while (result->next()); | |
| 60 | + | return offerList; | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | MarketOfferList IOMarket::getOwnOffers(MarketAction_t action, uint32_t playerId) | |
| 64 | + | { | |
| 65 | + | MarketOfferList offerList; | |
| 66 | + | ||
| 67 | + | const int32_t marketOfferDuration = g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION); | |
| 68 | + | ||
| 69 | + | std::ostringstream query; | |
| 70 | + | query << "SELECT `id`, `amount`, `price`, `created`, `itemtype` FROM `market_offers` WHERE `player_id` = " << playerId << " AND `sale` = " << action; | |
| 71 | + | ||
| 72 | + | DBResult_ptr result = Database::getInstance().storeQuery(query.str()); | |
| 73 | + | if (!result) { | |
| 74 | + | return offerList; | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | do { | |
| 78 | + | MarketOffer offer; | |
| 79 | + | offer.amount = result->getNumber<uint16_t>("amount"); | |
| 80 | + | offer.price = result->getNumber<uint32_t>("price"); | |
| 81 | + | offer.timestamp = result->getNumber<uint32_t>("created") + marketOfferDuration; | |
| 82 | + | offer.counter = result->getNumber<uint32_t>("id") & 0xFFFF; | |
| 83 | + | offer.itemId = result->getNumber<uint16_t>("itemtype"); | |
| 84 | + | offerList.push_back(offer); | |
| 85 | + | } while (result->next()); | |
| 86 | + | return offerList; | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | HistoryMarketOfferList IOMarket::getOwnHistory(MarketAction_t action, uint32_t playerId) | |
| 90 | + | { | |
| 91 | + | HistoryMarketOfferList offerList; | |
| 92 | + | ||
| 93 | + | std::ostringstream query; | |
| 94 | + | query << "SELECT `itemtype`, `amount`, `price`, `expires_at`, `state` FROM `market_history` WHERE `player_id` = " << playerId << " AND `sale` = " << action; | |
| 95 | + | ||
| 96 | + | DBResult_ptr result = Database::getInstance().storeQuery(query.str()); | |
| 97 | + | if (!result) { | |
| 98 | + | return offerList; | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | do { | |
| 102 | + | HistoryMarketOffer offer; | |
| 103 | + | offer.itemId = result->getNumber<uint16_t>("itemtype"); | |
| 104 | + | offer.amount = result->getNumber<uint16_t>("amount"); | |
| 105 | + | offer.price = result->getNumber<uint32_t>("price"); | |
| 106 | + | offer.timestamp = result->getNumber<uint32_t>("expires_at"); | |
| 107 | + | ||
| 108 | + | MarketOfferState_t offerState = static_cast<MarketOfferState_t>(result->getNumber<uint16_t>("state")); | |
| 109 | + | if (offerState == OFFERSTATE_ACCEPTEDEX) { | |
| 110 | + | offerState = OFFERSTATE_ACCEPTED; | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | offer.state = offerState; | |
| 114 | + | ||
| 115 | + | offerList.push_back(offer); | |
| 116 | + | } while (result->next()); | |
| 117 | + | return offerList; | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | void IOMarket::processExpiredOffers(DBResult_ptr result, bool) | |
| 121 | + | { | |
| 122 | + | if (!result) { | |
| 123 | + | return; | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | do { | |
| 127 | + | if (!IOMarket::moveOfferToHistory(result->getNumber<uint32_t>("id"), OFFERSTATE_EXPIRED)) { | |
| 128 | + | continue; | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | const uint32_t playerId = result->getNumber<uint32_t>("player_id"); | |
| 132 | + | const uint16_t amount = result->getNumber<uint16_t>("amount"); | |
| 133 | + | if (result->getNumber<uint16_t>("sale") == 1) { | |
| 134 | + | const ItemType& itemType = Item::items[result->getNumber<uint16_t>("itemtype")]; | |
| 135 | + | if (itemType.id == 0) { | |
| 136 | + | continue; | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | Player* player = g_game.getPlayerByGUID(playerId); | |
| 140 | + | if (!player) { | |
| 141 | + | player = new Player(nullptr); | |
| 142 | + | if (!IOLoginData::loadPlayerById(player, playerId)) { | |
| 143 | + | delete player; | |
| 144 | + | continue; | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | if (itemType.stackable) { | |
| 149 | + | uint16_t tmpAmount = amount; | |
| 150 | + | while (tmpAmount > 0) { | |
| 151 | + | uint16_t stackCount = std::min<uint16_t>(100, tmpAmount); | |
| 152 | + | Item* item = Item::CreateItem(itemType.id, stackCount); | |
| 153 | + | if (g_game.internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { | |
| 154 | + | delete item; | |
| 155 | + | break; | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | tmpAmount -= stackCount; | |
| 159 | + | } | |
| 160 | + | } else { | |
| 161 | + | int32_t subType; | |
| 162 | + | if (itemType.charges != 0) { | |
| 163 | + | subType = itemType.charges; | |
| 164 | + | } else { | |
| 165 | + | subType = -1; | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | for (uint16_t i = 0; i < amount; ++i) { | |
| 169 | + | Item* item = Item::CreateItem(itemType.id, subType); | |
| 170 | + | if (g_game.internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { | |
| 171 | + | delete item; | |
| 172 | + | break; | |
| 173 | + | } | |
| 174 | + | } | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | if (player->isOffline()) { | |
| 178 | + | IOLoginData::savePlayer(player); | |
| 179 | + | delete player; | |
| 180 | + | } | |
| 181 | + | } else { | |
| 182 | + | uint64_t totalPrice = result->getNumber<uint64_t>("price") * amount; | |
| 183 | + | ||
| 184 | + | Player* player = g_game.getPlayerByGUID(playerId); | |
| 185 | + | if (player) { | |
| 186 | + | player->setBankBalance(player->getBankBalance() + totalPrice); | |
| 187 | + | } else { | |
| 188 | + | IOLoginData::increaseBankBalance(playerId, totalPrice); | |
| 189 | + | } | |
| 190 | + | } | |
| 191 | + | } while (result->next()); | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | void IOMarket::checkExpiredOffers() | |
| 195 | + | { | |
| 196 | + | const time_t lastExpireDate = time(nullptr) - g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION); | |
| 197 | + | ||
| 198 | + | std::ostringstream query; | |
| 199 | + | query << "SELECT `id`, `amount`, `price`, `itemtype`, `player_id`, `sale` FROM `market_offers` WHERE `created` <= " << lastExpireDate; | |
| 200 | + | g_databaseTasks.addTask(query.str(), IOMarket::processExpiredOffers, true); | |
| 201 | + | ||
| 202 | + | int32_t checkExpiredMarketOffersEachMinutes = g_config.getNumber(ConfigManager::CHECK_EXPIRED_MARKET_OFFERS_EACH_MINUTES); | |
| 203 | + | if (checkExpiredMarketOffersEachMinutes <= 0) { | |
| 204 | + | return; | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | g_scheduler.addEvent(createSchedulerTask(checkExpiredMarketOffersEachMinutes * 60 * 1000, IOMarket::checkExpiredOffers)); | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | uint32_t IOMarket::getPlayerOfferCount(uint32_t playerId) | |
| 211 | + | { | |
| 212 | + | std::ostringstream query; | |
| 213 | + | query << "SELECT COUNT(*) AS `count` FROM `market_offers` WHERE `player_id` = " << playerId; | |
| 214 | + | ||
| 215 | + | DBResult_ptr result = Database::getInstance().storeQuery(query.str()); | |
| 216 | + | if (!result) { | |
| 217 | + | return 0; | |
| 218 | + | } | |
| 219 | + | return result->getNumber<int32_t>("count"); | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | MarketOfferEx IOMarket::getOfferByCounter(uint32_t timestamp, uint16_t counter) | |
| 223 | + | { | |
| 224 | + | MarketOfferEx offer; | |
| 225 | + | ||
| 226 | + | const int32_t created = timestamp - g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION); | |
| 227 | + | ||
| 228 | + | std::ostringstream query; | |
| 229 | + | query << "SELECT `id`, `sale`, `itemtype`, `amount`, `created`, `price`, `player_id`, `anonymous`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `player_name` FROM `market_offers` WHERE `created` = " << created << " AND (`id` & 65535) = " << counter << " LIMIT 1"; | |
| 230 | + | ||
| 231 | + | DBResult_ptr result = Database::getInstance().storeQuery(query.str()); | |
| 232 | + | if (!result) { | |
| 233 | + | offer.id = 0; | |
| 234 | + | offer.playerId = 0; | |
| 235 | + | return offer; | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | offer.id = result->getNumber<uint32_t>("id"); | |
| 239 | + | offer.type = static_cast<MarketAction_t>(result->getNumber<uint16_t>("sale")); | |
| 240 | + | offer.amount = result->getNumber<uint16_t>("amount"); | |
| 241 | + | offer.counter = result->getNumber<uint32_t>("id") & 0xFFFF; | |
| 242 | + | offer.timestamp = result->getNumber<uint32_t>("created"); | |
| 243 | + | offer.price = result->getNumber<uint32_t>("price"); | |
| 244 | + | offer.itemId = result->getNumber<uint16_t>("itemtype"); | |
| 245 | + | offer.playerId = result->getNumber<uint32_t>("player_id"); | |
| 246 | + | if (result->getNumber<uint16_t>("anonymous") == 0) { | |
| 247 | + | offer.playerName = result->getString("player_name"); | |
| 248 | + | } else { | |
| 249 | + | offer.playerName = "Anonymous"; | |
| 250 | + | } | |
| 251 | + | return offer; | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | void IOMarket::createOffer(uint32_t playerId, MarketAction_t action, uint32_t itemId, uint16_t amount, uint32_t price, bool anonymous) | |
| 255 | + | { | |
| 256 | + | std::ostringstream query; | |
| 257 | + | query << "INSERT INTO `market_offers` (`player_id`, `sale`, `itemtype`, `amount`, `price`, `created`, `anonymous`) VALUES (" << playerId << ',' << action << ',' << itemId << ',' << amount << ',' << price << ',' << time(nullptr) << ',' << anonymous << ')'; | |
| 258 | + | Database::getInstance().executeQuery(query.str()); | |
| 259 | + | } | |
| 260 | + | ||
| 261 | + | void IOMarket::acceptOffer(uint32_t offerId, uint16_t amount) | |
| 262 | + | { | |
| 263 | + | std::ostringstream query; | |
| 264 | + | query << "UPDATE `market_offers` SET `amount` = `amount` - " << amount << " WHERE `id` = " << offerId; | |
| 265 | + | Database::getInstance().executeQuery(query.str()); | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | void IOMarket::deleteOffer(uint32_t offerId) | |
| 269 | + | { | |
| 270 | + | std::ostringstream query; | |
| 271 | + | query << "DELETE FROM `market_offers` WHERE `id` = " << offerId; | |
| 272 | + | Database::getInstance().executeQuery(query.str()); | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | void IOMarket::appendHistory(uint32_t playerId, MarketAction_t type, uint16_t itemId, uint16_t amount, uint32_t price, time_t timestamp, MarketOfferState_t state) | |
| 276 | + | { | |
| 277 | + | std::ostringstream query; | |
| 278 | + | query << "INSERT INTO `market_history` (`player_id`, `sale`, `itemtype`, `amount`, `price`, `expires_at`, `inserted`, `state`) VALUES (" | |
| 279 | + | << playerId << ',' << type << ',' << itemId << ',' << amount << ',' << price << ',' | |
| 280 | + | << timestamp << ',' << time(nullptr) << ',' << state << ')'; | |
| 281 | + | g_databaseTasks.addTask(query.str()); | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | bool IOMarket::moveOfferToHistory(uint32_t offerId, MarketOfferState_t state) | |
| 285 | + | { | |
| 286 | + | const int32_t marketOfferDuration = g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION); | |
| 287 | + | ||
| 288 | + | Database& db = Database::getInstance(); | |
| 289 | + | ||
| 290 | + | std::ostringstream query; | |
| 291 | + | query << "SELECT `player_id`, `sale`, `itemtype`, `amount`, `price`, `created` FROM `market_offers` WHERE `id` = " << offerId; | |
| 292 | + | ||
| 293 | + | DBResult_ptr result = db.storeQuery(query.str()); | |
| 294 | + | if (!result) { | |
| 295 | + | return false; | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | query.str(std::string()); | |
| 299 | + | query << "DELETE FROM `market_offers` WHERE `id` = " << offerId; | |
| 300 | + | if (!db.executeQuery(query.str())) { | |
| 301 | + | return false; | |
| 302 | + | } | |
| 303 | + | ||
| 304 | + | appendHistory(result->getNumber<uint32_t>("player_id"), static_cast<MarketAction_t>(result->getNumber<uint16_t>("sale")), result->getNumber<uint16_t>("itemtype"), result->getNumber<uint16_t>("amount"), result->getNumber<uint32_t>("price"), result->getNumber<uint32_t>("created") + marketOfferDuration, state); | |
| 305 | + | return true; | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | void IOMarket::updateStatistics() | |
| 309 | + | { | |
| 310 | + | std::ostringstream query; | |
| 311 | + | query << "SELECT `sale` AS `sale`, `itemtype` AS `itemtype`, COUNT(`price`) AS `num`, MIN(`price`) AS `min`, MAX(`price`) AS `max`, SUM(`price`) AS `sum` FROM `market_history` WHERE `state` = " << OFFERSTATE_ACCEPTED << " GROUP BY `itemtype`, `sale`"; | |
| 312 | + | DBResult_ptr result = Database::getInstance().storeQuery(query.str()); | |
| 313 | + | if (!result) { | |
| 314 | + | return; | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | do { | |
| 318 | + | MarketStatistics* statistics; | |
| 319 | + | if (result->getNumber<uint16_t>("sale") == MARKETACTION_BUY) { | |
| 320 | + | statistics = &purchaseStatistics[result->getNumber<uint16_t>("itemtype")]; | |
| 321 | + | } else { | |
| 322 | + | statistics = &saleStatistics[result->getNumber<uint16_t>("itemtype")]; | |
| 323 | + | } | |
| 324 | + | ||
| 325 | + | statistics->numTransactions = result->getNumber<uint32_t>("num"); | |
| 326 | + | statistics->lowestPrice = result->getNumber<uint32_t>("min"); | |
| 327 | + | statistics->totalPrice = result->getNumber<uint64_t>("sum"); | |
| 328 | + | statistics->highestPrice = result->getNumber<uint32_t>("max"); | |
| 329 | + | } while (result->next()); | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | MarketStatistics* IOMarket::getPurchaseStatistics(uint16_t itemId) | |
| 333 | + | { | |
| 334 | + | auto it = purchaseStatistics.find(itemId); | |
| 335 | + | if (it == purchaseStatistics.end()) { | |
| 336 | + | return nullptr; | |
| 337 | + | } | |
| 338 | + | return &it->second; | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | MarketStatistics* IOMarket::getSaleStatistics(uint16_t itemId) | |
| 342 | + | { | |
| 343 | + | auto it = saleStatistics.find(itemId); | |
| 344 | + | if (it == saleStatistics.end()) { | |
| 345 | + | return nullptr; | |
| 346 | + | } | |
| 347 | + | return &it->second; | |
| 348 | + | } |
| @@ -0,0 +1,63 @@ | |||
| 1 | + | /** | |
| 2 | + | * The Forgotten Server - a free and open-source MMORPG server emulator | |
| 3 | + | * Copyright (C) 2019 Mark Samman <mark.samman@gmail.com> | |
| 4 | + | * | |
| 5 | + | * This program is free software; you can redistribute it and/or modify | |
| 6 | + | * it under the terms of the GNU General Public License as published by | |
| 7 | + | * the Free Software Foundation; either version 2 of the License, or | |
| 8 | + | * (at your option) any later version. | |
| 9 | + | * | |
| 10 | + | * This program is distributed in the hope that it will be useful, | |
| 11 | + | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 12 | + | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 13 | + | * GNU General Public License for more details. | |
| 14 | + | * | |
| 15 | + | * You should have received a copy of the GNU General Public License along | |
| 16 | + | * with this program; if not, write to the Free Software Foundation, Inc., | |
| 17 | + | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| 18 | + | */ | |
| 19 | + | ||
| 20 | + | #ifndef FS_IOMARKET_H_B981E52C218C42D3B9EF726EBF0E92C9 | |
| 21 | + | #define FS_IOMARKET_H_B981E52C218C42D3B9EF726EBF0E92C9 | |
| 22 | + | ||
| 23 | + | #include "enums.h" | |
| 24 | + | #include "database.h" | |
| 25 | + | ||
| 26 | + | class IOMarket | |
| 27 | + | { | |
| 28 | + | public: | |
| 29 | + | static IOMarket& getInstance() { | |
| 30 | + | static IOMarket instance; | |
| 31 | + | return instance; | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | static MarketOfferList getActiveOffers(MarketAction_t action, uint16_t itemId); | |
| 35 | + | static MarketOfferList getOwnOffers(MarketAction_t action, uint32_t playerId); | |
| 36 | + | static HistoryMarketOfferList getOwnHistory(MarketAction_t action, uint32_t playerId); | |
| 37 | + | ||
| 38 | + | static void processExpiredOffers(DBResult_ptr result, bool); | |
| 39 | + | static void checkExpiredOffers(); | |
| 40 | + | ||
| 41 | + | static uint32_t getPlayerOfferCount(uint32_t playerId); | |
| 42 | + | static MarketOfferEx getOfferByCounter(uint32_t timestamp, uint16_t counter); | |
| 43 | + | ||
| 44 | + | static void createOffer(uint32_t playerId, MarketAction_t action, uint32_t itemId, uint16_t amount, uint32_t price, bool anonymous); | |
| 45 | + | static void acceptOffer(uint32_t offerId, uint16_t amount); | |
| 46 | + | static void deleteOffer(uint32_t offerId); | |
| 47 | + | ||
| 48 | + | static void appendHistory(uint32_t playerId, MarketAction_t type, uint16_t itemId, uint16_t amount, uint32_t price, time_t timestamp, MarketOfferState_t state); | |
| 49 | + | static bool moveOfferToHistory(uint32_t offerId, MarketOfferState_t state); | |
| 50 | + | ||
| 51 | + | void updateStatistics(); | |
| 52 | + | ||
| 53 | + | MarketStatistics* getPurchaseStatistics(uint16_t itemId); | |
| 54 | + | MarketStatistics* getSaleStatistics(uint16_t itemId); | |
| 55 | + | ||
| 56 | + | private: | |
| 57 | + | IOMarket() = default; | |
| 58 | + | ||
| 59 | + | std::map<uint16_t, MarketStatistics> purchaseStatistics; | |
| 60 | + | std::map<uint16_t, MarketStatistics> saleStatistics; | |
| 61 | + | }; | |
| 62 | + | ||
| 63 | + | #endif |