Conversation
Added checks to ensure destination is not full Added additional full checks and documented intention to add locks prior to gate/city jumps Removed debug True Removed newline Added City locking to warp logic
sepalani
left a comment
There was a problem hiding this comment.
This is my first pass on this PR. I'll try to have a look at these if/elif/else to find a cleaner way to refactor it.
| self.sendAnsAlert(PatID4.AnsLayerJump, | ||
| "<LF=8><BODY><CENTER>You're already there.<END>", | ||
| seq) |
There was a problem hiding this comment.
Are these alert messages necessary? I remember when I was reversing the Warp features, the game performed these checks without asking the server nor sending the packet. Isn't an early return statement missing as well?
| return self.state == SessionState.CITY or\ | ||
| self.state == SessionState.CIRCLE or\ | ||
| self.state == SessionState.CIRCLE_STANDBY or\ | ||
| self.state == SessionState.QUEST |
There was a problem hiding this comment.
return self.state in (SessionState.CITY, SessionState.CIRCLE,
SessionState.CIRCLE_STANDBY, SessionState.QUEST)| "<LF=8><BODY><CENTER>Destination is full.<END>", | ||
| seq) | ||
| return | ||
| else: |
There was a problem hiding this comment.
If the previous if/elif return early, this else statement can be removed and its code indentation reduced. Let's avoid if/elif/else forests when we have the opportunity. Most of the code in this else block can be deduplicated too.
| JP: レイヤ移動要求(位置バイナリ) | ||
| TR: Layer move request (position binary) | ||
|
|
||
| A relay of the target user's layer_host. |
There was a problem hiding this comment.
You should add a comment stating that this code is "racy" and the sanity checks are provided on a best-efforts basis.
As already stated we have no reasonable way to ensure the validity of these pieces of information. Preventing the warp due to outdated information is fine, IMHO. Indeed, we have no control over the other distant servers and their state can change between our check and the warp. The worst case being: a server changing from being available to full which these checks can't account for unless we lock the whole server.
Moreover, enforcing/locking a city or even worse a gate or server will severely harm performance, even more so if it's a distant peer. Which will prevent all its population to alter its state when someone is warping to it. If there are no strong reasons, you should avoid using lock here or add a comment to justify its benefits.
For instance, locking a city or a gate from another (distant) server shouldn't be possible. I really doubt that Capcom did these sanity checks anyway, though, I might be wrong.
There was a problem hiding this comment.
I was thinking about that, and my idea was that they would have a method of communicating to the other server to reserve a slot. That way you avoid having an issue of state changing mid flight. But I don't know if they actually did that or if it would be fast enough to not create a noticeable slowdown in the process.
| def get_server_full(self, server_id): | ||
| return DB.get_server(server_id).is_full() | ||
|
|
||
| def get_gate_full(self, server_id, gate_id): | ||
| return DB.get_gate(server_id, gate_id).is_full() | ||
|
|
||
| def get_city_full(self, server_id, gate_id, city_id): | ||
| return DB.get_city(server_id, gate_id, city_id).is_full() | ||
|
|
||
| def get_city_empty(self, server_id, gate_id, city_id): | ||
| return DB.get_city(server_id, gate_id, city_id).is_empty() | ||
|
|
||
| def find_gate(self, server_id, gate_id): | ||
| return DB.get_gate(server_id, gate_id) | ||
|
|
||
| def find_city(self, server_id, gate_id, city_id): | ||
| return DB.get_city(server_id, gate_id, city_id) |
There was a problem hiding this comment.
I'm really not a huge fan of this since the session module is mainly used to cache the server data and hide the implementation logic (for instance which DB backend we use, which function to call based on the packet we received and its parameters, etc.). That's also why most of its functions don't ask for the server_id as parameter and use the local_info when needed rather than being a simple wrapper to call the default database instance.
I'll let it slide for now since we don't provide a better abstraction to query the database directly, especially for other servers. AFAICT, this point seems still valid with the state abstraction you proposed in another PR because parts of the session modules (including the ones this PR added) needs to take into account whether or not the city/gate/server is handled by the current server process or not.
| def sendAnsLayerJump(self, seq): | ||
| """NtcLayerJumpGo packet. | ||
|
|
||
| ID: 64170200 | ||
| JP: レイヤ予約移動実行返答 | ||
| TR: Layer reservation move execution reply |
There was a problem hiding this comment.
| def sendAnsLayerJump(self, seq): | |
| """NtcLayerJumpGo packet. | |
| ID: 64170200 | |
| JP: レイヤ予約移動実行返答 | |
| TR: Layer reservation move execution reply | |
| def sendAnsLayerJump(self, seq): | |
| """AnsLayerJump packet. | |
| ID: 64100200 | |
| JP: レイヤ移動返答(位置バイナリ) | |
| TR: Layer move reply (position binary) |
| def recvReqLayerJumpReady(self, packet_id, data, seq): | ||
| """AnsLayerJump packet. | ||
|
|
||
| ID: 64100200 | ||
| JP: レイヤ移動返答(位置バイナリ) | ||
| TR: Layer move reply (position binary) |
There was a problem hiding this comment.
| def recvReqLayerJumpReady(self, packet_id, data, seq): | |
| """AnsLayerJump packet. | |
| ID: 64100200 | |
| JP: レイヤ移動返答(位置バイナリ) | |
| TR: Layer move reply (position binary) | |
| def recvReqLayerJumpReady(self, packet_id, data, seq): | |
| """ReqLayerJumpReady packet. | |
| ID: 64160100 | |
| JP: レイヤ予約移動確認要求 | |
| TR: Layer reservation move confirmation request |
| def sendNtcLayerJumpReady(self, seq): | ||
| """ReqLayerJumpReady packet. | ||
|
|
||
| ID: 64160200 | ||
| JP: レイヤ予約移動確認返答 | ||
| TR: Layer reservation move confirmation reply | ||
| """ |
There was a problem hiding this comment.
| def sendNtcLayerJumpReady(self, seq): | |
| """ReqLayerJumpReady packet. | |
| ID: 64160200 | |
| JP: レイヤ予約移動確認返答 | |
| TR: Layer reservation move confirmation reply | |
| """ | |
| def sendNtcLayerJumpReady(self, seq): | |
| """NtcLayerJumpReady packet. | |
| ID: 64160200 | |
| JP: レイヤ予約移動確認返答 | |
| TR: Layer reservation move confirmation reply | |
| """ |
sic: I think this function was wrongly identified when I generated the constants so we should rename it AnsLayerJumpReady later at some point or add a comment stating that this function was wrongly named in game if that was the case.
| def sendNtcLayerJumpGo(self, seq): | ||
| """NtcLayerJumpGo packet. | ||
|
|
||
| ID: 64170200 | ||
| JP: レイヤ予約移動実行返答 | ||
| TR: Layer reservation move execution reply | ||
| """ |
There was a problem hiding this comment.
sic: Ditto, regarding its name. Feel free to ignore this comment.
This PR handles warping in the following cases:
"Different Server" warps are a multi-step process that periodically checks to see if the destination is full (or empty, in the case of a city.) The steps are as follows:
RecLayerJump/AnsLayerJump(Game initiates warp. First check to see if destination is valid happens.)ReqLayerJumpReady/NtcLayerJumpReady(Game asks to initiate Server->Server hop, does so using standard FMP disconnect/connect sequences upon reply withNtcpacket. Second check to see if destination is valid happens.)ReqLayerJumpGo/NtcLayerJumpGo(Game, now having completed the Server->Server hop, asks to enter their destination Gate/City. Third check to see if destination is valid happens.)Testing has shown that the process is able to be gracefully aborted through an
AnsAlertpacket at any step. For example, if the Server->Server jump is completed, and then the process is aborted, the user will be returned to the "server select" screen. If the process is aborted before the Server->Server jump is completed, the user will remain in their current location.TODO in a future PR: Implementation of
Gatelocking at the relevant steps. Currently,Citylocking is implemented.