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,135 @@ 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 , * args = e .args
368+
369+ await self .message_queue_service .publish (
370+ config .MQ_EXCHANGE_NAME ,
371+ "error.match.create" ,
372+ {"error_code" : code , "args" : args },
373+ correlation_id = message .correlation_id
374+ )
375+ else :
376+ await self .message_queue_service .publish (
377+ config .MQ_EXCHANGE_NAME ,
378+ "success.match.create" ,
379+ {"game_id" : game .id },
380+ correlation_id = message .correlation_id
381+ )
382+
383+ async def _handle_mq_matchmaking_request (
384+ self ,
385+ message : aio_pika .IncomingMessage
386+ ):
387+ self ._logger .debug (
388+ "Got matchmaking request: %s" , message .correlation_id
389+ )
390+ request = json .loads (message .body )
391+ # TODO: Use id instead of name?
392+ queue_name = request .get ("matchmaker_queue" )
393+ map_name = request ["map_name" ]
394+ game_name = request ["game_name" ]
395+ participants = request ["participants" ]
396+ featured_mod = request .get ("featured_mod" )
397+ if not featured_mod and not queue_name :
398+ raise KeyError ("featured_mod" )
399+
400+ if queue_name and queue_name not in self .queues :
401+ raise Exception ("invalid_request" , "invalid queue" )
402+
403+ if not participants :
404+ raise Exception ("invalid_request" , "empty participants" )
405+
406+ player_ids = [participant ["player_id" ] for participant in participants ]
407+ missing_players = [
408+ id for id in player_ids if self .player_service [id ] is None
409+ ]
410+ if missing_players :
411+ raise Exception (
412+ "players_not_found" ,
413+ * [{"player_id" : id } for id in missing_players ]
414+ )
415+
416+ all_players = [
417+ self .player_service [player_id ] for player_id in player_ids
418+ ]
419+ non_idle_players = [
420+ player for player in all_players
421+ if player .state != PlayerState .IDLE
422+ ]
423+ if non_idle_players :
424+ raise Exception (
425+ "invalid_state" ,
426+ [
427+ {"player_id" : player .id , "state" : player .state .name }
428+ for player in all_players
429+ ]
430+ )
431+
432+ queue = self .queues [queue_name ] if queue_name else None
433+ featured_mod = featured_mod or queue .featured_mod
434+ host = all_players [0 ]
435+ guests = all_players [1 :]
436+
437+ for player in all_players :
438+ player .state = PlayerState .STARTING_AUTOMATCH
439+
440+ try :
441+ game = self .game_service .create_game (
442+ game_class = LadderGame ,
443+ game_mode = featured_mod ,
444+ host = host ,
445+ name = "Matchmaker Game" ,
446+ mapname = map_name ,
447+ matchmaker_queue_id = queue .id if queue else None ,
448+ rating_type = queue .rating_type if queue else None ,
449+ max_players = len (participants )
450+ )
451+ game .init_mode = InitMode .AUTO_LOBBY
452+ game .set_name_unchecked (game_name )
453+
454+ for participant in participants :
455+ player_id = participant ["player_id" ]
456+ faction = Faction .from_value (participant ["faction" ])
457+ team = participant ["team" ]
458+ slot = participant ["slot" ]
459+
460+ game .set_player_option (player_id , "Faction" , faction .value )
461+ game .set_player_option (player_id , "Team" , team )
462+ game .set_player_option (player_id , "StartSpot" , slot )
463+ game .set_player_option (player_id , "Army" , slot )
464+ game .set_player_option (player_id , "Color" , slot )
465+
466+ await self .launch_game (game , host , guests )
467+
468+ return game
469+ except Exception :
470+ self ._logger .exception ("" )
471+ await game .on_game_end ()
472+
473+ for player in all_players :
474+ if player .state == PlayerState .STARTING_AUTOMATCH :
475+ player .state = PlayerState .IDLE
476+
477+ raise
478+
328479 def on_match_found (
329480 self ,
330481 s1 : Search ,
@@ -465,7 +616,7 @@ async def launch_game(
465616 def game_options (player : Player ) -> GameLaunchOptions :
466617 return options ._replace (
467618 team = game .get_player_option (player .id , "Team" ),
468- faction = player .faction ,
619+ faction = game . get_player_option ( player .id , "Faction" ) ,
469620 map_position = game .get_player_option (player .id , "StartSpot" )
470621 )
471622
0 commit comments