EvEmu  0.8.4
11 September 2021
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
MarketMgr.cpp
Go to the documentation of this file.
1 
14 #include "Client.h"
15 #include "EVEServerConfig.h"
16 #include "StaticDataMgr.h"
17 #include "StatisticMgr.h"
18 #include "account/AccountService.h"
19 #include "cache/ObjCacheService.h"
21 #include "market/MarketMgr.h"
22 #include "station/StationDataMgr.h"
23 
24 /*
25  * MARKET__ERROR
26  * MARKET__WARNING
27  * MARKET__MESSAGE
28  * MARKET__DEBUG
29  * MARKET__TRACE
30  * MARKET__DB_ERROR
31  * MARKET__DB_TRACE
32  */
33 
35 : m_marketGroups(nullptr),
36 m_manager(nullptr)
37 {
38  m_timeStamp = 0;
39 }
40 
42 {
43  //PyDecRef(m_marketGroups);
44 }
45 
47 {
50  sLog.Warning(" MarketMgr", "Market Manager has been closed." );
51 }
52 
54 {
55  m_manager = pManager;
56 
58 
59  Populate();
60  sLog.Blue(" MarketMgr", "Market Manager Initialized.");
61  return 1;
62 }
63 
65 {
66  double start = GetTimeMSeconds();
68 
69  Process();
70 
71  // market orders stored as {regionID/typeID} --do we want to store orders in memory for loaded region??
72  // m_db.GetOrders(call.client->GetRegionID(), args.arg);
73 
74  sLog.Blue(" MarketMgr", "Market Manager loaded in %.3fms.", (GetTimeMSeconds() - start));
75  sLog.Cyan(" MarketMgr", "Market Manager Updates Price History every %u hours.", sConfig.market.HistoryUpdateTime);
76 }
77 
79 {
80  /* get info in current market data? */
81 }
82 
84 {
85  // make cache timer of xx(time) then invalidate the price history cache
86 
87  //if (m_timeStamp > GetFileTimeNow())
88  // UpdatePriceHistory();
89 }
90 
92 {
93 
94 }
95 
97 {
98 
99 }
100 
102 {
103  m_timeStamp = GetFileTimeNow() + (EvE::Time::Hour * sConfig.market.HistoryUpdateTime);
105 
106  DBerror err;
107  int64 cutoff_time = m_timeStamp;
108  cutoff_time -= cutoff_time % EvE::Time::Day; //round down to an even day boundary.
109  cutoff_time -= EvE::Time::Day * 2; //the cutoff between "new" and "old" price history in days
110 
112  //build the history record from the recent market transactions.
113  sDatabase.RunQuery(err,
114  "INSERT INTO"
115  " mktHistory"
116  " (regionID, typeID, historyDate, lowPrice, highPrice, avgPrice, volume, orders)"
117  " SELECT"
118  " regionID,"
119  " typeID,"
120  " transactionDate,"
121  " MIN(price),"
122  " MAX(price),"
123  " AVG(price),"
124  " SUM(quantity),"
125  " COUNT(transactionID)"
126  " FROM mktTransactions"
127  " WHERE transactionType=%u AND transactionDate < %li",
128  //" GROUP BY regionID, typeID, transactionDate",
129  Market::Type::Sell, cutoff_time);
130 
132  // remove the transactions which have been aged out?
133  if (sConfig.market.DeleteOldTransactions)
134  sDatabase.RunQuery(err, "DELETE FROM mktTransactions WHERE transactionDate < %li", (cutoff_time - EvE::Time::Year));
135 }
136 
137  /*DBColumnTypeMap colmap;
138  * colmap["historyDate"] = DBTYPE_FILETIME;
139  * colmap["lowPrice"] = DBTYPE_CY;
140  * colmap["highPrice"] = DBTYPE_CY;
141  * colmap["avgPrice"] = DBTYPE_CY;
142  * colmap["volume"] = DBTYPE_I8;
143  * colmap["orders"] = DBTYPE_I4;
144  * SELECT transactionDate AS historyDate, MIN(price) AS lowPrice, MAX(price) AS highPrice, AVG(price) AS avgPrice, quantity AS volume, COUNT(transactionID) AS orders FROM mktTransactions WHERE regionID=10000030 AND typeID=487 AND transactionType=0 AND historyDate > 20604112 GROUP BY historyDate
145  */
146 
147 // there is a 1 day difference (from 0000UTC) between "Old" and "New" prices
149  PyRep* result(nullptr);
150  std::string method_name ("GetNewHistory_");
151  method_name += std::to_string(regionID);
152  method_name += "_";
153  method_name += std::to_string(typeID);
154  ObjectCachedMethodID method_id("marketProxy", method_name.c_str());
155  //check to see if this method is in the cache already.
156  if (!m_manager->cache_service->IsCacheLoaded(method_id)) {
157  //this method is not in cache yet, load up the contents and cache it
158  DBQueryResult res;
159 
161  if(!sDatabase.RunQuery(res,
162  "SELECT historyDate, lowPrice, highPrice, avgPrice, volume, orders"
163  " FROM mktHistory "
164  " WHERE regionID=%u AND typeID=%u"
165  " AND historyDate > %li LIMIT %u",
166  regionID, typeID, (m_timeStamp - EvE::Time::Day), sConfig.market.NewPriceLimit))
167  {
168  _log(DATABASE__ERROR, "Error in query: %s", res.error.c_str());
169  return nullptr;
170  }
171  _log(MARKET__DB_TRACE, "MarketMgr::GetNewPriceHistory() - Fetched %u buy orders for type %u in region %u from mktTransactions", res.GetRowCount(), typeID, regionID);
172 
173  result = DBResultToCRowset(res);
174  if (result == nullptr) {
175  _log(MARKET__DB_ERROR, "Failed to load cache, generating empty contents.");
176  result = PyStatic.NewNone();
177  }
178  m_manager->cache_service->GiveCache(method_id, &result);
179  }
180 
181  //now we know its in the cache one way or the other, so build a
182  //cached object cached method call result.
184 
185  if (is_log_enabled(MARKET__DB_TRACE))
186  result->Dump(MARKET__DB_TRACE, " ");
187  return result;
188 }
189 
191  PyRep* result(nullptr);
192  std::string method_name ("GetOldHistory_");
193  method_name += std::to_string(regionID);
194  method_name += "_";
195  method_name += std::to_string(typeID);
196  ObjectCachedMethodID method_id("marketProxy", method_name.c_str());
197  //check to see if this method is in the cache already.
198  if (!m_manager->cache_service->IsCacheLoaded(method_id)) {
199  //this method is not in cache yet, load up the contents and cache it
200  DBQueryResult res;
201 
203  if(!sDatabase.RunQuery(res,
204  "SELECT historyDate, lowPrice, highPrice, avgPrice, volume, orders"
205  " FROM mktHistory WHERE regionID=%u AND typeID=%u"
206  " AND historyDate > %li AND historyDate < %li LIMIT %u",
207  regionID, typeID, (m_timeStamp - (EvE::Time::Day *3)), (m_timeStamp - EvE::Time::Day), sConfig.market.OldPriceLimit))
208  {
209  _log(DATABASE__ERROR, "Error in query: %s", res.error.c_str());
210  return nullptr;
211  }
212  _log(MARKET__DB_TRACE, "MarketMgr::GetOldPriceHistory() - Fetched %u orders for type %u in region %u from mktHistory", res.GetRowCount(), typeID, regionID);
213 
214  result = DBResultToCRowset(res);
215  if (result == nullptr) {
216  _log(MARKET__DB_ERROR, "Failed to load cache, generating empty contents.");
217  result = PyStatic.NewNone();
218  }
219  m_manager->cache_service->GiveCache(method_id, &result);
220  }
221 
222  //now we know its in the cache one way or the other, so build a
223  //cached object cached method call result.
225 
226  if (is_log_enabled(MARKET__DB_TRACE))
227  result->Dump(MARKET__DB_TRACE, " ");
228  return result;
229 }
230 
231 void MarketMgr::SendOnOwnOrderChanged(Client* pClient, uint32 orderID, uint8 action, bool isCorp/*false*/, PyRep* order/*nullptr*/) {
232  if (pClient == nullptr)
233  return;
234  Notify_OnOwnOrderChanged ooc;
235  if (order != nullptr) {
236  ooc.order = order;
237  } else {
238  ooc.order = m_db.GetOrderRow(orderID);
239  }
240 
241  switch (action) {
242  case Market::Action::Add:
243  ooc.reason = "Add";
244  break;
246  ooc.reason = "Modify";
247  break;
249  ooc.reason = "Expiry";
250  break;
251  }
252 
253  ooc.isCorp = isCorp;
254  PyTuple* tmp = ooc.Encode();
255  // send journal blink and call 'self.RefreshOrders()' in client
256  if (isCorp) {
257  sEntityList.CorpNotify(pClient->GetCorporationID(), 125 /*MarketOrder*/, "OnOwnOrderChanged", "*corpid&corprole", tmp);
258  } else {
259  pClient->SendNotification("OnOwnOrderChanged", "clientID", &tmp);
260  }
261 }
262 
263 
265 {
266  std::string method_name ("GetOrders_");
267  method_name += std::to_string(regionID);
268  method_name += "_";
269  method_name += std::to_string(typeID);
270  ObjectCachedMethodID method_id("marketProxy", method_name.c_str());
271  m_manager->cache_service->InvalidateCache( method_id );
272 }
273 
274 
276 /*
277  * def BrokersFee(self, stationID, amount, commissionPercentage):
278  * if amount < 0.0:
279  * raise AttributeError('Amount must be positive')
280  * orderValue = float(amount)
281  * station = sm.GetService('ui').GetStation(stationID)
282  * stationOwnerID = None
283  * if station is not None:
284  * stationOwnerID = station.ownerID
285  * factionChar = 0
286  * corpChar = 0
287  * if stationOwnerID:
288  * if util.IsNPC(stationOwnerID):
289  * factionID = sm.GetService('faction').GetFaction(stationOwnerID)
290  * factionChar = sm.GetService('standing').GetStanding(factionID, eve.session.charid) or 0.0
291  * corpChar = sm.GetService('standing').GetStanding(stationOwnerID, eve.session.charid) or 0.0
292  * weightedStanding = (0.7 * factionChar + 0.3 * corpChar) / 10.0
293  * commissionPercentage = commissionPercentage * 2.0 ** (-2 * weightedStanding)
294  * tax = util.KeyVal()
295  * tax.amt = commissionPercentage * orderValue
296  * tax.percentage = commissionPercentage
297  * if tax.amt <= const.mktMinimumFee:
298  * tax.amt = const.mktMinimumFee
299  * tax.percentage = -1.0
300  * return tax
301  */
302 
303 bool MarketMgr::ExecuteBuyOrder(Client* seller, uint32 orderID, InventoryItemRef iRef, Call_PlaceCharOrder& args, uint16 accountKey/*Account::KeyType::Cash*/) {
304 
306  if (!m_db.GetOrderInfo(orderID, oInfo)) {
307  _log(MARKET__ERROR, "ExecuteBuyOrder - Failed to get order info for #%u.", orderID);
308  return true;
309  }
310 
311  /* will this method also be used to buy/sell using aurm?
312  * unknown yet
313  */
314 
315  // get buyer id and determine if buyer is player or corp (or bot for later)
316  bool isPlayer(false), isCorp(false), isTrader(false);
317  if (IsPlayerCorp(oInfo.ownerID)) {
318  // buyer is player corp
319  isCorp = true;
320  } else if (oInfo.ownerID == 1) {
321  oInfo.ownerID = stDataMgr.GetOwnerID(args.stationID);
322  } else if (IsCharacterID(oInfo.ownerID)) {
323  isPlayer = true;
324  } else if (IsTraderJoe(oInfo.ownerID)) {
325  isTrader = true;
326  } else {
327  // none of above conditionals hit....
328  _log(MARKET__WARNING, "ExecuteBuyOrder - ownerID %u not corp, not char, not system, not joe.", oInfo.ownerID);
329  // send the player some kind of notification about the market order, some standard market error should suffice
330  seller->SendNotifyMsg ("Your order cannot be processed at this time, please try again later.");
331  return false;
332  }
333 
334  // quantity status of seller's item vs buyer's order
336  if (iRef->quantity() == args.quantity) {
337  qtyStatus = Market::QtyStatus::Complete;
338  //use the owner change packet to alert the buyer of the new item
339  if (isPlayer) {
340  iRef->Donate(oInfo.ownerID, args.stationID, flagHangar, true);
341  } else if (isCorp) {
342  iRef->Donate(oInfo.ownerID, args.stationID, flagCorpMarket, true);
343  } else if (isTrader) {
344  // trader joe is a placeholder ID to trash every item sold to him
345  iRef->Delete ();
346  }
347  } else if (iRef->quantity() > args.quantity) {
348  // qty for sale > buy order amt
349  qtyStatus = Market::QtyStatus::Over;
350  //need to split item up...
351  if (isTrader) {
352  // trader joe is a blackhole, just subtract the amount of items we're selling to him and call it a day
353  iRef->AlterQuantity(-args.quantity, true);
354  } else {
355  InventoryItemRef siRef = iRef->Split(args.quantity);
356  if (siRef.get() == nullptr) {
357  _log(MARKET__ERROR, "ExecuteBuyOrder - Failed to split %u %s.", siRef->itemID(), siRef->name());
358  return true;
359  }
360  //use the owner change packet to alert the buyer of the new item
361  if (isPlayer) {
362  siRef->Donate(oInfo.ownerID, args.stationID, flagHangar, true);
363  } else if (isCorp) {
364  siRef->Donate(oInfo.ownerID, args.stationID, flagCorpMarket, true);
365  }
366  }
367  } else {
368  // qty for sale < buy order amt
369  qtyStatus = Market::QtyStatus::Under;
370  //use the owner change packet to alert the buyer of the new item
371  if (isPlayer) {
372  iRef->Donate(oInfo.ownerID, args.stationID, flagHangar, true);
373  } else if (isCorp) {
374  iRef->Donate(oInfo.ownerID, args.stationID, flagCorpMarket, true);
375  } else if (isTrader) {
376  iRef->Delete ();
377  }
378  }
379 
380  uint32 qtySold(args.quantity);
381  switch (qtyStatus) {
383  // this should never hit. make error here.
384  } break;
385  case Market::QtyStatus::Under: // order requires more items than seller is offering. delete args.quantity and update order
386  case Market::QtyStatus::Complete: { // order qty matches item qty. delete args.quantity and delete order
387  args.quantity = 0;
388  } break;
390  // more for sale than order requires. update args.quantity and delete order
391  args.quantity -= qtySold;
392  } break;
393  }
394 
395  float money = args.price * qtySold;
396  std::string reason = "DESC: Buying items in ";
397  reason += stDataMgr.GetStationName(args.stationID).c_str();
398  uint32 sellerWalletOwnerID = 0;
399  uint8 level = seller->GetChar ()->GetSkillLevel (EvESkill::Accounting);
400  float tax = EvEMath::Market::SalesTax (sConfig.market.salesTax, level) * money;
401 
402  if (args.useCorp) {
403  // make sure the user has permissions to take money from the corporation account
404  if (
405  (accountKey == 1000 && (seller->GetCorpRole () & Corp::Role::AccountCanTake1) == 0) ||
406  (accountKey == 1001 && (seller->GetCorpRole () & Corp::Role::AccountCanTake2) == 0) ||
407  (accountKey == 1002 && (seller->GetCorpRole () & Corp::Role::AccountCanTake3) == 0) ||
408  (accountKey == 1003 && (seller->GetCorpRole () & Corp::Role::AccountCanTake4) == 0) ||
409  (accountKey == 1004 && (seller->GetCorpRole () & Corp::Role::AccountCanTake5) == 0) ||
410  (accountKey == 1005 && (seller->GetCorpRole () & Corp::Role::AccountCanTake6) == 0) ||
411  (accountKey == 1006 && (seller->GetCorpRole () & Corp::Role::AccountCanTake7) == 0)
412  )
413  throw UserError("CrpAccessDenied").AddFormatValue ("reason", new PyString ("You do not have access to that wallet"));
414 
415  sellerWalletOwnerID = seller->GetCorporationID ();
416  _log(MARKET__DEBUG, "ExecuteBuyOrder - Seller is Corp: Price: %.2f, Tax: %.2f", money, tax);
417  } else {
418  sellerWalletOwnerID = seller->GetCharacterID ();
419  _log(MARKET__DEBUG, "ExecuteBuyOrder - Seller is Player: Price: %.2f, Tax: %.2f", money, tax);
420  }
421 
422  AccountService::TranserFunds (sellerWalletOwnerID, corpSCC, tax, reason.c_str (),
423  Journal::EntryType::TransactionTax, orderID, accountKey);
424 
425  // send wallet blink event and record the transaction in their journal.
426  reason.clear();
427  reason += "DESC: Selling items in ";
428  reason += stDataMgr.GetStationName(args.stationID).c_str();
429  // this is fulfilling a buy order. seller will receive isk from escrow if buyer is player or corp
430  if (isPlayer or isCorp) {
431  //give the money to the seller from the escrow acct at station
432  AccountService::TranserFunds(stDataMgr.GetOwnerID(args.stationID), seller->GetCharacterID(), \
433  money, reason.c_str(), Journal::EntryType::MarketTransaction, orderID, \
434  Account::KeyType::Escrow, accountKey);
435  } else {
436  // npc buyer. direct xfer to seller
437  AccountService::TranserFunds(oInfo.ownerID, seller->GetCharacterID(), money, reason.c_str(), \
439  }
440 
441  // add data to StatisticMgr
442  sStatMgr.Add(Stat::iskMarket, money);
443 
444  //record this sell transaction in market_transactions
446  data.accountKey = accountKey;
447  data.isBuy = Market::Type::Sell;
448  data.isCorp = args.useCorp;
449  data.memberID = args.useCorp?seller->GetCharacterID():0;
450  data.clientID = args.useCorp?seller->GetCorporationID():seller->GetCharacterID();
451  data.price = args.price;
452  data.quantity = args.quantity;
453  data.stationID = args.stationID;
454  data.regionID = sDataMgr.GetStationRegion(args.stationID);
455  data.typeID = args.typeID;
456 
457  if (!m_db.RecordTransaction(data)) {
458  _log(MARKET__ERROR, "ExecuteBuyOrder - Failed to record sale side of transaction.");
459  }
460 
461  if (isPlayer or isCorp) {
462  // update data for other side if player or player corp
463  data.isBuy = Market::Type::Buy;
464  data.clientID = oInfo.ownerID;
465  data.memberID = isCorp?oInfo.memberID:0;
466  if (!m_db.RecordTransaction(data)) {
467  _log(MARKET__ERROR, "ExecuteBuyOrder - Failed to record buy side of transaction.");
468  }
469  }
470 
471  if (qtyStatus == Market::QtyStatus::Under) {
472  uint32 newQty(oInfo.quantity - args.quantity);
473  _log(MARKET__TRACE, "ExecuteBuyOrder - Partially satisfied order #%u, altering quantity to %u.", orderID, newQty);
474  if (!m_db.AlterOrderQuantity(orderID, newQty)) {
475  _log(MARKET__ERROR, "ExecuteBuyOrder - Failed to alter quantity of order #%u.", orderID);
476  return true;
477  }
478  InvalidateOrdersCache(oInfo.regionID, args.typeID);
479  if (isPlayer or isCorp)
480  SendOnOwnOrderChanged(seller, orderID, Market::Action::Modify, args.useCorp);
481 
482  return false;
483  }
484 
485  _log(MARKET__TRACE, "ExecuteBuyOrder - Satisfied order #%u, deleting.", orderID);
486  PyRep* order = m_db.GetOrderRow(orderID);
487  if (!m_db.DeleteOrder(orderID)) {
488  _log(MARKET__ERROR, "ExecuteBuyOrder - Failed to delete order #%u.", orderID);
489  return true;
490  }
491  InvalidateOrdersCache(oInfo.regionID, args.typeID);
492  if (isPlayer or isCorp)
493  SendOnOwnOrderChanged(seller, orderID, Market::Action::Expiry, args.useCorp, order);
494  return true;
495 }
496 
497 void MarketMgr::ExecuteSellOrder(Client* buyer, uint32 orderID, Call_PlaceCharOrder& args) {
499  if (!m_db.GetOrderInfo(orderID, oInfo)) {
500  _log(MARKET__ERROR, "ExecuteSellOrder - Failed to get info about sell order %u.", orderID);
501  return;
502  }
503 
504  bool orderConsumed(false);
505  if (args.quantity > oInfo.quantity)
506  args.quantity = oInfo.quantity;
507  if (args.quantity == oInfo.quantity)
508  orderConsumed = true;
509 
510  if (sDataMgr.IsStation(oInfo.ownerID))
511  oInfo.ownerID = stDataMgr.GetOwnerID(oInfo.ownerID);
512 
514  float money = args.price * args.quantity;
515  // send wallet blink event and record the transaction in their journal.
516  std::string reason = "DESC: Buying market items in ";
517  reason += stDataMgr.GetStationName(args.stationID).c_str();
518  // this will throw if funds not available.
519  AccountService::TranserFunds(buyer->GetCharacterID(), oInfo.ownerID, money, reason.c_str(), \
521 
522  uint32 sellerCharacterID = 0;
523 
524  if (oInfo.isCorp) {
525  sellerCharacterID = oInfo.memberID;
526  } else {
527  sellerCharacterID = oInfo.ownerID;
528  }
529 
530  uint8 accountingLevel(0);
531  uint8 taxEvasionLevel(0);
532  Client* pSeller = sEntityList.FindClientByCharID (sellerCharacterID);
533 
534  if (pSeller == nullptr) {
535  accountingLevel = CharacterDB::GetSkillLevel (sellerCharacterID, EvESkill::Accounting);
536  taxEvasionLevel = CharacterDB::GetSkillLevel (sellerCharacterID, EvESkill::TaxEvasion);
537  } else {
538  accountingLevel = pSeller->GetChar ()->GetSkillLevel (EvESkill::Accounting);
539  taxEvasionLevel = pSeller->GetChar ()->GetSkillLevel (EvESkill::TaxEvasion);
540  }
541 
542  float tax = EvEMath::Market::SalesTax (sConfig.market.salesTax, accountingLevel, taxEvasionLevel) * money;
543  AccountService::TranserFunds (oInfo.ownerID, corpSCC, tax, reason.c_str (),
545  // after money is xferd, create and add item.
546  ItemData idata(args.typeID, ownerStation, locTemp, flagNone, args.quantity);
547  InventoryItemRef iRef = sItemFactory.SpawnItem(idata);
548  if (iRef.get() == nullptr)
549  return;
550 
551  //use the owner change packet to alert the buyer of the new item
552  iRef->Donate(buyer->GetCharacterID(), args.stationID, flagHangar, true);
553 
554  // add data to StatisticMgr
555  sStatMgr.Add(Stat::iskMarket, money);
556 
557  Client* seller(nullptr);
558  if (IsCharacterID(oInfo.ownerID))
559  seller = sEntityList.FindClientByCharID(oInfo.ownerID);
560 
561  if (orderConsumed) {
562  _log(MARKET__TRACE, "ExecuteSellOrder - satisfied order #%u, deleting.", orderID);
563  PyRep* order = m_db.GetOrderRow(orderID);
564  if (!m_db.DeleteOrder(orderID)) {
565  _log(MARKET__ERROR, "ExecuteSellOrder - Failed to delete order #%u.", orderID);
566  return;
567  }
568  InvalidateOrdersCache(oInfo.regionID, args.typeID);
569  SendOnOwnOrderChanged(seller, orderID, Market::Action::Expiry, args.useCorp, order);
570  } else {
571  uint32 newQty(oInfo.quantity - args.quantity);
572  _log(MARKET__TRACE, "ExecuteSellOrder - Partially satisfied order #%u, altering quantity to %u.", orderID, newQty);
573  if (!m_db.AlterOrderQuantity(orderID, newQty)) {
574  _log(MARKET__ERROR, "ExecuteSellOrder - Failed to alter quantity of order #%u.", orderID);
575  return;
576  }
577  InvalidateOrdersCache(oInfo.regionID, args.typeID);
578  SendOnOwnOrderChanged(seller, orderID, Market::Action::Modify, args.useCorp);
579  }
580 
581  //record this transaction in market_transactions
584  data.accountKey = Account::KeyType::Cash; // args.useCorp?accountKey: Account::KeyType::Cash;
585  data.isBuy = Market::Type::Buy;
586  data.isCorp = args.useCorp;
587  data.memberID = args.useCorp?buyer->GetCharacterID():0;
588  data.clientID = args.useCorp?buyer->GetCorporationID():buyer->GetCharacterID();
589  data.price = args.price;
590  data.quantity = args.quantity;
591  data.stationID = args.stationID;
592  data.regionID = sDataMgr.GetStationRegion(args.stationID);
593  data.typeID = args.typeID;
594 
595  if (!m_db.RecordTransaction(data)) {
596  _log(MARKET__ERROR, "ExecuteSellOrder - Failed to record buy side of transaction.");
597  }
598 
599  // update data for other side
600  data.accountKey = Account::KeyType::Cash; // args.useCorp?accountKey: Account::KeyType::Cash;
601  data.isBuy = Market::Type::Sell;
602  data.clientID = oInfo.ownerID;
603  if (!m_db.RecordTransaction(data)) {
604  _log(MARKET__ERROR, "ExecuteSellOrder - Failed to record sell side of transaction.");
605  }
606 }
607 
608 
609 // after finding price data from Crucible, this may be moot. -allan 28Feb21
611 {
612  /* method to estimate item base price, based on materials to manufacture that item
613  *
614  * mineral prices are queried from db with a +10% markup
615  *
616  * ships and modules are loaded and queried from static data for manufacturing materials
617  * those materials are queried (as required) for minerals needed
618  * once a total mineral value has been calculated, calculate estimated cost based on
619  * current mineral values
620  *
621  * final prices will have markup based on item type
622  *
623  *
624  * NOTES FOR IMPLEMETING THIS SHIT
625  *
626  * //SELECT typeID, materialTypeID, quantity FROM invTypeMaterials
627  * EvERam::RamMaterials ramMatls = EvERam::RamMaterials();
628  * ramMatls.quantity = row.GetInt(2);
629  * ramMatls.materialTypeID = row.GetInt(1);
630  *
631  * eventually, this will query all items that can be manufactured from BP or refined into minerals
632  *
633  */
634 
635  // get mineral prices and put into data map
636  // typeID/data{typeID, price, name}
637  std::map<uint16, Market::matlData> mineralMap;
638  mineralMap.clear();
639  sDataMgr.GetMineralData(mineralMap); // 8
640 
641  // this will have to use db to get current data.
642  // mineral prices are (will be) updated via a 'price average' method yet to be written
643  MarketDB::GetMineralPrices(mineralMap);
644 
645 
646  // get 'building blocks' used for cap ships and put into data map
647  //block typeID/vector<data{materialTypeID, qty}>
648  std::map<uint16, Market::matlData> materialMap;
649  materialMap.clear();
650  sDataMgr.GetComponentData(materialMap); // 125
651 
652  // get compounds from ice and put into data map
653  sDataMgr.GetCompoundData(materialMap); // 7
654 
655  // get misc commodities and put into data map
656  sDataMgr.GetMiscCommodityData(materialMap); // 456
657 
658  // get salvage items for rigs and other items made from them
659  sDataMgr.GetSalvageData(materialMap); // 53
660 
661  // get PI resources
662  sDataMgr.GetPIResourceData(materialMap); // 15
663 
664  // get PI commodities
665  sDataMgr.GetPICommodityData(materialMap); // 66
666 
667  // hack to add this item to materialMap, instead of getting entire group
668  // 22175 is Codebreaker I, which is a reproc item from Purloined Sansha Codebreaker.
670  data.typeID = 22175;
671  data.name = "Codebreaker I";
672  materialMap[22175] = data;
673 
674  // this will have to use db to get current data.
675  // mineral prices are (will be) updated via a 'price average' method yet to be written
676  MarketDB::GetMaterialPrices(materialMap);
677 
678  // add minerals to material maps
679  materialMap.insert(mineralMap.begin(), mineralMap.end());
680  //sDataMgr.GetMineralData(materialMap); // 8
681 
682 
683  // item typeID/data{inventory data}
684  std::map<uint16, Inv::TypeData> itemMap;
685  itemMap.clear();
686  // this gets only ships
687  //MarketDB::GetShipIDs(itemMap);
688  // this gets all items made from minerals either directly or indirectly
690 
691  //item typeID/vector<data{materialTypeID, qty}>
692  std::map<uint16, std::vector<EvERam::RamMaterials>> itemMatMap;
693  itemMatMap.clear();
694  std::map<uint16, Inv::TypeData>::iterator itemItr = itemMap.begin();
695  for (; itemItr != itemMap.end(); ++itemItr) {
696  // pull data for this item -need r/w iterator to work
697  sDataMgr.GetType(itemItr->first, itemItr->second);
698 
699  // get materials required for this item
700  std::vector<EvERam::RamMaterials> matVec;
701  matVec.clear();
702  sDataMgr.GetRamMaterials(itemItr->first, matVec);
703  itemMatMap[itemItr->first] = matVec;
704  }
705 
706 
707  // estimate price of item based on mineral requirements
708  bool found(true);
709  uint8 mLevel(0);
710  double current(0);
711  Inv::GrpData gData = Inv::GrpData();
713  // item typeID/data{inventory data}
714  std::map<uint16, Inv::TypeData> missingItemMap;
715  missingItemMap.clear();
716  std::map<uint16, Market::matlData>::iterator materialItr = materialMap.begin();
717  std::map<uint16, std::vector<EvERam::RamMaterials>>::iterator itemMatItr = itemMatMap.end();
718  for (itemItr = itemMap.begin(); itemItr != itemMap.end(); ++itemItr) {
719  itemMatItr = itemMatMap.find(itemItr->first);
720  if (itemMatItr == itemMatMap.end())
721  continue;
722 
723  found = true;
724  current = itemItr->second.basePrice;
725  // reset basePrice
726  itemItr->second.basePrice = 0.0;
727  // sum mineral counts with current prices for this ship
728  for (auto cur2 : itemMatItr->second) {
729  materialItr = materialMap.find(cur2.materialTypeID);
730  if (materialItr == materialMap.end()) {
731  sLog.Error(" SetBasePrice", "resource %u for %s(%u) not found in materialMap", \
732  cur2.materialTypeID, itemItr->second.name.c_str(), itemItr->first);
733  missingItemMap[itemItr->first] = Inv::TypeData();
734  found = false;
735  continue;
736  }
737  itemItr->second.basePrice += (materialItr->second.price * cur2.quantity);
738  }
739  if (!found)
740  continue;
741 
742  // add manuf costs to base price
743  // currently uses production time to add cost at line rental default of 1k install and 2500/hr
744  if (sDataMgr.GetBpDataForItem(itemItr->first, bpData)) {
745  itemItr->second.basePrice += 1000 + (2500 * (bpData.productionTime / 3600)); // time is in seconds
746  }
747 
748  mLevel = itemItr->second.metaLvl;
749  gData = Inv::GrpData();
750  sDataMgr.GetGroup(itemItr->second.groupID, gData);
751 
752  // apply modifier to base price according to item category (complexity, rarity, demand)
753  // these should also be adjusted for portion size
754  switch (gData.catID) {
756  itemItr->second.basePrice /= itemItr->second.portionSize;
757  // modify price based on meta
758  switch (mLevel) {
759  case 0: { //basic
760  itemItr->second.basePrice *= 2;
761  } break;
762  case 1: { //t1
763  itemItr->second.basePrice *= 2.5;
764  } break;
765  case 2: { //t2
766  itemItr->second.basePrice *= 3.5;
767  } break;
768  }
769  } break;
775  if (mLevel)
776  itemItr->second.basePrice *= mLevel;
777  itemItr->second.basePrice /= itemItr->second.portionSize;
778  } break;
780  if (mLevel)
781  itemItr->second.basePrice *= mLevel;
782  // asteroids cannot be 'created' per se, but the mined ore can be sold.
783  // this cat covers mined ore, so use same pricing method as charges
784  if (itemItr->first < 28000)
785  itemItr->second.basePrice /= itemItr->second.portionSize;
786  } break;
788  itemItr->second.basePrice *= 2;
789  // multiply price by metaLvl
790  if (mLevel) {
791  switch (gData.id) {
805  itemItr->second.basePrice *= (mLevel + 1);
806  } break;
807  default: {
808  itemItr->second.basePrice *= (mLevel + 10);
809  } break;
810  }
811  }
812  } break;
814  // multiply price by metaLvl
815  if (mLevel)
816  itemItr->second.basePrice *= mLevel;
817  // modify price based on class
818  switch (gData.id) {
823  itemItr->second.basePrice *= 2.1; // +80%
824  } break;
826  itemItr->second.basePrice *= 1.2; // +20%
827  } break;
840  itemItr->second.basePrice *= 1.1; // +10%
841  } break;
842  // these are good...
847  itemItr->second.basePrice *= 1.5; // +50%
848  } break;
857  // these can only be crated in pos.
861  itemItr->second.basePrice *= 0.6; // -40% these are all outrageous at calculated prices
862  } break;
864  itemItr->second.basePrice *= 0.35; // this one is weird...
865  } break;
867  itemItr->second.basePrice *= 3; // x3 for shuttles
868  } break;
871  itemItr->second.basePrice *= 1000; // x1000
872  } break;
873  }
874  } break;
876  // for some reason, station pricing is way off....
877  itemItr->second.basePrice *= 100;
878  } break;
879  }
880 
881  if (itemItr->second.basePrice < 0.01) {
882  sLog.Error(" SetBasePrice", "Calculated price for %s(%u) is 0", \
883  itemItr->second.name.c_str(), itemItr->first);
884  } /*else {
885  sLog.Blue(" SetBasePrice", "Calculated price for %u %s(cat:%u - %s - meta %u) from %.2f to %.2f", \
886  itemItr->first, itemItr->second.name.c_str(), gData.catID, gData.name.c_str(), \
887  itemItr->second.metaLvl, current, itemItr->second.basePrice);
888  }*/
889  }
890 
891  // check for missing items and get average price using crucible mkt data table
892  if (missingItemMap.empty()) {
893  // update db for 'new' base price
894  MarketDB::UpdateInvPrice(itemMap);
895  } else {
896  MarketDB::GetCruPriceAvg(missingItemMap);
897  MarketDB::UpdateInvPrice(missingItemMap);
898  sLog.Error(" SetBasePrice", "Missing material. run again.");
899  }
900 }
901 
902 
904 {
905  // get mineral prices and put into data map
906  // typeID/data{typeID, price, name}
907  std::map<uint16, Market::matlData> mineralMap;
908  mineralMap.clear();
909  sDataMgr.GetMineralData(mineralMap); // 8
910 
911  // update mineral price
912  MarketDB::UpdateMktPrice(mineralMap);
913 
914 }
915 
917 {
918  /*
919  std::map<uint16, Market::matlData> materialMap;
920  materialMap.clear();
921  // dont update minerals...they are set.
922  //sDataMgr.GetMineralData(materialMap); // 8
923  sDataMgr.GetSalvageData(materialMap); // 53
924  sDataMgr.GetCompoundData(materialMap); // 7
925  sDataMgr.GetComponentData(materialMap); // 114
926  sDataMgr.GetPIResourceData(materialMap); // 15
927  sDataMgr.GetPICommodityData(materialMap); // 66
928 
929  MarketDB::GetCruPriceAvg(materialMap);
930 
931  MarketDB::UpdateMktPrice(materialMap);
932  */
933 
934  sLog.Warning(" SetBasePrice", "Getting types.");
935  std::map<uint16, Inv::TypeData> types;
936  sDataMgr.GetTypes(types); //19669 unique items in type data
937  sLog.Green(" SetBasePrice", "GetTypes returned %u items. Getting price avg.", types.size());
938  // delete the typeID '0'
939  types.erase(0);
940 
941  MarketDB::GetCruPriceAvg(types); //7723 unique items in price data
942  sLog.Green(" SetBasePrice", "Got Avg prices. updating items.");
943  MarketDB::UpdateInvPrice(types); //7712 non-zero prices
944  sLog.Cyan(" SetBasePrice", "Completed.");
945 }
#define IsTraderJoe(itemID)
Definition: EVE_Defines.h:395
Base Python wire object.
Definition: PyRep.h:66
#define sConfig
A macro for easier access to the singleton.
uint32 regionID
Definition: EVE_Market.h:80
unsigned __int8 uint8
Definition: eve-compat.h:46
virtual InventoryItemRef Split(int32 qty=0, bool notify=true, bool silent=false)
void Donate(uint32 new_owner=ownerSystem, uint32 new_location=locTemp, EVEItemFlags new_flag=flagNone, bool notify=true)
#define sStatMgr
Definition: StatisticMgr.h:68
void SendNotification(const PyAddress &dest, EVENotificationStream &noti, bool seq=true)
Definition: Client.cpp:2245
bool GetOrderInfo(uint32 orderID, Market::OrderInfo &oInfo)
Definition: MarketDB.cpp:253
#define sDatabase
Definition: dbcore.h:199
PyRep * m_marketGroups
Definition: MarketMgr.h:73
#define _log(type, fmt,...)
Definition: logsys.h:124
#define stDataMgr
PyObjectEx * DBResultToCRowset(DBQueryResult &result)
Definition: EVEDBUtils.cpp:402
void SystemStartup(SystemData &data)
Definition: MarketMgr.cpp:91
Python string.
Definition: PyRep.h:430
uint32 stationID
Definition: EVE_Market.h:79
uint32 quantity
Definition: EVE_Market.h:81
int64 GetCorpRole() const
Definition: Client.h:129
PyRep * GetMarketGroups()
Definition: MarketDB.cpp:401
static void GetMineralPrices(std::map< uint16, Market::matlData > &data)
Definition: MarketDB.cpp:543
void UpdatePriceHistory()
Definition: MarketMgr.cpp:101
int32 GetCharacterID() const
Definition: Client.h:113
PyRep * GetOldPriceHistory(uint32 regionID, uint32 typeID)
Definition: MarketMgr.cpp:190
void GetCruPrices()
Definition: MarketMgr.cpp:916
int32 GetCorporationID() const
Definition: Client.h:123
static void UpdateInvPrice(std::map< uint16, Inv::TypeData > &data)
Definition: MarketDB.cpp:570
PyRep * GetNewPriceHistory(uint32 regionID, uint32 typeID)
Definition: MarketMgr.cpp:148
#define sEntityList
Definition: EntityList.h:208
UserError & AddFormatValue(const char *name, PyRep *value)
Fluent version of the protected AddKeyword, allows for adding a keyword to the exception.
const char * name()
void SendNotifyMsg(const char *fmt,...)
Definition: Client.cpp:2776
CharacterRef GetChar() const
Definition: Client.h:164
Python tuple.
Definition: PyRep.h:567
void Dump(FILE *into, const char *pfx) const
Dumps object to file.
Definition: PyRep.cpp:84
std::string name
Definition: EVE_Market.h:138
void InvalidateOrdersCache(uint32 regionID, uint32 typeID)
Definition: MarketMgr.cpp:264
* args
#define is_log_enabled(type)
Definition: logsys.h:78
#define sLog
Evaluates to a NewLog instance.
Definition: LogNew.h:250
void SendOnOwnOrderChanged(Client *pClient, uint32 orderID, uint8 action, bool isCorp=false, PyRep *order=nullptr)
Definition: MarketMgr.cpp:231
MarketDB m_db
Definition: MarketMgr.h:70
const char * c_str() const
Definition: dbcore.h:48
double GetTimeMSeconds()
Definition: utils_time.cpp:104
PyRep * GetOrderRow(uint32 orderID)
Definition: MarketDB.cpp:172
uint16 typeID
Definition: EVE_Market.h:77
static uint8 GetSkillLevel(uint32 charID, uint16 skillTypeID)
void GetInfo()
Definition: MarketMgr.cpp:78
static void TranserFunds(uint32 fromID, uint32 toID, double amount, std::string reason="", uint8 entryTypeID=Journal::EntryType::Undefined, uint32 referenceID=0, uint16 fromKey=Account::KeyType::Cash, uint16 toKey=Account::KeyType::Cash, Client *pClient=nullptr)
bool AlterQuantity(int32 qty, bool notify=false)
int Initialize(PyServiceMgr *pManager)
Definition: MarketMgr.cpp:53
uint32 accountKey
Definition: EVE_Market.h:82
uint32 productionTime
Definition: EVE_RAM.h:120
#define PyStatic
Definition: PyRep.h:1209
X * get() const
Definition: RefPtr.h:213
static void GetCruPriceAvg(std::map< uint16, Inv::TypeData > &data)
Definition: MarketDB.cpp:590
void SetBasePrice()
Definition: MarketMgr.cpp:610
#define IsPlayerCorp(itemID)
Definition: EVE_Defines.h:241
bool RecordTransaction(Market::TxData &data)
Definition: MarketDB.cpp:380
static void SetUpdateTime(int64 setTime)
Definition: MarketDB.cpp:470
uint32 memberID
Definition: EVE_Market.h:83
#define PyDecRef(op)
Definition: PyRep.h:57
#define IsCharacterID(itemID)
Definition: EVE_Defines.h:206
Python object "ccp_exceptions.UserError".
Definition: PyExceptions.h:121
Definition: Client.h:66
unsigned __int32 uint32
Definition: eve-compat.h:50
void InvalidateCache(const PyRep *objectID)
uint32 clientID
Definition: EVE_Market.h:78
void GiveCache(const PyRep *objectID, PyRep **contents)
void ExecuteSellOrder(Client *buyer, uint32 orderID, Call_PlaceCharOrder &args)
Definition: MarketMgr.cpp:497
double GetFileTimeNow()
Definition: utils_time.cpp:84
static void GetMaterialPrices(std::map< uint16, Market::matlData > &data)
Definition: MarketDB.cpp:531
DBerror error
Definition: dbcore.h:69
signed __int64 int64
Definition: eve-compat.h:51
float SalesTax(float baseSalesTax, uint8 accountingLvl=0, uint8 taxEvasionLvl=0)
Definition: EvEMath.cpp:215
ObjCacheService * cache_service
Definition: PyServiceMgr.h:78
static void GetManufacturedItems(std::map< uint16, Inv::TypeData > &data)
Definition: MarketDB.cpp:519
void Populate()
Definition: MarketMgr.cpp:64
int8 GetSkillLevel(uint16 skillTypeID, bool zeroForNotInjected=true) const
Definition: Character.cpp:575
void Process()
Definition: MarketMgr.cpp:83
size_t GetRowCount()
Definition: dbcore.h:72
bool AlterOrderQuantity(uint32 orderID, uint32 new_qty)
Definition: MarketDB.cpp:298
virtual void Delete()
#define sItemFactory
Definition: ItemFactory.h:165
int64 m_timeStamp
Definition: MarketMgr.h:75
bool DeleteOrder(uint32 orderID)
Definition: MarketDB.cpp:316
void SystemShutdown(SystemData &data)
Definition: MarketMgr.cpp:96
void Close()
Definition: MarketMgr.cpp:46
unsigned __int16 uint16
Definition: eve-compat.h:48
bool ExecuteBuyOrder(Client *seller, uint32 orderID, InventoryItemRef iRef, Call_PlaceCharOrder &args, uint16 accountKey=Account::KeyType::Cash)
Definition: MarketMgr.cpp:303
PyObject * MakeObjectCachedMethodCallResult(const PyRep *objectID, const char *versionCheck="run")
Definition: dbcore.h:39
uint32 itemID() const
Definition: InventoryItem.h:98
static void UpdateMktPrice(std::map< uint16, Market::matlData > &data)
Definition: MarketDB.cpp:583
bool IsCacheLoaded(const PyRep *objectID) const
PyServiceMgr * m_manager
Definition: MarketMgr.h:71
int32 quantity() const
Definition: InventoryItem.h:97
#define sDataMgr
static int64 GetUpdateTime()
Definition: MarketDB.cpp:456
void UpdateMineralPrice()
Definition: MarketMgr.cpp:903