104 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
		
		
			
		
	
	
			104 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
|   | # Wonder Trades
 | ||
|  | 
 | ||
|  | Pokemon Emerald uses Archipelago's data storage to reproduce what the Pokemon series calls wonder trading. Wonder | ||
|  | trading is meant as a sort of gacha game surprise trade where you give up one of your pokemon and at some point in the | ||
|  | future you'll receive one in return from another player who decided to participate. In practice, small groups will be | ||
|  | able to use it as a means of simple trading as well by coordinating when they participate. | ||
|  | 
 | ||
|  | The goal of the implementation used by Pokemon Emerald is to allow players to interact with an NPC in-game to deposit | ||
|  | and withdraw pokemon without having to touch their client. The client will automatically detect their state, look for | ||
|  | available trades, and notify the player when they've received something. | ||
|  | 
 | ||
|  | It's also intended to work for Pokemon games other than Emerald, should any other games decide to opt in and implement | ||
|  | the feature into their clients. | ||
|  | 
 | ||
|  | ## Data Storage Format
 | ||
|  | 
 | ||
|  | There is one wonder trade entry per team at `pokemon_wonder_trades_{team number}`. | ||
|  | 
 | ||
|  | It should be a dict that looks something like this: | ||
|  | 
 | ||
|  | ```json | ||
|  | { | ||
|  |   "_lock": 0, | ||
|  |   "0": [3, "{some json data}"], | ||
|  |   "3": [2, "{some json data}"] | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Lock
 | ||
|  | 
 | ||
|  | `_lock` tells you whether you're allowed to try to modify the key. Its value should be either `0` to represent an | ||
|  | unlocked state, or a timestamp represented by time since Epoch in ms (`int(time.time_ns() / 1000000)`). | ||
|  | [See below](#preventing-race-conditions) for more info. | ||
|  | 
 | ||
|  | ### Non-lock Keys
 | ||
|  | 
 | ||
|  | All other keys are just non-negative integers as strings. You can think of them as wonder trade slots. Pidgeon holes | ||
|  | with a label. For consistency and ease of use, keep the keys between 0 and 255, and prefer the lowest number you can | ||
|  | use. They ONLY act as names that can be easily written to and removed from. | ||
|  | - You SHOULD NOT rely on those numbers being contiguous or starting at 0. | ||
|  | - You SHOULD NOT rely on a "trade" residing at a single slot until it is removed. | ||
|  | - You SHOULD NOT assume that the number has any significance to a player's slot, or trade order, or anything really. | ||
|  | 
 | ||
|  | ### Values
 | ||
|  | 
 | ||
|  | The first entry in the tuple represents which slot put the pokemon up for trade. You could use this to display in your | ||
|  | game or client who the trade came from, but its primary purpose is to discriminate entries you can take from those you | ||
|  | can't. You don't want to send something to the server, see that the server has something to take, and then take your own | ||
|  | pokemon right back. | ||
|  | 
 | ||
|  | The JSON data should match the schema currently located at `data/trade_pokemon_schema.json`. It should be universally | ||
|  | understandable by anything trying to interact with wonder trades. Of course, some Pokemon games include more data than | ||
|  | others for a given pokemon, some games don't have species introduced in later generations, and some data is of a | ||
|  | different format, has different values, or is even spelled differently. The hope is that translating to and from JSON is | ||
|  | reasonable for any game (or at least any game likely to be integrated into AP), and you can easily tell from the JSON | ||
|  | whether your game is capable of giving the pokemon to the player in-game. | ||
|  | 
 | ||
|  | ## Preventing Race Conditions
 | ||
|  | 
 | ||
|  | This caused by far the most headache of implementing wonder trades. You should be very thorough in trying to prevent | ||
|  | issues here. | ||
|  | 
 | ||
|  | If you prefer more technical explanations, the Pokemon Emerald client has documented wonder trade functions. The rest of | ||
|  | this section explains what problems are being solved and why the solutions work. | ||
|  | 
 | ||
|  | The problem that needs solving is that your client needs to know what the value of the trade data is before it commits | ||
|  | some sort of action. By design, multiple clients are writing to and removing from the same key in data storage, so if | ||
|  | two clients try to interact and there's ambiguity in what the data looks like, it will cause issues of duplication and | ||
|  | loss of data. | ||
|  | 
 | ||
|  | For example, client 1 and client 2 both see a pokemon that they can take, so they copy the pokemon to their respective | ||
|  | games, and both send a command to remove that pokemon from the data store. The first command works and removes the | ||
|  | entry, which sends an update to both clients that there no longer exists a pokemon at that slot. And then the second | ||
|  | command, which was already sent, tries to remove the same entry. At best, the data was duplicated, and at worst the | ||
|  | server raises an exception or crashes. | ||
|  | 
 | ||
|  | Thankfully, when you receive an update from the server that a storage value changed, it will tell you both the previous | ||
|  | and current value. That's where the lock comes in. At a basic level, your client attempts to claim ownership of the key | ||
|  | temporarily while it makes its modifications, and all other clients respect that claim by not interacting until the lock | ||
|  | is released. You know you locked the key because the `SetReply` you receive for modifying the lock is the one that set | ||
|  | it from an unlocked state to a locked state. When two clients try to lock at the same time, one will see an unlocked | ||
|  | state move to a locked state, and the other will see an already locked state move to a locked state. You can identify | ||
|  | whether a `SetReply` was triggered by your client's `Set` by attaching a uuid to the `Set` command, which will also be | ||
|  | attached to the `SetReply`. See the Emerald client for an example. | ||
|  | 
 | ||
|  | Which brings us to problem 2, which is the scenario where a client crashes or closes before unlocking the key. One rogue | ||
|  | client might prevent all other clients from ever interacting with wonder trading again. | ||
|  | 
 | ||
|  | So for this reason, the lock is a timestamp, and the key is considered "locked" if that timestamp is less than 5 seconds | ||
|  | in the past. If a client dies after locking, its lock will expire, and other clients will be able to make modifications. | ||
|  | Setting the lock to 0 is the canonical way of marking it as unlocked, but it's not a special case really. It's | ||
|  | equivalent to marking the key as last locked in 1970. | ||
|  | 
 | ||
|  | Which brings us to problem 3. Multiple clients which want to obtain the lock can only check whether the lock is | ||
|  | obtainable by refreshing the current lock's timestamp. So two clients trying to secure a lock made by a dead client may | ||
|  | trade back and forth, updating the lock to see if it is expired yet, seeing that it is not, and then waiting 5 seconds | ||
|  | while the other client does the same thing, which causes the lock to again be less than 5 seconds old. | ||
|  | 
 | ||
|  | Using a cooldown period longer than the time to expire only increases the minimum number of clients that can trigger | ||
|  | this cycle. Instead, the solution is to double your cooldown every time you bounce off an expired lock (and reset it | ||
|  | once you acquire it). Eventually the amount of time every client is waiting will be enough to create a gap large enough | ||
|  | for one client to consider the lock expired, and it will acquire the lock, make its changes, and set the lock state to | ||
|  | definitively unlocked, which will let the next client claim it, and so on. |