-
Notifications
You must be signed in to change notification settings - Fork 403
Client Lock and Handling Client Commands
Game requests from the client to server - and the corresponding response are completely de-coupled and asynchronous. This is mostly fine - however it is possible during network latency for a client to send the same command multiple times before the first one has resolve.
We handle this to some extent today via a client lock mechanism. This is implemented in the gameboard.cljs file as follows:
(defonce lock (atom false))
(defn send-command
([command] (send-command command nil))
([command {:keys [no-lock] :as args}]
(when (or (not @lock) no-lock)
(try (js/ga "send" "event" "game" command) (catch js/Error e))
(when-not no-lock (reset! lock true))
(ws/ws-send! [:netrunner/action {:command command :args args}]))))
(defn handle-state [state]
(swap! game-state #(assoc state :side (:side @game-state)))
(reset! last-state @game-state)
(reset! lock false))
(defn handle-diff [diff]
(swap! game-state #(differ/patch @last-state diff))
(swap! last-state #(identity @game-state))
(reset! lock false))How does this work?
- We initialise an atom set to false meaning the client can take actions
- The send-command function will send a command to the server if lock = false, and the set lock to true
- The send-command function will not set a command if lock=true
- The lock is cleared on reception of new state from the server. However this state change does not necessarily correspond to a command send from THIS client. Any game state change will clear the lock.
Some logs showing how this looks server side:
Clicking on Cache
"handle-game-action:" {:command "ability", :args {:card {:faction "Criminal", :limited 3, :rotated false, :cycle_code "lunar", :runner-abilities [], :zone ["rig" "program"], :cid 11, :new true, :added-virus-counter true, :art nil, :type "Program", :abilities [{:label "Gain 1 [Credits]"}], :image_url "https://www.cardgamedb.com/forums/uploads/an/med_ADN17_37.png", :title "Cache", :counter {:virus 3}, :uniqueness false, :host nil, :packquantity 3, :normalizedtitle "cache", :code "06037", :side "Runner", :cost 1, :hosted nil, :subroutines [], :installed true, :memoryunits 1, :implementation "full", :set_code "tsb", :subtype "Virus", :previous-zone ["hand"]}, :ability 0}}
Handle Action passes the request to the resolve action function
"handle action:" "ability" ":" :runner ":" {:card {:faction "Criminal", :limited 3, :rotated false, :cycle_code "lunar", :runner-abilities [], :zone ["rig" "program"], :cid 11, :new true, :added-virus-counter true, :art nil, :type "Program", :abilities [{:label "Gain 1 [Credits]"}], :image_url "https://www.cardgamedb.com/forums/uploads/an/med_ADN17_37.png", :title "Cache", :counter {:virus 3}, :uniqueness false, :host nil, :packquantity 3, :normalizedtitle "cache", :code "06037", :side "Runner", :cost 1, :hosted nil, :subroutines [], :installed true, :memoryunits 1, :implementation "full", :set_code "tsb", :subtype "Virus", :previous-zone ["hand"]}, :ability 0}
Diffs are sent back to the clients.
"public-diffs-corp" [{:runner {:rig {:program [0 {:counter {:virus 2}}]}, :credit 5}, :eid 61, :log [:+ {:user "system", :text "wozzit uses Cache to gain 1 [Credits]."}]} {}] "public-diffs-runner" [{:runner {:rig {:program [0 {:counter {:virus 2}}]}, :credit 5}, :eid 61, :log [:+ {:user "system", :text "wozzit uses Cache to gain 1 [Credits]."}]} {}]
Ideas to Improve This
-
Add an request-id that the client sends to the server. When the action is resolved, pass this request-id back to the client which will make it unblock. This would require that id to flow through handle-game-action, handle-action, resolve-ability, and differ related functions. This would be for every game effect interaction - multiple per ability, vs eid on the server side.
-
Server side locking (in addition to client side). This would require the server to lock on a game effect request and ignore subsequent requests until it passes a response back for this game only!! This would still require some data to pass through handle-game-action, handle-action, resolve-ability, and differ related functions so the server knows when the request is completed. The client would lock on sending a request too. In the diff message some data will tell the client when to unlock.
Number 2 seems easier. No ID to track.
To make this more fun we also have a long list of commands which the client can send which it locks on. A scan of these makes it seem wise to continue to lock on all of them - though maybe messages could be pulled out.
(def commands
{"concede" core/concede
"system-msg" #(core/system-msg %1 %2 (:msg %3))
"change" core/change
"move" core/move-card
"mulligan" core/mulligan
"keep" core/keep-hand
"start-turn" core/start-turn
"end-phase-12" core/end-phase-12
"end-turn" core/end-turn
"draw" core/click-draw
"credit" core/click-credit
"purge" core/do-purge
"remove-tag" core/remove-tag
"play" core/play
"rez" #(core/rez %1 %2 (:card %3) nil)
"derez" #(core/derez %1 %2 (:card %3))
"run" core/click-run
"no-action" core/no-action
"corp-phase-43" core/corp-phase-43
"continue" core/continue
"access" core/successful-run
"jack-out" core/jack-out
"advance" core/advance
"score" #(core/score %1 %2 (game.core/get-card %1 (:card %3)))
"choice" core/resolve-prompt
"select" core/select
"shuffle" core/shuffle-deck
"ability" core/play-ability
"runner-ability" core/play-runner-ability
"subroutine" core/play-subroutine
"trash-resource" core/trash-resource
"dynamic-ability" core/play-dynamic-ability
"toast" core/toast
"view-deck" core/view-deck
"close-deck" core/close-deck})