diff --git a/dndserver/console.py b/dndserver/console.py index ab05c12..bd79465 100644 --- a/dndserver/console.py +++ b/dndserver/console.py @@ -3,6 +3,17 @@ from dndserver.enums.items import ItemType, Rarity, Item as ItemEnum from dndserver.models import Item from dndserver.utils import get_user +from dndserver.matchmaking import virtualServers +from dndserver.protos.Defines import Define_Game + + +def list_servers(): + for difficulty, servers in virtualServers.items(): + print(f"Servers for Difficulty: {Define_Game.DifficultyType.Name(difficulty)}") + for server in servers: + print( + f"IP: {server.ip}:{server.port}, Capacity: {server.slots}, Players: {len(server.players)}" + ) def list_items(filter=None): @@ -54,6 +65,7 @@ def exit(): "/give_item": {"function": give_item, "help": "/give_item [amount]"}, "/list_items": {"function": list_items, "help": "/list_items [filter]"}, "/list_rarity": {"function": list_rarity, "help": "/list_rarity [filter]"}, + "/servers": {"function": list_servers, "help": "/servers - display every server available"}, "/exit": {"function": exit, "help": "/exit"} # add more commands here } diff --git a/dndserver/handlers/lobby.py b/dndserver/handlers/lobby.py index 884babe..6926874 100644 --- a/dndserver/handlers/lobby.py +++ b/dndserver/handlers/lobby.py @@ -3,7 +3,7 @@ from dndserver.models import Character from dndserver.objects.party import Party from dndserver.objects.state import State -from dndserver.persistent import parties, sessions +from dndserver.persistent import parties, sessions, matchmaking_users from dndserver.protos import PacketCommand as pc from dndserver.protos.Account import SC2S_LOBBY_ENTER_REQ, SS2C_LOBBY_ENTER_RES from dndserver.protos.Lobby import ( @@ -16,6 +16,12 @@ SS2C_LOBBY_REGION_SELECT_RES, SS2C_OPEN_LOBBY_MAP_RES, ) +from dndserver.protos.InGame import ( + SC2S_AUTO_MATCH_REG_REQ, + SS2C_AUTO_MATCH_REG_RES, + SS2C_AUTO_MATCH_REG_TEAM_NOT, +) +from dndserver.utils import get_party, get_user, make_header def enter_lobby(ctx, msg): @@ -74,3 +80,24 @@ def open_map_select(ctx, msg): req.ParseFromString(msg) res = SS2C_OPEN_LOBBY_MAP_RES() return res + + +def auto_match(ctx, msg): + """Occurs when the client attempts to find a match""" + req = SC2S_AUTO_MATCH_REG_REQ() + req.ParseFromString(msg) + party = get_party(account_id=sessions[ctx.transport].account.id) + matchteam = SS2C_AUTO_MATCH_REG_TEAM_NOT(result=pc.SUCCESS, mode=req.mode) + if req.mode == SC2S_AUTO_MATCH_REG_REQ.MODE.REGISTER: + matchmaking_users.append({"party": party, "difficulty": req.mode}) + elif req.mode == SC2S_AUTO_MATCH_REG_REQ.MODE.CANCEL: + try: + matchmaking_users.remove({"party": party, "difficulty": req.mode}) + except ValueError: + pass + if len(party.players) > 1: + header = make_header(matchteam) + for user in party.players: + transport, _ = get_user(account_id=user.account.id) + transport.write(header + matchteam.SerializeToString()) + return SS2C_AUTO_MATCH_REG_RES(result=SS2C_AUTO_MATCH_REG_RES.RESULT.SUCCESS) diff --git a/dndserver/matchmaking.py b/dndserver/matchmaking.py new file mode 100644 index 0000000..aec4eb3 --- /dev/null +++ b/dndserver/matchmaking.py @@ -0,0 +1,91 @@ +import time +from enum import Enum +from dndserver.persistent import matchmaking_users +from dndserver.protos.InGame import SS2C_FLOOR_MATCHMAKED_NOT, SS2C_ENTER_GAME_SERVER_NOT +from dndserver.protos.Character import SACCOUNT_NICKNAME +from dndserver.protos.Defines import Define_Game +from dndserver.utils import get_user, make_header + + +class ServerStatus(Enum): + OFF = 0 + STARTED = 1 + + +class Server: + def __init__(self, ip, port, slots, players, status): + self.ip = ip + self.port = port + self.slots = slots + self.players = players + self.status = status + + +virtualServers = { + Define_Game.DifficultyType.NORMAL: [ + Server("127.0.0.1", 7777, 10, [], ServerStatus.STARTED), + Server("127.0.0.1", 10002, 10, [], ServerStatus.STARTED), + Server("127.0.0.1", 10003, 10, [], ServerStatus.STARTED), + ], + Define_Game.DifficultyType.HIGH_ROLLER: [ + Server("127.0.0.1", 7777, 10, [], ServerStatus.STARTED), + Server("127.0.0.1", 20002, 10, [], ServerStatus.STARTED), + Server("127.0.0.1", 20003, 10, [], ServerStatus.STARTED), + ], + Define_Game.DifficultyType.GOBLIN: [ + Server("127.0.0.1", 7777, 10, [], ServerStatus.STARTED), + Server("127.0.0.1", 30002, 10, [], ServerStatus.STARTED), + Server("127.0.0.1", 30003, 10, [], ServerStatus.STARTED), + ], + Define_Game.DifficultyType.RUINS: [ + Server("127.0.0.1", 7777, 10, [], ServerStatus.STARTED), + Server("127.0.0.1", 40002, 10, [], ServerStatus.STARTED), + Server("127.0.0.1", 40003, 10, [], ServerStatus.STARTED), + ], +} + + +def get_available_server(party): + """Gets the available servers, taking into account the party size""" + """and the difficulty (gamemode)""" + playerCount = len(party["party"].players) + availableServers = list( + filter(lambda x: len(x.players) + playerCount <= x.slots and + x.status == ServerStatus.STARTED, virtualServers[party["difficulty"]]) + ) + if len(availableServers) > 0: + return availableServers[0] + else: + return None + + +def matchmaking(): + """Main loop for the matchmaking thread""" + while True: + time.sleep(2) + for party in matchmaking_users: + server = get_available_server(party) + if server is None: + print("Server not found. Searching...") + continue + # We have found a server for the party. Now we need to notify them. + for player in party["party"].players: + transport, _ = get_user(account_id=player.account.id) + match = SS2C_FLOOR_MATCHMAKED_NOT(port=server.port, ip=server.ip, sessionId=str(player.account.id)) + header = make_header(match) + transport.write(header + match.SerializeToString()) + res = SS2C_ENTER_GAME_SERVER_NOT( + port=server.port, + ip=server.ip, + sessionId=str(player.account.id), + accountId=str(player.account.id), + isReconnect=0, + nickName=SACCOUNT_NICKNAME( + originalNickName=player.character.nickname, + streamingModeNickName=player.character.streaming_nickname, + ), + ) + header = make_header(res) + transport.write(header + res.SerializeToString()) + server.players.append(player) + matchmaking_users.remove(party) diff --git a/dndserver/persistent.py b/dndserver/persistent.py index a5b5cfa..c54e1e6 100644 --- a/dndserver/persistent.py +++ b/dndserver/persistent.py @@ -1,2 +1,3 @@ parties = [] sessions = {} +matchmaking_users = [] diff --git a/dndserver/protocol.py b/dndserver/protocol.py index b0eb126..f005786 100644 --- a/dndserver/protocol.py +++ b/dndserver/protocol.py @@ -120,6 +120,7 @@ def dataReceived(self, data: bytes) -> None: pc.C2S_GATHERING_HALL_CHANNEL_SELECT_REQ: gatheringhall.gathering_hall_select_channel, pc.C2S_GATHERING_HALL_CHANNEL_EXIT_REQ: gatheringhall.gathering_hall_channel_exit, pc.C2S_GATHERING_HALL_TARGET_EQUIPPED_ITEM_REQ: gatheringhall.gathering_hall_equip, + pc.C2S_AUTO_MATCH_REG_REQ: lobby.auto_match, } handler = [k for k in handlers.keys() if k == _id] if not handler: diff --git a/dndserver/server.py b/dndserver/server.py index fe4a8ea..14ab973 100644 --- a/dndserver/server.py +++ b/dndserver/server.py @@ -5,10 +5,10 @@ from twisted.internet import reactor import threading - from dndserver.config import config from dndserver.protocol import GameFactory from dndserver.console import console +from dndserver.matchmaking import matchmaking async def main(): @@ -23,6 +23,10 @@ async def main(): tcpFactory = GameFactory() reactor.listenTCP(config.server.port, tcpFactory) + # Start the matchmaking function in a separate thread + matchmaking_thread = threading.Thread(target=matchmaking) + matchmaking_thread.start() + # Start the console function in a separate thread console_thread = threading.Thread(target=console) console_thread.start()