database.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966
  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 database module."""
  6. import random
  7. import sqlite3
  8. import time
  9. from other import utils
  10. from threading import RLock, local as thread_local
  11. CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  12. MEDIA_VERSIONS = {
  13. "0903131810": "ROMJ08",
  14. "0906222136": "RMHJ08",
  15. "1001301045": "RMHE08",
  16. "1002121503": "RMHP08",
  17. }
  18. BLANK_CAPCOM_ID = "******"
  19. RESERVE_DC_TIMEOUT = 40.0
  20. def new_random_str(length=6):
  21. return "".join(random.choice(CHARSET) for _ in range(length))
  22. class ServerType(object):
  23. OPEN = 1
  24. ROOKIE = 2
  25. EXPERT = 3
  26. RECRUITING = 4
  27. class LayerState(object):
  28. EMPTY = 1
  29. FULL = 2
  30. JOINABLE = 3
  31. class Lockable(object):
  32. def __init__(self):
  33. self._lock = RLock()
  34. def lock(self):
  35. return self
  36. def __enter__(self):
  37. # Returns True if lock was acquired, False otherwise
  38. return self._lock.acquire()
  39. def __exit__(self, *args):
  40. self._lock.release()
  41. class Players(Lockable):
  42. def __init__(self, capacity):
  43. assert capacity > 0, "Collection capacity can't be zero"
  44. self.slots = [None for _ in range(capacity)]
  45. self.used = 0
  46. super(Players, self).__init__()
  47. def get_used_count(self):
  48. return self.used
  49. def get_capacity(self):
  50. return len(self.slots)
  51. def add(self, item):
  52. with self.lock():
  53. if self.used >= len(self.slots):
  54. return -1
  55. item_index = self.index(item)
  56. if item_index != -1:
  57. return item_index
  58. for i, v in enumerate(self.slots):
  59. if v is not None:
  60. continue
  61. self.slots[i] = item
  62. self.used += 1
  63. return i
  64. return -1
  65. def remove(self, item):
  66. assert item is not None, "Item != None"
  67. with self.lock():
  68. if self.used < 1:
  69. return False
  70. if isinstance(item, int):
  71. if item >= self.get_capacity():
  72. return False
  73. self.slots[item] = None
  74. self.used -= 1
  75. return True
  76. for i, v in enumerate(self.slots):
  77. if v != item:
  78. continue
  79. self.slots[i] = None
  80. self.used -= 1
  81. return True
  82. return False
  83. def index(self, item):
  84. assert item is not None, "Item != None"
  85. for i, v in enumerate(self.slots):
  86. if v == item:
  87. return i
  88. return -1
  89. def clear(self):
  90. with self.lock():
  91. for i in range(self.get_capacity()):
  92. self.slots[i] = None
  93. def find_first(self, **kwargs):
  94. if self.used < 1:
  95. return None
  96. for p in self.slots:
  97. if p is None:
  98. continue
  99. for k, v in kwargs.items():
  100. if getattr(p, k) != v:
  101. break
  102. else:
  103. return p
  104. return None
  105. def find_by_capcom_id(self, capcom_id):
  106. return self.find_first(capcom_id=capcom_id)
  107. def __len__(self):
  108. return self.used
  109. def __iter__(self):
  110. if self.used < 1:
  111. return
  112. for i, v in enumerate(self.slots):
  113. if v is None:
  114. continue
  115. yield i, v
  116. class Circle(Lockable):
  117. def __init__(self, parent):
  118. # type: (City) -> None
  119. self.parent = parent
  120. self.leader = None
  121. self.players = Players(4)
  122. self.departed = False
  123. self.quest_id = 0
  124. self.embarked = False
  125. self.password = None
  126. self.remarks = None
  127. self.unk_byte_0x0e = 0
  128. super(Circle, self).__init__()
  129. def get_population(self):
  130. return len(self.players)
  131. def get_capacity(self):
  132. return self.players.get_capacity()
  133. def is_full(self):
  134. return self.get_population() == self.get_capacity()
  135. def is_empty(self):
  136. return self.leader is None
  137. def is_joinable(self):
  138. return not self.departed and not self.is_full()
  139. def has_password(self):
  140. return self.password is not None
  141. def reset_players(self, capacity):
  142. with self.lock():
  143. self.players = Players(capacity)
  144. def reset(self):
  145. with self.lock():
  146. self.leader = None
  147. self.reset_players(4)
  148. self.departed = False
  149. self.quest_id = 0
  150. self.embarked = False
  151. self.password = None
  152. self.remarks = None
  153. self.unk_byte_0x0e = 0
  154. class City(Lockable):
  155. LAYER_DEPTH = 3
  156. def __init__(self, name, parent):
  157. # type: (str, Gate) -> None
  158. self.name = name
  159. self.parent = parent
  160. self.state = LayerState.EMPTY
  161. self.players = Players(4)
  162. self.optional_fields = []
  163. self.leader = None
  164. self.reserved = None
  165. self.circles = [
  166. # One circle per player
  167. Circle(self) for _ in range(self.get_capacity())
  168. ]
  169. super(City, self).__init__()
  170. def get_population(self):
  171. return len(self.players)
  172. def in_quest_players(self):
  173. return sum(p.is_in_quest() for _, p in self.players)
  174. def get_capacity(self):
  175. return self.players.get_capacity()
  176. def get_state(self):
  177. if self.reserved:
  178. return LayerState.FULL
  179. size = self.get_population()
  180. if size == 0:
  181. return LayerState.EMPTY
  182. elif size < self.get_capacity():
  183. return LayerState.JOINABLE
  184. else:
  185. return LayerState.FULL
  186. def get_pathname(self):
  187. pathname = self.name
  188. it = self.parent
  189. while it is not None:
  190. pathname = it.name + "\t" + pathname
  191. it = it.parent
  192. return pathname
  193. def get_first_empty_circle(self):
  194. with self.lock():
  195. for index, circle in enumerate(self.circles):
  196. if circle.is_empty():
  197. return circle, index
  198. return None, None
  199. def get_circle_for(self, leader_session):
  200. with self.lock():
  201. for index, circle in enumerate(self.circles):
  202. if circle.leader == leader_session:
  203. return circle, index
  204. return None, None
  205. def clear_circles(self):
  206. with self.lock():
  207. for circle in self.circles:
  208. circle.reset()
  209. def reserve(self, reserve):
  210. with self.lock():
  211. if reserve:
  212. self.reserved = time.time()
  213. else:
  214. self.reserved = None
  215. def reset(self):
  216. with self.lock():
  217. self.state = LayerState.EMPTY
  218. self.players.clear()
  219. self.optional_fields = []
  220. self.leader = None
  221. self.reserved = None
  222. self.clear_circles()
  223. class Gate(object):
  224. LAYER_DEPTH = 2
  225. def __init__(self, name, parent, city_count=40, player_capacity=100):
  226. # type: (str, Server, int, int) -> None
  227. self.name = name
  228. self.parent = parent
  229. self.state = LayerState.EMPTY
  230. self.cities = [
  231. City("City{}".format(i), self)
  232. for i in range(1, city_count+1)
  233. ]
  234. self.players = Players(player_capacity)
  235. self.optional_fields = []
  236. def get_population(self):
  237. return len(self.players) + sum((
  238. city.get_population()
  239. for city in self.cities
  240. ))
  241. def get_capacity(self):
  242. return self.players.get_capacity()
  243. def get_state(self):
  244. size = self.get_population()
  245. if size == 0:
  246. return LayerState.EMPTY
  247. elif size < self.get_capacity():
  248. return LayerState.JOINABLE
  249. else:
  250. return LayerState.FULL
  251. class Server(object):
  252. LAYER_DEPTH = 1
  253. def __init__(self, name, server_type, gate_count=40, capacity=2000,
  254. addr=None, port=None):
  255. self.name = name
  256. self.parent = None
  257. self.server_type = server_type
  258. self.addr = addr
  259. self.port = port
  260. self.gates = [
  261. Gate("City Gate{}".format(i), self)
  262. for i in range(1, gate_count+1)
  263. ]
  264. self.players = Players(capacity)
  265. def get_population(self):
  266. return len(self.players) + sum((
  267. gate.get_population() for gate in self.gates
  268. ))
  269. def get_capacity(self):
  270. return self.players.get_capacity()
  271. def new_servers():
  272. servers = []
  273. servers.extend([
  274. Server("Valor{}".format(i), ServerType.OPEN)
  275. for i in range(1, 5)
  276. ])
  277. servers.extend([
  278. Server("Beginners{}".format(i), ServerType.ROOKIE)
  279. for i in range(1, 3)
  280. ])
  281. servers.extend([
  282. Server("Veterans{}".format(i), ServerType.EXPERT)
  283. for i in range(1, 3)
  284. ])
  285. servers.extend([
  286. Server("Greed{}".format(i), ServerType.RECRUITING)
  287. for i in range(1, 5)
  288. ])
  289. return servers
  290. class TempDatabase(object):
  291. """A temporary database.
  292. TODO:
  293. - Finish the implementation
  294. - Make this thread-safe
  295. - [Feature request] Send unread friend requests on next login
  296. * Imply saving the message info along the Capcom ID
  297. """
  298. def __init__(self):
  299. self.consoles = {
  300. # Online support code => Capcom IDs
  301. }
  302. self.sessions = {
  303. # PAT Ticket => Owner's session
  304. }
  305. self.capcom_ids = {
  306. # Capcom ID => Owner's name and session
  307. }
  308. self.friend_requests = {
  309. # Capcom ID => List of friend requests from Capcom IDs
  310. }
  311. self.friend_lists = {
  312. # Capcom ID => List of Capcom IDs
  313. # TODO: May need stable index, see Players class
  314. }
  315. self.servers = new_servers()
  316. def get_support_code(self, session):
  317. """Get the online support code or create one."""
  318. support_code = session.online_support_code
  319. if support_code is None:
  320. while True:
  321. support_code = new_random_str(11)
  322. if support_code not in self.consoles:
  323. session.online_support_code = support_code
  324. break
  325. # Create some default users
  326. if support_code not in self.consoles:
  327. self.consoles[support_code] = [BLANK_CAPCOM_ID] * 6
  328. return support_code
  329. def new_pat_ticket(self, session):
  330. """Generates a new PAT ticket for the session."""
  331. while True:
  332. session.pat_ticket = new_random_str(11)
  333. if session.pat_ticket not in self.sessions:
  334. break
  335. self.sessions[session.pat_ticket] = session
  336. return session.pat_ticket
  337. def use_capcom_id(self, session, capcom_id, name=None):
  338. """Attach the session to the Capcom ID."""
  339. assert capcom_id in self.capcom_ids, "Capcom ID doesn't exist"
  340. not_in_use = self.capcom_ids[capcom_id]["session"] is None
  341. assert not_in_use, "Capcom ID is already in use"
  342. name = name or self.capcom_ids[capcom_id]["name"]
  343. self.capcom_ids[capcom_id] = {"name": name, "session": session}
  344. # TODO: Check if stable index is required
  345. if capcom_id not in self.friend_lists:
  346. self.friend_lists[capcom_id] = []
  347. if capcom_id not in self.friend_requests:
  348. self.friend_requests[capcom_id] = []
  349. return name
  350. def use_user(self, session, index, name):
  351. """Use User from the slot or create one if empty"""
  352. assert 1 <= index <= 6, "Invalid Capcom ID slot"
  353. index -= 1
  354. users = self.consoles[session.online_support_code]
  355. while users[index] == BLANK_CAPCOM_ID:
  356. capcom_id = new_random_str(6)
  357. if capcom_id not in self.capcom_ids:
  358. self.capcom_ids[capcom_id] = {"name": name, "session": None}
  359. users[index] = capcom_id
  360. break
  361. else:
  362. capcom_id = users[index]
  363. name = self.use_capcom_id(session, capcom_id, name)
  364. session.capcom_id = capcom_id
  365. session.hunter_name = name
  366. def get_session(self, pat_ticket):
  367. """Returns existing PAT session or None."""
  368. session = self.sessions.get(pat_ticket)
  369. if session and session.capcom_id:
  370. self.use_capcom_id(session, session.capcom_id, session.hunter_name)
  371. return session
  372. def disconnect_session(self, session):
  373. """Detach the session from its Capcom ID."""
  374. if not session.capcom_id:
  375. # Capcom ID isn't chosen yet with OPN/LMP servers
  376. return
  377. self.capcom_ids[session.capcom_id]["session"] = None
  378. def delete_session(self, session):
  379. """Delete the session from the database."""
  380. self.disconnect_session(session)
  381. pat_ticket = session.pat_ticket
  382. if pat_ticket in self.sessions:
  383. del self.sessions[pat_ticket]
  384. def get_users(self, session, first_index, count):
  385. """Returns Capcom IDs tied to the session."""
  386. users = self.consoles[session.online_support_code]
  387. capcom_ids = [
  388. (i, (capcom_id, self.capcom_ids.get(capcom_id, {})))
  389. for i, capcom_id in enumerate(users[:count], first_index)
  390. ]
  391. size = len(capcom_ids)
  392. if size < count:
  393. capcom_ids.extend([
  394. (index, (BLANK_CAPCOM_ID, {}))
  395. for index in range(first_index+size, first_index+count)
  396. ])
  397. return capcom_ids
  398. def join_server(self, session, index):
  399. if session.local_info["server_id"] is not None:
  400. self.leave_server(session, session.local_info["server_id"])
  401. server = self.get_server(index)
  402. server.players.add(session)
  403. session.local_info["server_id"] = index
  404. session.local_info["server_name"] = server.name
  405. return server
  406. def leave_server(self, session, index):
  407. self.get_server(index).players.remove(session)
  408. session.local_info["server_id"] = None
  409. session.local_info["server_name"] = None
  410. def get_server_time(self):
  411. pass
  412. def get_game_time(self):
  413. pass
  414. def get_servers(self):
  415. return self.servers
  416. def get_server(self, index):
  417. assert 0 < index <= len(self.servers), "Invalid server index"
  418. return self.servers[index - 1]
  419. def get_gates(self, server_id):
  420. return self.get_server(server_id).gates
  421. def get_gate(self, server_id, index):
  422. gates = self.get_gates(server_id)
  423. assert 0 < index <= len(gates), "Invalid gate index"
  424. return gates[index - 1]
  425. def join_gate(self, session, server_id, index):
  426. gate = self.get_gate(server_id, index)
  427. gate.parent.players.remove(session)
  428. gate.players.add(session)
  429. session.local_info["gate_id"] = index
  430. session.local_info["gate_name"] = gate.name
  431. return gate
  432. def leave_gate(self, session):
  433. gate = self.get_gate(session.local_info["server_id"],
  434. session.local_info["gate_id"])
  435. gate.parent.players.add(session)
  436. gate.players.remove(session)
  437. session.local_info["gate_id"] = None
  438. session.local_info["gate_name"] = None
  439. def get_cities(self, server_id, gate_id):
  440. return self.get_gate(server_id, gate_id).cities
  441. def get_city(self, server_id, gate_id, index):
  442. cities = self.get_cities(server_id, gate_id)
  443. assert 0 < index <= len(cities), "Invalid city index"
  444. return cities[index - 1]
  445. def reserve_city(self, server_id, gate_id, index, reserve):
  446. city = self.get_city(server_id, gate_id, index)
  447. with city.lock():
  448. reserved_time = city.reserved
  449. if reserve and reserved_time and \
  450. time.time()-reserved_time < RESERVE_DC_TIMEOUT:
  451. return False
  452. city.reserve(reserve)
  453. return True
  454. def get_all_users(self, server_id, gate_id, city_id):
  455. """Search for users in layers and its children.
  456. Let's assume wildcard search isn't possible for servers and gates.
  457. A wildcard search happens when the id is zero.
  458. """
  459. assert 0 < server_id, "Invalid server index"
  460. assert 0 < gate_id, "Invalid gate index"
  461. gate = self.get_gate(server_id, gate_id)
  462. users = list(gate.players)
  463. cities = [
  464. self.get_city(server_id, gate_id, city_id)
  465. ] if city_id else self.get_cities(server_id, gate_id)
  466. for city in cities:
  467. users.extend(list(city.players))
  468. return users
  469. def find_users(self, capcom_id="", hunter_name=""):
  470. assert capcom_id or hunter_name, "Search can't be empty"
  471. users = []
  472. for user_id, user_info in self.capcom_ids.items():
  473. session = user_info["session"]
  474. if not session:
  475. continue
  476. if capcom_id and capcom_id not in user_id:
  477. continue
  478. if hunter_name and \
  479. hunter_name.lower() not in user_info["name"].lower():
  480. continue
  481. users.append(session)
  482. return users
  483. def get_user_name(self, capcom_id):
  484. if capcom_id not in self.capcom_ids:
  485. return ""
  486. return self.capcom_ids[capcom_id]["name"]
  487. def create_city(self, session, server_id, gate_id, index,
  488. settings, optional_fields):
  489. city = self.get_city(server_id, gate_id, index)
  490. with city.lock():
  491. city.optional_fields = optional_fields
  492. city.leader = session
  493. city.reserved = None
  494. return city
  495. def join_city(self, session, server_id, gate_id, index):
  496. city = self.get_city(server_id, gate_id, index)
  497. with city.lock():
  498. city.parent.players.remove(session)
  499. city.players.add(session)
  500. session.local_info["city_name"] = city.name
  501. session.local_info["city_id"] = index
  502. return city
  503. def leave_city(self, session):
  504. city = self.get_city(session.local_info["server_id"],
  505. session.local_info["gate_id"],
  506. session.local_info["city_id"])
  507. with city.lock():
  508. city.parent.players.add(session)
  509. city.players.remove(session)
  510. if not city.get_population():
  511. city.reset()
  512. session.local_info["city_id"] = None
  513. session.local_info["city_name"] = None
  514. def layer_detail_search(self, server_type, fields):
  515. cities = []
  516. def match_city(city, fields):
  517. with city.lock():
  518. return all((
  519. field in city.optional_fields
  520. for field in fields
  521. ))
  522. for server in self.servers:
  523. if server.server_type != server_type:
  524. continue
  525. for gate in server.gates:
  526. if not gate.get_population():
  527. continue
  528. cities.extend([
  529. city
  530. for city in gate.cities
  531. if match_city(city, fields)
  532. ])
  533. return cities
  534. def add_friend_request(self, sender_id, recipient_id):
  535. # Friend invite can be sent to arbitrary Capcom ID
  536. if any(cid not in self.capcom_ids
  537. for cid in (sender_id, recipient_id)):
  538. return False
  539. if sender_id not in self.friend_requests[recipient_id]:
  540. self.friend_requests[recipient_id].append(sender_id)
  541. return True
  542. def accept_friend(self, capcom_id, friend_id, accepted):
  543. assert capcom_id in self.capcom_ids and friend_id in self.capcom_ids
  544. # Prevent duplicate if requests were sent both ways
  545. if accepted and friend_id not in self.friend_lists[capcom_id]:
  546. self.friend_lists[capcom_id].append(friend_id)
  547. self.friend_lists[friend_id].append(capcom_id)
  548. if capcom_id in self.friend_requests[friend_id]:
  549. self.friend_requests[friend_id].remove(capcom_id)
  550. if friend_id in self.friend_requests[capcom_id]:
  551. self.friend_requests[capcom_id].remove(friend_id)
  552. return True
  553. def delete_friend(self, capcom_id, friend_id):
  554. assert capcom_id in self.capcom_ids and friend_id in self.capcom_ids
  555. self.friend_lists[capcom_id].remove(friend_id)
  556. # TODO: find footage to see if it's removed in the other friend list
  557. # i.e. self.friend_lists[friend_id].remove(capcom_id)
  558. # AFAICT, there is no NtcFriendDelete packet
  559. return True
  560. def get_friends(self, capcom_id, first_index=None, count=None):
  561. assert capcom_id in self.capcom_ids
  562. begin = 0 if first_index is None else (first_index - 1)
  563. end = count if count is None else (begin + count)
  564. return [
  565. (k, self.capcom_ids[k]["name"])
  566. for k in self.friend_lists[capcom_id]
  567. if k in self.capcom_ids # Skip unknown Capcom IDs
  568. ][begin:end]
  569. class SafeSqliteConnection(object):
  570. """Safer SQLite connection wrapper."""
  571. def __init__(self, *args, **kwargs):
  572. self.__connection = sqlite3.connect(*args, **kwargs)
  573. self.__connection.row_factory = sqlite3.Row
  574. # Avoid "unicode argument without an encoding" error
  575. # Fix python2/3 text_factory = str vs bytes
  576. # NB: data types aren't enforced
  577. self.__connection.text_factory = utils.to_str
  578. def __enter__(self):
  579. return self.__connection.__enter__()
  580. def __exit__(self, type, value, traceback):
  581. return self.__connection.__exit__(type, value, traceback)
  582. def __getattribute__(self, name):
  583. if name.startswith("_"):
  584. return object.__getattribute__(self, name)
  585. return self.__connection.__getattribute__(name)
  586. def __setattr__(self, name, value):
  587. if name.startswith("_"):
  588. return object.__setattr__(self, name, value)
  589. return self.__connection.__setattr__(name, value)
  590. def __del__(self):
  591. self.__connection.close()
  592. class ThreadSafeSqliteConnection(object):
  593. """Proxy object for thread local SQLite connection.
  594. SQLite connection/cursor can't be accessed nor closed from other threads.
  595. However, multiple threads can connect to the same file database.
  596. """
  597. def __init__(self, *args, **kwargs):
  598. self.__args = args
  599. self.__kwargs = kwargs
  600. self.__thread_ns = thread_local()
  601. self.__get_connection()
  602. def __enter__(self):
  603. return self.__get_connection().__enter__()
  604. def __exit__(self, type, value, traceback):
  605. return self.__get_connection().__exit__(type, value, traceback)
  606. def __getattribute__(self, name):
  607. if name.startswith("_"):
  608. return object.__getattribute__(self, name)
  609. return self.__get_connection().__getattribute__(name)
  610. def __setattr__(self, name, value):
  611. if name.startswith("_"):
  612. return object.__setattr__(self, name, value)
  613. return self.__get_connection().__setattr__(name, value)
  614. def __get_connection(self):
  615. this = getattr(self.__thread_ns, "connection", None)
  616. if this is None:
  617. self.__thread_ns.connection = SafeSqliteConnection(*self.__args,
  618. **self.__kwargs)
  619. this = self.__thread_ns.connection
  620. return this
  621. class TempSQLiteDatabase(TempDatabase):
  622. """Hybrid SQLite/TempDatabase.
  623. The following data need to be retained after a shutdown:
  624. - Online support code and its Capcom IDs
  625. - Friend list per Capcom ID
  626. - Properties (at least the hunter name) per Capcom ID
  627. TODO:
  628. - Need proper documentation, especially for the overridable interface
  629. """
  630. DATABASE_NAME = "mh3sp.db"
  631. def __init__(self):
  632. self.parent = super(TempSQLiteDatabase, self)
  633. self.parent.__init__()
  634. self.connection = ThreadSafeSqliteConnection(self.DATABASE_NAME,
  635. timeout=10.0)
  636. self.create_database()
  637. self.populate_database()
  638. def create_database(self):
  639. with self.connection as cursor:
  640. # Create consoles table
  641. #
  642. # TODO:
  643. # - Should we add a "media_version" column to distinguish games?
  644. cursor.execute(
  645. "CREATE TABLE IF NOT EXISTS consoles"
  646. " (support_code TEXT, slot_index INTEGER,"
  647. " capcom_id TEXT, name BLOB)"
  648. )
  649. cursor.execute(
  650. "CREATE UNIQUE INDEX IF NOT EXISTS profiles_uniq_idx"
  651. " ON consoles(support_code, slot_index)"
  652. ) # TODO: Fix support code generation to prevent race condition
  653. cursor.execute(
  654. "CREATE UNIQUE INDEX IF NOT EXISTS capcom_ids_uniq_idx"
  655. " ON consoles(capcom_id)"
  656. ) # TODO: Fix Capcom ID generation to prevent race condition
  657. # Create friend lists table
  658. cursor.execute(
  659. "CREATE TABLE IF NOT EXISTS friend_lists"
  660. " (capcom_id TEXT, friend_id TEXT)"
  661. )
  662. cursor.execute(
  663. "CREATE UNIQUE INDEX IF NOT EXISTS friends_uniq_idx"
  664. " ON friend_lists(capcom_id, friend_id)"
  665. )
  666. def populate_database(self):
  667. with self.connection as cursor:
  668. rows = cursor.execute("SELECT * FROM consoles")
  669. for support_code, slot_index, capcom_id, name in rows:
  670. # Enforcing BLOB type since sometimes it's retrieved as TEXT
  671. name = utils.to_bytes(name)
  672. if support_code not in self.consoles:
  673. self.consoles[support_code] = [BLANK_CAPCOM_ID] * 6
  674. self.consoles[support_code][slot_index - 1] = capcom_id
  675. self.capcom_ids[capcom_id] = {"name": name, "session": None}
  676. self.friend_lists[capcom_id] = []
  677. rows = cursor.execute("SELECT * FROM friend_lists")
  678. for capcom_id, friend_id in rows:
  679. self.friend_lists[capcom_id].append(friend_id)
  680. def force_update(self):
  681. """For debugging purpose."""
  682. with self.connection as cursor:
  683. for support_code, capcom_ids in self.consoles.items():
  684. for slot_index, capcom_id in enumerate(capcom_ids, 1):
  685. info = self.capcom_ids.get(capcom_id, {"name": b""})
  686. cursor.execute(
  687. "INSERT OR IGNORE INTO consoles VALUES (?,?,?,?)",
  688. (support_code, slot_index, capcom_id, info["name"])
  689. )
  690. for capcom_id, friend_ids in self.friend_lists.items():
  691. for friend_id in friend_ids:
  692. cursor.execute(
  693. "INSERT OR IGNORE INTO friend_lists VALUES (?,?)",
  694. (capcom_id, friend_id)
  695. )
  696. def use_user(self, session, index, name):
  697. result = self.parent.use_user(session, index, name)
  698. with self.connection as cursor:
  699. cursor.execute(
  700. "INSERT OR REPLACE INTO consoles VALUES (?,?,?,?)",
  701. (session.online_support_code, index,
  702. session.capcom_id, session.hunter_name)
  703. )
  704. return result
  705. def accept_friend(self, capcom_id, friend_id, accepted):
  706. if accepted:
  707. with self.connection as cursor:
  708. cursor.execute(
  709. "INSERT INTO friend_lists VALUES (?,?)",
  710. (capcom_id, friend_id)
  711. )
  712. cursor.execute(
  713. "INSERT INTO friend_lists VALUES (?,?)",
  714. (friend_id, capcom_id)
  715. )
  716. return self.parent.accept_friend(capcom_id, friend_id, accepted)
  717. def delete_friend(self, capcom_id, friend_id):
  718. with self.connection as cursor:
  719. cursor.execute(
  720. "DELETE FROM friend_lists"
  721. " WHERE capcom_id = ? AND friend_id = ?",
  722. (capcom_id, friend_id)
  723. )
  724. return self.parent.delete_friend(capcom_id, friend_id)
  725. class DebugDatabase(TempSQLiteDatabase):
  726. """For testing purpose."""
  727. def __init__(self, *args, **kwargs):
  728. super(DebugDatabase, self).__init__(*args, **kwargs)
  729. CONSOLES = {
  730. # To use it, replace with a real support code
  731. "TEST_CONSOLE_1": [
  732. "AAAAAA", "BBBBBB", "CCCCCC", "DDDDDD", "EEEEEE", "FFFFFF"
  733. ],
  734. "TEST_CONSOLE_2": [
  735. "111111", "222222", "333333", "444444", "555555", "666666"
  736. ],
  737. }
  738. for key in CONSOLES:
  739. self.consoles.setdefault(key, CONSOLES[key])
  740. CAPCOM_IDS = {
  741. "AAAAAA": {"name": b"Hunt A", "session": None},
  742. "BBBBBB": {"name": b"Hunt B", "session": None},
  743. "CCCCCC": {"name": b"Hunt C", "session": None},
  744. "DDDDDD": {"name": b"Hunt D", "session": None},
  745. "EEEEEE": {"name": b"Hunt E", "session": None},
  746. "FFFFFF": {"name": b"Hunt F", "session": None},
  747. "111111": {"name": b"Hunt 1", "session": None},
  748. "222222": {"name": b"Hunt 2", "session": None},
  749. "333333": {"name": b"Hunt 3", "session": None},
  750. "444444": {"name": b"Hunt 4", "session": None},
  751. "555555": {"name": b"Hunt 5", "session": None},
  752. "666666": {"name": b"Hunt 6", "session": None},
  753. }
  754. for key in CAPCOM_IDS:
  755. self.capcom_ids.setdefault(key, CAPCOM_IDS[key])
  756. FRIEND_REQUESTS = {
  757. "AAAAAA": [],
  758. "BBBBBB": [],
  759. "CCCCCC": [],
  760. "DDDDDD": [],
  761. "EEEEEE": [],
  762. "FFFFFF": [],
  763. "111111": [],
  764. "222222": [],
  765. "333333": [],
  766. "444444": [],
  767. "555555": [],
  768. "666666": [],
  769. }
  770. for key in FRIEND_REQUESTS:
  771. self.friend_requests.setdefault(key, FRIEND_REQUESTS[key])
  772. FRIEND_LISTS = {
  773. "AAAAAA": [],
  774. "BBBBBB": [],
  775. "CCCCCC": [],
  776. "DDDDDD": [],
  777. "EEEEEE": [],
  778. "FFFFFF": [],
  779. "111111": [],
  780. "222222": [],
  781. "333333": [],
  782. "444444": [],
  783. "555555": [],
  784. "666666": [],
  785. }
  786. for key in FRIEND_LISTS:
  787. self.friend_lists.setdefault(key, FRIEND_LISTS[key])
  788. # Hack to force update the database with the debug data once
  789. if hasattr(self, "force_update"):
  790. self.force_update()
  791. CURRENT_DB = TempSQLiteDatabase()
  792. def get_instance():
  793. return CURRENT_DB