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
30extern ConfigManager g_config;
31extern Game g_game;
32
33MarketOfferList 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
63MarketOfferList 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
89HistoryMarketOfferList 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
120void 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
194void 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
210uint32_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
222MarketOfferEx 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
254void 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
261void 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
268void 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
275void 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
284bool 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
308void 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
332MarketStatistics* 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
341MarketStatistics* 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}
349