diff options
Diffstat (limited to 'doc/devel/netplay')
-rw-r--r-- | doc/devel/netplay/notes | 21 | ||||
-rw-r--r-- | doc/devel/netplay/protocol | 336 | ||||
-rw-r--r-- | doc/devel/netplay/states | 178 | ||||
-rw-r--r-- | doc/devel/netplay/todo | 120 |
4 files changed, 655 insertions, 0 deletions
diff --git a/doc/devel/netplay/notes b/doc/devel/netplay/notes new file mode 100644 index 0000000..afac628 --- /dev/null +++ b/doc/devel/netplay/notes @@ -0,0 +1,21 @@ +As the game currently unfortunately works with polling, this is how +to integrate network handling with the game. + +In the function called periodically by DoInput(), there should be a call +to netInput() somewhere at the beginning, and a call to flushPacketQueues() +somewhere at the end. + +netInput() checks all connections for incoming packets, and calls +the appropriate packet handlers. + +flushPacketQueues() sends all pending packets on their way, for all +connections. + +In between, you can call functions that enqueue packets. +You would also check the network state here to determine whether you need to +act on some packet that has been delivered to the local party. + + + + + diff --git a/doc/devel/netplay/protocol b/doc/devel/netplay/protocol new file mode 100644 index 0000000..5b82a1f --- /dev/null +++ b/doc/devel/netplay/protocol @@ -0,0 +1,336 @@ +There are several types of negotiations used to synchronised the parties +of a network connection. + +- Continue when we know the other is ready ("Ready") + This is used when both parties need to sending information to the + other side, but what each party is doing does not interfere with + what the other party is doing. +- Agree on changes ("Update") + This is used when the parties have changes to make to common data. +- Mutual agreement on an action ("Confirm") + This is used to end a state where both parties are modifying + common data. Both parties have to agree with the data for either + party to continue. +- Reset a connection. This is used to abort a game in progress and return + to the fleet setup menu. + +============================================================================ + +"Ready" negotiation. + +Sometimes the parties need to notify eachother of their local state, +and then go on when both are ready. For this purpose both parties signify +that they are ready by sending the READY message. When a party is ready +and has received notice that the other party is ready, it can go on. + +States: +0. notReady - send nor received READY + !localReady && !remoteReady +1. localReady - sent READY, not yet received READY + localReady && !remoteReady +2. remoteReady - received READY, not yet sent READY + !localReady && remoteReady +3. ready - sent and received READY + +Messages: +- READY - "I have nothing further to send at this point" + + +From state 0 (notReady): +local decision -> Send READY, goto 1 +received READY -> goto 2 + +From state 1 (localReady): +received READY -> goto 3 + +From state 2 (remoteReady): +local decision -> Send READY goto 3 + + +============================================================================ + +"Update" negotiation. + +During configuration, both sides may change the same properties. So that the +two sides don't have to take turns, the changes are made locally, and then +the changes are synchronised. + +To this end, each side has a state containing two copies of each property: + 1. The value of the property as it is locally + 2. The last value which it sent to the other side (until it is no longer + relevant for the protocol) + +The basic idea of the Update protocol is: +- both sides each send and receive one packet before being allowed to + send another one (we'll call this a "turn" here) +- when a packet has been sent and one has been received in a turn, + and the sent and received values are the same, then that value is the + agreed upon value. If the sent and received values differ, then a + tie breaker determines which one prevails. +- when the first local change of a turn is made, the change is sent to + the other side +- when a local change has been made while a packet has already been + sent this turn, the change will be made locally, but communicating the + change will be postponed +- when a turn ends while a change has been postponed, and this change isn't + negated by a remote change, then the change will be sent +- when a remote change arrives, and a packet has not been sent this turn, + the local state is updated with the change, and the same packet is sent + to the other side to confirm the change + +The tie breaker is required to always let one side win, and the other +side lose, given the same property. +Any function satisfying this requirement is usable, but the currently +used one will return true for the side which 'owns' the property, and +false on the other side. +Another tie breaker could be one which always lets the same side win, +regardless of the property. + + +The protocol: + +From state 1, {own=x0, sent=--}: +1a Local change x1 -> send(x1); state=2:{own=x1,sent=x1} +1b Received UPDATE(x1) -> send(x1); state=1:{own=x1,sent=--} + +From state 2, {own=x0, sent=x0}: +2a Local change x1 -> state=3:{own=x1,sent=x0} +2b Received UPDATE(x0) -> state=1:{own=x0,sent=--} +2c+ Received UPDATE(x1) -> state=1:{own=x0,sent=--} if winTieBreak +2c- Received UPDATE(x1) -> state=1:{own=x1,sent=--} if !winTieBreak + +From state 3, {own=x1, sent=x0}: +3a Local change x0 -> state=2:{own=x0,sent=x0} +3b Local change !x0 -> state=3:{own=xN,sent=x0} +3c Received UPDATE(x0) -> send(x1); state=2:{own=x1,sent=x1} +3d+ Received UPDATE(!x0) -> send(x1); state=2:{own=x1,sent=x1} if winTieBreak +3d- Received UPDATE(!x0) -> state=1:{own=x?,sent=--} if !winTieBreak + + +Explanation: +We keep track of the local value ('own'), and whether or not we sent a packet +in this turn ('sent' != '--'), and if we did, the last packet which we sent +('sent'). When we proceed to the next turn, 'sent' is set to '--'. + +State 1: We have not yet sent a packet this turn (which implies that we +haven't made a local change this turn). +1a. A local change is made. We update our local value, and send this to + the remote side. +1b. A remote change arrives. We don't have any local change ourselves, + so we accept the remote change, and sent it back to confirm. + With both a packet sent and one received, the turn ends, and 'sent' + is set back to '--'. + +State 2: We have sent a packet this turn (after making a local change), and +have not changed our local value since (or we have changed it and changed +it back). +2a. A local change is made. We update our local value, but we have already + sent a packet this turn, so we can't report it until the next turn. +2b. A remote change arrives, and it is equal to both our local value and + the value which we sent to the other side this turn. + This remote notification may be a confirmation of our update, or a + coincidental identical remote change. + Either way, the packet acts as a confirmation, and we do not need + to change anything. With both a packet sent and received, the turn ends + and so 'sent' is set back to '--'. +2c. A remote change arrives, and it is not equal to our local value (which + is the same as the value which we sent). + The tie breaker decides which value prevails. The same value will + prevail on the remote side, so there is no need to send any confirmation + packets. The turn ends, and 'sent' is set back to '--'. + +State 3: We have sent a packet this turn, and have made a local change since. +3a. A local change is made back to the value which we sent. No action + is required. +3b. A local change is made. We update our local value, but we have already + sent a packet this turn, so we can't report it until the next turn. +3c. A remote change arrives, and it is equal to the value which we sent + (which isn't equal to the current local value). + The sent/received value is the accepted value and the turn ends. + But we have changed our value since already, so as the next turn + starts, we immediately send an update. +3d. A remote change arrives, and it is not equal to the value which we sent + this turn. + + We win the tie break, so the value which we sent prevails. + But we have changed our value since already, so as the next turn + starts, we immediately send an update. + - We lose the tie break, so the value which the remote value sent + prevails. We accept the value and the turn ends. + (An alternative would be to consider the local change(s) which + we made after our change was sent as made in the following turn, + in which case we would send our current local value in the next turn. + The advantage would be that 3d- would become equal to 3d+, + so these could be joined (with 3c too), which saves a few + if-statements in the code, but this requires another packet to be sent + and replied to in what currently is 3d-.) + + +Proof outline that this works: +- The states can never get out of sync: + After both a packet has been sent and received, both sides (temporarilly) + have accepted the same value +- There can be no indefinite loop without ongoing local changes. + Once there are no more local changes: + from state 3 we always go to state 2 or 1, + from state 2 we always go to state 1 + from state 1 we can only go back to state 1, and only when a packet + has been received. + So eventually, both sides are in states 1. With both sides in state 1, + no packets have been sent in the current turn, so no more packets are + there to be received. +- Both sides can make changes, as long as the side which wins the tie breaks + stops making changes now and then: + Without making local changes, a side will go to state 1 eventually. + If a side makes a local change, and this is received by the other side + while it is in state 1, then that other side will accept the change. + + +The Confirm negatiation is used to finish the Update negotiation. +This works because local changes triggered by the reception of remote changes +are treated as local modifications for the purpose of the Confirm negotiation. +And this can be done because whenever the Confirm negotiation is in a state +where remote changes may be expected, local modifications are still allowed +(possily after sending a CANCEL packet). + + +============================================================================ + +"Confirm" negotiation. + +Some actions (like agreeing on a configuration) require confirmation +from both parties. This section documents the handshaking protocol involved. + +Each player must manually confirm the action. +After a player has confirmed an action, he may cancel it as long as +he hasn't received a confirmation from the other party. + +All messages arrive in the order sent. + + +States: +0. waiting + !handshake.canceling && !handshake.localOk && !handshake.remoteOk +1. localOk (cancelable) - sent CONFIRM1 (since last CANCEL) + !handshake.canceling && handshake.localOk && !handshake.remoteOk +2. remoteOk (cancelable) - received CONFIRM1 + !handshake.canceling && !handshake.localOk && handshake.remoteOk +3. committed - sent CONFIRM1 (since last CANCEL, + received CONFIRM1, + sent CONFIRM2 (since last CANCEL) + !handshake.canceling && handshake.localOk && handshake.remoteOk +4. cancelWaiting - sent CANCEL + handshake.canceling && !handshake.localOk && !handshake.remoteOk +5. cancelLocalOk - sent CANCEL and ready to send CONFIRM1, + but received no CANCELACK + handshake.canceling && handshake.localOk && !handshake.remoteOk +6. cancelRemoteOk - sent CANCEL and received CONFIRM1, + but received no CANCELACK + handshake.canceling && !handshake.localOk && handshake.remoteOk +7. cancelCommitted - sent CANCEL and ready to send CONFIRM2, + received CONFIRM1, + but received no CANCELACK + handshake.canceling && handshake.localOk && handshake.remoteOk +8. done - sent and received CONFIRM1 and CONFIRM2 + (since last CANCEL) + + +Handshake messages: +- CONFIRM1 - "the current local configuration is OK for me" +- CONFIRM2 - "acknowledging your CONFIRM1; my own configuration is unchanged + since I sent CONFIRM1 (after the last CANCEL)" +- CANCEL - "forget about my earlier CONFIRM1" +- CANCELACK - "received your CANCEL" +MESSAGE(x) indicates any other message. + + +From state 0: (waiting) +local confirmation -> Send CONFIRM1, goto 1 +local changes -> Send MESSAGE(changes) (goto 0) +received CONFIRM1 -> goto 2 +received MESSAGE(changes) -> Process(changes) (goto 0) + +From state 1: (localOk) +local cancel -> Send CANCEL, goto 4 +received CONFIRM1 -> Send CONFIRM2, goto 3 +received CONFIRM2 -> Send CONFIRM2, goto 8 +received MESSAGE(changes) -> Send CANCEL, Process(changes), goto 4 + +From state 2: (remoteOk) +local confirmation -> Send CONFIRM2, goto 3 +local changes -> Send MESSAGE(changes), (goto 2) +received CANCEL -> Send CANCELACK, goto 0 + +From state 3: (committed) +received CONFIRM2 -> goto 8 +received CANCEL -> Send CANCELACK, goto 1 + +From state 4: (cancelWaiting) +local changes -> Send MESSAGE(changes), (goto 4) +local confirmation -> goto 5 +received CONFIRM1 -> goto 6 +received CONFIRM2 -> goto 6 +received CANCELACK -> goto 0 +received MESSAGE(changes) -> Process(changes), (goto 4) + +From state 5: (cancelLocalOk) +local cancel -> goto 4 +received CONFIRM1 -> goto 7 +received CONFIRM2 -> goto 7 +received CANCELACK -> SEND CONFIRM1 goto 1 +received MESSAGE(changes) -> Process(changes), goto 4 + +From state 6: (cancelRemoteOk) +local confirmation -> goto 7 +local changes -> Send MESSAGE(changes), (goto 6) +received CONFIRM2 -> (goto 6) +received CANCEL -> Send CANCELACK, goto 4 +received CANCELACK -> goto 2 + +From state 7: (cancelCommitted) +received CONFIRM2 -> (goto 7) +received CANCEL -> Send CANCELACK, goto 5 +received CANCELACK -> Send CONFIRM2, goto 3 + +On receiving local confirmation, sending CONFIRM2 is a shortcut for +sending CONFIRM1 followed by CONFIRM2. Receiving CONFIRM2 from localOk +and cancelLocalOk is accepted just for this shortcut. + + +To prove there are no race conditions, I examine all the combinations +of states and messages that are underway. Whenever the order of actions +isn't fixed, the result should be the same (eg. recv(CONFIRM1) followed +by send(CANCEL) should leave the party in the same state as when +the send(CANCEL) preceded the recv(CONFIRM1)). +I also check whether it is possible for packets to arrive that +aren't expected. + + +============================================================================ + +"Reset" negotiation. + +See src/sc2code/netplay/proto/reset.c + + +============================================================================ + +Battle ending negotiation. + +This negotation consists of: +1. a 'Ready' negotiation, before stopping sending frame data +2. communication of each sides current battle frame count +3. the side running behind processes more frames to catch up + + +States: + +0. Playing +1. localReady +2. remoteReady +3. countSent +4. catchingUp +5. awaitingCatchup + +// Unfinished... partially described in readyForBattleEndPlayer() + + diff --git a/doc/devel/netplay/states b/doc/devel/netplay/states new file mode 100644 index 0000000..9727172 --- /dev/null +++ b/doc/devel/netplay/states @@ -0,0 +1,178 @@ +== Any connected state == +Some packets may be sent and received in any state except +NetState_unconnected. +These are: PING, ACK, ABORT, RESET +These are not listed below at each individual state. + +Whenever a connection is aborted, the state is returned to +NetState_unconnected. This state transition is not listed below at each +individual state. + + +== NetState_unconnected == +NetState_unconnected is the initial state. + +NetConnection.state: NULL + +Packets ok to send: none +Packets ok to receive: none + +Next state: + NetState_connecting -- connection attempt in progress + + +== NetState_connecting == +NetState_connecting indicates that a connection is in progress. + +When the connection is established, the state is changed to NetState_init +and InputFunc is set to DoNetworkInit. + +NetConnection.state: instance of ConnectStateData + +Packets ok to send: none +Packets ok to receive: none + +Next state: + NetState_init -- connection established + + +== NetState_init == +NetState_init is for initialising the connection before actual game +information is sent. + +As this state is entered, an INIT packet is sent. When an INIT packet +has also been received, the state is set to NetState_inSetup and +InputFunc is set to DoMelee. + +NetConnection.state: instance of BattleStateData + +Packets ok to send: INIT +Packets ok to receive: INIT + +Next state: + NetState_inSetup -- received an INIT packet + + +== NetState_inSetup == +NetState_inSetup is the state in which the fleet configuration is negotiated. + +This does not necessarilly mean that the fleet setup screen is visible; +this state is also held after a battle when the battle outcome is still +displayed. + +Each side may send fleet configuration changes to the other side, by means of +FLEET and TEAMNAME packets. Agreement on configuration settings is provided +through the Update negotiation. +The Confirm negotiation is used to end this state and go to +NetState_preBattle. At this time InputFunc is set to DoPreMelee. + +NetConnection.state: instance of BattleStateData + +Packets ok to send: FLEET, TEAMNAME, HANDSHAKE0, HANDSHAKE1, + HANDSHAKECANCEL, HANDSHAKECANCELACK +Packets ok to receive: FLEET, TEAMNAME, HANDSHAKE0, HANDSHAKE1, + HANDSHAKECANCEL, HANDSHAKECANCELACK + +Next state: + NetState_preBattle -- configuration has been confirmed + + +== NetState_preBattle == +NetState_preBattle is used for non-interactive battle negotiations. + +One side sends the random seed; the other receives it. +Both sides send their input delay value. +The Ready negotiation is used to end this state and go to +NetState_interBattle. + +NetConnection.state: instance of BattleStateData + +Packets ok to send: SEEDRANDOM, INPUTDELAY, READY +Packets ok to receive: SEEDRANDOM, INPUTDELAY, READY + +Next state: + NetState_interBattle -- ready to continue + + +== NetState_interBattle == +NetState_interBattle is used to allow each side to do some local +initialisations before moving on. +The Ready negotiation is used to end this state and go to +NetState_selectShip, or if all sides have selected a ship, to +NetState_inBattle, or if there are no more ships in a fleet, +to NetState_inSetup. + +If there are no more ships, the the Ready negotiation is used to +enter NetState_inSetup. + +NetConnection.state: instance of BattleStateData + +Packets ok to send: READY +Packets ok to receive: READY + +Next state: + NetState_selectShip -- ready to select the next ship + NetState_inBattle -- ready to start the battle + NetState_inSetup -- no more ships; game over + + +== NetState_selectShip == +NetState_selectShip is where a side may select their ship. The other +side is waiting for notice of this selection. +As soon as the selection has been sent or received, the state is changed +back to NetState_interBattle. + +NetConnection.state: instance of BattleStateData + +Packets ok to send: SELECTSHIP +Packets ok to receive: SELECTSHIP + +Next state: + NetState_interBattle -- a selection has been made + + +== NetState_inBattle == +NetState_inBattle is where the actual melee takes place. +Both sides send their input until the game is over, at which point +the Ready negotiation is used to end this state and go to +the NetState_endingBattle state. Until the Ready negotiation has been +completed, the simulation is continuing. + +NetConnection.state: instance of BattleStateData + +Packets ok to send: BATTLEINPUT, READY +Packets ok to receive: BATTLEINPUT, READY + +Next state: + NetState_endingBattle -- ready to end the battle + + +== NetState_endingBattle == +NetState_endingBattle is where the local side waits for the remote +battle frame count, after it has sent its own. When it arrives, +the state changes to NetState_endingBattle2. + +NetConnection.state: instance of BattleStateData + +Packets ok to send: BATTLEINPUT, FRAMECOUNT +Packets ok to receive: BATTLEINPUT, FRAMECOUNT + +Next state: + NetState_endingBattle2 -- we know when to end the battle + + +== NetState_endingBattle2 == +NetState_endingBattle2 is where the side with the lowest battle frame count +catches up with the other other side, while the other side waits. +The Ready negotiation is used to signal that each side is ready, +and the state changes to NetState_interBattle. + +NetConnection.state: instance of BattleStateData + +Packets ok to send: BATTLEINPUT, READY +Packets ok to receive: BATTLEINPUT, READY + +Next state: + NetState_interBattle -- get ready for the next ship + + diff --git a/doc/devel/netplay/todo b/doc/devel/netplay/todo new file mode 100644 index 0000000..33102c7 --- /dev/null +++ b/doc/devel/netplay/todo @@ -0,0 +1,120 @@ +High priority items: + + +Medium-priority: +- For the battle ending synchronisation, set the end at at least + getBattleInputDelay() + 1 frames in the future (instead of just 1), + so that there will be no hickup during the end synchronisation. + Also check this value for incoming packets. +- If a player only moves away from 'Battle!' there's no need for the other + to have to reconfirm. +- decent pause handling +- make compilation of crc.c and checksum.c conditional. +- negotiate checksum interval +- Closing and destroying of NetConnections is a terrible mess. +- Checks allConnected() shouldn't be needed if CHECK_ABORT is set + on disconnect. +- Shorten or lengthen the time between frames depending on how full the input + buffer is, to prevent one side constantly stalling when the connection in + one direction is slower than in the other direction. + + +Low-priority: +- Some difference in pictures to indicated confirmed/unconfirmed. + A check mark perhaps. +- Check whether the random seed and frame delay have been agreed before + continuing (in doConfirmSettings). +- Replacement for TOS. It is IPv4 only. +- Integrate network check functions with doInput + It will be easy to get rid of the separate threads then too. +- The state changes from interBattle to interBattle. That shouldn't happen, + but it doesn't seem to cause any problems. Need to investigate. + Addition: negotiateReadyConnections() is called again just to make sure + all sides pass this checkpoint. This is not a problem. It should be + documented in STATES though. +- More files define NETCONNECTION_INTERNAL than they should. +- voice transmission during the game (using an external lib) + read ramjee94adaptive.pdf +- Keep-alive packets. Store time of last packet sent, use alarms to determine + when to send the next one. Count received messages? +- Some way to (optionally) hide your fleet setup until the start of the + game? With either a previously determined maximum fleet value, or + just display a number to the opponent. +- (when ships stats are in the content) Negotiate the ship stats, so people + can play with non-default ships. +- Pre-fleet-setup setup + The values negotiated could include handicapping (different values for + each player) + - hide fleets (also handicapping here) + - maximum number of ships per side + - maximum ship cost per side + - ship properties +- Send taunts (prerecorded samples) at the touch of a button? + Use taunt add-on packages? Send the opponent's package before the start + of a game? + + +Future improvements/optimisations: +- For BSD sockets: use dup2() to move fds to lower values, so that less fds + have to be checked on select(). +- Use writev() to send multiple packets in one syscall, instead of + calling send() for each packet. +- Refusing games with both parties network controlled is not always + necessary. In theory it should be possible to have a client work + as "proxy". The client can watch as the actual players play the game. + Checking for "loops" would be tricky (eventually there should be a human + or computer controller for each side). +- Concurrent selection of ships. Note that if this is handled properly, it + will also be easy to take care of the "Allow Shofixti to choose last" bug. + Note that one party will still have to send his choice to the other side + first, which may be "exploited". Encryption would take care of this, + but at the least make sure the same player who gets to chose first + every time. +- meta-server. Use HTTP? Existing libs can be used, no problems with NAT, + human-readable. Speed is not an issue. +- move to UDP. Repeat past battleinput packets for each new packet that + is sent until they are confirmed. +- Once the protocol is stable: register the port number with IANA +- Per state data (in NetConnection.stateData) is unnecessarilly complicated. + Putting all fields directly in NetConnection simplifies things a lot. + It's not as generic, but this code won't be used elsewhere anyhow. +- Send captain names to the other side + + +Bugs: +- as positions are dependant on the screen resolution, you won't be able + to keep sync on games with a different resolution. +- collision detection is dependant on the images used. Using different + graphics packs will result in a desync. +- Both sides need identical battle frame rates. This value is not + negotiated. +- If one player closes the connection while the other player is selecting + a ship to put in his fleet, or loading a fleet, the "Network Control" + button won't be updated. +- If one side is computer-controlled, sync loss will occur, because the AI + uses the RNG. It needs its own RNG context. +- Pressing F10 to exit the supermelee setup when a connection is active + will cause an attempt to draw a NULL frame. (possibly the disconnect + feedback) + + +Final actions: +- Check out TODO, XXX, WORK tags +- memleak testing + - check for and remove mtrace()/muntrace() calls. +- update documentation + - FILES, also note which part of the separation they are in +- check coding style (search for '\>(') +- compile with maximum warnings + + +To put in the announcement of Netplay: +- Slow connections is acceptable. Packet loss isn't. + + +Bugs and todos unrelated to netplay. +- other player being able to choose the next ship after 3 seconds + of inactivity +- DoRunAway() shouldn't be handled in ProcessInput() + + |