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.
|