From 85a6b12009e1346a56c1306ae6b7004a76564b14 Mon Sep 17 00:00:00 2001 From: Kevin Barkevich Date: Fri, 10 Mar 2023 16:28:56 -0500 Subject: [PATCH] Added distinction between "database" and server state --- fmp_server.py | 2 +- mh/database.py | 570 +--------------------------------------------- mh/session.py | 56 ++--- mh/state.py | 597 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 633 insertions(+), 592 deletions(-) create mode 100644 mh/state.py diff --git a/fmp_server.py b/fmp_server.py index ee7b5ea..01c31cb 100644 --- a/fmp_server.py +++ b/fmp_server.py @@ -19,7 +19,7 @@ along with this program. If not, see . """ -from mh.database import Players +from mh.state import Players import mh.pat_item as pati from mh.constants import * from mh.pat import PatRequestHandler, PatServer diff --git a/mh/database.py b/mh/database.py index e93b698..4244002 100644 --- a/mh/database.py +++ b/mh/database.py @@ -20,8 +20,6 @@ """ import random -import time -from threading import RLock CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -39,337 +37,6 @@ def new_random_str(length=6): return "".join(random.choice(CHARSET) for _ in range(length)) -class ServerType(object): - OPEN = 1 - ROOKIE = 2 - EXPERT = 3 - RECRUITING = 4 - - -class LayerState(object): - JOINABLE = 0 - EMPTY = 1 - FULL = 2 - - -class Lockable(object): - def __init__(self): - self._lock = RLock() - - def lock(self): - return self - - def __enter__(self): - # Returns True if lock was acquired, False otherwise - return self._lock.acquire() - - def __exit__(self, *args): - self._lock.release() - - -class Players(Lockable): - def __init__(self, capacity): - assert capacity > 0, "Collection capacity can't be zero" - - self.slots = [None for _ in range(capacity)] - self.used = 0 - super(Players, self).__init__() - - def get_used_count(self): - return self.used - - def get_capacity(self): - return len(self.slots) - - def add(self, item): - with self.lock(): - if self.used >= len(self.slots): - return -1 - - item_index = self.index(item) - if item_index != -1: - return item_index - - for i, v in enumerate(self.slots): - if v is not None: - continue - - self.slots[i] = item - self.used += 1 - return i - - return -1 - - def remove(self, item): - assert item is not None, "Item != None" - - with self.lock(): - if self.used < 1: - return False - - if isinstance(item, int): - if item >= self.get_capacity(): - return False - - self.slots[item] = None - self.used -= 1 - return True - - for i, v in enumerate(self.slots): - if v != item: - continue - - self.slots[i] = None - self.used -= 1 - return True - - return False - - def index(self, item): - assert item is not None, "Item != None" - - for i, v in enumerate(self.slots): - if v == item: - return i - - return -1 - - def clear(self): - with self.lock(): - for i in range(self.get_capacity()): - self.slots[i] = None - - def find_first(self, **kwargs): - if self.used < 1: - return None - - for p in self.slots: - if p is None: - continue - - for k, v in kwargs.items(): - if getattr(p, k) != v: - break - else: - return p - - return None - - def find_by_capcom_id(self, capcom_id): - return self.find_first(capcom_id=capcom_id) - - def __len__(self): - return self.used - - def __iter__(self): - if self.used < 1: - raise StopIteration - - for i, v in enumerate(self.slots): - if v is None: - continue - - yield i, v - - -class Circle(Lockable): - def __init__(self, parent): - self.parent = parent - self.leader = None - self.players = Players(4) - self.departed = False - self.quest_id = 0 - self.embarked = False - self.password = None - self.remarks = None - - self.unk_byte_0x0e = 0 - super(Circle, self).__init__() - - def get_population(self): - return len(self.players) - - def get_capacity(self): - return self.players.get_capacity() - - def is_full(self): - return self.get_population() == self.get_capacity() - - def is_empty(self): - return self.leader is None - - def is_joinable(self): - return not self.departed and not self.is_full() - - def has_password(self): - return self.password is not None - - def reset_players(self, capacity): - with self.lock(): - self.players = Players(capacity) - - def reset(self): - with self.lock(): - self.leader = None - self.reset_players(4) - self.departed = False - self.quest_id = 0 - self.embarked = False - self.password = None - self.remarks = None - - self.unk_byte_0x0e = 0 - - -class City(Lockable): - LAYER_DEPTH = 3 - - def __init__(self, name, parent): - self.name = name - self.parent = parent - self.state = LayerState.EMPTY - self.players = Players(4) - self.optional_fields = [] - self.leader = None - self.reserved = None - self.circles = [ - # One circle per player - Circle(self) for _ in range(self.get_capacity()) - ] - super(City, self).__init__() - - def get_population(self): - return len(self.players) - - def in_quest_players(self): - return sum(p.is_in_quest() for _, p in self.players) - - def get_capacity(self): - return self.players.get_capacity() - - def get_state(self): - size = self.get_population() - if size == 0: - return LayerState.EMPTY - elif size < self.get_capacity(): - return LayerState.JOINABLE - else: - return LayerState.FULL - - def get_pathname(self): - pathname = self.name - it = self.parent - while it is not None: - pathname = it.name + "\t" + pathname - it = it.parent - return pathname - - def get_first_empty_circle(self): - with self.lock(): - for index, circle in enumerate(self.circles): - if circle.is_empty(): - return circle, index - return None, None - - def get_circle_for(self, leader_session): - with self.lock(): - for index, circle in enumerate(self.circles): - if circle.leader == leader_session: - return circle, index - return None, None - - def clear_circles(self): - with self.lock(): - for circle in self.circles: - circle.reset() - - def reserve(self, reserve): - with self.lock(): - if reserve: - self.reserved = time.time() - else: - self.reserved = None - - -class Gate(object): - LAYER_DEPTH = 2 - - def __init__(self, name, parent, city_count=40, player_capacity=100): - self.name = name - self.parent = parent - self.state = LayerState.EMPTY - self.cities = [ - City("City{}".format(i), self) - for i in range(1, city_count+1) - ] - self.players = Players(player_capacity) - self.optional_fields = [] - - def get_population(self): - return len(self.players) + sum(( - city.get_population() - for city in self.cities - )) - - def get_capacity(self): - return self.players.get_capacity() - - def get_state(self): - size = self.get_population() - if size == 0: - return LayerState.EMPTY - elif size < self.get_capacity(): - return LayerState.JOINABLE - else: - return LayerState.FULL - - -class Server(object): - LAYER_DEPTH = 1 - - def __init__(self, name, server_type, gate_count=40, capacity=2000, - addr=None, port=None): - self.name = name - self.parent = None - self.server_type = server_type - self.addr = addr - self.port = port - self.gates = [ - Gate("City Gate{}".format(i), self) - for i in range(1, gate_count+1) - ] - self.players = Players(capacity) - - def get_population(self): - return len(self.players) + sum(( - gate.get_population() for gate in self.gates - )) - - def get_capacity(self): - return self.players.get_capacity() - - -def new_servers(): - servers = [] - servers.extend([ - Server("Valor{}".format(i), ServerType.OPEN) - for i in range(1, 5) - ]) - servers.extend([ - Server("Beginners{}".format(i), ServerType.ROOKIE) - for i in range(1, 3) - ]) - servers.extend([ - Server("Veterans{}".format(i), ServerType.EXPERT) - for i in range(1, 3) - ]) - servers.extend([ - Server("Greed{}".format(i), ServerType.RECRUITING) - for i in range(1, 5) - ]) - return servers - - class TempDatabase(object): """A temporary database. @@ -382,13 +49,6 @@ def __init__(self): self.consoles = { # Online support code => Capcom IDs } - self.sessions = { - # PAT Ticket => Owner's session - } - self.capcom_ids = { - # Capcom ID => Owner's session - } - self.servers = new_servers() def get_support_code(self, session): """Get the online support code or create one.""" @@ -408,231 +68,13 @@ def get_support_code(self, session): ] return support_code - def new_pat_ticket(self, session): - """Generates a new PAT ticket for the session.""" - while True: - session.pat_ticket = new_random_str(11) - if session.pat_ticket not in self.sessions: - break - self.sessions[session.pat_ticket] = session - return session.pat_ticket - - def use_capcom_id(self, session, capcom_id, name=None): - """Attach the session to the Capcom ID.""" - assert capcom_id in self.capcom_ids, "Capcom ID doesn't exist" - - not_in_use = self.capcom_ids[capcom_id]["session"] is None - assert not_in_use, "Capcom ID is already in use" - - name = name or self.capcom_ids[capcom_id]["name"] - self.capcom_ids[capcom_id] = {"name": name, "session": session} - return name - - def use_user(self, session, index, name): - """Use User from the slot or create one if empty""" - assert 1 <= index <= 6, "Invalid Capcom ID slot" - index -= 1 - users = self.consoles[session.online_support_code] - while users[index] == "******": - capcom_id = new_random_str(6) - if capcom_id not in self.capcom_ids: - self.capcom_ids[capcom_id] = {"name": name, "session": None} - users[index] = capcom_id - break - else: - capcom_id = users[index] - name = self.use_capcom_id(session, capcom_id, name) - session.capcom_id = capcom_id - session.hunter_name = name - - def get_session(self, pat_ticket): - """Returns existing PAT session or None.""" - session = self.sessions.get(pat_ticket) - if session and session.capcom_id: - self.use_capcom_id(session, session.capcom_id, session.hunter_name) - return session - - def disconnect_session(self, session): - """Detach the session from its Capcom ID.""" - if not session.capcom_id: - # Capcom ID isn't chosen yet with OPN/LMP servers - return - self.capcom_ids[session.capcom_id]["session"] = None - - def delete_session(self, session): - """Delete the session from the database.""" - self.disconnect_session(session) - pat_ticket = session.pat_ticket - if pat_ticket in self.sessions: - del self.sessions[pat_ticket] - - def get_users(self, session, first_index, count): - """Returns Capcom IDs tied to the session.""" - users = self.consoles[session.online_support_code] - capcom_ids = [ - (i, (capcom_id, self.capcom_ids.get(capcom_id, {}))) - for i, capcom_id in enumerate(users[:count], first_index) - ] - size = len(capcom_ids) - if size < count: - capcom_ids.extend([ - (index, ("******", {})) - for index in range(first_index+size, first_index+count) - ]) - return capcom_ids - - def join_server(self, session, index): - if session.local_info["server_id"] is not None: - self.leave_server(session, session.local_info["server_id"]) - server = self.get_server(index) - server.players.add(session) - session.local_info["server_id"] = index - session.local_info["server_name"] = server.name - return server - - def leave_server(self, session, index): - self.get_server(index).players.remove(session) - session.local_info["server_id"] = None - session.local_info["server_name"] = None - - def get_server_time(self): - pass - - def get_game_time(self): - pass - - def get_servers(self): - return self.servers - - def get_server(self, index): - assert 0 < index <= len(self.servers), "Invalid server index" - return self.servers[index - 1] - - def get_gates(self, server_id): - return self.get_server(server_id).gates - - def get_gate(self, server_id, index): - gates = self.get_gates(server_id) - assert 0 < index <= len(gates), "Invalid gate index" - return gates[index - 1] - - def join_gate(self, session, server_id, index): - gate = self.get_gate(server_id, index) - gate.parent.players.remove(session) - gate.players.add(session) - session.local_info["gate_id"] = index - session.local_info["gate_name"] = gate.name - return gate - - def leave_gate(self, session): - gate = self.get_gate(session.local_info["server_id"], - session.local_info["gate_id"]) - gate.parent.players.add(session) - gate.players.remove(session) - session.local_info["gate_id"] = None - session.local_info["gate_name"] = None - - def get_cities(self, server_id, gate_id): - return self.get_gate(server_id, gate_id).cities - - def get_city(self, server_id, gate_id, index): - cities = self.get_cities(server_id, gate_id) - assert 0 < index <= len(cities), "Invalid city index" - return cities[index - 1] - - def reserve_city(self, server_id, gate_id, index, reserve): - city = self.get_city(server_id, gate_id, index) - with city.lock(): - reserved_time = city.reserved - if reserve and reserved_time and \ - time.time()-reserved_time < RESERVE_DC_TIMEOUT: - return False - city.reserve(reserve) - return True - - def get_all_users(self, server_id, gate_id, city_id): - """Search for users in layers and its children. - - Let's assume wildcard search isn't possible for servers and gates. - A wildcard search happens when the id is zero. - """ - assert 0 < server_id, "Invalid server index" - assert 0 < gate_id, "Invalid gate index" - gate = self.get_gate(server_id, gate_id) - users = list(gate.players) - cities = [ - self.get_city(server_id, gate_id, city_id) - ] if city_id else self.get_cities(server_id, gate_id) - for city in cities: - users.extend(list(city.players)) - return users - - def find_users(self, capcom_id="", hunter_name=""): - assert capcom_id or hunter_name, "Search can't be empty" - users = [] - for user_id, user_info in self.capcom_ids.items(): - session = user_info["session"] - if not session: - continue - if capcom_id and capcom_id not in user_id: - continue - if hunter_name and \ - hunter_name.lower() not in user_info["name"].lower(): - continue - users.append(session) - return users - - def create_city(self, session, server_id, gate_id, index, - settings, optional_fields): - city = self.get_city(server_id, gate_id, index) - with city.lock(): - city.optional_fields = optional_fields - city.leader = session - return city - - def join_city(self, session, server_id, gate_id, index): - city = self.get_city(server_id, gate_id, index) - with city.lock(): - city.parent.players.remove(session) - city.players.add(session) - session.local_info["city_name"] = city.name - session.local_info["city_id"] = index - return city - - def leave_city(self, session): - city = self.get_city(session.local_info["server_id"], - session.local_info["gate_id"], - session.local_info["city_id"]) - with city.lock(): - city.parent.players.add(session) - city.players.remove(session) - if not city.get_population(): - city.clear_circles() - session.local_info["city_id"] = None - session.local_info["city_name"] = None - - def layer_detail_search(self, server_type, fields): - cities = [] - - def match_city(city, fields): - with city.lock(): - return all(( - field in city.optional_fields - for field in fields - )) + def get_capcom_ids(self, online_support_code): + """Get the Capcom IDs associated with an online support code.""" + return self.consoles[online_support_code] - for server in self.servers: - if server.server_type != server_type: - continue - for gate in server.gates: - if not gate.get_population(): - continue - cities.extend([ - city - for city in gate.cities - if match_city(city, fields) - ]) - return cities + def assign_capcom_id(self, online_support_code, index, capcom_id): + """Assign a Capcom ID to an online support code.""" + self.consoles[online_support_code][index] = capcom_id CURRENT_DB = TempDatabase() diff --git a/mh/session.py b/mh/session.py index 41c0d9b..ae7bd38 100644 --- a/mh/session.py +++ b/mh/session.py @@ -22,11 +22,13 @@ import struct import mh.database as db +import mh.state as state import mh.pat_item as pati from other.utils import to_bytearray, to_str DB = db.get_instance() +STATE = state.get_instance() class SessionState: @@ -81,7 +83,7 @@ def get(self, connection_data): self.online_support_code = to_str( pati.unpack_string(connection_data.online_support_code) ) - session = DB.get_session(self.pat_ticket) or self + session = STATE.get_session(self.pat_ticket) or self if session != self: assert session.connection is None, "Session is already in use" session.connection = self.connection @@ -106,7 +108,7 @@ def disconnect(self): """ self.layer_end() self.connection = None - DB.disconnect_session(self) + STATE.disconnect_session(self) def delete(self): """Delete the current session. @@ -116,37 +118,37 @@ def delete(self): - We should probably create a SessionManager thread per server. """ if not self.request_reconnection: - DB.delete_session(self) + STATE.delete_session(self) def is_jap(self): """TODO: Heuristic using the connection data to detect region.""" pass def new_pat_ticket(self): - DB.new_pat_ticket(self) + STATE.new_pat_ticket(self) return to_bytearray(self.pat_ticket) def get_users(self, first_index, count): - return DB.get_users(self, first_index, count) + return STATE.get_users(self, first_index, count) def use_user(self, index, name): - DB.use_user(self, index, name) + STATE.use_user(self, index, name) def get_servers(self): - return DB.get_servers() + return STATE.get_servers() def get_server(self): assert self.local_info['server_id'] is not None - return DB.get_server(self.local_info['server_id']) + return STATE.get_server(self.local_info['server_id']) def get_gate(self): assert self.local_info['gate_id'] is not None - return DB.get_gate(self.local_info['server_id'], + return STATE.get_gate(self.local_info['server_id'], self.local_info['gate_id']) def get_city(self): assert self.local_info['city_id'] is not None - return DB.get_city(self.local_info['server_id'], + return STATE.get_city(self.local_info['server_id'], self.local_info['gate_id'], self.local_info['city_id']) @@ -209,10 +211,10 @@ def layer_detail_search(self, detailed_fields): (field_id, value) for field_id, field_type, value in detailed_fields ] # Convert detailed to simple optional fields - return DB.layer_detail_search(server_type, fields) + return STATE.layer_detail_search(server_type, fields) def join_server(self, server_id): - return DB.join_server(self, server_id) + return STATE.join_server(self, server_id) def get_layer_children(self): if self.layer == 0: @@ -231,60 +233,60 @@ def get_layer_sibling(self): def find_users_by_layer(self, server_id, gate_id, city_id, first_index, count, recursive=False): if recursive: - players = DB.get_all_users(server_id, gate_id, city_id) + players = STATE.get_all_users(server_id, gate_id, city_id) else: layer = \ - DB.get_city(server_id, gate_id, city_id) if city_id else \ - DB.get_gate(server_id, gate_id) if gate_id else \ - DB.get_server(server_id) + STATE.get_city(server_id, gate_id, city_id) if city_id else \ + STATE.get_gate(server_id, gate_id) if gate_id else \ + STATE.get_server(server_id) players = list(layer.players) start = first_index - 1 return players[start:start+count] def find_users(self, capcom_id, hunter_name, first_index, count): - users = DB.find_users(capcom_id, hunter_name) + users = STATE.find_users(capcom_id, hunter_name) start = first_index - 1 return users[start:start+count] def leave_server(self): - DB.leave_server(self, self.local_info["server_id"]) + STATE.leave_server(self, self.local_info["server_id"]) def get_gates(self): - return DB.get_gates(self.local_info["server_id"]) + return STATE.get_gates(self.local_info["server_id"]) def join_gate(self, gate_id): - DB.join_gate(self, self.local_info["server_id"], gate_id) + STATE.join_gate(self, self.local_info["server_id"], gate_id) self.state = SessionState.GATE def leave_gate(self): - DB.leave_gate(self) + STATE.leave_gate(self) self.state = SessionState.LOG_IN def get_cities(self): - return DB.get_cities(self.local_info["server_id"], + return STATE.get_cities(self.local_info["server_id"], self.local_info["gate_id"]) def is_city_empty(self, city_id): - return DB.get_city(self.local_info["server_id"], self.local_info["gate_id"], city_id).get_state() == db.LayerState.EMPTY + return STATE.get_city(self.local_info["server_id"], self.local_info["gate_id"], city_id).get_state() == state.LayerState.EMPTY def reserve_city(self, city_id, reserve): - return DB.reserve_city(self.local_info["server_id"], self.local_info["gate_id"], city_id, reserve) + return STATE.reserve_city(self.local_info["server_id"], self.local_info["gate_id"], city_id, reserve) def create_city(self, city_id, settings, optional_fields): - return DB.create_city(self, + return STATE.create_city(self, self.local_info["server_id"], self.local_info["gate_id"], city_id, settings, optional_fields) def join_city(self, city_id): - DB.join_city(self, + STATE.join_city(self, self.local_info["server_id"], self.local_info["gate_id"], city_id) self.state = SessionState.CITY def leave_city(self): - DB.leave_city(self) + STATE.leave_city(self) self.state = SessionState.GATE def try_transfer_city_leadership(self): diff --git a/mh/state.py b/mh/state.py new file mode 100644 index 0000000..8b7717e --- /dev/null +++ b/mh/state.py @@ -0,0 +1,597 @@ +"""Monster Hunter state module. + + Monster Hunter 3 Server Project + Copyright (C) 2023 Sepalani, Ze SpyRo + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +""" + + +from mh import database +import time +from threading import RLock + +class ServerType(object): + OPEN = 1 + ROOKIE = 2 + EXPERT = 3 + RECRUITING = 4 + + +class LayerState(object): + JOINABLE = 0 + EMPTY = 1 + FULL = 2 + + +class Lockable(object): + def __init__(self): + self._lock = RLock() + + def lock(self): + return self + + def __enter__(self): + # Returns True if lock was acquired, False otherwise + return self._lock.acquire() + + def __exit__(self, *args): + self._lock.release() + + +class Players(Lockable): + def __init__(self, capacity): + assert capacity > 0, "Collection capacity can't be zero" + + self.slots = [None for _ in range(capacity)] + self.used = 0 + super(Players, self).__init__() + + def get_used_count(self): + return self.used + + def get_capacity(self): + return len(self.slots) + + def add(self, item): + with self.lock(): + if self.used >= len(self.slots): + return -1 + + item_index = self.index(item) + if item_index != -1: + return item_index + + for i, v in enumerate(self.slots): + if v is not None: + continue + + self.slots[i] = item + self.used += 1 + return i + + return -1 + + def remove(self, item): + assert item is not None, "Item != None" + + with self.lock(): + if self.used < 1: + return False + + if isinstance(item, int): + if item >= self.get_capacity(): + return False + + self.slots[item] = None + self.used -= 1 + return True + + for i, v in enumerate(self.slots): + if v != item: + continue + + self.slots[i] = None + self.used -= 1 + return True + + return False + + def index(self, item): + assert item is not None, "Item != None" + + for i, v in enumerate(self.slots): + if v == item: + return i + + return -1 + + def clear(self): + with self.lock(): + for i in range(self.get_capacity()): + self.slots[i] = None + + def find_first(self, **kwargs): + if self.used < 1: + return None + + for p in self.slots: + if p is None: + continue + + for k, v in kwargs.items(): + if getattr(p, k) != v: + break + else: + return p + + return None + + def find_by_capcom_id(self, capcom_id): + return self.find_first(capcom_id=capcom_id) + + def __len__(self): + return self.used + + def __iter__(self): + if self.used < 1: + raise StopIteration + + for i, v in enumerate(self.slots): + if v is None: + continue + + yield i, v + + +class Circle(Lockable): + def __init__(self, parent): + self.parent = parent + self.leader = None + self.players = Players(4) + self.departed = False + self.quest_id = 0 + self.embarked = False + self.password = None + self.remarks = None + + self.unk_byte_0x0e = 0 + super(Circle, self).__init__() + + def get_population(self): + return len(self.players) + + def get_capacity(self): + return self.players.get_capacity() + + def is_full(self): + return self.get_population() == self.get_capacity() + + def is_empty(self): + return self.leader is None + + def is_joinable(self): + return not self.departed and not self.is_full() + + def has_password(self): + return self.password is not None + + def reset_players(self, capacity): + with self.lock(): + self.players = Players(capacity) + + def reset(self): + with self.lock(): + self.leader = None + self.reset_players(4) + self.departed = False + self.quest_id = 0 + self.embarked = False + self.password = None + self.remarks = None + + self.unk_byte_0x0e = 0 + + +class City(Lockable): + LAYER_DEPTH = 3 + + def __init__(self, name, parent): + self.name = name + self.parent = parent + self.state = LayerState.EMPTY + self.players = Players(4) + self.optional_fields = [] + self.leader = None + self.reserved = None + self.circles = [ + # One circle per player + Circle(self) for _ in range(self.get_capacity()) + ] + super(City, self).__init__() + + def get_population(self): + return len(self.players) + + def in_quest_players(self): + return sum(p.is_in_quest() for _, p in self.players) + + def get_capacity(self): + return self.players.get_capacity() + + def get_state(self): + size = self.get_population() + if size == 0: + return LayerState.EMPTY + elif size < self.get_capacity(): + return LayerState.JOINABLE + else: + return LayerState.FULL + + def get_pathname(self): + pathname = self.name + it = self.parent + while it is not None: + pathname = it.name + "\t" + pathname + it = it.parent + return pathname + + def get_first_empty_circle(self): + with self.lock(): + for index, circle in enumerate(self.circles): + if circle.is_empty(): + return circle, index + return None, None + + def get_circle_for(self, leader_session): + with self.lock(): + for index, circle in enumerate(self.circles): + if circle.leader == leader_session: + return circle, index + return None, None + + def clear_circles(self): + with self.lock(): + for circle in self.circles: + circle.reset() + + def reserve(self, reserve): + with self.lock(): + if reserve: + self.reserved = time.time() + else: + self.reserved = None + + +class Gate(object): + LAYER_DEPTH = 2 + + def __init__(self, name, parent, city_count=40, player_capacity=100): + self.name = name + self.parent = parent + self.state = LayerState.EMPTY + self.cities = [ + City("City{}".format(i), self) + for i in range(1, city_count+1) + ] + self.players = Players(player_capacity) + self.optional_fields = [] + + def get_population(self): + return len(self.players) + sum(( + city.get_population() + for city in self.cities + )) + + def get_capacity(self): + return self.players.get_capacity() + + def get_state(self): + size = self.get_population() + if size == 0: + return LayerState.EMPTY + elif size < self.get_capacity(): + return LayerState.JOINABLE + else: + return LayerState.FULL + + +class Server(object): + LAYER_DEPTH = 1 + + def __init__(self, name, server_type, gate_count=40, capacity=2000, + addr=None, port=None): + self.name = name + self.parent = None + self.server_type = server_type + self.addr = addr + self.port = port + self.gates = [ + Gate("City Gate{}".format(i), self) + for i in range(1, gate_count+1) + ] + self.players = Players(capacity) + + def get_population(self): + return len(self.players) + sum(( + gate.get_population() for gate in self.gates + )) + + def get_capacity(self): + return self.players.get_capacity() + + +def new_servers(): + servers = [] + servers.extend([ + Server("Valor{}".format(i), ServerType.OPEN) + for i in range(1, 5) + ]) + servers.extend([ + Server("Beginners{}".format(i), ServerType.ROOKIE) + for i in range(1, 3) + ]) + servers.extend([ + Server("Veterans{}".format(i), ServerType.EXPERT) + for i in range(1, 3) + ]) + servers.extend([ + Server("Greed{}".format(i), ServerType.RECRUITING) + for i in range(1, 5) + ]) + return servers + + +class State(object): + def __init__(self): + self.sessions = { + # PAT Ticket => Owner's session + } + self.capcom_ids = { + # Capcom ID => Owner's session + } + self.servers = new_servers() + + def new_pat_ticket(self, session): + """Generates a new PAT ticket for the session.""" + while True: + session.pat_ticket = database.new_random_str(11) + if session.pat_ticket not in self.sessions: + break + self.sessions[session.pat_ticket] = session + return session.pat_ticket + + def use_capcom_id(self, session, capcom_id, name=None): + """Attach the session to the Capcom ID.""" + assert capcom_id in self.capcom_ids, "Capcom ID doesn't exist" + + not_in_use = self.capcom_ids[capcom_id]["session"] is None + assert not_in_use, "Capcom ID is already in use" + + name = name or self.capcom_ids[capcom_id]["name"] + self.capcom_ids[capcom_id] = {"name": name, "session": session} + return name + + def use_user(self, session, index, name): + """Use User from the slot or create one if empty""" + assert 1 <= index <= 6, "Invalid Capcom ID slot" + index -= 1 + users = database.get_instance().get_capcom_ids(session.online_support_code) + while users[index] == "******": + capcom_id = database.new_random_str(6) + if capcom_id not in self.capcom_ids: + self.capcom_ids[capcom_id] = {"name": name, "session": None} + database.get_instance().assign_capcom_id(session.online_support_code, index, capcom_id) + break + else: + capcom_id = users[index] + name = self.use_capcom_id(session, capcom_id, name) + session.capcom_id = capcom_id + session.hunter_name = name + + def get_session(self, pat_ticket): + """Returns existing PAT session or None.""" + session = self.sessions.get(pat_ticket) + if session and session.capcom_id: + self.use_capcom_id(session, session.capcom_id, session.hunter_name) + return session + + def disconnect_session(self, session): + """Detach the session from its Capcom ID.""" + if not session.capcom_id: + # Capcom ID isn't chosen yet with OPN/LMP servers + return + self.capcom_ids[session.capcom_id]["session"] = None + + def delete_session(self, session): + """Delete the session from the database.""" + self.disconnect_session(session) + pat_ticket = session.pat_ticket + if pat_ticket in self.sessions: + del self.sessions[pat_ticket] + + def get_users(self, session, first_index, count): + """Returns Capcom IDs tied to the session.""" + users = database.get_instance().get_capcom_ids(session.online_support_code) + capcom_ids = [ + (i, (capcom_id, self.capcom_ids.get(capcom_id, {}))) + for i, capcom_id in enumerate(users[:count], first_index) + ] + size = len(capcom_ids) + if size < count: + capcom_ids.extend([ + (index, ("******", {})) + for index in range(first_index+size, first_index+count) + ]) + return capcom_ids + + def join_server(self, session, index): + if session.local_info["server_id"] is not None: + self.leave_server(session, session.local_info["server_id"]) + server = self.get_server(index) + server.players.add(session) + session.local_info["server_id"] = index + session.local_info["server_name"] = server.name + return server + + def leave_server(self, session, index): + self.get_server(index).players.remove(session) + session.local_info["server_id"] = None + session.local_info["server_name"] = None + + def get_server_time(self): + pass + + def get_game_time(self): + pass + + def get_servers(self): + return self.servers + + def get_server(self, index): + assert 0 < index <= len(self.servers), "Invalid server index" + return self.servers[index - 1] + + def get_gates(self, server_id): + return self.get_server(server_id).gates + + def get_gate(self, server_id, index): + gates = self.get_gates(server_id) + assert 0 < index <= len(gates), "Invalid gate index" + return gates[index - 1] + + def join_gate(self, session, server_id, index): + gate = self.get_gate(server_id, index) + gate.parent.players.remove(session) + gate.players.add(session) + session.local_info["gate_id"] = index + session.local_info["gate_name"] = gate.name + return gate + + def leave_gate(self, session): + gate = self.get_gate(session.local_info["server_id"], + session.local_info["gate_id"]) + gate.parent.players.add(session) + gate.players.remove(session) + session.local_info["gate_id"] = None + session.local_info["gate_name"] = None + + def get_cities(self, server_id, gate_id): + return self.get_gate(server_id, gate_id).cities + + def get_city(self, server_id, gate_id, index): + cities = self.get_cities(server_id, gate_id) + assert 0 < index <= len(cities), "Invalid city index" + return cities[index - 1] + + def reserve_city(self, server_id, gate_id, index, reserve): + city = self.get_city(server_id, gate_id, index) + with city.lock(): + reserved_time = city.reserved + if reserve and reserved_time and \ + time.time()-reserved_time < RESERVE_DC_TIMEOUT: + return False + city.reserve(reserve) + return True + + def get_all_users(self, server_id, gate_id, city_id): + """Search for users in layers and its children. + + Let's assume wildcard search isn't possible for servers and gates. + A wildcard search happens when the id is zero. + """ + assert 0 < server_id, "Invalid server index" + assert 0 < gate_id, "Invalid gate index" + gate = self.get_gate(server_id, gate_id) + users = list(gate.players) + cities = [ + self.get_city(server_id, gate_id, city_id) + ] if city_id else self.get_cities(server_id, gate_id) + for city in cities: + users.extend(list(city.players)) + return users + + def find_users(self, capcom_id="", hunter_name=""): + assert capcom_id or hunter_name, "Search can't be empty" + users = [] + for user_id, user_info in self.capcom_ids.items(): + session = user_info["session"] + if not session: + continue + if capcom_id and capcom_id not in user_id: + continue + if hunter_name and \ + hunter_name.lower() not in user_info["name"].lower(): + continue + users.append(session) + return users + + def create_city(self, session, server_id, gate_id, index, + settings, optional_fields): + city = self.get_city(server_id, gate_id, index) + with city.lock(): + city.optional_fields = optional_fields + city.leader = session + return city + + def join_city(self, session, server_id, gate_id, index): + city = self.get_city(server_id, gate_id, index) + with city.lock(): + city.parent.players.remove(session) + city.players.add(session) + session.local_info["city_name"] = city.name + session.local_info["city_id"] = index + return city + + def leave_city(self, session): + city = self.get_city(session.local_info["server_id"], + session.local_info["gate_id"], + session.local_info["city_id"]) + with city.lock(): + city.parent.players.add(session) + city.players.remove(session) + if not city.get_population(): + city.clear_circles() + session.local_info["city_id"] = None + session.local_info["city_name"] = None + + def layer_detail_search(self, server_type, fields): + cities = [] + + def match_city(city, fields): + with city.lock(): + return all(( + field in city.optional_fields + for field in fields + )) + + for server in self.servers: + if server.server_type != server_type: + continue + for gate in server.gates: + if not gate.get_population(): + continue + cities.extend([ + city + for city in gate.cities + if match_city(city, fields) + ]) + return cities + + +CURRENT_STATE = State() + + +def get_instance(): + return CURRENT_STATE