88from collections import defaultdict
99from typing import Dict , List , Optional , Set , Tuple
1010
11+ import aio_pika
1112import aiocron
1213from sqlalchemy import and_ , func , select , text , true
1314
3031 matchmaker_queue_map_pool
3132)
3233from .decorators import with_logger
34+ from .factions import Faction
3335from .game_service import GameService
3436from .games import Game , InitMode , LadderGame
3537from .matchmaker import MapPool , MatchmakerQueue , OnMatchedCallback , Search
38+ from .message_queue_service import MessageQueueService
39+ from .player_service import PlayerService
3640from .players import Player , PlayerState
3741from .protocol import DisconnectedError
3842from .types import GameLaunchOptions , Map , NeroxisGeneratedMap
@@ -55,17 +59,35 @@ def __init__(
5559 self ,
5660 database : FAFDatabase ,
5761 game_service : GameService ,
62+ player_service : PlayerService ,
63+ message_queue_service : MessageQueueService
5864 ):
5965 self ._db = database
6066 self ._informed_players : Set [Player ] = set ()
6167 self .game_service = game_service
68+ self .player_service = player_service
69+ self .message_queue_service = message_queue_service
6270 self .queues = {}
71+ self ._initialized = False
6372
6473 self ._searches : Dict [Player , Dict [str , Search ]] = defaultdict (dict )
6574
6675 async def initialize (self ) -> None :
76+ if self ._initialized :
77+ return
78+
6779 await self .update_data ()
80+ await self .message_queue_service .declare_exchange (
81+ config .MQ_EXCHANGE_NAME
82+ )
83+ await self .message_queue_service .consume (
84+ config .MQ_EXCHANGE_NAME ,
85+ "request.match.create" ,
86+ self .handle_mq_matchmaking_request
87+ )
88+
6889 self ._update_cron = aiocron .crontab ("*/10 * * * *" , func = self .update_data )
90+ self ._initialized = True
6991
7092 async def update_data (self ) -> None :
7193 async with self ._db .acquire () as conn :
@@ -325,6 +347,137 @@ def write_rating_progress(self, player: Player, rating_type: str) -> None:
325347 )
326348 })
327349
350+ async def handle_mq_matchmaking_request (
351+ self ,
352+ message : aio_pika .IncomingMessage
353+ ):
354+ try :
355+ game = await self ._handle_mq_matchmaking_request (message )
356+ except Exception as e :
357+ if isinstance (e , GameLaunchError ):
358+ code = "launch_failed"
359+ args = [{"player_id" : player .id } for player in e .players ]
360+ elif isinstance (e , json .JSONDecodeError ):
361+ code = "malformed_request"
362+ args = [{"message" : str (e )}]
363+ elif isinstance (e , KeyError ):
364+ code = "malformed_request"
365+ args = [{"message" : f"missing { e .args [0 ]} " }]
366+ else :
367+ code = e .args [0 ]
368+ args = e .args [1 :]
369+
370+ await self .message_queue_service .publish (
371+ config .MQ_EXCHANGE_NAME ,
372+ "error.match.create" ,
373+ {"error_code" : code , "args" : args },
374+ correlation_id = message .correlation_id
375+ )
376+ else :
377+ await self .message_queue_service .publish (
378+ config .MQ_EXCHANGE_NAME ,
379+ "success.match.create" ,
380+ {"game_id" : game .id },
381+ correlation_id = message .correlation_id
382+ )
383+
384+ async def _handle_mq_matchmaking_request (
385+ self ,
386+ message : aio_pika .IncomingMessage
387+ ):
388+ self ._logger .debug (
389+ "Got matchmaking request: %s" , message .correlation_id
390+ )
391+ request = json .loads (message .body )
392+ # TODO: Use id instead of name?
393+ queue_name = request .get ("matchmaker_queue" )
394+ map_name = request ["map_name" ]
395+ game_name = request ["game_name" ]
396+ participants = request ["participants" ]
397+ if queue_name :
398+ featured_mod = request .get ("featured_mod" )
399+ else :
400+ featured_mod = request ["featured_mod" ]
401+
402+ if queue_name and queue_name not in self .queues :
403+ raise Exception ("invalid_request" , "invalid queue" )
404+
405+ if not participants :
406+ raise Exception ("invalid_request" , "empty participants" )
407+
408+ player_ids = [participant ["player_id" ] for participant in participants ]
409+ missing_players = [
410+ id for id in player_ids if self .player_service [id ] is None
411+ ]
412+ if missing_players :
413+ raise Exception (
414+ "players_not_found" ,
415+ * [{"player_id" : id } for id in missing_players ]
416+ )
417+
418+ all_players = [
419+ self .player_service [player_id ] for player_id in player_ids
420+ ]
421+ non_idle_players = [
422+ player for player in all_players
423+ if player .state != PlayerState .IDLE
424+ ]
425+ if non_idle_players :
426+ raise Exception (
427+ "invalid_state" ,
428+ [
429+ {"player_id" : player .id , "state" : player .state .name }
430+ for player in all_players
431+ ]
432+ )
433+
434+ queue = self .queues [queue_name ] if queue_name else None
435+ featured_mod = featured_mod or queue .featured_mod
436+ host = all_players [0 ]
437+ guests = all_players [1 :]
438+
439+ for player in all_players :
440+ player .state = PlayerState .STARTING_AUTOMATCH
441+
442+ try :
443+ game = self .game_service .create_game (
444+ game_class = LadderGame ,
445+ game_mode = featured_mod ,
446+ host = host ,
447+ name = "Matchmaker Game" ,
448+ mapname = map_name ,
449+ matchmaker_queue_id = queue .id if queue else None ,
450+ rating_type = queue .rating_type if queue else None ,
451+ max_players = len (participants )
452+ )
453+ game .init_mode = InitMode .AUTO_LOBBY
454+ game .set_name_unchecked (game_name )
455+
456+ for participant in participants :
457+ player_id = participant ["player_id" ]
458+ faction = Faction .from_value (participant ["faction" ])
459+ team = participant ["team" ]
460+ slot = participant ["slot" ]
461+
462+ game .set_player_option (player_id , "Faction" , faction .value )
463+ game .set_player_option (player_id , "Team" , team )
464+ game .set_player_option (player_id , "StartSpot" , slot )
465+ game .set_player_option (player_id , "Army" , slot )
466+ game .set_player_option (player_id , "Color" , slot )
467+
468+ await self .launch_game (game , host , guests )
469+
470+ return game
471+ except Exception :
472+ self ._logger .exception ("" )
473+ await game .on_game_end ()
474+
475+ for player in all_players :
476+ if player .state == PlayerState .STARTING_AUTOMATCH :
477+ player .state = PlayerState .IDLE
478+
479+ raise
480+
328481 def on_match_found (
329482 self ,
330483 s1 : Search ,
@@ -465,7 +618,7 @@ async def launch_game(
465618 def game_options (player : Player ) -> GameLaunchOptions :
466619 return options ._replace (
467620 team = game .get_player_option (player .id , "Team" ),
468- faction = player .faction ,
621+ faction = game . get_player_option ( player .id , "Faction" ) ,
469622 map_position = game .get_player_option (player .id , "StartSpot" )
470623 )
471624
0 commit comments