session.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. #! /usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # SPDX-FileCopyrightText: Copyright (C) 2021-2023 MH3SP Server Project
  4. # SPDX-License-Identifier: AGPL-3.0-or-later
  5. """Monster Hunter session module."""
  6. import mh.database as db
  7. import mh.pat_item as pati
  8. from other.utils import to_bytearray, to_str
  9. DB = db.get_instance()
  10. class SessionState:
  11. UNKNOWN = -1
  12. LOG_IN = 0
  13. GATE = 1
  14. CITY = 2
  15. CIRCLE = 3
  16. CIRCLE_STANDBY = 4
  17. QUEST = 5
  18. class Session(object):
  19. """Server session class.
  20. TODO:
  21. - Finish the implementation
  22. """
  23. def __init__(self, connection_handler):
  24. """Create a session object."""
  25. self.local_info = {
  26. "server_id": None,
  27. "server_name": None,
  28. "gate_id": None,
  29. "gate_name": None,
  30. "city_id": None,
  31. "city_name": None,
  32. "city_size": 0,
  33. "city_capacity": 0,
  34. "circle_id": None,
  35. }
  36. self.connection = connection_handler
  37. self.online_support_code = None
  38. self.request_reconnection = False
  39. self.pat_ticket = None
  40. self.capcom_id = ""
  41. self.hunter_name = b""
  42. self.hunter_stats = None
  43. self.layer = 0
  44. self.state = SessionState.UNKNOWN
  45. self.binary_setting = b""
  46. self.search_payload = None
  47. self.hunter_info = pati.HunterSettings()
  48. def get(self, connection_data):
  49. """Return the session associated with the connection data, if any."""
  50. if hasattr(connection_data, "pat_ticket"):
  51. self.pat_ticket = to_str(
  52. pati.unpack_binary(connection_data.pat_ticket)
  53. )
  54. if hasattr(connection_data, "online_support_code"):
  55. self.online_support_code = to_str(
  56. pati.unpack_string(connection_data.online_support_code)
  57. )
  58. session = DB.get_session(self.pat_ticket) or self
  59. if session != self:
  60. assert session.connection is None, "Session is already in use"
  61. session.connection = self.connection
  62. self.connection = None
  63. # Preserve session during login process (From OPN to FMP)
  64. # if no online support code is found
  65. # Reset upon entering the FMP server (always)
  66. session.request_reconnection = \
  67. not ("pat_ticket" in connection_data or
  68. "online_support_code" in connection_data)
  69. return session
  70. def get_support_code(self):
  71. """Return the online support code."""
  72. return DB.get_support_code(self)
  73. def disconnect(self):
  74. """Disconnect the current session.
  75. It doesn't purge the session state nor its PAT ticket.
  76. """
  77. self.connection = None
  78. DB.disconnect_session(self)
  79. def delete(self):
  80. """Delete the current session.
  81. TODO:
  82. - Find a good place to purge old tickets.
  83. - We should probably create a SessionManager thread per server.
  84. """
  85. if not self.request_reconnection:
  86. DB.delete_session(self)
  87. def is_jap(self):
  88. """TODO: Heuristic using the connection data to detect region."""
  89. pass
  90. def new_pat_ticket(self):
  91. DB.new_pat_ticket(self)
  92. return to_bytearray(self.pat_ticket)
  93. def get_users(self, first_index, count):
  94. return DB.get_users(self, first_index, count)
  95. def use_user(self, index, name):
  96. DB.use_user(self, index, name)
  97. def get_servers(self):
  98. return DB.get_servers()
  99. def get_server(self):
  100. assert self.local_info['server_id'] is not None
  101. return DB.get_server(self.local_info['server_id'])
  102. def get_gate(self):
  103. assert self.local_info['gate_id'] is not None
  104. return DB.get_gate(self.local_info['server_id'],
  105. self.local_info['gate_id'])
  106. def get_city(self):
  107. assert self.local_info['city_id'] is not None
  108. return DB.get_city(self.local_info['server_id'],
  109. self.local_info['gate_id'],
  110. self.local_info['city_id'])
  111. def get_circle(self):
  112. assert self.local_info['circle_id'] is not None
  113. return self.get_city().circles[self.local_info['circle_id']]
  114. def layer_start(self):
  115. self.layer = 0
  116. self.state = SessionState.LOG_IN
  117. return pati.getDummyLayerData()
  118. def layer_end(self):
  119. if self.layer > 1:
  120. # City path
  121. if self.local_info['circle_id'] is not None:
  122. # TODO: Address that circle_id is zero-based
  123. self.leave_circle()
  124. self.leave_city()
  125. if self.layer > 0:
  126. # Gate path
  127. self.leave_gate()
  128. if not self.request_reconnection:
  129. # Server path (executed at gate and higher)
  130. self.leave_server()
  131. elif not self.request_reconnection and self.local_info["server_id"]:
  132. # Server path (executed if exiting from gate list)
  133. self.leave_server()
  134. self.layer = 0
  135. self.state = SessionState.UNKNOWN
  136. def layer_down(self, layer_id):
  137. if self.layer == 0:
  138. self.join_gate(layer_id)
  139. elif self.layer == 1:
  140. self.join_city(layer_id)
  141. else:
  142. assert False, "Can't go down a layer"
  143. self.layer += 1
  144. def layer_create(self, layer_id, settings, optional_fields):
  145. if self.layer == 1:
  146. self.create_city(layer_id, settings, optional_fields)
  147. else:
  148. assert False, "Can't create a layer from L{}".format(self.layer)
  149. self.layer_down(layer_id)
  150. def layer_up(self):
  151. if self.layer == 1:
  152. self.leave_gate()
  153. elif self.layer == 2:
  154. self.leave_city()
  155. else:
  156. assert False, "Can't go up a layer"
  157. self.layer -= 1
  158. def layer_detail_search(self, detailed_fields):
  159. server_type = self.get_server().server_type
  160. fields = [
  161. (field_id, value)
  162. for field_id, field_type, value in detailed_fields
  163. ] # Convert detailed to simple optional fields
  164. return DB.layer_detail_search(server_type, fields)
  165. def join_server(self, server_id):
  166. return DB.join_server(self, server_id)
  167. def get_layer_children(self):
  168. if self.layer == 0:
  169. return self.get_gates()
  170. elif self.layer == 1:
  171. return self.get_cities()
  172. assert False, "Unsupported layer to get children"
  173. def get_layer_sibling(self):
  174. if self.layer == 1:
  175. return self.get_gates()
  176. elif self.layer == 2:
  177. return self.get_cities()
  178. assert False, "Unsupported layer to get sibling"
  179. def find_users_by_layer(self, server_id, gate_id, city_id,
  180. first_index, count, recursive=False):
  181. if recursive:
  182. players = DB.get_all_users(server_id, gate_id, city_id)
  183. else:
  184. layer = \
  185. DB.get_city(server_id, gate_id, city_id) if city_id else \
  186. DB.get_gate(server_id, gate_id) if gate_id else \
  187. DB.get_server(server_id)
  188. players = list(layer.players)
  189. start = first_index - 1
  190. return players[start:start+count]
  191. def find_user_by_capcom_id(self, capcom_id):
  192. sessions = DB.find_users(capcom_id=capcom_id)
  193. if sessions:
  194. return sessions[0]
  195. return None
  196. def find_users(self, capcom_id, hunter_name, first_index, count):
  197. users = DB.find_users(capcom_id, hunter_name)
  198. start = first_index - 1
  199. return users[start:start+count]
  200. def get_user_name(self, capcom_id):
  201. return DB.get_user_name(capcom_id)
  202. def leave_server(self):
  203. DB.leave_server(self, self.local_info["server_id"])
  204. def get_gates(self):
  205. return DB.get_gates(self.local_info["server_id"])
  206. def join_gate(self, gate_id):
  207. DB.join_gate(self, self.local_info["server_id"], gate_id)
  208. self.state = SessionState.GATE
  209. def leave_gate(self):
  210. DB.leave_gate(self)
  211. self.state = SessionState.LOG_IN
  212. def get_cities(self):
  213. return DB.get_cities(self.local_info["server_id"],
  214. self.local_info["gate_id"])
  215. def is_city_empty(self, city_id):
  216. return DB.get_city(self.local_info["server_id"],
  217. self.local_info["gate_id"],
  218. city_id).get_state() == db.LayerState.EMPTY
  219. def reserve_city(self, city_id, reserve):
  220. return DB.reserve_city(self.local_info["server_id"],
  221. self.local_info["gate_id"],
  222. city_id, reserve)
  223. def create_city(self, city_id, settings, optional_fields):
  224. return DB.create_city(self,
  225. self.local_info["server_id"],
  226. self.local_info["gate_id"],
  227. city_id, settings, optional_fields)
  228. def join_city(self, city_id):
  229. DB.join_city(self,
  230. self.local_info["server_id"],
  231. self.local_info["gate_id"],
  232. city_id)
  233. self.state = SessionState.CITY
  234. def leave_city(self):
  235. DB.leave_city(self)
  236. self.state = SessionState.GATE
  237. def try_transfer_city_leadership(self):
  238. if self.local_info['city_id'] is None:
  239. return None
  240. city = self.get_city()
  241. with city.lock():
  242. if city.leader != self:
  243. return None
  244. for _, player in city.players:
  245. if player == self:
  246. continue
  247. city.leader = player
  248. return player
  249. return None
  250. def try_transfer_circle_leadership(self):
  251. if self.local_info['circle_id'] is None:
  252. # TODO: Address that circle_id is zero-based
  253. return None, None
  254. circle = self.get_circle()
  255. with circle.lock(), circle.players.lock():
  256. if circle.leader != self or circle.get_population() <= 1 \
  257. or not circle.departed:
  258. return None, None
  259. for i, player in circle.players:
  260. if player == self:
  261. continue
  262. circle.leader = player
  263. return i, player
  264. return None, None
  265. def join_circle(self, circle_id):
  266. # TODO: Move this to the database
  267. self.local_info['circle_id'] = circle_id
  268. self.state = SessionState.CIRCLE
  269. def set_circle_standby(self, val):
  270. assert self.state == SessionState.CIRCLE or \
  271. self.state == SessionState.CIRCLE_STANDBY or \
  272. self.state == SessionState.QUEST
  273. self.state = \
  274. SessionState.CIRCLE_STANDBY if val else SessionState.CIRCLE
  275. def is_circle_standby(self):
  276. return self.state == SessionState.CIRCLE_STANDBY
  277. def is_in_quest(self):
  278. return self.state == SessionState.QUEST
  279. def set_in_quest(self):
  280. self.state = SessionState.QUEST
  281. def leave_circle(self):
  282. # TODO: Move this to the database
  283. circle = self.get_circle()
  284. with circle.lock():
  285. self.local_info['circle_id'] = None
  286. self.state = SessionState.CITY
  287. if circle.leader == self:
  288. circle.reset()
  289. else:
  290. circle.players.remove(self)
  291. def get_layer(self):
  292. if self.layer == 0:
  293. return self.get_server()
  294. elif self.layer == 1:
  295. return self.get_gate()
  296. elif self.layer == 2:
  297. return self.get_city()
  298. else:
  299. assert False, "Can't find layer"
  300. def get_layer_players(self):
  301. return self.get_layer().players
  302. def get_layer_path(self):
  303. return pati.LayerPath(self.local_info['server_id'], self.local_info['gate_id'],
  304. self.local_info['city_id'])
  305. def get_layer_host_data(self):
  306. """LayerUserInfo's layer_host."""
  307. return self.get_layer_path().pack()
  308. def get_optional_fields(self):
  309. """LayerUserInfo's optional fields."""
  310. location = int(self.is_in_quest()) # City - 0, Quest - 1
  311. hunter_rank = self.hunter_info.rank()
  312. weapon_type = self.hunter_info.weapon_type()
  313. return [
  314. (1, (weapon_type << 24) | location),
  315. (2, hunter_rank << 16)
  316. ]
  317. def add_friend_request(self, capcom_id):
  318. return DB.add_friend_request(self.capcom_id, capcom_id)
  319. def accept_friend(self, capcom_id, accepted=True):
  320. return DB.accept_friend(self.capcom_id, capcom_id, accepted)
  321. def delete_friend(self, capcom_id):
  322. return DB.delete_friend(self.capcom_id, capcom_id)
  323. def get_friends(self, first_index=None, count=None):
  324. return DB.get_friends(self.capcom_id, first_index, count)