8
8
from collections import defaultdict
9
9
from typing import Dict , List , Optional , Set , Tuple
10
10
11
+ import aio_pika
11
12
import aiocron
12
13
from sqlalchemy import and_ , func , select , text , true
13
14
30
31
matchmaker_queue_map_pool
31
32
)
32
33
from .decorators import with_logger
34
+ from .factions import Faction
33
35
from .game_service import GameService
34
36
from .games import Game , InitMode , LadderGame
35
37
from .matchmaker import MapPool , MatchmakerQueue , OnMatchedCallback , Search
38
+ from .message_queue_service import MessageQueueService
39
+ from .player_service import PlayerService
36
40
from .players import Player , PlayerState
37
41
from .protocol import DisconnectedError
38
42
from .types import GameLaunchOptions , Map , NeroxisGeneratedMap
@@ -55,17 +59,35 @@ def __init__(
55
59
self ,
56
60
database : FAFDatabase ,
57
61
game_service : GameService ,
62
+ player_service : PlayerService ,
63
+ message_queue_service : MessageQueueService
58
64
):
59
65
self ._db = database
60
66
self ._informed_players : Set [Player ] = set ()
61
67
self .game_service = game_service
68
+ self .player_service = player_service
69
+ self .message_queue_service = message_queue_service
62
70
self .queues = {}
71
+ self ._initialized = False
63
72
64
73
self ._searches : Dict [Player , Dict [str , Search ]] = defaultdict (dict )
65
74
66
75
async def initialize (self ) -> None :
76
+ if self ._initialized :
77
+ return
78
+
67
79
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
+
68
89
self ._update_cron = aiocron .crontab ("*/10 * * * *" , func = self .update_data )
90
+ self ._initialized = True
69
91
70
92
async def update_data (self ) -> None :
71
93
async with self ._db .acquire () as conn :
@@ -325,6 +347,135 @@ def write_rating_progress(self, player: Player, rating_type: str) -> None:
325
347
)
326
348
})
327
349
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
+
328
479
def on_match_found (
329
480
self ,
330
481
s1 : Search ,
@@ -465,7 +616,7 @@ async def launch_game(
465
616
def game_options (player : Player ) -> GameLaunchOptions :
466
617
return options ._replace (
467
618
team = game .get_player_option (player .id , "Team" ),
468
- faction = player .faction ,
619
+ faction = game . get_player_option ( player .id , "Faction" ) ,
469
620
map_position = game .get_player_option (player .id , "StartSpot" )
470
621
)
471
622
0 commit comments