From 20ae1224c7cd981629f2ec0d26a10f94cc1f0789 Mon Sep 17 00:00:00 2001 From: Kevin Barkevich Date: Sat, 25 Feb 2023 21:11:23 -0500 Subject: [PATCH] Implemented warping between cities, gates, and servers 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 --- fmp_server.py | 232 ++++++++++++++++++++++++++++++++++++++++++++++++- mh/database.py | 12 +++ mh/pat.py | 6 +- mh/session.py | 26 +++++- 4 files changed, 272 insertions(+), 4 deletions(-) diff --git a/fmp_server.py b/fmp_server.py index ee7b5ea..1409eed 100644 --- a/fmp_server.py +++ b/fmp_server.py @@ -40,6 +40,237 @@ def recvAnsConnection(self, packet_id, data, seq): self.server.debug("Connection: {!r}".format(connection_data)) self.sendNtcLogin(3, connection_data, seq) + def recvReqLayerJump(self, packet_id, data, seq): + """ReqLayerJump packet. + + ID: 64100100 + JP: レイヤ移動要求(位置バイナリ) + TR: Layer move request (position binary) + + A relay of the target user's layer_host. + """ + length, unk1, server_id, unk2, gate_id, city_id = struct.unpack_from(">HIIHHH", data) + + if self.session.local_info["server_id"] == server_id and\ + self.session.local_info["gate_id"] == gate_id and\ + self.session.local_info["city_id"] == city_id: + self.sendAnsAlert(PatID4.AnsLayerJump, + "
You're already there.", + seq) + elif self.session.local_info["server_id"] != server_id: + # Different Server + if self.session.get_server_full(server_id) or\ + self.session.get_gate_full(server_id, gate_id) or\ + (city_id and self.session.get_city_full(server_id, gate_id, city_id)): + self.sendAnsAlert(PatID4.AnsLayerJump, + "
Destination is full.", + seq) + return + else: + # TODO: Lock the gate before the player checks its capacity. + # with self.session.find_gate(server_id, gate_id).lock() + if self.session.local_info["gate_id"] != gate_id: + # Same Server, different Gate + if city_id: + city = self.session.find_city(server_id, gate_id, city_id) + with city.lock(): + if city.is_empty(): + self.sendAnsAlert(PatID4.AnsLayerJump, + "
City no longer exists.", + seq) + return + elif self.session.get_gate_full(server_id, gate_id) or\ + city.is_full(): + self.sendAnsAlert(PatID4.AnsLayerJump, + "
Destination is full.", + seq) + return + + # Step 1: Leave current location + while self.session.layer > 0: + self.notify_layer_departure() + self.session.layer_up() + # Step 2: Join Gate + self.session.layer_down(gate_id) + user = pati.LayerUserInfo() + user.capcom_id = pati.String(self.session.capcom_id) + user.hunter_name = pati.String(self.session.hunter_name) + user.stats = pati.Binary(self.session.hunter_info.pack()) + + data = pati.lp2_string(self.session.capcom_id) + data += user.pack() + self.server.layer_broadcast(self.session, PatID4.NtcLayerIn, + data, seq) + # Step 3: Join City + self.session.layer_down(city_id) + else: + if self.session.get_gate_full(server_id, gate_id): + self.sendAnsAlert(PatID4.AnsLayerJump, + "
Destination is full.", + seq) + return + + # Step 1: Leave current location + while self.session.layer > 0: + self.notify_layer_departure() + self.session.layer_up() + # Step 2: Join Gate + self.session.layer_down(gate_id) + user = pati.LayerUserInfo() + user.capcom_id = pati.String(self.session.capcom_id) + user.hunter_name = pati.String(self.session.hunter_name) + user.stats = pati.Binary(self.session.hunter_info.pack()) + + data = pati.lp2_string(self.session.capcom_id) + data += user.pack() + self.server.layer_broadcast(self.session, PatID4.NtcLayerIn, + data, seq) + else: + # Same Server, same Gate + if city_id: + city = self.session.find_city(server_id, gate_id, city_id) + with city.lock(): + if city.is_empty(): + self.sendAnsAlert(PatID4.AnsLayerJump, + "
City no longer exists.", + seq) + return + elif city.is_full(): + self.sendAnsAlert(PatID4.AnsLayerJump, + "
Destination is full.", + seq) + return + + # Step 1: Leave current location + while self.session.layer > 1: + self.notify_layer_departure() + self.session.layer_up() + # Step 2: Join City + self.session.layer_down(city_id) + else: + # Step 1: Leave current location + while self.session.layer > 1: + self.notify_layer_departure() + self.session.layer_up() + self.sendAnsLayerJump(seq) + + def sendAnsLayerJump(self, seq): + """NtcLayerJumpGo packet. + + ID: 64170200 + JP: レイヤ予約移動実行返答 + TR: Layer reservation move execution reply + """ + self.send_packet(PatID4.AnsLayerJump, b"", seq) + + def recvReqLayerJumpReady(self, packet_id, data, seq): + """AnsLayerJump packet. + + ID: 64100200 + JP: レイヤ移動返答(位置バイナリ) + TR: Layer move reply (position binary) + + Client asking permission to switch Servers. + """ + length, unk1, server_id, unk2, gate_id, city_id = struct.unpack_from(">HIIHHH", data) + if self.session.get_server_full(server_id) or\ + self.session.get_gate_full(server_id, gate_id) or\ + (city_id and self.session.get_city_full(server_id, gate_id, city_id)): + self.sendAnsAlert(PatID4.NtcLayerJumpReady, + "
Destination is full.", + seq) + return + # Step 1: Leave current location + # ( continued in sendNtcLayerJumpReady() ) + while self.session.layer > 0: + self.notify_layer_departure() + self.session.layer_up() + self.sendNtcLayerJumpReady(seq) + + def sendNtcLayerJumpReady(self, seq): + """ReqLayerJumpReady packet. + + ID: 64160200 + JP: レイヤ予約移動確認返答 + TR: Layer reservation move confirmation reply + """ + # Step 2: Join Server + # ( continued in recvReqLayerJumpGo() ) + unk = 0x00000004 + data = struct.pack(">I", unk) + self.send_packet(PatID4.NtcLayerJumpReady, data, seq) + + def recvReqLayerJumpGo(self, packet_id, data, seq): + """ReqLayerJumpGo packet. + + ID: 64170100 + JP: レイヤ予約移動実行要求 + TR: Layer reserved move execution request + + Client, having switched servers, asking permission + to enter the target gate/city. + """ + length, unk1, server_id, unk2, gate_id, city_id = struct.unpack_from(">HIIHHH", data) + + # TODO: Lock the gate before the player checks its capacity. + # with self.session.find_gate(server_id, gate_id).lock() + if city_id: + city = self.session.find_city(server_id, gate_id, city_id) + with city.lock(): + if city.is_empty(): + self.sendAnsAlert(PatID4.AnsLayerJump, + "
City no longer exists.", + seq) + return + elif self.session.get_gate_full(server_id, gate_id) or\ + city.is_full(): + self.sendAnsAlert(PatID4.AnsLayerCreateHead, + "
Destination is full.", + seq) + return + + # Step 3: Join Gate + self.session.layer_down(gate_id) + user = pati.LayerUserInfo() + user.capcom_id = pati.String(self.session.capcom_id) + user.hunter_name = pati.String(self.session.hunter_name) + user.stats = pati.Binary(self.session.hunter_info.pack()) + + data = pati.lp2_string(self.session.capcom_id) + data += user.pack() + self.server.layer_broadcast(self.session, PatID4.NtcLayerIn, + data, seq) + # Step 4: Join City + self.session.layer_down(city_id) + else: + if self.session.get_gate_full(server_id, gate_id): + self.sendAnsAlert(PatID4.AnsLayerCreateHead, + "
Destination is full.", + seq) + return + + # Step 3: Join Gate + self.session.layer_down(gate_id) + user = pati.LayerUserInfo() + user.capcom_id = pati.String(self.session.capcom_id) + user.hunter_name = pati.String(self.session.hunter_name) + user.stats = pati.Binary(self.session.hunter_info.pack()) + + data = pati.lp2_string(self.session.capcom_id) + data += user.pack() + self.server.layer_broadcast(self.session, PatID4.NtcLayerIn, + data, seq) + self.sendNtcLayerJumpGo(seq) + + def sendNtcLayerJumpGo(self, seq): + """NtcLayerJumpGo packet. + + ID: 64170200 + JP: レイヤ予約移動実行返答 + TR: Layer reservation move execution reply + """ + self.send_packet(PatID4.NtcLayerJumpGo, b"", seq) + def sendAnsLayerDown(self, layer_id, layer_set, seq): """AnsLayerDown packet. @@ -433,7 +664,6 @@ def sendAnsCircleMatchOptionSet(self, options, seq): pati.unpack_byte(options.is_standby) == 1 self.session.set_circle_standby(is_standby) - circle = self.session.get_circle() options.capcom_id = pati.String(self.session.capcom_id) options.hunter_name = pati.String(self.session.hunter_name) diff --git a/mh/database.py b/mh/database.py index e93b698..31aa4ec 100644 --- a/mh/database.py +++ b/mh/database.py @@ -290,6 +290,12 @@ def reserve(self, reserve): else: self.reserved = None + def is_full(self): + return self.get_population() >= self.get_capacity() + + def is_empty(self): + return self.get_population() == 0 + class Gate(object): LAYER_DEPTH = 2 @@ -323,6 +329,9 @@ def get_state(self): else: return LayerState.FULL + def is_full(self): + return self.get_population() >= self.get_capacity() + class Server(object): LAYER_DEPTH = 1 @@ -348,6 +357,9 @@ def get_population(self): def get_capacity(self): return self.players.get_capacity() + def is_full(self): + return self.get_population() >= self.get_capacity() + def new_servers(): servers = [] diff --git a/mh/pat.py b/mh/pat.py index 29b91e1..afa3a29 100644 --- a/mh/pat.py +++ b/mh/pat.py @@ -1271,8 +1271,10 @@ def sendAnsUserSearchInfo(self, capcom_id, search_info, seq): ])) user_info.unk_byte_0x0b = pati.Byte(1) user_info.unk_string_0x0c = pati.String("StrC") - user_info.city_capacity = pati.Long(4) - user_info.city_size = pati.Long(3) + if user.is_in_city(): + city = user.get_city() + user_info.city_capacity = pati.Long(city.get_capacity()) + user_info.city_size = pati.Long(city.get_population()) # This fields are used to identify a user. # Specifically when a client is deserializing data from the packets diff --git a/mh/session.py b/mh/session.py index 41c0d9b..fa7987b 100644 --- a/mh/session.py +++ b/mh/session.py @@ -139,6 +139,24 @@ def get_server(self): assert self.local_info['server_id'] is not None return DB.get_server(self.local_info['server_id']) + 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) + def get_gate(self): assert self.local_info['gate_id'] is not None return DB.get_gate(self.local_info['server_id'], @@ -287,6 +305,12 @@ def leave_city(self): DB.leave_city(self) self.state = SessionState.GATE + def is_in_city(self): + return self.state == SessionState.CITY or\ + self.state == SessionState.CIRCLE or\ + self.state == SessionState.CIRCLE_STANDBY or\ + self.state == SessionState.QUEST + def try_transfer_city_leadership(self): if self.local_info['city_id'] is None: return None @@ -368,7 +392,7 @@ def get_layer_players(self): def get_layer_host_data(self): """LayerUserInfo's layer_host.""" - return struct.pack("IIHHH", + return struct.pack(">IIHHH", 3, # layer depth? self.local_info["server_id"] or 0, 1, # ???