drafts for market system. It would be better if I remove the inbox system and market is still working like shit

This commit is contained in:
ErikasKontenis 2022-04-09 21:31:51 +03:00
parent 396464b940
commit f839f0b637
46 changed files with 5402 additions and 506 deletions

View File

@ -13,6 +13,7 @@ function updateFeatures(version)
end
-- you can add custom features here, list of them is in the modules\gamelib\const.lua
g_game.enableFeature(GamePlayerMarket)
--g_game.enableFeature(GameClientPing)
--g_game.enableFeature(GameExtendedOpcode)
--g_game.enableFeature(GameMinimapLimitedToSingleFloor) -- it will generate minimap only for current floor

View File

@ -740,6 +740,19 @@ local function onMarketMessage(messageMode, message)
Market.displayMessage(message)
end
local function dump(o)
if type(o) == 'table' then
local s = '{ '
for k,v in pairs(o) do
if type(k) ~= 'number' then k = '"'..k..'"' end
s = s .. '['..k..'] = ' .. dump(v) .. ','
end
return s .. '} '
else
return tostring(o)
end
end
local function initMarketItems(items)
for c = MarketCategory.First, MarketCategory.Last do
marketItems[c] = {}
@ -768,7 +781,6 @@ local function initMarketItems(items)
tradeAs = entry.id
}
}
-- add new market item
if marketItems[entry.category] ~= nil then
table.insert(marketItems[entry.category], marketItem)

View File

@ -37,39 +37,33 @@ local function readMarketOffer(msg, action, var)
return MarketOffer.new({timestamp, counter}, action, Item.create(itemId), amount, price, playerName, state, var)
end
local function dump(o)
if type(o) == 'table' then
local s = '{ '
for k,v in pairs(o) do
if type(k) ~= 'number' then k = '"'..k..'"' end
s = s .. '['..k..'] = ' .. dump(v) .. ','
end
return s .. '} '
else
return tostring(o)
end
end
-- parsing protocols
local function parseMarketEnter(protocol, msg)
local items
if g_game.getClientVersion() < 944 then
items = {}
local itemsCount = msg:getU16()
for i = 1, itemsCount do
local itemId = msg:getU16()
local category = msg:getU8()
local name = msg:getString()
table.insert(items, {
id = itemId,
category = category,
name = name
local version = 981
local items = {}
table.insert(items, {
id = 3264,
category = 20,
name = "sword"
})
end
end
local balance = 0
if g_game.getProtocolVersion() <= 1250 or not g_game.getFeature(GameTibia12Protocol) then
if g_game.getProtocolVersion() >= 981 or g_game.getProtocolVersion() < 944 then
balance = msg:getU64()
else
balance = msg:getU32()
end
end
local vocation = -1
if g_game.getProtocolVersion() >= 944 and g_game.getProtocolVersion() < 950 then
vocation = msg:getU8() -- get vocation id
end
local offers = msg:getU8()
local vocation = -1
local offers = msg:getU8()
local balance = msg:getU64();
local depotItems = {}
local depotCount = msg:getU16()
for i = 1, depotCount do
@ -78,8 +72,10 @@ local function parseMarketEnter(protocol, msg)
depotItems[itemId] = itemCount
end
print(dump(depotItems))
signalcall(Market.onMarketEnter, depotItems, offers, balance, vocation, items)
--signalcall(Market.onMarketEnter, depotItems, offers, balance, vocation, items)
return true
end
@ -89,9 +85,11 @@ local function parseMarketLeave(protocol, msg)
end
local function parseMarketDetail(protocol, msg)
print("DETAILS")
local itemId = msg:getU16()
print (itemId + "assadsd")
local descriptions = {}
table.insert(descriptions, {2, "28"})
for i = MarketItemDescription.First, MarketItemDescription.Last do
if msg:peekU16() ~= 0x00 then
table.insert(descriptions, {i, msg:getString()}) -- item descriptions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,8 @@ ropeSpotBlock = false
showMonsterLoot = true
blockHeight = false
dropItems = false
marketOfferDuration = 1
premiumToCreateMarketOffer = false
-- Combat settings
-- NOTE: valid values for worldType are: "pvp", "no-pvp" and "pvp-enforced"

View File

@ -33801,4 +33801,18 @@ Flags = {Bottom,Unpass,Unmove}
TypeID = 7475
Name = "a sarcophagus"
Flags = {Bottom,Unpass,Unmove}
Flags = {Bottom,Unpass,Unmove}
TypeID = 7476
Name = "your inbox"
Flags = {Container,Unmove}
Attributes = {Capacity=30}
TypeID = 7477
Name = "the market"
Flags = {Unmove}
TypeID = 7478
Name = "your store inbox"
Flags = {Container,Unmove}
Attributes = {Capacity=30}

View File

@ -55,6 +55,47 @@ CREATE TABLE `account_bans` (
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS `player_inboxitems` (
`player_id` int NOT NULL,
`sid` int NOT NULL,
`pid` int NOT NULL DEFAULT '0',
`itemtype` smallint unsigned NOT NULL,
`count` smallint NOT NULL DEFAULT '0',
`attributes` blob NOT NULL,
UNIQUE KEY `player_id_2` (`player_id`, `sid`),
FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8;
CREATE TABLE IF NOT EXISTS `market_history` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`player_id` int NOT NULL,
`sale` tinyint NOT NULL DEFAULT '0',
`itemtype` smallint unsigned NOT NULL,
`amount` smallint unsigned NOT NULL,
`price` bigint unsigned NOT NULL DEFAULT '0',
`expires_at` bigint unsigned NOT NULL,
`inserted` bigint unsigned NOT NULL,
`state` tinyint unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `player_id` (`player_id`, `sale`),
FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8;
CREATE TABLE IF NOT EXISTS `market_offers` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`player_id` int NOT NULL,
`sale` tinyint NOT NULL DEFAULT '0',
`itemtype` smallint unsigned NOT NULL,
`amount` smallint unsigned NOT NULL,
`created` bigint unsigned NOT NULL,
`anonymous` tinyint NOT NULL DEFAULT '0',
`price` bigint unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `sale` (`sale`,`itemtype`),
KEY `created` (`created`),
FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8;
--
-- Table structure for table `account_ban_history`
--

View File

@ -273,6 +273,7 @@ ReturnValue Actions::internalUseItem(Player* player, const Position& pos, uint8_
DepotLocker* myDepotLocker = player->getDepotLocker(depot->getDepotId(), true);
myDepotLocker->setParent(depot->getParent()->getTile());
openContainer = myDepotLocker;
player->setLastDepotId(depot->getDepotId());
} else {
openContainer = container;
}
@ -325,6 +326,17 @@ bool Actions::useItem(Player* player, const Position& pos, uint8_t index, Item*
player->setNextAction(OTSYS_TIME() + g_config.getNumber(ConfigManager::ACTIONS_DELAY_INTERVAL));
player->stopWalk();
if (item->getID() == ITEM_MARKET)
{
if (player->getLastDepotId() == -1) {
return false;
}
player->sendMarketEnter(player->getLastDepotId());
return true;
}
if (isHotkey) {
uint32_t count = 0;
if (item->isRune()) {

View File

@ -63,6 +63,8 @@ bool ConfigManager::load()
integer[GAME_PORT] = getGlobalNumber(L, "gameProtocolPort", 7172);
integer[LOGIN_PORT] = getGlobalNumber(L, "loginProtocolPort", 7171);
integer[STATUS_PORT] = getGlobalNumber(L, "statusProtocolPort", 7171);
integer[MARKET_OFFER_DURATION] = getGlobalNumber(L, "marketOfferDuration", 30 * 24 * 60 * 60);
}
boolean[SHOW_MONSTER_LOOT] = getGlobalBoolean(L, "showMonsterLoot", true);
@ -74,6 +76,7 @@ bool ConfigManager::load()
boolean[FREE_PREMIUM] = getGlobalBoolean(L, "freePremium", false);
boolean[REPLACE_KICK_ON_LOGIN] = getGlobalBoolean(L, "replaceKickOnLogin", true);
boolean[ALLOW_CLONES] = getGlobalBoolean(L, "allowClones", false);
boolean[MARKET_PREMIUM] = getGlobalBoolean(L, "premiumToCreateMarketOffer", true);
boolean[STAMINA_SYSTEM] = getGlobalBoolean(L, "staminaSystem", true);
boolean[WARN_UNSAFE_SCRIPTS] = getGlobalBoolean(L, "warnUnsafeScripts", true);
boolean[CONVERT_UNSAFE_SCRIPTS] = getGlobalBoolean(L, "convertUnsafeScripts", true);
@ -126,6 +129,8 @@ bool ConfigManager::load()
integer[KILLS_MONTH_BANISHMENT] = getGlobalNumber(L, "killsMonthBanishment", 10);
integer[STAIRHOP_DELAY] = getGlobalNumber(L, "stairJumpExhaustion", 2000);
integer[EXP_FROM_PLAYERS_LEVEL_RANGE] = getGlobalNumber(L, "expFromPlayersLevelRange", 75);
integer[CHECK_EXPIRED_MARKET_OFFERS_EACH_MINUTES] = getGlobalNumber(L, "checkExpiredMarketOffersEachMinutes", 60);
integer[MAX_MARKET_OFFERS_AT_A_TIME_PER_PLAYER] = getGlobalNumber(L, "maxMarketOffersAtATimePerPlayer", 100);
integer[MAX_PACKETS_PER_SECOND] = getGlobalNumber(L, "maxPacketsPerSecond", 25);
integer[NEWBIE_TOWN] = getGlobalNumber(L, "newbieTownId", 1);
integer[NEWBIE_LEVEL_THRESHOLD] = getGlobalNumber(L, "newbieLevelThreshold", 5);

View File

@ -41,6 +41,7 @@ class ConfigManager
ALLOW_CLONES,
BIND_ONLY_GLOBAL_ADDRESS,
OPTIMIZE_DATABASE,
MARKET_PREMIUM,
STAMINA_SYSTEM,
WARN_UNSAFE_SCRIPTS,
CONVERT_UNSAFE_SCRIPTS,
@ -113,6 +114,9 @@ class ConfigManager
LOGIN_PORT,
STATUS_PORT,
STAIRHOP_DELAY,
MARKET_OFFER_DURATION,
CHECK_EXPIRED_MARKET_OFFERS_EACH_MINUTES,
MAX_MARKET_OFFERS_AT_A_TIME_PER_PLAYER,
EXP_FROM_PLAYERS_LEVEL_RANGE,
MAX_PACKETS_PER_SECOND,
NEWBIE_TOWN,

View File

@ -21,6 +21,8 @@
#define FS_CONST_H_0A49B5996F074465BF44B90F4F780E8B
static constexpr int32_t NETWORKMESSAGE_MAXSIZE = 24590;
static constexpr int32_t MIN_MARKET_FEE = 20;
static constexpr int32_t MAX_MARKET_FEE = 100000;
enum MagicEffectClasses : uint8_t {
CONST_ME_NONE,
@ -281,6 +283,8 @@ enum item_t : uint16_t {
ITEM_DEPOT = 3502,
ITEM_LOCKER1 = 3497,
ITEM_INBOX = 7476,
ITEM_MARKET = 7477,
ITEM_MALE_CORPSE = 4240,
ITEM_FEMALE_CORPSE = 4247,
@ -301,6 +305,14 @@ enum item_t : uint16_t {
ITEM_DOCUMENT_RO = 2819, //read-only
};
enum ResourceTypes_t : uint8_t {
RESOURCE_BANK_BALANCE = 0x00,
RESOURCE_GOLD_EQUIPPED = 0x01,
RESOURCE_PREY_WILDCARDS = 0x0A,
RESOURCE_DAILYREWARD_STREAK = 0x14,
RESOURCE_DAILYREWARD_JOKERS = 0x15,
};
enum PlayerFlags : uint64_t {
PlayerFlag_CannotUseCombat = 1 << 0,
PlayerFlag_CannotAttackPlayer = 1 << 1,

View File

@ -19,6 +19,8 @@
#include "otpch.h"
#include "depotchest.h"
#include "inbox.h"
#include "container.h"
#include "iomap.h"
#include "game.h"
@ -264,12 +266,17 @@ ReturnValue Container::queryAdd(int32_t index, const Thing& thing, uint32_t coun
}
const Cylinder* cylinder = getParent();
if (!hasBitSet(FLAG_NOLIMIT, flags)) {
while (cylinder) {
if (cylinder == &thing) {
return RETURNVALUE_THISISIMPOSSIBLE;
}
if (dynamic_cast<const Inbox*>(cylinder)) {
return RETURNVALUE_CONTAINERNOTENOUGHROOM;
}
cylinder = cylinder->getParent();
}

68
src/depotchest.cpp Normal file
View File

@ -0,0 +1,68 @@
// Copyright 2022 The Forgotten Server Authors. All rights reserved.
// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file.
#include "otpch.h"
#include "depotchest.h"
#include "tools.h"
DepotChest::DepotChest(uint16_t type) :
Container(type), maxDepotItems(2000) {}
ReturnValue DepotChest::queryAdd(int32_t index, const Thing& thing, uint32_t count,
uint32_t flags, Creature* actor/* = nullptr*/) const
{
const Item* item = thing.getItem();
if (!item) {
return RETURNVALUE_NOTPOSSIBLE;
}
bool skipLimit = hasBitSet(FLAG_NOLIMIT, flags);
if (!skipLimit) {
int32_t addCount = 0;
if ((item->isStackable() && item->getItemCount() != count)) {
addCount = 1;
}
if (item->getTopParent() != this) {
if (const Container* container = item->getContainer()) {
addCount = container->getItemHoldingCount() + 1;
}
else {
addCount = 1;
}
}
if (getItemHoldingCount() + addCount > maxDepotItems) {
return RETURNVALUE_DEPOTISFULL;
}
}
return Container::queryAdd(index, thing, count, flags, actor);
}
void DepotChest::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t)
{
Cylinder* parent = getParent();
if (parent) {
parent->postAddNotification(thing, oldParent, index, LINK_PARENT);
}
}
void DepotChest::postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t)
{
Cylinder* parent = getParent();
if (parent) {
parent->postRemoveNotification(thing, newParent, index, LINK_PARENT);
}
}
Cylinder* DepotChest::getParent() const
{
if (parent) {
return parent->getParent();
}
return nullptr;
}

40
src/depotchest.h Normal file
View File

@ -0,0 +1,40 @@
// Copyright 2022 The Forgotten Server Authors. All rights reserved.
// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file.
#ifndef FS_DEPOTCHEST_H
#define FS_DEPOTCHEST_H
#include "container.h"
class DepotChest final : public Container
{
public:
explicit DepotChest(uint16_t type);
//serialization
void setMaxDepotItems(uint32_t maxitems) {
maxDepotItems = maxitems;
}
//cylinder implementations
ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count,
uint32_t flags, Creature* actor = nullptr) const override;
void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) override;
void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) override;
//overrides
bool canRemove() const override {
return false;
}
Cylinder* getParent() const override;
Cylinder* getRealParent() const override {
return parent;
}
private:
uint32_t maxDepotItems;
};
#endif // FS_DEPOTCHEST_H

View File

@ -23,6 +23,7 @@
#include "creature.h"
#include "player.h"
#include "tools.h"
#include "inbox.h"
DepotLocker::DepotLocker(uint16_t type) :
Container(type, 30), depotId(0) {}
@ -87,3 +88,12 @@ void DepotLocker::postRemoveNotification(Thing* thing, const Cylinder* newParent
parent->postRemoveNotification(thing, newParent, index, LINK_PARENT);
}
}
void DepotLocker::removeInbox(Inbox* inbox)
{
auto cit = std::find(itemlist.begin(), itemlist.end(), inbox);
if (cit == itemlist.end()) {
return;
}
itemlist.erase(cit);
}

View File

@ -22,6 +22,8 @@
#include "container.h"
class Inbox;
class DepotLocker final : public Container
{
public:
@ -51,6 +53,8 @@ class DepotLocker final : public Container
void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) final;
void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) final;
void removeInbox(Inbox* inbox);
bool canRemove() const final {
return false;
}

View File

@ -63,6 +63,26 @@ enum VipStatus_t : uint8_t {
VIPSTATUS_ONLINE = 1,
};
enum MarketAction_t {
MARKETACTION_BUY = 0,
MARKETACTION_SELL = 1,
};
enum MarketRequest_t {
MARKETREQUEST_OWN_HISTORY = 1,
MARKETREQUEST_OWN_OFFERS = 2,
MARKETREQUEST_ITEM = 3,
};
enum MarketOfferState_t {
OFFERSTATE_ACTIVE = 0,
OFFERSTATE_CANCELLED = 1,
OFFERSTATE_EXPIRED = 2,
OFFERSTATE_ACCEPTED = 3,
OFFERSTATE_ACCEPTEDEX = 255,
};
enum OperatingSystem_t : uint8_t {
CLIENTOS_NONE = 0,
@ -351,6 +371,8 @@ enum ReturnValue {
RETURNVALUE_YOUDONTOWNTHISHOUSE,
RETURNVALUE_TRADEPLAYERALREADYOWNSAHOUSE,
RETURNVALUE_TRADEPLAYERHIGHESTBIDDER,
RETURNVALUE_CANNOTMOVEITEMISNOTSTOREITEM,
RETURNVALUE_ITEMCANNOTBEMOVEDTHERE,
RETURNVALUE_YOUCANNOTTRADETHISHOUSE,
};
@ -371,6 +393,48 @@ struct LightInfo {
constexpr LightInfo(uint8_t level, uint8_t color) : level(level), color(color) {}
};
struct MarketOffer {
uint64_t price;
uint32_t timestamp;
uint16_t amount;
uint16_t counter;
uint16_t itemId;
std::string playerName;
};
struct MarketOfferEx {
MarketOfferEx() = default;
MarketOfferEx(MarketOfferEx&& other) :
id(other.id), playerId(other.playerId), timestamp(other.timestamp), price(other.price),
amount(other.amount), counter(other.counter), itemId(other.itemId), type(other.type),
playerName(std::move(other.playerName)) {}
uint32_t id;
uint32_t playerId;
uint32_t timestamp;
uint64_t price;
uint16_t amount;
uint16_t counter;
uint16_t itemId;
MarketAction_t type;
std::string playerName;
};
struct HistoryMarketOffer {
uint32_t timestamp;
uint64_t price;
uint16_t itemId;
uint16_t amount;
MarketOfferState_t state;
};
struct MarketStatistics {
uint32_t numTransactions = 0;
uint32_t highestPrice = 0;
uint64_t totalPrice = 0;
uint32_t lowestPrice = 0;
};
enum CombatOrigin
{
ORIGIN_NONE,
@ -398,4 +462,7 @@ struct CombatDamage
}
};
using MarketOfferList = std::list<MarketOffer>;
using HistoryMarketOfferList = std::list<HistoryMarketOffer>;
#endif

View File

@ -130,6 +130,9 @@ bool Events::load()
else if (methodName == "onLookInTrade") {
playerOnLookInTrade = event;
}
else if (methodName == "onLookInMarket") {
playerOnLookInMarket = event;
}
else if (methodName == "onTradeRequest") {
playerOnTradeRequest = event;
}
@ -499,6 +502,33 @@ void Events::eventPlayerOnLookInTrade(Player* player, Player* partner, Item* ite
scriptInterface.callVoidFunction(4);
}
bool Events::eventPlayerOnLookInMarket(Player* player, const ItemType* itemType)
{
// Player:onLookInMarket(itemType) or Player.onLookInMarket(self, itemType)
if (playerOnLookInMarket == -1) {
return true;
}
if (!scriptInterface.reserveScriptEnv()) {
std::cout << "[Error - Events::eventPlayerOnLookInMarket] Call stack overflow" << std::endl;
return false;
}
ScriptEnvironment* env = scriptInterface.getScriptEnv();
env->setScriptId(playerOnLookInMarket, &scriptInterface);
lua_State* L = scriptInterface.getLuaState();
scriptInterface.pushFunction(playerOnLookInMarket);
LuaScriptInterface::pushUserdata<Player>(L, player);
LuaScriptInterface::setMetatable(L, -1, "Player");
LuaScriptInterface::pushUserdata<const ItemType>(L, itemType);
LuaScriptInterface::setMetatable(L, -1, "ItemType");
return scriptInterface.callFunction(2);
}
bool Events::eventPlayerOnMoveItem(Player* player, Item* item, uint16_t count, const Position& fromPosition, const Position& toPosition, Cylinder* fromCylinder, Cylinder* toCylinder)
{
// Player:onMoveItem(item, count, fromPosition, toPosition) or Player.onMoveItem(self, item, count, fromPosition, toPosition, fromCylinder, toCylinder)

View File

@ -49,6 +49,7 @@ public:
void eventPlayerOnLook(Player* player, const Position& position, Thing* thing, uint8_t stackpos, int32_t lookDistance);
void eventPlayerOnLookInBattleList(Player* player, Creature* creature, int32_t lookDistance);
void eventPlayerOnLookInTrade(Player* player, Player* partner, Item* item, int32_t lookDistance);
bool eventPlayerOnLookInMarket(Player* player, const ItemType* itemType);
bool eventPlayerOnMoveItem(Player* player, Item* item, uint16_t count, const Position& fromPosition, const Position& toPosition, Cylinder* fromCylinder, Cylinder* toCylinder);
void eventPlayerOnItemMoved(Player* player, Item* item, uint16_t count, const Position& fromPosition, const Position& toPosition, Cylinder* fromCylinder, Cylinder* toCylinder);
bool eventPlayerOnMoveCreature(Player* player, Creature* creature, const Position& fromPosition, const Position& toPosition);
@ -79,6 +80,7 @@ private:
int32_t playerOnLook;
int32_t playerOnLookInBattleList;
int32_t playerOnLookInTrade;
int32_t playerOnLookInMarket;
int32_t playerOnMoveItem;
int32_t playerOnItemMoved;
int32_t playerOnMoveCreature;

View File

@ -28,6 +28,7 @@
#include "game.h"
#include "actions.h"
#include "iologindata.h"
#include "iomarket.h"
#include "talkaction.h"
#include "spells.h"
#include "configmanager.h"
@ -37,6 +38,8 @@
#include "scheduler.h"
#include "databasetasks.h"
#include "movement.h"
#include "inbox.h"
#include "depotchest.h"
extern ConfigManager g_config;
extern Actions* g_actions;
@ -310,6 +313,7 @@ Thing* Game::internalGetThing(Player* player, const Position& pos, int32_t index
//inventory
slots_t slot = static_cast<slots_t>(pos.y);
return player->getInventoryItem(slot);
}
void Game::internalGetPosition(Item* item, Position& pos, uint8_t& stackpos)
@ -4452,6 +4456,473 @@ void Game::playerDebugAssert(uint32_t playerId, const std::string& assertLine, c
}
}
void Game::playerLeaveMarket(uint32_t playerId)
{
Player* player = getPlayerByID(playerId);
if (!player) {
return;
}
player->setInMarket(false);
}
void Game::playerBrowseMarket(uint32_t playerId, uint16_t spriteId)
{
Player* player = getPlayerByID(playerId);
if (!player) {
return;
}
if (!player->isInMarket()) {
return;
}
const ItemType& it = Item::items[spriteId];
if (it.id == 0) {
return;
}
const MarketOfferList& buyOffers = IOMarket::getActiveOffers(MARKETACTION_BUY, it.id);
const MarketOfferList& sellOffers = IOMarket::getActiveOffers(MARKETACTION_SELL, it.id);
player->sendMarketBrowseItem(it.id, buyOffers, sellOffers);
g_events->eventPlayerOnLookInMarket(player, &it);
}
void Game::playerBrowseMarketOwnOffers(uint32_t playerId)
{
Player* player = getPlayerByID(playerId);
if (!player) {
return;
}
if (!player->isInMarket()) {
return;
}
const MarketOfferList& buyOffers = IOMarket::getOwnOffers(MARKETACTION_BUY, player->getGUID());
const MarketOfferList& sellOffers = IOMarket::getOwnOffers(MARKETACTION_SELL, player->getGUID());
player->sendMarketBrowseOwnOffers(buyOffers, sellOffers);
}
void Game::playerBrowseMarketOwnHistory(uint32_t playerId)
{
Player* player = getPlayerByID(playerId);
if (!player) {
return;
}
if (!player->isInMarket()) {
return;
}
const HistoryMarketOfferList& buyOffers = IOMarket::getOwnHistory(MARKETACTION_BUY, player->getGUID());
const HistoryMarketOfferList& sellOffers = IOMarket::getOwnHistory(MARKETACTION_SELL, player->getGUID());
player->sendMarketBrowseOwnHistory(buyOffers, sellOffers);
}
void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spriteId, uint16_t amount, uint64_t price, bool anonymous)
{
if (amount == 0 || amount > 64000) {
return;
}
if (price == 0 || price > 999999999999) {
return;
}
if (type != MARKETACTION_BUY && type != MARKETACTION_SELL) {
return;
}
Player* player = getPlayerByID(playerId);
if (!player) {
return;
}
if (!player->isInMarket()) {
return;
}
if (g_config.getBoolean(ConfigManager::MARKET_PREMIUM) && !player->isPremium()) {
player->sendTextMessage(MESSAGE_STATUS_DEFAULT, "Only premium accounts may create offers for that object.");
return;
}
const ItemType& it = Item::items[spriteId];
if (it.id == 0) {
return;
}
if (!it.stackable && amount > 2000) {
return;
}
const uint32_t maxOfferCount = g_config.getNumber(ConfigManager::MAX_MARKET_OFFERS_AT_A_TIME_PER_PLAYER);
if (maxOfferCount != 0 && IOMarket::getPlayerOfferCount(player->getGUID()) >= maxOfferCount) {
return;
}
uint64_t fee = (price / 100.) * amount;
if (fee < MIN_MARKET_FEE) {
fee = MIN_MARKET_FEE;
}
else if (fee > MAX_MARKET_FEE) {
fee = MAX_MARKET_FEE;
}
if (type == MARKETACTION_SELL) {
if (fee > (player->getMoney() + player->bankBalance)) {
return;
}
DepotLocker* depotLocker = player->getDepotLocker(player->getLastDepotId(), false);
if (!depotLocker) {
return;
}
std::forward_list<Item*> itemList = getMarketItemList(it.id, amount, depotLocker, player->getInbox());
if (itemList.empty()) {
return;
}
if (it.stackable) {
uint16_t tmpAmount = amount;
for (Item* item : itemList) {
uint16_t removeCount = std::min<uint16_t>(tmpAmount, item->getItemCount());
tmpAmount -= removeCount;
internalRemoveItem(item, removeCount);
if (tmpAmount == 0) {
break;
}
}
}
else {
for (Item* item : itemList) {
internalRemoveItem(item);
}
}
const auto debitCash = std::min(player->getMoney(), fee);
const auto debitBank = fee - debitCash;
removeMoney(player, debitCash);
player->bankBalance -= debitBank;
}
else {
uint64_t totalPrice = static_cast<uint64_t>(price) * amount;
totalPrice += fee;
if (totalPrice > (player->getMoney() + player->bankBalance)) {
return;
}
const auto debitCash = std::min(player->getMoney(), totalPrice);
const auto debitBank = totalPrice - debitCash;
removeMoney(player, debitCash);
player->bankBalance -= debitBank;
}
IOMarket::createOffer(player->getGUID(), static_cast<MarketAction_t>(type), it.id, amount, price, anonymous);
player->sendMarketEnter(player->getLastDepotId());
const MarketOfferList& buyOffers = IOMarket::getActiveOffers(MARKETACTION_BUY, it.id);
const MarketOfferList& sellOffers = IOMarket::getActiveOffers(MARKETACTION_SELL, it.id);
player->sendMarketBrowseItem(it.id, buyOffers, sellOffers);
}
void Game::playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16_t counter)
{
Player* player = getPlayerByID(playerId);
if (!player) {
return;
}
if (!player->isInMarket()) {
return;
}
MarketOfferEx offer = IOMarket::getOfferByCounter(timestamp, counter);
if (offer.id == 0 || offer.playerId != player->getGUID()) {
return;
}
if (offer.type == MARKETACTION_BUY) {
player->bankBalance += offer.price * offer.amount;
player->sendMarketEnter(player->getLastDepotId());
}
else {
const ItemType& it = Item::items[offer.itemId];
if (it.id == 0) {
return;
}
if (it.stackable) {
uint16_t tmpAmount = offer.amount;
while (tmpAmount > 0) {
int32_t stackCount = std::min<int32_t>(100, tmpAmount);
Item* item = Item::CreateItem(it.id, stackCount);
if (internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) {
delete item;
break;
}
tmpAmount -= stackCount;
}
}
else {
int32_t subType;
if (it.charges != 0) {
subType = it.charges;
}
else {
subType = -1;
}
for (uint16_t i = 0; i < offer.amount; ++i) {
Item* item = Item::CreateItem(it.id, subType);
if (internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) {
delete item;
break;
}
}
}
}
IOMarket::moveOfferToHistory(offer.id, OFFERSTATE_CANCELLED);
offer.amount = 0;
offer.timestamp += g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION);
player->sendMarketCancelOffer(offer);
player->sendMarketEnter(player->getLastDepotId());
}
void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16_t counter, uint16_t amount)
{
if (amount == 0 || amount > 64000) {
return;
}
Player* player = getPlayerByID(playerId);
if (!player) {
return;
}
if (!player->isInMarket()) {
return;
}
MarketOfferEx offer = IOMarket::getOfferByCounter(timestamp, counter);
if (offer.id == 0) {
return;
}
uint32_t offerAccountId = IOLoginData::getAccountIdByPlayerId(offer.playerId);
if (offerAccountId == player->getAccount()) {
player->sendTextMessage(MESSAGE_STATUS_DEFAULT, "You cannot accept your own offer.");
return;
}
if (amount > offer.amount) {
return;
}
const ItemType& it = Item::items[offer.itemId];
if (it.id == 0) {
return;
}
uint64_t totalPrice = offer.price * amount;
if (offer.type == MARKETACTION_BUY) {
DepotLocker* depotLocker = player->getDepotLocker(player->getLastDepotId(), false);
if (!depotLocker) {
return;
}
std::forward_list<Item*> itemList = getMarketItemList(it.id, amount, depotLocker, player->getInbox());
if (itemList.empty()) {
return;
}
Player* buyerPlayer = getPlayerByGUID(offer.playerId);
if (!buyerPlayer) {
buyerPlayer = new Player(nullptr);
if (!IOLoginData::loadPlayerById(buyerPlayer, offer.playerId)) {
delete buyerPlayer;
return;
}
}
if (it.stackable) {
uint16_t tmpAmount = amount;
for (Item* item : itemList) {
uint16_t removeCount = std::min<uint16_t>(tmpAmount, item->getItemCount());
tmpAmount -= removeCount;
internalRemoveItem(item, removeCount);
if (tmpAmount == 0) {
break;
}
}
}
else {
for (Item* item : itemList) {
internalRemoveItem(item);
}
}
player->bankBalance += totalPrice;
if (it.stackable) {
uint16_t tmpAmount = amount;
while (tmpAmount > 0) {
uint16_t stackCount = std::min<uint16_t>(100, tmpAmount);
Item* item = Item::CreateItem(it.id, stackCount);
if (internalAddItem(buyerPlayer->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) {
delete item;
break;
}
tmpAmount -= stackCount;
}
}
else {
int32_t subType;
if (it.charges != 0) {
subType = it.charges;
}
else {
subType = -1;
}
for (uint16_t i = 0; i < amount; ++i) {
Item* item = Item::CreateItem(it.id, subType);
if (internalAddItem(buyerPlayer->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) {
delete item;
break;
}
}
}
if (buyerPlayer->isOffline()) {
IOLoginData::savePlayer(buyerPlayer);
delete buyerPlayer;
}
else {
buyerPlayer->onReceiveMail();
}
}
else {
if (totalPrice > (player->getMoney() + player->bankBalance)) {
return;
}
const auto debitCash = std::min(player->getMoney(), totalPrice);
const auto debitBank = totalPrice - debitCash;
removeMoney(player, debitCash);
player->bankBalance -= debitBank;
if (it.stackable) {
uint16_t tmpAmount = amount;
while (tmpAmount > 0) {
uint16_t stackCount = std::min<uint16_t>(100, tmpAmount);
Item* item = Item::CreateItem(it.id, stackCount);
if (internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) {
delete item;
break;
}
tmpAmount -= stackCount;
}
}
else {
int32_t subType;
if (it.charges != 0) {
subType = it.charges;
}
else {
subType = -1;
}
for (uint16_t i = 0; i < amount; ++i) {
Item* item = Item::CreateItem(it.id, subType);
if (internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) {
delete item;
break;
}
}
}
Player* sellerPlayer = getPlayerByGUID(offer.playerId);
if (sellerPlayer) {
sellerPlayer->bankBalance += totalPrice;
}
else {
IOLoginData::increaseBankBalance(offer.playerId, totalPrice);
}
player->onReceiveMail();
}
const int32_t marketOfferDuration = g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION);
IOMarket::appendHistory(player->getGUID(), (offer.type == MARKETACTION_BUY ? MARKETACTION_SELL : MARKETACTION_BUY), offer.itemId, amount, offer.price, offer.timestamp + marketOfferDuration, OFFERSTATE_ACCEPTEDEX);
IOMarket::appendHistory(offer.playerId, offer.type, offer.itemId, amount, offer.price, offer.timestamp + marketOfferDuration, OFFERSTATE_ACCEPTED);
offer.amount -= amount;
if (offer.amount == 0) {
IOMarket::deleteOffer(offer.id);
}
else {
IOMarket::acceptOffer(offer.id, amount);
}
player->sendMarketEnter(player->getLastDepotId());
offer.timestamp += marketOfferDuration;
player->sendMarketAcceptOffer(offer);
}
std::forward_list<Item*> Game::getMarketItemList(uint16_t wareId, uint16_t sufficientCount, DepotLocker* depotLocker, Inbox* inbox)
{
std::forward_list<Item*> itemList;
uint16_t count = 0;
std::list<Container*> containers{ depotLocker, inbox };
do {
Container* container = containers.front();
containers.pop_front();
for (Item* item : container->getItemList()) {
Container* c = item->getContainer();
if (c && !c->empty()) {
containers.push_back(c);
continue;
}
const ItemType& itemType = Item::items[item->getID()];
if (itemType.id != wareId) {
continue;
}
if (c && (!itemType.isContainer() || c->capacity() != itemType.maxItems)) {
continue;
}
if (!item->hasMarketAttributes()) {
continue;
}
itemList.push_front(item);
count += Item::countByType(item, -1);
if (count >= sufficientCount) {
return itemList;
}
}
} while (!containers.empty());
return std::forward_list<Item*>();
}
void Game::parsePlayerExtendedOpcode(uint32_t playerId, uint8_t opcode, const std::string& buffer)
{
Player* player = getPlayerByID(playerId);

View File

@ -406,6 +406,15 @@ class Game
void playerContinueRuleViolationReport(Player* player, const std::string& text);
void parsePlayerExtendedOpcode(uint32_t playerId, uint8_t opcode, const std::string& buffer);
void playerLeaveMarket(uint32_t playerId);
void playerBrowseMarket(uint32_t playerId, uint16_t spriteId);
void playerBrowseMarketOwnOffers(uint32_t playerId);
void playerBrowseMarketOwnHistory(uint32_t playerId);
void playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spriteId, uint16_t amount, uint64_t price, bool anonymous);
void playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16_t counter);
void playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16_t counter, uint16_t amount);
std::forward_list<Item*> getMarketItemList(uint16_t wareId, uint16_t sufficientCount, DepotLocker* depotLocker, Inbox* inbox);
void closeRuleViolationReport(Player* player);
void cancelRuleViolationReport(Player* player);

View File

@ -26,6 +26,7 @@
#include "game.h"
#include "configmanager.h"
#include "bed.h"
#include "inbox.h"
extern ConfigManager g_config;
extern Game g_game;

57
src/inbox.cpp Normal file
View File

@ -0,0 +1,57 @@
// Copyright 2022 The Forgotten Server Authors. All rights reserved.
// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file.
#include "otpch.h"
#include "inbox.h"
#include "tools.h"
Inbox::Inbox(uint16_t type) : Container(type, 30) {}
ReturnValue Inbox::queryAdd(int32_t, const Thing& thing, uint32_t,
uint32_t flags, Creature*) const
{
if (!hasBitSet(FLAG_NOLIMIT, flags)) {
return RETURNVALUE_CONTAINERNOTENOUGHROOM;
}
const Item* item = thing.getItem();
if (!item) {
return RETURNVALUE_NOTPOSSIBLE;
}
if (item == this) {
return RETURNVALUE_THISISIMPOSSIBLE;
}
if (!item->isPickupable()) {
return RETURNVALUE_CANNOTPICKUP;
}
return RETURNVALUE_NOERROR;
}
void Inbox::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t)
{
Cylinder* parent = getParent();
if (parent) {
parent->postAddNotification(thing, oldParent, index, LINK_PARENT);
}
}
void Inbox::postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t)
{
Cylinder* parent = getParent();
if (parent) {
parent->postRemoveNotification(thing, newParent, index, LINK_PARENT);
}
}
Cylinder* Inbox::getParent() const
{
if (parent) {
return parent->getParent();
}
return nullptr;
}

32
src/inbox.h Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2022 The Forgotten Server Authors. All rights reserved.
// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file.
#ifndef FS_INBOX_H
#define FS_INBOX_H
#include "container.h"
class Inbox final : public Container
{
public:
explicit Inbox(uint16_t type);
//cylinder implementations
ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count,
uint32_t flags, Creature* actor = nullptr) const override;
void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) override;
void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) override;
//overrides
bool canRemove() const override {
return false;
}
Cylinder* getParent() const override;
Cylinder* getRealParent() const override {
return parent;
}
};
#endif // FS_INBOX_H

View File

@ -22,6 +22,9 @@
#include "iologindata.h"
#include "configmanager.h"
#include "game.h"
#include "depotchest.h"
#include "inbox.h"
#include <fmt/core.h>
extern ConfigManager g_config;
extern Game g_game;
@ -480,7 +483,8 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result)
}
}
}
} else {
}
else {
ItemMap::const_iterator it2 = itemMap.find(pid);
if (it2 == itemMap.end()) {
continue;
@ -494,6 +498,35 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result)
}
}
//load inbox items
itemMap.clear();
if ((result = db->storeQuery(fmt::format("SELECT `pid`, `sid`, `itemtype`, `count`, `attributes` FROM `player_inboxitems` WHERE `player_id` = {:d} ORDER BY `sid` DESC", player->getGUID())))) {
loadItems(itemMap, result);
for (ItemMap::const_reverse_iterator it = itemMap.rbegin(), end = itemMap.rend(); it != end; ++it) {
const std::pair<Item*, int32_t>& pair = it->second;
Item* item = pair.first;
int32_t pid = pair.second;
if (pid >= 0 && pid < 100) {
player->getInbox()->internalAddThing(item);
}
else {
ItemMap::const_iterator it2 = itemMap.find(pid);
if (it2 == itemMap.end()) {
continue;
}
Container* container = it2->second.first->getContainer();
if (container) {
container->internalAddThing(item);
}
}
}
}
//load storage map
query.str(std::string());
query << "SELECT `key`, `value` FROM `player_storage` WHERE `player_id` = " << player->getGUID();
@ -761,22 +794,37 @@ bool IOLoginData::savePlayer(Player* player)
return false;
}
//save depot items
query.str(std::string());
query << "DELETE FROM `player_depotitems` WHERE `player_id` = " << player->getGUID();
if (player->lastDepotId != -1) {
//save depot items
if (!db->executeQuery(fmt::format("DELETE FROM `player_depotitems` WHERE `player_id` = {:d}", player->getGUID()))) {
return false;
}
if (!db->executeQuery(query.str())) {
DBInsert depotQuery("INSERT INTO `player_depotitems` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES ");
itemList.clear();
for (const auto& it : player->depotLockerMap) {
itemList.emplace_back(it.first, it.second);
}
if (!saveItems(player, itemList, depotQuery, propWriteStream)) {
return false;
}
}
//save inbox items
if (!db->executeQuery(fmt::format("DELETE FROM `player_inboxitems` WHERE `player_id` = {:d}", player->getGUID()))) {
return false;
}
DBInsert depotQuery("INSERT INTO `player_depotitems` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES ");
DBInsert inboxQuery("INSERT INTO `player_inboxitems` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES ");
itemList.clear();
for (const auto& it : player->depotLockerMap) {
itemList.emplace_back(it.first, it.second);
for (Item* item : player->getInbox()->getItemList()) {
itemList.emplace_back(0, item);
}
if (!saveItems(player, itemList, depotQuery, propWriteStream)) {
if (!saveItems(player, itemList, inboxQuery, propWriteStream)) {
return false;
}
@ -985,3 +1033,14 @@ void IOLoginData::removePremiumDays(uint32_t accountId, int32_t removeDays)
query << "UPDATE `accounts` SET `premdays` = `premdays` - " << removeDays << " WHERE `id` = " << accountId;
Database::getInstance()->executeQuery(query.str());
}
uint32_t IOLoginData::getAccountIdByPlayerId(uint32_t playerId)
{
Database* db = Database::getInstance();
DBResult_ptr result = db->storeQuery(fmt::format("SELECT `account_id` FROM `players` WHERE `id` = {:d}", playerId));
if (!result) {
return 0;
}
return result->getNumber<uint32_t>("account_id");
}

View File

@ -59,6 +59,7 @@ class IOLoginData
static void addPremiumDays(uint32_t accountId, int32_t addDays);
static void removePremiumDays(uint32_t accountId, int32_t removeDays);
static uint32_t getAccountIdByPlayerId(uint32_t playerId);
protected:
typedef std::map<uint32_t, std::pair<Item*, uint32_t>> ItemMap;

307
src/iomarket.cpp Normal file
View File

@ -0,0 +1,307 @@
// Copyright 2022 The Forgotten Server Authors. All rights reserved.
// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file.
#include "otpch.h"
#include "iomarket.h"
#include "configmanager.h"
#include "databasetasks.h"
#include "game.h"
#include "inbox.h"
#include "iologindata.h"
#include "scheduler.h"
#include <fmt/core.h>
extern ConfigManager g_config;
extern Game g_game;
MarketOfferList IOMarket::getActiveOffers(MarketAction_t action, uint16_t itemId)
{
MarketOfferList offerList;
DBResult_ptr result = Database::getInstance()->storeQuery(fmt::format("SELECT `id`, `amount`, `price`, `created`, `anonymous`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `player_name` FROM `market_offers` WHERE `sale` = {:d} AND `itemtype` = {:d}", action, itemId));
if (!result) {
return offerList;
}
const int32_t marketOfferDuration = g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION);
do {
MarketOffer offer;
offer.amount = result->getNumber<uint16_t>("amount");
offer.price = result->getNumber<uint64_t>("price");
offer.timestamp = result->getNumber<uint32_t>("created") + marketOfferDuration;
offer.counter = result->getNumber<uint32_t>("id") & 0xFFFF;
if (result->getNumber<uint16_t>("anonymous") == 0) {
offer.playerName = result->getString("player_name");
}
else {
offer.playerName = "Anonymous";
}
offerList.push_back(offer);
} while (result->next());
return offerList;
}
MarketOfferList IOMarket::getOwnOffers(MarketAction_t action, uint32_t playerId)
{
MarketOfferList offerList;
const int32_t marketOfferDuration = g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION);
DBResult_ptr result = Database::getInstance()->storeQuery(fmt::format("SELECT `id`, `amount`, `price`, `created`, `itemtype` FROM `market_offers` WHERE `player_id` = {:d} AND `sale` = {:d}", playerId, action));
if (!result) {
return offerList;
}
do {
MarketOffer offer;
offer.amount = result->getNumber<uint16_t>("amount");
offer.price = result->getNumber<uint64_t>("price");
offer.timestamp = result->getNumber<uint32_t>("created") + marketOfferDuration;
offer.counter = result->getNumber<uint32_t>("id") & 0xFFFF;
offer.itemId = result->getNumber<uint16_t>("itemtype");
offerList.push_back(offer);
} while (result->next());
return offerList;
}
HistoryMarketOfferList IOMarket::getOwnHistory(MarketAction_t action, uint32_t playerId)
{
HistoryMarketOfferList offerList;
DBResult_ptr result = Database::getInstance()->storeQuery(fmt::format("SELECT `itemtype`, `amount`, `price`, `expires_at`, `state` FROM `market_history` WHERE `player_id` = {:d} AND `sale` = {:d}", playerId, action));
if (!result) {
return offerList;
}
do {
HistoryMarketOffer offer;
offer.itemId = result->getNumber<uint16_t>("itemtype");
offer.amount = result->getNumber<uint16_t>("amount");
offer.price = result->getNumber<uint64_t>("price");
offer.timestamp = result->getNumber<uint32_t>("expires_at");
MarketOfferState_t offerState = static_cast<MarketOfferState_t>(result->getNumber<uint16_t>("state"));
if (offerState == OFFERSTATE_ACCEPTEDEX) {
offerState = OFFERSTATE_ACCEPTED;
}
offer.state = offerState;
offerList.push_back(offer);
} while (result->next());
return offerList;
}
void IOMarket::processExpiredOffers(DBResult_ptr result, bool)
{
if (!result) {
return;
}
do {
if (!IOMarket::moveOfferToHistory(result->getNumber<uint32_t>("id"), OFFERSTATE_EXPIRED)) {
continue;
}
const uint32_t playerId = result->getNumber<uint32_t>("player_id");
const uint16_t amount = result->getNumber<uint16_t>("amount");
if (result->getNumber<uint16_t>("sale") == 1) {
const ItemType& itemType = Item::items[result->getNumber<uint16_t>("itemtype")];
if (itemType.id == 0) {
continue;
}
Player* player = g_game.getPlayerByGUID(playerId);
if (!player) {
player = new Player(nullptr);
if (!IOLoginData::loadPlayerById(player, playerId)) {
delete player;
continue;
}
}
if (itemType.stackable) {
uint16_t tmpAmount = amount;
while (tmpAmount > 0) {
uint16_t stackCount = std::min<uint16_t>(100, tmpAmount);
Item* item = Item::CreateItem(itemType.id, stackCount);
if (g_game.internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) {
delete item;
break;
}
tmpAmount -= stackCount;
}
}
else {
int32_t subType;
if (itemType.charges != 0) {
subType = itemType.charges;
}
else {
subType = -1;
}
for (uint16_t i = 0; i < amount; ++i) {
Item* item = Item::CreateItem(itemType.id, subType);
if (g_game.internalAddItem(player->getInbox(), item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) {
delete item;
break;
}
}
}
if (player->isOffline()) {
IOLoginData::savePlayer(player);
delete player;
}
}
else {
uint64_t totalPrice = result->getNumber<uint64_t>("price") * amount;
Player* player = g_game.getPlayerByGUID(playerId);
if (player) {
player->setBankBalance(player->getBankBalance() + totalPrice);
}
else {
IOLoginData::increaseBankBalance(playerId, totalPrice);
}
}
} while (result->next());
}
void IOMarket::checkExpiredOffers()
{
const time_t lastExpireDate = time(nullptr) - g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION);
g_databaseTasks.addTask(fmt::format("SELECT `id`, `amount`, `price`, `itemtype`, `player_id`, `sale` FROM `market_offers` WHERE `created` <= {:d}", lastExpireDate), IOMarket::processExpiredOffers, true);
int32_t checkExpiredMarketOffersEachMinutes = g_config.getNumber(ConfigManager::CHECK_EXPIRED_MARKET_OFFERS_EACH_MINUTES);
if (checkExpiredMarketOffersEachMinutes <= 0) {
return;
}
g_scheduler.addEvent(createSchedulerTask(checkExpiredMarketOffersEachMinutes * 60 * 1000, &IOMarket::checkExpiredOffers));
}
uint32_t IOMarket::getPlayerOfferCount(uint32_t playerId)
{
DBResult_ptr result = Database::getInstance()->storeQuery(fmt::format("SELECT COUNT(*) AS `count` FROM `market_offers` WHERE `player_id` = {:d}", playerId));
if (!result) {
return 0;
}
return result->getNumber<int32_t>("count");
}
MarketOfferEx IOMarket::getOfferByCounter(uint32_t timestamp, uint16_t counter)
{
MarketOfferEx offer;
const int32_t created = timestamp - g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION);
DBResult_ptr result = Database::getInstance()->storeQuery(fmt::format("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` = {:d} AND (`id` & 65535) = {:d} LIMIT 1", created, counter));
if (!result) {
offer.id = 0;
offer.playerId = 0;
return offer;
}
offer.id = result->getNumber<uint32_t>("id");
offer.type = static_cast<MarketAction_t>(result->getNumber<uint16_t>("sale"));
offer.amount = result->getNumber<uint16_t>("amount");
offer.counter = result->getNumber<uint32_t>("id") & 0xFFFF;
offer.timestamp = result->getNumber<uint32_t>("created");
offer.price = result->getNumber<uint64_t>("price");
offer.itemId = result->getNumber<uint16_t>("itemtype");
offer.playerId = result->getNumber<uint32_t>("player_id");
if (result->getNumber<uint16_t>("anonymous") == 0) {
offer.playerName = result->getString("player_name");
}
else {
offer.playerName = "Anonymous";
}
return offer;
}
void IOMarket::createOffer(uint32_t playerId, MarketAction_t action, uint32_t itemId, uint16_t amount, uint64_t price, bool anonymous)
{
Database::getInstance()->executeQuery(fmt::format("INSERT INTO `market_offers` (`player_id`, `sale`, `itemtype`, `amount`, `price`, `created`, `anonymous`) VALUES ({:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d})", playerId, action, itemId, amount, price, time(nullptr), anonymous));
}
void IOMarket::acceptOffer(uint32_t offerId, uint16_t amount)
{
Database::getInstance()->executeQuery(fmt::format("UPDATE `market_offers` SET `amount` = `amount` - {:d} WHERE `id` = {:d}", amount, offerId));
}
void IOMarket::deleteOffer(uint32_t offerId)
{
Database::getInstance()->executeQuery(fmt::format("DELETE FROM `market_offers` WHERE `id` = {:d}", offerId));
}
void IOMarket::appendHistory(uint32_t playerId, MarketAction_t type, uint16_t itemId, uint16_t amount, uint64_t price, time_t timestamp, MarketOfferState_t state)
{
g_databaseTasks.addTask(fmt::format("INSERT INTO `market_history` (`player_id`, `sale`, `itemtype`, `amount`, `price`, `expires_at`, `inserted`, `state`) VALUES ({:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d}, {:d})", playerId, type, itemId, amount, price, timestamp, time(nullptr), state));
}
bool IOMarket::moveOfferToHistory(uint32_t offerId, MarketOfferState_t state)
{
const int32_t marketOfferDuration = g_config.getNumber(ConfigManager::MARKET_OFFER_DURATION);
Database* db = Database::getInstance();
DBResult_ptr result = db->storeQuery(fmt::format("SELECT `player_id`, `sale`, `itemtype`, `amount`, `price`, `created` FROM `market_offers` WHERE `id` = {:d}", offerId));
if (!result) {
return false;
}
if (!db->executeQuery(fmt::format("DELETE FROM `market_offers` WHERE `id` = {:d}", offerId))) {
return false;
}
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<uint64_t>("price"), result->getNumber<uint32_t>("created") + marketOfferDuration, state);
return true;
}
void IOMarket::updateStatistics()
{
DBResult_ptr result = Database::getInstance()->storeQuery(fmt::format("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` = {:d} GROUP BY `itemtype`, `sale`", OFFERSTATE_ACCEPTED));
if (!result) {
return;
}
do {
MarketStatistics* statistics;
if (result->getNumber<uint16_t>("sale") == MARKETACTION_BUY) {
statistics = &purchaseStatistics[result->getNumber<uint16_t>("itemtype")];
}
else {
statistics = &saleStatistics[result->getNumber<uint16_t>("itemtype")];
}
statistics->numTransactions = result->getNumber<uint32_t>("num");
statistics->lowestPrice = result->getNumber<uint64_t>("min");
statistics->totalPrice = result->getNumber<uint64_t>("sum");
statistics->highestPrice = result->getNumber<uint64_t>("max");
} while (result->next());
}
MarketStatistics* IOMarket::getPurchaseStatistics(uint16_t itemId)
{
auto it = purchaseStatistics.find(itemId);
if (it == purchaseStatistics.end()) {
return nullptr;
}
return &it->second;
}
MarketStatistics* IOMarket::getSaleStatistics(uint16_t itemId)
{
auto it = saleStatistics.find(itemId);
if (it == saleStatistics.end()) {
return nullptr;
}
return &it->second;
}

47
src/iomarket.h Normal file
View File

@ -0,0 +1,47 @@
// Copyright 2022 The Forgotten Server Authors. All rights reserved.
// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file.
#ifndef FS_IOMARKET_H
#define FS_IOMARKET_H
#include "database.h"
#include "enums.h"
class IOMarket
{
public:
static IOMarket& getInstance() {
static IOMarket instance;
return instance;
}
static MarketOfferList getActiveOffers(MarketAction_t action, uint16_t itemId);
static MarketOfferList getOwnOffers(MarketAction_t action, uint32_t playerId);
static HistoryMarketOfferList getOwnHistory(MarketAction_t action, uint32_t playerId);
static void processExpiredOffers(DBResult_ptr result, bool);
static void checkExpiredOffers();
static uint32_t getPlayerOfferCount(uint32_t playerId);
static MarketOfferEx getOfferByCounter(uint32_t timestamp, uint16_t counter);
static void createOffer(uint32_t playerId, MarketAction_t action, uint32_t itemId, uint16_t amount, uint64_t price, bool anonymous);
static void acceptOffer(uint32_t offerId, uint16_t amount);
static void deleteOffer(uint32_t offerId);
static void appendHistory(uint32_t playerId, MarketAction_t type, uint16_t itemId, uint16_t amount, uint64_t price, time_t timestamp, MarketOfferState_t state);
static bool moveOfferToHistory(uint32_t offerId, MarketOfferState_t state);
void updateStatistics();
MarketStatistics* getPurchaseStatistics(uint16_t itemId);
MarketStatistics* getSaleStatistics(uint16_t itemId);
private:
IOMarket() = default;
std::map<uint16_t, MarketStatistics> purchaseStatistics;
std::map<uint16_t, MarketStatistics> saleStatistics;
};
#endif // FS_IOMARKET_H

View File

@ -1302,3 +1302,44 @@ void Item::startDecaying()
{
g_game.startDecay(this);
}
bool Item::hasMarketAttributes() const
{
if (!attributes) {
return true;
}
// discard items with custom boost and reflect
/*
TODO: find out if necessary
for (uint16_t i = 0; i < COMBAT_COUNT; ++i) {
if (getBoostPercent(indexToCombatType(i), false) > 0) {
return false;
}
Reflect tmpReflect = getReflect(indexToCombatType(i), false);
if (tmpReflect.chance != 0 || tmpReflect.percent != 0) {
return false;
}
}
*/
// discard items with other modified attributes
for (const auto& attr : attributes->getList()) {
if (attr.type == ITEM_ATTRIBUTE_CHARGES) {
uint16_t charges = static_cast<uint16_t>(attr.value.integer);
if (charges != items[id].charges) {
return false;
}
}
else if (attr.type == ITEM_ATTRIBUTE_DURATION) {
uint32_t duration = static_cast<uint32_t>(attr.value.integer);
if (duration != getDefaultDuration()) {
return false;
}
}
else {
return false;
}
}
return true;
}

View File

@ -776,6 +776,8 @@ class Item : virtual public Thing
virtual void startDecaying();
bool hasMarketAttributes() const;
void setLoadedFromMap(bool value) {
loadedFromMap = value;
}

View File

@ -258,6 +258,7 @@ class ItemType
bool useEvent = false;
bool multiUseEvent = false;
bool distUse = false;
bool storeItem = false;
bool disguise = false;
bool forceUse = false;
bool changeUse = false;

View File

@ -28,6 +28,7 @@
#include "protocolstatus.h"
#include "spells.h"
#include "iologindata.h"
#include "iomarket.h"
#include "configmanager.h"
#include "teleport.h"
#include "databasemanager.h"
@ -35,6 +36,8 @@
#include "monster.h"
#include "scheduler.h"
#include "databasetasks.h"
#include "inbox.h"
#include "depotchest.h"
extern Chat* g_chat;
extern Game g_game;
@ -1629,6 +1632,7 @@ void LuaScriptInterface::registerFunctions()
registerEnumIn("configKeys", ConfigManager::ALLOW_CLONES)
registerEnumIn("configKeys", ConfigManager::BIND_ONLY_GLOBAL_ADDRESS)
registerEnumIn("configKeys", ConfigManager::OPTIMIZE_DATABASE)
registerEnumIn("configKeys", ConfigManager::MARKET_PREMIUM)
registerEnumIn("configKeys", ConfigManager::STAMINA_SYSTEM)
registerEnumIn("configKeys", ConfigManager::WARN_UNSAFE_SCRIPTS)
registerEnumIn("configKeys", ConfigManager::CONVERT_UNSAFE_SCRIPTS)
@ -1685,6 +1689,9 @@ void LuaScriptInterface::registerFunctions()
registerEnumIn("configKeys", ConfigManager::LOGIN_PORT)
registerEnumIn("configKeys", ConfigManager::STATUS_PORT)
registerEnumIn("configKeys", ConfigManager::STAIRHOP_DELAY)
registerEnumIn("configKeys", ConfigManager::MARKET_OFFER_DURATION)
registerEnumIn("configKeys", ConfigManager::CHECK_EXPIRED_MARKET_OFFERS_EACH_MINUTES)
registerEnumIn("configKeys", ConfigManager::MAX_MARKET_OFFERS_AT_A_TIME_PER_PLAYER)
registerEnumIn("configKeys", ConfigManager::EXP_FROM_PLAYERS_LEVEL_RANGE)
registerEnumIn("configKeys", ConfigManager::MAX_PACKETS_PER_SECOND)
registerEnumIn("configKeys", ConfigManager::NEWBIE_TOWN)
@ -1989,6 +1996,7 @@ void LuaScriptInterface::registerFunctions()
registerMethod("Player", "getFreeCapacity", LuaScriptInterface::luaPlayerGetFreeCapacity);
registerMethod("Player", "getDepotChest", LuaScriptInterface::luaPlayerGetDepotChest);
registerMethod("Player", "getInbox", LuaScriptInterface::luaPlayerGetInbox);
registerMethod("Player", "getMurderTimestamps", LuaScriptInterface::luaPlayerGetMurderTimestamps);
registerMethod("Player", "getPlayerKillerEnd", LuaScriptInterface::luaPlayerGetPlayerKillerEnd);
@ -2300,6 +2308,9 @@ void LuaScriptInterface::registerFunctions()
registerMethod("ItemType", "getNutrition", LuaScriptInterface::luaItemTypeGetNutrition);
registerMethod("ItemType", "getRequiredLevel", LuaScriptInterface::luaItemTypeGetRequiredLevel);
registerMethod("ItemType", "getMarketBuyStatistics", LuaScriptInterface::luaItemTypeGetMarketBuyStatistics);
registerMethod("ItemType", "getMarketSellStatistics", LuaScriptInterface::luaItemTypeGetMarketSellStatistics);
registerMethod("ItemType", "hasSubType", LuaScriptInterface::luaItemTypeHasSubType);
// Combat
@ -7276,6 +7287,26 @@ int LuaScriptInterface::luaPlayerGetDepotChest(lua_State* L)
return 1;
}
int LuaScriptInterface::luaPlayerGetInbox(lua_State* L)
{
// player:getInbox()
Player* player = getUserdata<Player>(L, 1);
if (!player) {
lua_pushnil(L);
return 1;
}
Inbox* inbox = player->getInbox();
if (inbox) {
pushUserdata<Item>(L, inbox);
setItemMetatable(L, -1, inbox);
}
else {
pushBoolean(L, false);
}
return 1;
}
int LuaScriptInterface::luaPlayerGetMurderTimestamps(lua_State * L)
{
// player:getMurderTimestamps()
@ -10656,6 +10687,52 @@ int LuaScriptInterface::luaItemTypeGetRequiredLevel(lua_State* L)
return 1;
}
int LuaScriptInterface::luaItemTypeGetMarketBuyStatistics(lua_State* L)
{
// itemType:getMarketBuyStatistics()
const ItemType* itemType = getUserdata<const ItemType>(L, 1);
if (itemType) {
MarketStatistics* statistics = IOMarket::getInstance().getPurchaseStatistics(itemType->id);
if (statistics) {
lua_createtable(L, 4, 0);
setField(L, "numTransactions", statistics->numTransactions);
setField(L, "totalPrice", statistics->totalPrice);
setField(L, "highestPrice", statistics->highestPrice);
setField(L, "lowestPrice", statistics->lowestPrice);
}
else {
lua_pushnil(L);
}
}
else {
lua_pushnil(L);
}
return 1;
}
int LuaScriptInterface::luaItemTypeGetMarketSellStatistics(lua_State* L)
{
// itemType:getMarketSellStatistics()
const ItemType* itemType = getUserdata<const ItemType>(L, 1);
if (itemType) {
MarketStatistics* statistics = IOMarket::getInstance().getSaleStatistics(itemType->id);
if (statistics) {
lua_createtable(L, 4, 0);
setField(L, "numTransactions", statistics->numTransactions);
setField(L, "totalPrice", statistics->totalPrice);
setField(L, "highestPrice", statistics->highestPrice);
setField(L, "lowestPrice", statistics->lowestPrice);
}
else {
lua_pushnil(L);
}
}
else {
lua_pushnil(L);
}
return 1;
}
int LuaScriptInterface::luaItemTypeHasSubType(lua_State* L)
{
// itemType:hasSubType()

View File

@ -807,6 +807,7 @@ class LuaScriptInterface
static int luaPlayerGetFreeCapacity(lua_State* L);
static int luaPlayerGetDepotChest(lua_State* L);
static int luaPlayerGetInbox(lua_State* L);
static int luaPlayerGetMurderTimestamps(lua_State* L);
static int luaPlayerGetPlayerKillerEnd(lua_State* L);
@ -1110,6 +1111,9 @@ class LuaScriptInterface
static int luaItemTypeGetNutrition(lua_State* L);
static int luaItemTypeGetRequiredLevel(lua_State* L);
static int luaItemTypeGetMarketBuyStatistics(lua_State* L);
static int luaItemTypeGetMarketSellStatistics(lua_State* L);
static int luaItemTypeHasSubType(lua_State* L);
// Combat

View File

@ -24,6 +24,7 @@
#include "player.h"
#include "iologindata.h"
#include "town.h"
#include "inbox.h"
extern Game g_game;
@ -94,50 +95,42 @@ bool Mailbox::sendItem(Item* item) const
{
std::string receiver;
std::string townName;
townName = "thais";
if (!getDestination(item, receiver, townName)) {
return false;
}
if (receiver.empty() || townName.empty()) {
return false;
}
Town* town = g_game.map.towns.getTown(townName);
if (!town) {
/**No need to continue if its still empty**/
if (receiver.empty()) {
return false;
}
Player* player = g_game.getPlayerByName(receiver);
if (player) {
DepotLocker* depotLocker = player->getDepotLocker(town->getID(), true);
if (depotLocker) {
if (g_game.internalMoveItem(item->getParent(), depotLocker, INDEX_WHEREEVER,
item, item->getItemCount(), nullptr, FLAG_NOLIMIT) == RETURNVALUE_NOERROR) {
g_game.transformItem(item, item->getID() + 1);
player->onReceiveMail(town->getID());
return true;
}
if (g_game.internalMoveItem(item->getParent(), player->getInbox(), INDEX_WHEREEVER,
item, item->getItemCount(), nullptr, FLAG_NOLIMIT) == RETURNVALUE_NOERROR) {
g_game.transformItem(item, item->getID() + 1);
player->onReceiveMail();
return true;
}
} else {
}
else {
Player tmpPlayer(nullptr);
if (!IOLoginData::loadPlayerByName(&tmpPlayer, receiver)) {
return false;
}
DepotLocker* depotLocker = tmpPlayer.getDepotLocker(town->getID(), true);
if (depotLocker) {
if (g_game.internalMoveItem(item->getParent(), depotLocker, INDEX_WHEREEVER,
item, item->getItemCount(), nullptr, FLAG_NOLIMIT) == RETURNVALUE_NOERROR) {
g_game.transformItem(item, item->getID() + 1);
IOLoginData::savePlayer(&tmpPlayer);
return true;
}
if (g_game.internalMoveItem(item->getParent(), tmpPlayer.getInbox(), INDEX_WHEREEVER,
item, item->getItemCount(), nullptr, FLAG_NOLIMIT) == RETURNVALUE_NOERROR) {
g_game.transformItem(item, item->getID() + 1);
IOLoginData::savePlayer(&tmpPlayer);
return true;
}
}
return false;
}
bool Mailbox::getDestination(Item* item, std::string& name, std::string& town) const
{
const Container* container = item->getContainer();

View File

@ -22,6 +22,7 @@
#include "server.h"
#include "game.h"
#include "iomarket.h"
#ifndef _WIN32
#include <csignal> // for sigemptyset()
@ -305,6 +306,9 @@ void mainLoader(int, char*[], ServiceManager* services)
g_game.map.houses.payHouses(rentPeriod);
IOMarket::checkExpiredOffers();
IOMarket::getInstance().updateStatistics();
std::cout << ">> Loaded all modules, server starting up..." << std::endl;
#ifndef _WIN32

View File

@ -32,6 +32,8 @@
#include "monster.h"
#include "movement.h"
#include "scheduler.h"
#include "depotchest.h"
#include "inbox.h"
extern ConfigManager g_config;
extern Game g_game;
@ -46,8 +48,9 @@ MuteCountMap Player::muteCountMap;
uint32_t Player::playerAutoID = 0x10000000;
Player::Player(ProtocolGame_ptr p) :
Creature(), lastPing(OTSYS_TIME()), lastPong(lastPing), client(std::move(p))
Creature(), lastPing(OTSYS_TIME()), lastPong(lastPing), client(std::move(p)), inbox(new Inbox(ITEM_INBOX))
{
inbox->incrementReferenceCounter();
}
Player::~Player()
@ -60,9 +63,11 @@ Player::~Player()
}
for (const auto& it : depotLockerMap) {
it.second->decrementReferenceCounter();
it.second->removeInbox(inbox);
}
inbox->decrementReferenceCounter();
setWriteItem(nullptr);
setEditHouse(nullptr);
}
@ -579,14 +584,14 @@ bool Player::canSeeCreature(const Creature* creature) const
return true;
}
void Player::onReceiveMail(uint32_t townId) const
void Player::onReceiveMail() const
{
if (isNearDepotBox(townId)) {
if (isNearDepotBox()) {
sendTextMessage(MESSAGE_EVENT_ADVANCE, "New mail has arrived.");
}
}
bool Player::isNearDepotBox(uint32_t townId) const
bool Player::isNearDepotBox() const
{
const Position& pos = getPosition();
for (int32_t cx = -1; cx <= 1; ++cx) {
@ -597,25 +602,43 @@ bool Player::isNearDepotBox(uint32_t townId) const
}
if (DepotLocker* depotLocker = tile->getDepotLocker()) {
if (depotLocker->getDepotId() == townId) {
return true;
}
return true;
}
}
}
return false;
}
DepotChest* Player::getDepotChest(uint32_t depotId, bool autoCreate)
{
auto it = depotChests.find(depotId);
if (it != depotChests.end()) {
return it->second;
}
if (!autoCreate) {
return nullptr;
}
it = depotChests.emplace(depotId, new DepotChest(ITEM_DEPOT)).first;
it->second->setMaxDepotItems(getMaxDepotItems());
return it->second;
}
DepotLocker* Player::getDepotLocker(uint32_t depotId, bool autoCreate)
{
auto it = depotLockerMap.find(depotId);
if (it != depotLockerMap.end()) {
inbox->setParent(it->second);
return it->second;
}
if (autoCreate) {
DepotLocker* depotLocker = new DepotLocker(ITEM_LOCKER1);
depotLocker->setDepotId(depotId);
depotLocker->internalAddThing(Item::CreateItem(ITEM_MARKET));
depotLocker->internalAddThing(inbox);
Item* depotItem = Item::CreateItem(ITEM_DEPOT);
if (depotItem) {
depotLocker->internalAddThing(depotItem);
@ -1006,6 +1029,11 @@ void Player::onCreatureMove(Creature* creature, const Tile* newTile, const Posit
}
}
// leave market
if (inMarket) {
inMarket = false;
}
if (party) {
party->updateSharedExperience();
}

View File

@ -35,6 +35,7 @@
#include "town.h"
class BehaviourDatabase;
class DepotChest;
class House;
class NetworkMessage;
class Weapon;
@ -207,6 +208,10 @@ class Player final : public Creature, public Cylinder
bool isInWar(const Player* player) const;
bool isInWarList(uint32_t guild_id) const;
Inbox* getInbox() const {
return inbox;
}
uint16_t getClientIcons() const;
const GuildWarList& getGuildWarList() const {
@ -306,6 +311,20 @@ class Player final : public Creature, public Cylinder
return group;
}
void setInMarket(bool value) {
inMarket = value;
}
bool isInMarket() const {
return inMarket;
}
void setLastDepotId(int16_t newId) {
lastDepotId = newId;
}
int16_t getLastDepotId() const {
return lastDepotId;
}
void resetIdleTime() {
idleTime = 0;
resetLastWalkingTime();
@ -454,9 +473,10 @@ class Player final : public Creature, public Cylinder
void addConditionSuppressions(uint32_t conditions);
void removeConditionSuppressions(uint32_t conditions);
DepotChest* getDepotChest(uint32_t depotId, bool autoCreate);
DepotLocker* getDepotLocker(uint32_t depotId, bool autoCreate);
void onReceiveMail(uint32_t townId) const;
bool isNearDepotBox(uint32_t townId) const;
void onReceiveMail() const;
bool isNearDepotBox() const;
bool canSee(const Position& pos) const final;
bool canSeeCreature(const Creature* creature) const final;
@ -856,6 +876,42 @@ class Player final : public Creature, public Cylinder
client->sendToChannel(creature, type, text, channelId);
}
}
void sendMarketEnter(uint32_t depotId) const {
if (client) {
client->sendMarketEnter(depotId);
}
}
void sendMarketLeave() {
inMarket = false;
if (client) {
client->sendMarketLeave();
}
}
void sendMarketBrowseItem(uint16_t itemId, const MarketOfferList& buyOffers, const MarketOfferList& sellOffers) const {
if (client) {
client->sendMarketBrowseItem(itemId, buyOffers, sellOffers);
}
}
void sendMarketBrowseOwnOffers(const MarketOfferList& buyOffers, const MarketOfferList& sellOffers) const {
if (client) {
client->sendMarketBrowseOwnOffers(buyOffers, sellOffers);
}
}
void sendMarketBrowseOwnHistory(const HistoryMarketOfferList& buyOffers, const HistoryMarketOfferList& sellOffers) const {
if (client) {
client->sendMarketBrowseOwnHistory(buyOffers, sellOffers);
}
}
void sendMarketAcceptOffer(const MarketOfferEx& offer) const {
if (client) {
client->sendMarketAcceptOffer(offer);
}
}
void sendMarketCancelOffer(const MarketOfferEx& offer) const {
if (client) {
client->sendMarketCancelOffer(offer);
}
}
void sendTradeItemRequest(const std::string& traderName, const Item* item, bool ack) const {
if (client) {
client->sendTradeItemRequest(traderName, item, ack);
@ -1023,6 +1079,7 @@ class Player final : public Creature, public Cylinder
std::map<uint8_t, OpenContainer> openContainers;
std::map<uint32_t, DepotLocker*> depotLockerMap;
std::map<uint32_t, DepotChest*> depotChests;
std::map<uint32_t, int32_t> storageMap;
std::vector<OutfitEntry> outfits;
@ -1061,6 +1118,7 @@ class Player final : public Creature, public Cylinder
Guild* guild = nullptr;
const GuildRank* guildRank = nullptr;
Group* group = nullptr;
Inbox* inbox;
Item* tradeItem = nullptr;
Item* inventory[CONST_SLOT_LAST + 1] = {};
Item* writeItem = nullptr;
@ -1103,6 +1161,7 @@ class Player final : public Creature, public Cylinder
uint16_t staminaMinutes = 3360;
uint16_t maxWriteLen = 0;
int16_t lastDepotId = -1;
uint8_t soul = 0;
uint8_t blessings = 0;
@ -1118,6 +1177,7 @@ class Player final : public Creature, public Cylinder
AccountType_t accountType = ACCOUNT_TYPE_NORMAL;
bool secureMode = false;
bool inMarket = false;
bool ghostMode = false;
bool pzLocked = false;
bool isConnecting = false;

View File

@ -30,10 +30,13 @@
#include "configmanager.h"
#include "actions.h"
#include "game.h"
#include "inbox.h"
#include "iomarket.h"
#include "iologindata.h"
#include "waitlist.h"
#include "ban.h"
#include "scheduler.h"
#include "depotchest.h"
extern ConfigManager g_config;
extern Actions actions;
@ -451,6 +454,11 @@ void ProtocolGame::parsePacket(NetworkMessage& msg)
case 0xE8: parseDebugAssert(msg); break;
case 0xF0: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerShowQuestLog, player->getID()); break;
case 0xF1: parseQuestLine(msg); break;
case 0xF4: parseMarketLeave(); break;
case 0xF5: parseMarketBrowse(msg); break;
case 0xF6: parseMarketCreateOffer(msg); break;
case 0xF7: parseMarketCancelOffer(msg); break;
case 0xF8: parseMarketAcceptOffer(msg); break;
default:
std::cout << "Player: " << player->getName() << " sent an unknown packet header: 0x" << std::hex << static_cast<uint16_t>(recvbyte) << std::dec << "!" << std::endl;
break;
@ -997,6 +1005,61 @@ void ProtocolGame::parseQuestLine(NetworkMessage& msg)
addGameTask(&Game::playerShowQuestLine, player->getID(), questId);
}
void ProtocolGame::parseMarketLeave()
{
addGameTask(&Game::playerLeaveMarket, player->getID());
}
void ProtocolGame::parseMarketBrowse(NetworkMessage& msg)
{
uint8_t browseId = msg.get<uint8_t>();
if (browseId == MARKETREQUEST_OWN_OFFERS) {
addGameTask(&Game::playerBrowseMarketOwnOffers, player->getID());
}
else if (browseId == MARKETREQUEST_OWN_HISTORY) {
addGameTask(&Game::playerBrowseMarketOwnHistory, player->getID());
}
else {
uint16_t spriteID = msg.get<uint16_t>();
addGameTask(&Game::playerBrowseMarket, player->getID(), spriteID);
}
}
void ProtocolGame::parseMarketCreateOffer(NetworkMessage& msg)
{
uint8_t type = msg.getByte();
uint16_t spriteId = msg.get<uint16_t>();
const ItemType& it = Item::items[spriteId];
if (it.id == 0) {
return;
}
// TODO:
//else if (it.classification > 0) {
// msg.getByte(); // item tier
//}
uint16_t amount = msg.get<uint16_t>();
uint64_t price = msg.get<uint64_t>();
bool anonymous = (msg.getByte() != 0);
addGameTask(&Game::playerCreateMarketOffer, player->getID(), type, spriteId, amount, price, anonymous);
}
void ProtocolGame::parseMarketCancelOffer(NetworkMessage& msg)
{
uint32_t timestamp = msg.get<uint32_t>();
uint16_t counter = msg.get<uint16_t>();
addGameTask(&Game::playerCancelMarketOffer, player->getID(), timestamp, counter);
}
void ProtocolGame::parseMarketAcceptOffer(NetworkMessage& msg)
{
uint32_t timestamp = msg.get<uint32_t>();
uint16_t counter = msg.get<uint16_t>();
uint16_t amount = msg.get<uint16_t>();
addGameTask(&Game::playerAcceptMarketOffer, player->getID(), timestamp, counter, amount);
}
void ProtocolGame::parseSeekInContainer(NetworkMessage& msg)
{
uint8_t containerId = msg.getByte();
@ -1251,6 +1314,259 @@ void ProtocolGame::sendContainer(uint8_t cid, const Container* container, bool h
writeToOutputBuffer(msg);
}
void ProtocolGame::sendMarketEnter(uint32_t depotId)
{
NetworkMessage msg;
msg.addByte(0xF6);
msg.addByte(std::min<uint32_t>(IOMarket::getPlayerOfferCount(player->getGUID()), std::numeric_limits<uint8_t>::max()));
DepotLocker* depotLocker = player->getDepotLocker(depotId, false);
if (!depotLocker) {
msg.add<uint16_t>(0x00);
writeToOutputBuffer(msg);
return;
}
player->setInMarket(true);
msg.add<uint64_t>(player->getBankBalance());
std::map<uint16_t, uint32_t> depotItems;
std::forward_list<Container*> containerList{ depotLocker, player->getInbox() };
do {
Container* container = containerList.front();
containerList.pop_front();
for (Item* item : container->getItemList()) {
Container* c = item->getContainer();
if (c && !c->empty()) {
containerList.push_front(c);
continue;
}
const ItemType& itemType = Item::items[item->getID()];
if (itemType.id == 0) {
continue;
}
if (c && (!itemType.isContainer() || c->capacity() != itemType.maxItems)) {
continue;
}
if (!item->hasMarketAttributes()) {
continue;
}
depotItems[itemType.id] += Item::countByType(item, -1);
}
} while (!containerList.empty());
uint16_t itemsToSend = std::min<size_t>(depotItems.size(), std::numeric_limits<uint16_t>::max());
uint16_t i = 0;
msg.add<uint16_t>(itemsToSend);
for (std::map<uint16_t, uint32_t>::const_iterator it = depotItems.begin(); i < itemsToSend; ++it, ++i) {
const ItemType& itemType = Item::items[it->first];
msg.add<uint16_t>(itemType.id);
// TODO
//if (itemType.classification > 0) {
// msg.addByte(0);
//}
msg.add<uint16_t>(std::min<uint32_t>(0xFFFF, it->second));
}
writeToOutputBuffer(msg);
}
void ProtocolGame::sendMarketLeave()
{
NetworkMessage msg;
msg.addByte(0xF7);
writeToOutputBuffer(msg);
}
void ProtocolGame::sendMarketBrowseItem(uint16_t itemId, const MarketOfferList& buyOffers, const MarketOfferList& sellOffers)
{
NetworkMessage msg;
msg.addByte(0xF9);
msg.addByte(MARKETREQUEST_ITEM);
msg.addItemId(itemId);
// TODO
//if (Item::items[itemId].classification > 0) {
// msg.addByte(0); // item tier
//}
msg.add<uint32_t>(buyOffers.size());
for (const MarketOffer& offer : buyOffers) {
msg.add<uint32_t>(offer.timestamp);
msg.add<uint16_t>(offer.counter);
msg.add<uint16_t>(offer.amount);
msg.add<uint64_t>(offer.price);
msg.addString(offer.playerName);
}
msg.add<uint32_t>(sellOffers.size());
for (const MarketOffer& offer : sellOffers) {
msg.add<uint32_t>(offer.timestamp);
msg.add<uint16_t>(offer.counter);
msg.add<uint16_t>(offer.amount);
msg.add<uint64_t>(offer.price);
msg.addString(offer.playerName);
}
writeToOutputBuffer(msg);
}
void ProtocolGame::sendMarketAcceptOffer(const MarketOfferEx& offer)
{
NetworkMessage msg;
msg.addByte(0xF9);
msg.addByte(MARKETREQUEST_ITEM);
msg.addItemId(offer.itemId);
// TODO
//if (Item::items[offer.itemId].classification > 0) {
// msg.addByte(0);
//}
if (offer.type == MARKETACTION_BUY) {
msg.add<uint32_t>(0x01);
msg.add<uint32_t>(offer.timestamp);
msg.add<uint16_t>(offer.counter);
msg.add<uint16_t>(offer.amount);
msg.add<uint64_t>(offer.price);
msg.addString(offer.playerName);
msg.add<uint32_t>(0x00);
}
else {
msg.add<uint32_t>(0x00);
msg.add<uint32_t>(0x01);
msg.add<uint32_t>(offer.timestamp);
msg.add<uint16_t>(offer.counter);
msg.add<uint16_t>(offer.amount);
msg.add<uint64_t>(offer.price);
msg.addString(offer.playerName);
}
writeToOutputBuffer(msg);
}
void ProtocolGame::sendMarketBrowseOwnOffers(const MarketOfferList& buyOffers, const MarketOfferList& sellOffers)
{
NetworkMessage msg;
msg.addByte(0xF9);
msg.addByte(MARKETREQUEST_OWN_OFFERS);
msg.add<uint32_t>(buyOffers.size());
for (const MarketOffer& offer : buyOffers) {
msg.add<uint32_t>(offer.timestamp);
msg.add<uint16_t>(offer.counter);
msg.addItemId(offer.itemId);
// TODO
//if (Item::items[offer.itemId].classification > 0) {
// msg.addByte(0);
//}
msg.add<uint16_t>(offer.amount);
msg.add<uint64_t>(offer.price);
}
msg.add<uint32_t>(sellOffers.size());
for (const MarketOffer& offer : sellOffers) {
msg.add<uint32_t>(offer.timestamp);
msg.add<uint16_t>(offer.counter);
msg.addItemId(offer.itemId);
// TODO
//if (Item::items[offer.itemId].classification > 0) {
// msg.addByte(0);
//}
msg.add<uint16_t>(offer.amount);
msg.add<uint64_t>(offer.price);
}
writeToOutputBuffer(msg);
}
void ProtocolGame::sendMarketCancelOffer(const MarketOfferEx& offer)
{
NetworkMessage msg;
msg.addByte(0xF9);
msg.addByte(MARKETREQUEST_OWN_OFFERS);
if (offer.type == MARKETACTION_BUY) {
msg.add<uint32_t>(0x01);
msg.add<uint32_t>(offer.timestamp);
msg.add<uint16_t>(offer.counter);
msg.addItemId(offer.itemId);
// TODO
//if (Item::items[offer.itemId].classification > 0) {
// msg.addByte(0);
//}
msg.add<uint16_t>(offer.amount);
msg.add<uint64_t>(offer.price);
msg.add<uint32_t>(0x00);
}
else {
msg.add<uint32_t>(0x00);
msg.add<uint32_t>(0x01);
msg.add<uint32_t>(offer.timestamp);
msg.add<uint16_t>(offer.counter);
msg.addItemId(offer.itemId);
// TODO
//if (Item::items[offer.itemId].classification > 0) {
// msg.addByte(0);
//}
msg.add<uint16_t>(offer.amount);
msg.add<uint64_t>(offer.price);
}
writeToOutputBuffer(msg);
}
void ProtocolGame::sendMarketBrowseOwnHistory(const HistoryMarketOfferList& buyOffers, const HistoryMarketOfferList& sellOffers)
{
uint32_t i = 0;
std::map<uint32_t, uint16_t> counterMap;
uint32_t buyOffersToSend = std::min<uint32_t>(buyOffers.size(), 810 + std::max<int32_t>(0, 810 - sellOffers.size()));
uint32_t sellOffersToSend = std::min<uint32_t>(sellOffers.size(), 810 + std::max<int32_t>(0, 810 - buyOffers.size()));
NetworkMessage msg;
msg.addByte(0xF9);
msg.addByte(MARKETREQUEST_OWN_HISTORY);
msg.add<uint32_t>(buyOffersToSend);
for (auto it = buyOffers.begin(); i < buyOffersToSend; ++it, ++i) {
msg.add<uint32_t>(it->timestamp);
msg.add<uint16_t>(counterMap[it->timestamp]++);
msg.addItemId(it->itemId);
// TODO
//if (Item::items[it->itemId].classification > 0) {
// msg.addByte(0);
//}
msg.add<uint16_t>(it->amount);
msg.add<uint64_t>(it->price);
msg.addByte(it->state);
}
counterMap.clear();
i = 0;
msg.add<uint32_t>(sellOffersToSend);
for (auto it = sellOffers.begin(); i < sellOffersToSend; ++it, ++i) {
msg.add<uint32_t>(it->timestamp);
msg.add<uint16_t>(counterMap[it->timestamp]++);
msg.addItemId(it->itemId);
// TODO
//if (Item::items[it->itemId].classification > 0) {
// msg.addByte(0);
//}
msg.add<uint16_t>(it->amount);
msg.add<uint64_t>(it->price);
msg.addByte(it->state);
}
writeToOutputBuffer(msg);
}
void ProtocolGame::sendQuestLog()
{
NetworkMessage msg;

View File

@ -115,6 +115,13 @@ class ProtocolGame final : public Protocol
void parseQuestLine(NetworkMessage& msg);
//market methods
void parseMarketLeave();
void parseMarketBrowse(NetworkMessage& msg);
void parseMarketCreateOffer(NetworkMessage& msg);
void parseMarketCancelOffer(NetworkMessage& msg);
void parseMarketAcceptOffer(NetworkMessage& msg);
void parseInviteToParty(NetworkMessage& msg);
void parseJoinParty(NetworkMessage& msg);
void parseRevokePartyInvite(NetworkMessage& msg);
@ -160,6 +167,13 @@ class ProtocolGame final : public Protocol
void sendCreatureTurn(const Creature* creature, uint32_t stackpos);
void sendCreatureSay(const Creature* creature, SpeakClasses type, const std::string& text, const Position* pos = nullptr);
void sendMarketEnter(uint32_t depotId);
void sendMarketLeave();
void sendMarketBrowseItem(uint16_t itemId, const MarketOfferList& buyOffers, const MarketOfferList& sellOffers);
void sendMarketAcceptOffer(const MarketOfferEx& offer);
void sendMarketBrowseOwnOffers(const MarketOfferList& buyOffers, const MarketOfferList& sellOffers);
void sendMarketCancelOffer(const MarketOfferEx& offer);
void sendMarketBrowseOwnHistory(const HistoryMarketOfferList& buyOffers, const HistoryMarketOfferList& sellOffers);
void sendQuestLog();
void sendQuestLine(const Quest* quest);

51
src/storeinbox.cpp Normal file
View File

@ -0,0 +1,51 @@
// Copyright 2022 The Forgotten Server Authors. All rights reserved.
// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file.
#include "otpch.h"
#include "storeinbox.h"
StoreInbox::StoreInbox(uint16_t type) : Container(type, 20, true, true) {}
ReturnValue StoreInbox::queryAdd(int32_t, const Thing& thing, uint32_t, uint32_t flags, Creature*) const
{
const Item* item = thing.getItem();
if (!item) {
return RETURNVALUE_NOTPOSSIBLE;
}
if (item == this) {
return RETURNVALUE_THISISIMPOSSIBLE;
}
if (!item->isPickupable()) {
return RETURNVALUE_CANNOTPICKUP;
}
if (!hasBitSet(FLAG_NOLIMIT, flags)) {
if (!item->isStoreItem()) {
return RETURNVALUE_CANNOTMOVEITEMISNOTSTOREITEM;
}
const Container* container = item->getContainer();
if (container && !container->empty()) {
return RETURNVALUE_ITEMCANNOTBEMOVEDTHERE;
}
}
return RETURNVALUE_NOERROR;
}
void StoreInbox::postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t)
{
if (parent) {
parent->postAddNotification(thing, oldParent, index, LINK_TOPPARENT);
}
}
void StoreInbox::postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t)
{
if (parent) {
parent->postRemoveNotification(thing, newParent, index, LINK_TOPPARENT);
}
}

33
src/storeinbox.h Normal file
View File

@ -0,0 +1,33 @@
// Copyright 2022 The Forgotten Server Authors. All rights reserved.
// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file.
#ifndef FS_STOREINBOX_H
#define FS_STOREINBOX_H
#include "container.h"
class StoreInbox final : public Container
{
public:
explicit StoreInbox(uint16_t type);
StoreInbox* getStoreInbox() override {
return this;
}
const StoreInbox* getStoreInbox() const override {
return this;
}
//cylinder implementations
ReturnValue queryAdd(int32_t index, const Thing& thing, uint32_t count,
uint32_t flags, Creature* actor = nullptr) const override;
void postAddNotification(Thing* thing, const Cylinder* oldParent, int32_t index, cylinderlink_t link = LINK_OWNER) override;
void postRemoveNotification(Thing* thing, const Cylinder* newParent, int32_t index, cylinderlink_t link = LINK_OWNER) override;
bool canRemove() const override {
return false;
}
};
#endif // FS_STOREINBOX_H

View File

@ -48,7 +48,7 @@ typedef std::vector<int32_t> IntegerVec;
StringVec explodeString(const std::string& inString, const std::string& separator, int32_t limit = -1);
IntegerVec vectorAtoi(const StringVec& stringVector);
inline bool hasBitSet(uint32_t flag, uint32_t flags) {
constexpr bool hasBitSet(uint32_t flag, uint32_t flags) {
return (flags & flag) != 0;
}

View File

@ -159,6 +159,7 @@
<ClCompile Include="..\src\database.cpp" />
<ClCompile Include="..\src\databasemanager.cpp" />
<ClCompile Include="..\src\databasetasks.cpp" />
<ClCompile Include="..\src\depotchest.cpp" />
<ClCompile Include="..\src\depotlocker.cpp" />
<ClCompile Include="..\src\events.cpp" />
<ClCompile Include="..\src\fileloader.cpp" />
@ -168,6 +169,7 @@
<ClCompile Include="..\src\guild.cpp" />
<ClCompile Include="..\src\house.cpp" />
<ClCompile Include="..\src\housetile.cpp" />
<ClCompile Include="..\src\inbox.cpp" />
<ClCompile Include="..\src\ioguild.cpp" />
<ClCompile Include="..\src\iologindata.cpp" />
<ClCompile Include="..\src\iomap.cpp" />
@ -218,6 +220,7 @@
<ClCompile Include="..\src\wildcardtree.cpp" />
<ClCompile Include="..\src\xtea.cpp" />
<ClCompile Include="..\src\quests.cpp" />
<ClCompile Include="..\src\iomarket.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\src\account.h" />
@ -240,6 +243,7 @@
<ClInclude Include="..\src\databasemanager.h" />
<ClInclude Include="..\src\databasetasks.h" />
<ClInclude Include="..\src\definitions.h" />
<ClInclude Include="..\src\depotchest.h" />
<ClInclude Include="..\src\depotlocker.h" />
<ClInclude Include="..\src\events.h" />
<ClInclude Include="..\src\enums.h" />
@ -250,6 +254,7 @@
<ClInclude Include="..\src\guild.h" />
<ClInclude Include="..\src\house.h" />
<ClInclude Include="..\src\housetile.h" />
<ClInclude Include="..\src\inbox.h" />
<ClInclude Include="..\src\ioguild.h" />
<ClInclude Include="..\src\iologindata.h" />
<ClInclude Include="..\src\iomap.h" />
@ -297,6 +302,7 @@
<ClInclude Include="..\src\wildcardtree.h" />
<ClInclude Include="..\src\xtea.h" />
<ClInclude Include="..\src\quests.h" />
<ClInclude Include="..\src\iomarket.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">