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