Vi skriver matchmaking til Dota fra 2014

Hej alle.

I foråret stødte jeg på et projekt, hvor folk havde lært at starte en Dota 2-server fra 2014 og dermed spille på den. Jeg er en stor fan af dette spil og kunne ikke lade denne unikke mulighed for at genopleve min barndom gå forbi.

Jeg dykker meget dybt ned i det, og det viste sig, at jeg skrev en Discord-bot, der håndterer næsten al funktionaliteten, som ikke understøttes i den gamle version af spillet, nemlig matchmaking.
Før alle opdateringer med botten blev lobbyer oprettet manuelt. Vi samlede 10 reaktioner på beskeden og samlede serveren manuelt, eller hostede en lokal lobby.

Vi skriver matchmaking til Dota fra 2014

Min programmeringsnatur kunne ikke holde til så meget manuelt arbejde, og i løbet af en nat skitserede jeg den enkleste version af botten, der automatisk startede serveren, når der var 10 personer.

Jeg besluttede at skrive med det samme i nodejs, fordi jeg ikke rigtig kan lide Python, og jeg føler mig mere komfortabel i dette miljø.

Det er min første oplevelse med at skrive en bot til Discord, men det viste sig at være ganske enkelt. Det officielle npm-modul discord.js giver et brugervenligt interface til at arbejde med beskeder, samle reaktioner osv.

Disclaimer: alle kodeeksempler er 'aktuelt', altså de har gennemgået flere iterationer af omskrivning om natten.

Kernen i matchmaking er 'køen', hvor spillere, der ønsker at spille, placeres, og de fjernes, når de ikke længere ønsker at spille eller har fundet et spil.

Sådan ser entiteten 'spiller' ud. Oprindeligt var det bare bruger-id'et i Discord, men planen er at lave en launcher/spil søgning fra hjemmesiden, men alt i sin tid.

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

Her er køens interface. Her bruges en abstraktion i form af en 'gruppe' i stedet for 'spillere'. For en enkelt spiller består gruppen af spilleren selv, og for spillere i en gruppe, består gruppen selvfølgelig af alle spillerne i gruppen.

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
}

Jeg besluttede at bruge begivenheder til at udveksle kontekst. Det passede til nogle tilfælde – ved begivenheden "spil fundet for 10 personer" kan jeg sende den nødvendige meddelelse direkte til spillerne og udføre den hovedforretningslogik – starte en opgave for at tjekke klarhed, forberede lobbyen til lancering og så videre.

Til IOC bruger jeg InversifyJS. Jeg har haft en god oplevelse med dette bibliotek. Hurtigt og nemt!

Vi har flere køer på serveren – der er kommet 1x1, almindelig/rangeret og et par custom modes. Derfor er der en singleton RoomService, som ligger mellem brugeren og spillet.

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))) {
            // hvis køen har denne gruppe, genindsætter vi gruppen
            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))) {
            // hvis køen har denne gruppe, fjerner vi gruppen
            q.remove(event.party)
          }
        });
      }
    );
  }

(Kodeeksemplet er til repræsentation af, hvordan processerne ser ud)

Her initialiserer jeg køen for hver af de implementerede spiltilstande, og jeg lytter også til ændringer i "grupperne" for at justere køerne og undgå visse konflikter.

Så, jeg er dygtig, jeg har indsættet kodefragmenter, der ikke har noget at gøre med emnet, nu lad os gå direkte til matchmaking.

Lad os overveje et tilfælde:

1) Brugeren ønsker at spille.

2) For at starte søgningen bruger han Gateway=Discord, det vil sige, at han reagerer på en meddelelse:

Vi skriver matchmaking til Dota fra 2014

3) Denne gateway går til RoomService og siger "Brugeren fra Discord ønsker at komme ind i køen, tilstand: u-rangeret spil".

4) RoomService accepterer gatewayens anmodning og placerer brugeren (eller rettere sagt, brugerens gruppe) i den rigtige kø.

5) Køen tjekker ved hver ændring, om der er nok spillere til et spil. Hvis det er muligt – udsender et event:

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

6) RoomService lytter åbenbart ivrigt til hver kø i spændt forventning om denne begivenhed. Vi modtager en liste over spillere, danner et virtuelt "værelse" ud af dem, og selvfølgelig emitterer vi begivenheden:

queue.addListener("room-found", (event: RoomFoundEvent) => {
      console.log(
        `Room found mode: [${mode}]. Tid til at finde et gratis værelse til disse fyre`
      );
      const room = this.getFreeRoom(mode);
      room.fill(event.players);

      this.onRoomFormed(room);
    });

7) Her er vi nået til den "højeste" instans — klassen Bot. Generelt håndterer den kommunikationen mellem gateways (hvor komisk det ser ud på russisk kan jeg ikke hjælpe) og forretningslogikken for matchmaking. Bot'en lytter til begivenheden og beordrer DiscordGateway til at sende en anmodning til alle brugere om klarhed.

Vi skriver matchmaking til Dota fra 2014

8) Hvis nogen afviste eller ikke accepterede spillet inden for 3 minutter, så SENDER VI IKKE dem tilbage til køen. Alle andre sendes tilbage til køen, og vi venter på, at der igen samles 10 personer. Hvis alle spillere accepterer spillet, så begynder den interessante del.

Konfiguration af den dedikerede server

Vi hoster spillene på VDS med Windows Server 2012. Der kan drages nogle konklusioner fra dette:

  1. Der er ingen Docker tilgængelig, hvilket ramte mig lige i hjertet.
  2. Vi sparer på lejen.

Opgaven er: at køre en proces på VDS fra VPS med Linux. Jeg skrev en simpel server i Flask. Ja, jeg elsker ikke Python, men hvad kan man gøre — det er hurtigere og lettere at skrive denne server med.

Den udfører 3 funktioner:

  1. Start serveren med konfiguration — valg af kort, antal spillere til at starte spillet, og sættene af plugins. Jeg vil ikke gå ind i plugins nu — det er en helt anden historie med liter kaffe om natten blandet med tårer og hår, der er blevet revet ud.
  2. Stop/omstart serveren i tilfælde af mislykkede forbindelser, som vi kun kan håndtere manuelt.

Her er alt simpelt, kodeeksempler er næsten irrelevante. Scriptet er 100 linjer langt.

Så snart 10 personer er samlet og har accepteret spillet, er serveren startet, og alle er ivrige efter at spille, modtager de en privat besked med et link til at forbinde til spillet.

Vi skriver matchmaking til Dota fra 2014

Når spilleren klikker på linket, forbindes de til spilserveren, og herefter går alt af sig selv. Efter cirka 25 minutter tømmes det virtuelle "værelse" for spillere.

Jeg beklager på forhånd for artiklens uheldige struktur, jeg har ikke skrevet her i lang tid, og der er for meget kode til at fremhæve vigtige dele. Bare en hurtig sammenblanding.

Hvis jeg ser interesse for emnet, vil der komme en anden del — den vil indeholde mine kvaler med plugins til srcds (Source dedicated server), og måske et ratingsystem og en mini-dotabuff, en side med spilstatistikker.

Nogle links:

  1. Vores hjemmeside (statistik, leaderboard, en lille landingsside og download af klienten)
  2. Discord-server

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster