Desenvolvendo matchmaking para Dota de 2014

Olá a todos.

Nesta primavera, encontrei um projeto em que pessoal aprendeu a rodar um servidor Dota 2 da versão de 2014, e assim, jogar nele. Sou um grande fã desse jogo e não pude resistir à oportunidade única de voltar à minha infância.

Mergulhei bem fundo e acabei escrevendo um bot para Discord, que responde por praticamente toda a funcionalidade que não é suportada na versão antiga do jogo, especificamente o matchmaking.
Antes de todas as novidades com o bot, os lobbies eram criados manualmente. Reuníamos 10 reações em uma mensagem e montávamos o servidor manualmente ou hospedávamos um lobby local.

Desenvolvendo matchmaking para Dota de 2014

A minha natureza de programador não suportou tanta mão de obra manual, e em uma noite escrevi a versão mais simples do bot, que levantava automaticamente o servidor quando 10 pessoas se juntavam.

Decidi escrever em nodejs de imediato, porque não gosto muito de Python, e me sinto mais confortável nesse ambiente.

Essa é a minha primeira experiência escrevendo um bot para Discord, mas na verdade tudo é bem simples. O módulo npm oficial discord.js oferece uma interface conveniente para trabalhar com mensagens, coleta de reações, etc.

Aviso: todos os exemplos de código são 'atuais', ou seja, passaram por várias iterações de reescrita durante as noites.

A base do matchmaking é uma 'fila', onde os jogadores que querem jogar são colocados e removidos quando desistem ou encontram um jogo.

Assim se parece a entidade 'jogador'. Inicialmente, era apenas um id de usuário no Discord, mas há planos para um launcher/busca de jogo no site, mas tudo a seu tempo.

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);
  }
}

Aqui está a interface da fila. Em vez de 'jogadores', utiliza-se uma abstração chamada 'grupo'. Para um jogador solitário, o grupo é apenas ele mesmo, e para jogadores em grupo, é, assim, 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
}

Decidi usar eventos para a troca de contexto. Isso se aplica a casos como: ao evento "jogo encontrado para 10 pessoas", posso enviar mensagens diretas aos jogadores com a informação necessária e executar a lógica de negócios principal — iniciar uma tarefa para verificar a prontidão, preparar o lobby para o início e assim por diante.

Para IOC, utilizo o InversifyJS. Tenho uma experiência agradável com esta biblioteca. Rápido e simples!

Temos várias filas em nosso servidor — adicionamos modos 1x1, normal/classificado e algumas personalizadas. Portanto, existe um singleton RoomService, que fica entre o usuário e a busca de 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))) {
            // se a fila tem essa party, re-adicionamos a 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))) {
            // se a fila tem essa party, a removemos
            q.remove(event.party)
          }
        });
      }
    );
  }

(Trecho de código para ilustrar como os processos se parecem)

Aqui, inicializo a fila para cada um dos modos de jogo implementados, além de escutar as mudanças de "grupos" para ajustar as filas e evitar alguns conflitos.

Assim, fiz bem, inseri trechos de código que não estão relacionados ao tópico, e agora vamos passar diretamente para o matchmaking.

Vamos considerar o caso:

1) O usuário quer jogar.

2) Para iniciar a busca, ele usa o Gateway=Discord, ou seja, reage a uma mensagem:

Desenvolvendo matchmaking para Dota de 2014

3) Este gateway vai ao RoomService e diz "O usuário do Discord quer entrar na fila, modo: jogo não classificado."

4) O RoomService aceita o pedido do gateway e insere o usuário (ou melhor, o grupo do usuário) na fila apropriada.

5) A fila verifica a cada alteração se há jogadores suficientes para iniciar o jogo. Se sim — emitimos o evento:

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

6) O RoomService, evidentemente, ouve com alegria cada fila na expectativa desse evento. Recebemos uma lista de jogadores, formamos uma "sala" virtual e, claro, emitimos o evento:

queue.addListener("room-found", (event: RoomFoundEvent) => {
      console.log(
        `Modo da sala encontrada: [${mode}]. Hora de conseguir uma sala gratuita para esses caras`
      );
      const room = this.getFreeRoom(mode);
      room.fill(event.players);

      this.onRoomFormed(room);
    });

7) Aqui chegamos à "instância superior" — a classe Bot. Em geral, ela lida com a comunicação entre os gateways (como isso soa engraçado em russo, não posso) e a lógica de negócios do matchmaking. O bot escuta o evento e ordena ao DiscordGateway enviar a todos os usuários uma verificação de prontidão.

Desenvolvendo matchmaking para Dota de 2014

8) Se alguém rejeitou ou não aceitou o jogo em 3 minutos, nós NÃO os devolvemos para a fila. Todos os outros são retornados à fila e aguardamos até que 10 pessoas se reúnam novamente. Se todos os jogadores aceitaram o jogo, a parte interessante começa.

Configuração do servidor dedicado

Nossos jogos são hospedados em VDS com Windows Server 2012. A partir disso, podemos tirar algumas conclusões:

  1. Não temos Docker, o que me atingiu no coração
  2. Estamos economizando no aluguel

A tarefa é: rodar um processo no VDS a partir de VPS com Linux. Escrevi um servidor simples em Flask. Sim, não gosto de Python, mas o que posso fazer — escrever este servidor nele é mais rápido e simples.

Ele executa 3 funções:

  1. Iniciar o servidor com configuração — escolha do mapa, número de jogadores para o início do jogo e um conjunto de plugins. Não vou falar sobre os plugins agora — isso é uma história separada com litros de café à noite misturados com lágrimas e cabelos arrancados.
  2. Parar/reiniciar o servidor em caso de conexões malsucedidas, que podemos lidar apenas manualmente.

Aqui tudo é simples, exemplos de código seriam até inadequados. Um script de 100 linhas

Assim que 10 pessoas se reúnem e aceitam o jogo, o servidor é iniciado e todos estão ansiosos para jogar, uma mensagem privada com o link para conectar ao jogo é enviada.

Desenvolvendo matchmaking para Dota de 2014

Ao clicar no link, o jogador se conecta ao servidor de jogo, e daí para frente tudo se resolve por si. Após cerca de 25 minutos, a "sala" virtual com os jogadores é limpa.

Peço desculpas pela desorganização do artigo, não escrevo aqui há muito tempo, e há código demais para destacar as partes importantes. Enfim.

Se eu ver interesse no tema, haverá uma segunda parte — nela estarei compartilhando minhas dificuldades com plugins para srcds (Servidor dedicado Source), 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 líderes, uma pequena landing page e download do cliente)
  2. Servidor Discord

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster