Table of Contents
This chapter describes one of the possible ways in which players can buy, sell, or exchange goods between themselves or with NPC's.
The process is basically the same in both situations. One player will initiate the trade, then a user interface will appear, which can be similar to the Inventory one, but showing the inventory (or partial inventory) of both parties. The two parties can then choose which items to buy, sell, or exchange, and once they both agree on the trade, the changes can be updated on the server.
Strict transaction control is recommended for trading so that there is little chance for exploitation, cheating, or item loss. Care should also be taken when creating temporary items, since this may lead to the duplication of items. For similar reasons, this control should be done on the server, to ensure that one client does not try to exploit another. Again, the authoritative copy of any inventory should live on the server, so that clients can re-read the information from it if they do not agree on the trade.
There are many levels of protection that can be obtained in a trading system, with each higher level involving more control overhead.
A trading system needs protection against hardware and software failures in order to ensure transaction atomicity, making sure that an item is neither lost nor duplicated. It means that a transaction in progress does not stop half way if a component in the system fails.
The possible approaches to item trade are listed below, in order of increased cost:
Simply swap the items, without performing database writes. This approach relies on built-in fault tolerance and disaster recovery, and therefore might have a reasonably large window for item loss or duplication.
-
Swap the items and immediately write to the database (two database writes per trade). This approach reduces the window for item loss and duplication.
-
Use a trading supervisor (three database writes per trade), leveraging BigWorld's fault tolerance capabilities. This approach results in a high overhead per item.
-
Make each item an entity.
-
Implement external banking-grade database transaction.
Like all forms of insurance, the game implementer must weigh up the importance of losing or duplicating items against the cost of such protection.
Before a trade transaction can take place, both parties must agree on the items being traded. If one of the parties is a NPC character, price tables can be used and this is not an issue. If both parties are players, care must be taken so that players cannot cheat one another.
Player misinterpretation can occur if players are not given the chance to inspect the items offered by their trade partner, or if the player can initiate the transaction after changing his offer once the other has already accepted the trade.
To avoid player misinterpretation problems, the sample implementation uses a trade protocol that allows players to offer, switch, inspect, accept, and refuse items at will until a consensus is reached and the trade can be processed. The protocol is described bellow.
-
The player offers an item. The item is locked locally and the offer is sent to the cell.
-
In
<res>
/scripts/client/Avatar.py:def onTradeOfferItem( self, itemSerial ): ... self._tradeOfferLock = self.inventoryMgr.itemsLock( [itemSerial], 0 ) self.cell.tradeOfferItemRequest( self._tradeOfferLock, itemSerial ) ...
-
-
The cell tries to confirm the lock with the base.
-
In
:<res>
/scripts/cell/Avatar.pydef tradeOfferItemRequest( self, sourceID, lockHandle, itemSerial ): ... self.tradeOutboundLock = lockHandle self.base.itemsLockRequest( lockHandle, [itemSerial], goldPieces )
-
-
The base repeats the lock done on the client and notifies the cell of the result.
-
In
:<res>
/scripts/base/Avatar.pydef itemsLockRequest( self, lockHandle, itemsSerials, goldPieces ): ... inventoryMgr = self.inventoryMgr ... inventoryMgr.itemsRelock( lockHandle, itemsSerials, goldPieces ) ... self.cell.itemsLockNotify( True, lockHandle, itemsSerials, itemsTypes, goldPieces ) ...
-
-
If the locking on the base was successful, the cell notifies the partner about the offer.
-
In
:<res>
/scripts/cell/Avatar.pydef itemsLockNotify( self, success, lockHandle ... ): ... partner = self._getModeTarget() ... partner.tradeOfferItem( itemsTypes[0] ) ... def tradeOfferItem( self, itemType ): self.tradeSelfAccepted = False self.client.tradeOfferItemNotify( itemType )
-
-
The player is notified about the offer made by his trade partner. The item is shown on the user interface.
-
In
:<res>
scripts/client/Avatar.pydef tradeOfferItemNotify( self, itemType ): ... self.inventoryGUI.showOfferedItem( itemType ) ...
-
-
The player accepts the item offered. The client notifies the cell about the approval.
-
In
:<res>
/scripts/client/Avatar.pydef onTradeAccept( self, accept ): ... self.cell.tradeAcceptRequest( accept )
-
-
The cell notifies the trade partner about the approval. The notification is immediately forwarded to the client.
-
In
:<res>
/scripts/cell/Avatar.pydef tradeAcceptRequest( self, sourceID, accepted ): ... self.tradeSelfAccepted = accepted partner = self._getModeTarget() partner.tradeAcceptNotify( accepted ) ... def tradeAcceptNotify( self, accepted ): ... self.client.tradeAcceptNotify( accepted ) self.tradePartnerAccepted = accepted ...
-
-
The player is notified about the approval by his trade partner. The user interface is updated to reflect that.
-
In
:<res>
/scripts/client/Avatar.pydef tradeAcceptNotify( self, accepted ): self.inventoryGUI.tradeOfferAccept( accepted )
-
Whenever the player's partner offers a new item, its accepted flag is set to False[2]. This ensures that a trade will never be processed without the explicit approval, from both parties, for the most current item offered.
Once both parties have agreed on the items to be traded, the trading can take place. The sample implementation makes use of a trading supervisor to coordinate and direct the trade.
In summary, the trading supervisor works as follows: each participant in a trade has a trade id, which increases monotonically. Before starting a new trade, the supervisor writes and entry for the pending trade into the database. The supervisor then requests the participants to perform the transaction. After completion of the transaction, each participant notifies the supervisor about the trade completed. The entry for pending trade is removed from the database. If the system starts up after a failure, the supervisor lists all pending trades from the database and replays them. The participants use their trade ids to determine whether they have carried out the trade. If they have not, the transaction is performed. If they did have, the redundant request is simply ignored.
-
When both avatar have agreed on items, the active cell instructs the base to initiate the transaction
-
In
<res>
/scripts/cell/Avatar.py:def tradeAcceptRequest( self, sourceID, accepted ): ... self._tryTradeBegin() def _tryTradeBegin( self ): if self.mode == Mode.TRADE_ACTIVE and \ self.tradePartnerAccepted and \ self.tradeSelfAccepted: ... self.base.tradeCommitActive( self.tradeOutboundLock ) partner = self._getModeTarget() partner.tradeCommitPassive( self.base )
Note
The sample implementation uses the concept active/passive roles in the trade.
The active participant will be the one requesting the trade to the trading supervisor. In the sample, the roles are chosen based on who started the trading mode.
You can use whatever criteria you think is best suited to your design (e.g., higher entity ID).
-
-
In the base, the avatar in the active participant informs the trading supervisor of the trade ids and the data about items and gold pieces being traded. The data about the trade includes information about each avatar’s giveaway: serial number and type ids of items and amount of gold pieces.
-
In
:<res>
/scripts/base/Avatar.py... def tradeSyncRequest( self, partnerBase, partnerTradeParams ): ... ourItemsTypes, ourGoldPieces = \ inventoryMgr.itemsLockedRetrieve( self.outItemsLock ) supervisor = BigWorld.globalBases[ "TradingSupervisor" ] selfTradeParams = { "dbID" : self.databaseID, \ "tradeID" : self.lastTradeID + 1, \ "lockHandle": self.outItemsLock, \ "itemsSerials": outItemsSerials, \ "itemsTypes": outItemsTypes, \ "goldPieces": outGoldPieces } if supervisor.commenceTrade( self, selfTradeParams, partnerBase, partnerTradeParams ): ...
-
-
The trading supervisor adds the trading data to its list of pending trades and writes itself to the database.
-
In
:<res>
/scripts/base/TradingSupervisor.py... def commenceTrade( self, A, paramsA, B, paramsB ): ... tradeLog = { "typeA": A.__class__.__name__, "paramsA": paramsA, "typeB": B.__class__.__name__, "paramsB": paramsB } self.recentTrades.append( tradeLog ) self.outstandingTrades.append( [A.id, B.id] ) def doTradeStep2( *args ): self._tradeStep2( A, paramsA, B, paramsB ) self.writeToDB( doTradeStep2 ) ...
-
-
Once the trading supervisor is notified by the database that write is complete, it instructs both avatars to modify their inventories, in order to reflect the result of the trade.
-
In
:<res>
/scripts/base/TradingSupervisor.py... def _tradeStep2( self, A, paramsA, B, paramsB ): A.tradeCommit( self, paramsA[ "tradeID" ], paramsA[ "lockHandle" ], paramsA[ "itemsSerials" ], paramsA[ "goldPieces" ], paramsB[ "itemsTypes" ], paramsB[ "goldPieces" ] ) B.tradeCommit( self, paramsB[ "tradeID" ], paramsB[ "lockHandle" ], paramsB[ "itemsSerials" ], paramsB[ "goldPieces" ], paramsA[ "itemsTypes" ], paramsA[ "goldPieces" ] )
-
-
The avatars modify their inventories, increment their trade id's, and write themselves to the database.
-
In
:<res>
/scripts/base/Avatar.py... def tradeCommit( self, supervisor, tradeID, outItemsLock, outItemsSerials, inItemsTypes, inGoldPieces ): TradeHelper.tradeCommit( self, supervisor, tradeID, outItemsLock, outItemsSerials, outGoldPieces, inItemsTypes, inGoldPieces )
-
In
:<res>
/scripts/base/TradeHelper.py... def tradeCommit( self, supervisor, tradeID, outItemsLock, outItemsSerials, inItemsTypes, inGoldPieces ): ... inItemsSerials = base.inventoryMgr.itemsTrade( outItemsSerials, outGoldPieces, inItemsTypes, [], inGoldPieces, outItemsLock ) ...
-
-
Once each avatar is notified by the database that its write is complete, it informs the trading supervisor.
-
In
:<res>
/scripts/base/TradeHelper.py... def tradeCommit( ... ): ... base.writeToDB( lambda *args: completeTrade( inItemsSerials ) )
-
-
Once the trading supervisor is informed by both parties that their writes are complete, it removes the trade from its list of pending trades.
-
In
:<res>
/scripts/base/TradingSupervisor.py... def completeTrade( self, who, tradeID ): nost = [] for t in self.outstandingTrades: if t[0] == who.id: if t[1] == 0: self.recentTrades.pop(len(nost)) print "TradingSupervisor: trade complete by A" else: nost.append( (0,t[1]) ) elif t[1] == who.id: if t[0] == 0: self.recentTrades.pop(len(nost)) print "TradingSupervisor: trade complete by B" else: nost.append( (t[0],0) ) else: nost.append( t ) self.outstandingTrades = nost
-
The system has been designed to cope with component failure. The sample implementation follows the steps below:
-
Upon start up, the trading supervisor consult its list of pending trades:
-
In
:<res>
/scripts/base/TradingSupervisor.pydef __init__( self ): ... if self.recentTrades == []: ... else: ... for trade in self.recentTrades: self._replayTrade( trade ) ...
-
-
The supervisor extracts from the database the avatars involved in the trades (in case they have not already been extracted):
-
In
:<res>
/scripts/base/TradingSupervisor.pydef __init__( self ): ... for trade in self.recentTrades: self._replayTrade( trade ) def _replayTrade( self, trade ): traderMBs = [None, None] BigWorld.createBaseFromDBID( trade[ "typeA" ], trade[ "paramsA" ][ "dbID" ], lambda mb, dbID, wasActive: self.__collectMailbox ( trade[ "typeA" ], trade, traderMBs, 0, mb ) ) BigWorld.createBaseFromDBID( trade[ "typeB" ], trade[ "paramsB" ][ "dbID" ], lambda mb, dbID, wasActive: self.__collectMailbox ( trade[ "typeB" ], trade, traderMBs, 1, mb ) ) def __collectMailbox ( self, entityType, trade, traderMBs, ind, box ): ... traderMBs[ ind ] = box if traderMBs[ ind^1 ] == None: return # still missing other mailbox self._tradeStep2( traderMBs[0], trade[ "paramsA" ], traderMBs[1], trade[ "paramsB" ] )
-
-
It then replays the trades, by instructing the avatars to perform it[5].
Note
Overall, the procedure is similar to a journal file system, i.e., the intentions are first written to the database, then the operation is performed, and finally the intentions are marked as complete.
In the case of a global trading supervisor, the supervisor must be
created when the system is first started. In the sample implementation,
that is done in the base personality script
FantasyDemo.py
.
Whenever a new base is started, a three-step procedure is performed to bring up the global TradingSupervisor.
-
Check if a global supervisor has not already been created.
-
In
:<res>
/scripts/base/FantasyDemo.pyimport BigWorld ... def onBaseAppReady( isBootstrap ): ... TradingSupervisor.wakeupTradingSupervisor() ...
-
In
:<res>
/scripts/base/TradingSupervisor.pydef wakeupTradingSupervisor(): ... if BigWorld.globalBases.has_key( 'TradingSupervisor' ): ... self: doStep2() ...
-
-
If not, try to load one from the database. If one is found there, the system has failed, and there can be some trades pending. For more details on replaying trades, see Dealing with failure conditions.
-
In
:<res>
/scripts/base/TradingSupervisor.pydef wakeupTradingSupervisor(): .... def doStep2(): BigWorld.createBaseFromDB( 'TradingSupervisor', 'TradingSupervisor', doStep3 ) ...
-
-
If none was found in the database, create a new supervisor (the system is coming up after a clean shutdown).
-
In
:<res>
/scripts/base/TradingSupervisor.pydef wakeupTradingSupervisor(): .... def doStep3( result ): if result: ... else: BigWorld.createEntity( 'TradingSupervisor' ) ...
The fact that this three-step sequence is performed on every base that comes up allow two or more bases that are starting up simultaneously to create one supervisor each (if all get False from their query for the global base in Creating and destroying the trading supervisor). In case two or more supervisors are created, only one of them will be able to register itself globally. The ones that do not will destroy themselves immediately.
-
In
:<res>
/scripts/base/TradingSupervisor.pyclass TradingSupervisor( BigWorld.Base ): .... def __init__( self ): ... def registerGloballyResult( success ): if not success: self.destroy() self.registerGlobally( self.globalName, registerGloballyResult )
In 2 in the supervisor startup procedure, the system is assumed to have come from a clear shutdown if the trading supervisor cannot be retrieved from the database. One way to make sure this is always the case is deleting the trading supervisor from the database whenever the system is shutdown.
-
In
:<res>
/scripts/base/FantasyDemo.pydef destroyTradingSupervisor(): .... dbid = supervisor.databaseID supervisor.destroy() ...
-
In
:<res>
/scripts/base/TradingSupervisor.pydef destroyTradingSupervisor(): .... dbid = supervisor.databaseID supervisor.destroy()
-
The example uses only one system-wide trading supervisor. However, it is easily scalable to as many as necessary.
Note
Care should be taken as to not create multiple trading supervisors on a single BaseApp when restoring them in a controlled startup.
A simple way to do this would be to create one trading supervisor instance per BaseApp, and access it through a Python global variable on that BaseApp. This could be done in the onBaseAppReady callback of the personality script or via an accessor that creates it on first access.
There are also alternatives to how this idea is implemented. For example, the pending trade could be stored with one of the traders. This reduces the number of writes to the database and avoids scalability issues. An extra field would be required per Avatar.
Despite the high level of protection against crashes afforded by this example solution, there are still conditions in which the trade can go awry.
-
Database corruption
If the database corrupts itself, then the list of pending transactions might not be available. Most databases have good protection against this.
-
Scripting errors
Care must also be taken to ensure that the inventory is not modified in other unexpected ways. This may include ensuring any items in a trade are not lost before it is complete. For details, see Locking inventory items.