Escrevendo matchmaking para Dota 2014

Olá.



Nesta primavera me deparei com um projeto em que os caras aprenderam a rodar o servidor Dota 2 da versão 2014 e, consequentemente, jogar nele. Sou um grande fã deste jogo, e não poderia deixar de aproveitar a oportunidade única de mergulhar na minha infância.



Eu mergulhei muito fundo, e aconteceu que escrevi um bot Discord, que é responsável por quase todas as funcionalidades não suportadas na versão antiga do jogo, ou seja, matchmaking.

Antes de todas as inovações com o bot, o lobby foi criado manualmente. Coletamos 10 respostas a uma mensagem e montamos manualmente um servidor ou hospedamos um lobby local.







Minha natureza de programador não suportava tanto trabalho manual, e da noite para o dia eu esbocei a versão mais simples do bot, que automaticamente ativou o servidor quando 10 pessoas foram recrutadas.



Decidi escrever imediatamente em nodejs, porque realmente não gosto de python e me sinto mais confortável neste ambiente.



Esta é minha primeira experiência de escrever um bot para o Discord, mas acabou sendo muito simples. O módulo oficial npm discord.js fornece uma interface conveniente para trabalhar com mensagens, coletar reações, etc.



Isenção de responsabilidade: todos os exemplos de código estão "atualizados", o que significa que passaram por várias iterações de reescrita durante a noite.



O núcleo do matchmaking é a "fila" na qual os jogadores que querem jogar são colocados e removidos quando não querem ou não encontram um jogo.



É assim que a essência do "jogador" se parece. Inicialmente, era apenas um ID de usuário no Discord, mas os planos incluem um launcher / busca por um jogo no site, mas primeiro as coisas mais importantes.



export enum Realm {
  DISCORD,
  EXTERNAL,
}

export default class QueuePlayer {
  constructor(public readonly realm: Realm, public readonly id: string) {}

  public is(qp: QueuePlayer): boolean {
    return this.realm === qp.realm && this.id === qp.id;
  }

  static Discord(id: string) {
    return new QueuePlayer(Realm.DISCORD, id);
  }

  static External(id: string) {
    return new QueuePlayer(Realm.EXTERNAL, id);
  }
}


E aqui está a interface da fila. Aqui, em vez de "jogadores", uma abstração na forma de um "grupo" é usada. Para um único jogador, o grupo consiste em ele mesmo e, para os jogadores em um grupo, respectivamente, de todos os jogadores do grupo.



export default interface IQueue extends EventEmitter {
  inQueue: QueuePlayer[]
  put(uid: Party): boolean;
  remove(uid: Party): boolean;
  removeAll(ids: Party[]): void;

  mode: MatchmakingMode
  roomSize: number;
  clear(): void
}


Decidiu usar eventos para trocar contexto. Adequado para casos - para o evento "encontrei um jogo para 10 pessoas", você pode enviar a mensagem desejada aos jogadores em mensagens privadas e executar a lógica de negócios principal - lançar uma tarefa para verificar a prontidão, preparar o lobby para o lançamento e assim por diante.



Para IOC, estou usando InversifyJS. Tenho uma experiência agradável com esta biblioteca. Rápido e fácil!



Temos várias filas no servidor - adicionamos modos 1x1, normal / classificação e alguns personalizados. Portanto, existe um único RoomService que fica entre o usuário e a busca do jogo.



constructor(
    @inject(GameServers) private gameServers: GameServers,
    @inject(MatchStatsService) private stats: MatchStatsService,
    @inject(PartyService) private partyService: PartyService
  ) {
    super();
    this.initQueue(MatchmakingMode.RANKED);
    this.initQueue(MatchmakingMode.UNRANKED);
    this.initQueue(MatchmakingMode.SOLOMID);
    this.initQueue(MatchmakingMode.DIRETIDE);
    this.initQueue(MatchmakingMode.GREEVILING);
    this.partyService.addListener(
      "party-update",
      (event: PartyUpdatedEvent) => {
        this.queues.forEach((q) => {
          if (has(q.queue, (t) => t.is(event.party))) {
            // if queue has this party, we re-add party
            this.leaveQueue(event.qp, q.mode)
            this.enterQueue(event.qp, q.mode)
          }
        });
      }
    );

    this.partyService.addListener(
      "party-removed",
      (event: PartyUpdatedEvent) => {
        this.queues.forEach((q) => {
          if (has(q.queue, (t) => t.is(event.party))) {
            // if queue has this party, we re-add party
            q.remove(event.party)
          }
        });
      }
    );
  }


(Codifique noodles para ter uma ideia de como são os processos)



Aqui eu inicializo uma fila para cada um dos modos de jogo implementados, e também ouço as mudanças nos "grupos" para corrigir as filas e evitar alguns conflitos.



Então, estou ótimo, eu inseri trechos de código que não têm nada a ver com o tópico e agora vamos passar diretamente para a mastmaking.



Considere um caso:



1) O usuário deseja jogar.



2) Para iniciar a busca, ele usa Gateway = Discord, ou seja, coloca uma reação à mensagem:







3) Este gateway vai para RoomService e diz "O usuário da discórdia quer entrar na fila, modo: jogo não classificado."



4) RoomService aceita a solicitação do gateway e a empurra para a fila desejada do usuário (mais precisamente, o grupo de usuários).



5) A fila verifica se há jogadores suficientes para jogar a cada mudança. Se possível, emita um evento:



private onRoomFound(players: Party[]) {
    this.emit("room-found", {
      players,
    });
  }


6) A RoomService fica obviamente feliz em ouvir cada fila, ansiosa e ansiosa por este evento. Na entrada recebemos uma lista de jogadores, formamos uma "sala" virtual deles e, claro, emitimos um evento:



queue.addListener("room-found", (event: RoomFoundEvent) => {
      console.log(
        `Room found mode: [${mode}]. Time to get free room for these guys`
      );
      const room = this.getFreeRoom(mode);
      room.fill(event.players);

      this.onRoomFormed(room);
    });


7) Então chegamos à instância "mais alta" - a classe Bot . Em geral, ele lida com a conexão entre os gateways (como isso parece ridículo em russo, não consigo) e a lógica de negócios da combinação. O bot escuta o evento e ordena que o DiscordGateway envie uma verificação de prontidão a todos os usuários.







8) Se alguém rejeitar ou não aceitar o jogo em 3 minutos, NÃO o devolvemos à fila. Devolvemos todos os outros à fila e esperamos que 10 pessoas sejam recrutadas novamente. Se todos os jogadores aceitarem o jogo, começa a parte divertida.



Configuração de servidor dedicado



Nossos jogos são hospedados em VDS com servidor Windows 2012. Várias conclusões podem ser tiradas disso:



  1. Não há docker nele, o que atingiu meu coração
  2. Economizamos no aluguel


A tarefa é iniciar o processo em VDS com VPS em Linux. Escreveu um servidor simples no Flask. Sim, eu não gosto de python, mas o que posso fazer - escrever este servidor nele é mais rápido e fácil.



Possui 3 funções:



  1. Lançamento do servidor com configuração - seleção de mapa, número de jogadores para iniciar o jogo e um conjunto de plug-ins. Não vou escrever sobre plug-ins agora - esta é uma história separada com litros de café à noite misturados com lágrimas e cabelo rasgado.
  2. Parar / reiniciar o servidor em caso de conexões malsucedidas, que só podemos tratar manualmente.


Tudo é simples aqui, os exemplos de código são até inadequados. Script para 100 linhas



Então, quando 10 pessoas se juntaram e aceitaram o jogo, o servidor está funcionando e todos estão ansiosos para jogar, um link para se conectar ao jogo vem em mensagens privadas.







Ao clicar no link, o jogador se conecta ao servidor do jogo e pronto. Após cerca de 25 minutos, a "sala" virtual com os jogadores é limpa.



Peço desculpas antecipadamente pela estranheza do artigo, não escrevo aqui há muito tempo e há muito código para destacar seções importantes. Macarrão, em resumo.



Se eu vir interesse no assunto, então haverá uma segunda parte - ela conterá meus tormentos com plugins para srcds (servidor dedicado de origem), e, provavelmente, um sistema de classificação e mini-dotabuff, um site com estatísticas de jogos.



Alguns links:



  1. Nosso site (estatísticas, tabela de classificação, pequenos landos e download do cliente)
  2. Servidor Discord



All Articles