Helló mindenkinek.
Idén tavasszal rátaláltam egy projektre, ahol a fiúk megtanulták, hogyan indítsanak Dota 2 szervert a 2014-es verzióban, és ennek megfelelően játszhassanak rajta. Nagy rajongója vagyok ennek a játéknak, és nem tudtam elmenni a különleges lehetőség mellett, hogy visszatérjek a gyermekkoromban.
Rendkívül mélyen merültem el a dologban, és végül írtam egy Discord botot, amely szinte az összes funkciót ellátja, amelyet a régi verzió nem támogat, például a matchmakinget.
A bot bevezetése előtt a lobbik manuálisan készültek. 10 reakciót gyűjtöttünk egy üzenetre, majd manuálisan összehoztuk a szervert, vagy helyi lobbikat hosztoltunk.

Programozói természetem nem bírta el a sok manuális munkát, és egy éjszaka alatt megírtam a legbasicabb verziót a botból, amely automatikusan elindítja a szervert, amikor 10 ember összegyűlik.
Node.js-ben kezdtem írni, mert nem túl kedvelem a Python-t, és jobban érzem magam ebben a környezetben.
Ez az első tapasztalatom a Discord bot írásában, de meglepően egyszerűnek bizonyult. Az official npm modul, a discord.js kényelmes interfészt biztosít az üzenetek kezelésére, a reakciók összegyűjtésére stb.
Felelősségkizárás: minden kódemelvény 'aktuális', tehát átment néhány éjszakai újraíráson.
A matchmaking alapja a 'sor', amelybe a játékosok kerülnek, akik játszani akarnak, és kikerülnek, amikor meggondolják magukat vagy találnak egy játékot.
Így néz ki a 'játékos' entitás. Eredetileg csak a Discord felhasználó azonosítója volt, de terveim között szerepel a launcher/játékkeresés a weboldalról, de erről később.
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);
}
}
Íme a sor interfésze. Itt a 'játékosok' helyett egy 'csoport' absztrakciót használnak. Egyéni játékos számára a csoport önmagából áll, míg a csoportban lévő játékosok esetében a csoport összes játékosából.
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
}
Elhatároztam, hogy eseményeket használok a kontextus cseréjére. Ez illeszkedett az esetekhez — az "10 fős játék találva" esemény alapján küldhetünk kért üzenetet a játékosoknak közvetlen üzenetben, és végrehajthatjuk a fő üzleti logikát — elindítani egy feladatot a készenlét ellenőrzésére, előkészíteni a lobbyt a kezdéshez és így tovább.
Az IOC-hoz az InversifyJS-t használom. Fényes tapasztalataim vannak ezzel a könyvtárral. Gyors és egyszerű!
Több sor várakozik a szerverünkön — bejöttek az 1x1, normál/ranglistás módban, és néhány egyedi játék. Ezért van egy singleton RoomService, amely a felhasználó és a játékkeresés között helyezkedik el.
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))) {
// ha a sorban van ez a párt, akkor újra hozzáadjuk a pártot
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))) {
// ha a sorban van ez a párt, akkor eltávolítjuk a pártot
q.remove(event.party)
}
});
}
);
}
(A kód példája annak bemutatására, hogy körülbelül hogy néznek ki a folyamatok)
Itt inicializálom a sort a játék által megvalósított minden egyes módhoz, és figyelem a "csoportok" változásait, hogy finomhangolhassam a sorokat és elkerüljem a bizonyos konfliktusokat.
Na szóval, ügyes vagyok, beillesztettem kóddarabokat, amelyek semmilyen módon nem kapcsolódnak a témához, most pedig térjünk rá a matchmakingre.
Nézzük meg az esetet:
1) A felhasználó játszani akar.
2) Ahhoz, hogy elkezdje a keresést, használja a Gateway=Discord szolgáltatást, azaz reagál az üzenetre:

3) Ez a gateway eljut a RoomService-hez, és mondja: "A Discordból származó felhasználó szeretne belépni a sorba, mód: nem ranglista játék".
4) A RoomService elfogadja a gateway kérését, és a felhasználót ( pontosabban a felhasználó csoportját) a megfelelő sorba helyezi.
5) A sor minden változásnál ellenőrzi, hogy elegendő játékos van-e a játékhoz. Ha igen — kibocsátunk egy eseményt:
private onRoomFound(players: Party[]) {
this.emit("room-found", {
players,
});
}
6) A RoomService, nyilvánvalóan, örömmel hallgat minden várakozási sort ezen esemény izgatott várakozásával. Beérkezik egy listája a játékosoknak, és ezekből létrehozzuk a virtuális "szobát", és természetesen kibocsátjuk az eseményt:
queue.addListener("room-found", (event: RoomFoundEvent) => {
console.log(
`Szoba megtalálva mód: [${mode}]. Ideje ingyenes szobát szerezni ezeknek az embereknek`
);
const room = this.getFreeRoom(mode);
room.fill(event.players);
this.onRoomFormed(room);
});
7) Elérkeztünk a "legmagasabb" szintű példányhoz — a klasszhoz Bot. Általában a kapuek és a matchmaking üzleti logika közötti kapcsolattartással foglalkozik. A bot figyeli az eseményt, és utasítja a DiscordGateway-t, hogy küldjön minden felhasználónak készenléti ellenőrzést.

8) Ha valaki három perc alatt elutasítja vagy nem fogadja el a játékot, akkor NEM visszük vissza őt a sorba. Minden más visszatér a sorba, és várunk, amíg újra összegyűlik 10 fő. Ha minden játékos elfogadta a játékot, akkor kezdődik az izgalmas rész.
Dedicated server konfiguráció
A játékaink VDS-en, Windows Server 2012-n vannak hosztolva. Ebből néhány következtetést le lehet vonni:
- Nincs Docker, ami a szívembe döfött.
- Költséget takarítunk meg a bérlésen.
A feladat az, hogy VPS-en Linuxon folyamatot indítsunk a VDS-en. Írtam egy egyszerű szervert Flask-ban. Igen, nem szeretem a Python-t, de mit tehetek — ezen a nyelven gyorsabb és könnyebb megírni ezt a szervert.
Három funkciót lát el:
- A szerver indítása konfigurációval — a térkép, a játékhoz szükséges játékosok száma, és a pluginek kiválasztása. A pluginekről most nem írok — ez egy külön történet, rengeteg kávéval az éjszaka közepén, vegyítve könnyekkel és kihúzott hajakkal.
- A szerver leállítása/újraindítása sikertelen kapcsolatok esetén, amelyeket csak manuálisan tudunk kezelni.
Itt minden egyszerű, a kód példák nem is relevánsak. A script 100 sorból áll.
Tehát, amikor 10 ember összegyűlik és elfogadja a játékot, a szerver elindul, és mindenki játszani vágyik, a privát üzenetekben megérkezik a játékhoz való kapcsolódás linkje.

A linkre kattintva a játékos csatlakozik a játék szerverhez, és a többi már automatikusan történik. Kb. 25 perc múlva a virtuális "szoba" játékosokkal kiürül.
Előre is elnézést kérek a cikk zavarosságáért, rég írtam ide, és túl sok a kód, hogy kiemeljem a fontos részeket. Röviden, csak egy kis felfordulás.
Ha látom, hogy van érdeklődés a téma iránt, készítek egy második részt — abban az én szenvedéseim lesznek a srcds (Source dedicated server) bővítményekkel, és valószínűleg egy értékelési rendszer és egy mini-dotabuff, egy statisztikai oldal a játékokhoz.
Néhány link:
Forrás: habr.com
