Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 231 additions & 1 deletion fmp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

@InusualZ InusualZ Mar 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

"""
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,
"<LF=8><BODY><CENTER>You're already there.<END>",
seq)
Comment on lines +57 to +59
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

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,
"<LF=8><BODY><CENTER>Destination is full.<END>",
seq)
return
else:
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

# 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,
"<LF=8><BODY><CENTER>City no longer exists.<END>",
seq)
return
elif self.session.get_gate_full(server_id, gate_id) or\
city.is_full():
self.sendAnsAlert(PatID4.AnsLayerJump,
"<LF=8><BODY><CENTER>Destination is full.<END>",
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,
"<LF=8><BODY><CENTER>Destination is full.<END>",
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,
"<LF=8><BODY><CENTER>City no longer exists.<END>",
seq)
return
elif city.is_full():
self.sendAnsAlert(PatID4.AnsLayerJump,
"<LF=8><BODY><CENTER>Destination is full.<END>",
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
Comment on lines +157 to +162
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)

"""
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)
Comment on lines +166 to +171
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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


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,
"<LF=8><BODY><CENTER>Destination is full.<END>",
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
"""
Comment on lines +190 to +196
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

# 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,
"<LF=8><BODY><CENTER>City no longer exists.<END>",
seq)
return
elif self.session.get_gate_full(server_id, gate_id) or\
city.is_full():
self.sendAnsAlert(PatID4.AnsLayerCreateHead,
"<LF=8><BODY><CENTER>Destination is full.<END>",
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,
"<LF=8><BODY><CENTER>Destination is full.<END>",
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
"""
Comment on lines +265 to +271
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sic: Ditto, regarding its name. Feel free to ignore this comment.

self.send_packet(PatID4.NtcLayerJumpGo, b"", seq)

def sendAnsLayerDown(self, layer_id, layer_set, seq):
"""AnsLayerDown packet.

Expand Down Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions mh/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 = []
Expand Down
6 changes: 4 additions & 2 deletions mh/pat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 25 additions & 1 deletion mh/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +142 to +158
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 get_gate(self):
assert self.local_info['gate_id'] is not None
return DB.get_gate(self.local_info['server_id'],
Expand Down Expand Up @@ -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
Comment on lines +309 to +312
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return self.state in (SessionState.CITY, SessionState.CIRCLE,
                      SessionState.CIRCLE_STANDBY, SessionState.QUEST)


def try_transfer_city_leadership(self):
if self.local_info['city_id'] is None:
return None
Expand Down Expand Up @@ -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, # ???
Expand Down