Skip to content

Commit 4ccb319

Browse files
committed
So this works huh
1 parent 5588f96 commit 4ccb319

File tree

8 files changed

+4116
-3081
lines changed

8 files changed

+4116
-3081
lines changed

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
"@opentelemetry/resources": "^1.28.0",
4444
"@opentelemetry/sdk-node": "^0.55.0",
4545
"@opentelemetry/semantic-conventions": "^1.28.0",
46+
"amqp-connection-manager": "^4.1.14",
47+
"amqplib": "^0.10.5",
4648
"cache-manager": "^5.7.6",
4749
"cache-manager-redis-yet": "^5.1.4",
4850
"cheerio": "^1.0.0",
@@ -69,7 +71,6 @@
6971
"@swc/cli": "^0.5.1",
7072
"@swc/core": "^1.9.3",
7173
"@testcontainers/postgresql": "^10.15.0",
72-
"@types/cron": "^2.4.3",
7374
"@types/express": "^5.0.0",
7475
"@types/jest": "29.5.14",
7576
"@types/js-yaml": "^4.0.9",
@@ -83,7 +84,7 @@
8384
"jest": "29.7.0",
8485
"nodemon": "^3.1.7",
8586
"prettier": "^3.4.1",
86-
"supertest": "^6.3.4",
87+
"supertest": "^7.0.0",
8788
"testcontainers": "^10.15.0",
8889
"ts-jest": "29.2.5",
8990
"ts-loader": "^9.5.1",

src/app.module.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Module } from '@nestjs/common';
22
import { AppService } from 'app.service';
33
import { CqrsModule } from '@nestjs/cqrs';
4-
import { ClientsModule, RedisOptions, Transport } from '@nestjs/microservices';
4+
import { ClientsModule, RedisOptions, RmqOptions, Transport } from '@nestjs/microservices';
55
import { GameServerDomain } from 'gameserver';
66
import { CoreController } from 'core.controller';
77
import { TypeOrmModule } from '@nestjs/typeorm';
@@ -59,6 +59,34 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm/dist/interfaces/typeorm-op
5959
inject: [ConfigService],
6060
}),
6161
TypeOrmModule.forFeature(Entities),
62+
ClientsModule.registerAsync([
63+
{
64+
name: 'RMQ',
65+
useFactory(config: ConfigService): RmqOptions {
66+
return {
67+
transport: Transport.RMQ,
68+
options: {
69+
urls: [
70+
{
71+
hostname: config.get<string>('rabbitmq.host'),
72+
port: config.get<number>('rabbitmq.port'),
73+
protocol: 'amqp',
74+
username: config.get<string>('rabbitmq.user'),
75+
password: config.get<string>('rabbitmq.password'),
76+
},
77+
],
78+
queue: config.get<string>('rabbitmq.queue'),
79+
queueOptions: {
80+
durable: true,
81+
},
82+
prefetchCount: 5,
83+
},
84+
};
85+
},
86+
inject: [ConfigService],
87+
imports: [],
88+
},
89+
]),
6290
ClientsModule.registerAsync([
6391
{
6492
name: "QueryCore",

src/core.controller.ts

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { MatchFailedEvent } from 'gateway/events/match-failed.event';
1818
import { PlayerAbandonedEvent } from 'gateway/events/bans/player-abandoned.event';
1919
import { LobbyReadyEvent } from 'gateway/events/lobby-ready.event';
2020
import { ConfigService } from '@nestjs/config';
21+
import { SrcdsServerStartedEvent } from 'gateway/events/srcds-server-started.event';
2122

2223
@Controller()
2324
export class CoreController {
@@ -33,6 +34,11 @@ export class CoreController {
3334
if (this.config.get("prod")) this.ebus.publish(buff);
3435
}
3536

37+
@EventPattern(SrcdsServerStartedEvent.name)
38+
async SrcdsServerStartedEvent(data: SrcdsServerStartedEvent) {
39+
this.event(SrcdsServerStartedEvent, data);
40+
}
41+
3642
@EventPattern(RoomReadyEvent.name)
3743
async RoomReadyEvent(data: RoomReadyEvent) {
3844
this.event(RoomReadyEvent, data);
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,16 @@
11
import { CommandHandler, EventBus, ICommandHandler, QueryBus } from '@nestjs/cqrs';
22
import { Inject, Logger } from '@nestjs/common';
33
import { FindGameServerCommand } from 'gameserver/command/FindGameServer/find-game-server.command';
4-
import { GameSessionCreatedEvent } from 'gateway/events/game-session-created.event';
54
import { GameServerSessionRepository } from 'gameserver/repository/game-server-session.repository';
65
import { GameServerSessionEntity } from 'gameserver/model/game-server-session.entity';
76
import { InjectRepository } from '@nestjs/typeorm';
87
import { Repository } from 'typeorm';
9-
import { GameServerEntity } from 'gameserver/model/game-server.entity';
108
import { ClientProxy } from '@nestjs/microservices';
119
import {
1210
FullMatchPlayer,
1311
GSMatchInfo,
1412
LaunchGameServerCommand,
1513
} from 'gateway/commands/LaunchGameServer/launch-game-server.command';
16-
import { LaunchGameServerResponse } from 'gateway/commands/LaunchGameServer/launch-game-server.response';
17-
import { timeout } from 'rxjs/operators';
18-
import { ServerNotRespondingEvent } from 'gameserver/event/server-not-responding.event';
19-
import { MatchCancelledEvent } from 'gateway/events/match-cancelled.event';
20-
import { inspect } from 'util';
21-
import { Subject } from 'rxjs';
22-
import { asyncMap } from 'rxjs-async-map';
23-
import { KillServerRequestedEvent } from 'gateway/events/gs/kill-server-requested.event';
24-
import { MatchStartedEvent } from 'gateway/events/match-started.event';
25-
import { GameServerInfo } from 'gateway/shared-types/game-server-info';
26-
import { MatchInfo } from 'gateway/events/room-ready.event';
2714
import { GetUserInfoQuery } from 'gateway/queries/GetUserInfo/get-user-info.query';
2815
import { GetUserInfoQueryResult } from 'gateway/queries/GetUserInfo/get-user-info-query.result';
2916
import { MatchEntity } from 'gameserver/model/match.entity';
@@ -32,16 +19,14 @@ import { Dota_GameMode } from 'gateway/shared-types/dota-game-mode';
3219
import { MatchmakingModeMappingEntity } from 'gameserver/model/matchmaking-mode-mapping.entity';
3320
import { Dota_Map } from 'gateway/shared-types/dota-map';
3421
import { GamePreparedEvent } from 'gameserver/event/game-prepared.event';
22+
import { ConfigService } from '@nestjs/config';
3523

3624
@CommandHandler(FindGameServerCommand)
3725
export class FindGameServerHandler
3826
implements ICommandHandler<FindGameServerCommand>
3927
{
4028
private readonly logger = new Logger(FindGameServerHandler.name);
4129

42-
private pendingGamesPool: Subject<FindGameServerCommand> =
43-
new Subject<FindGameServerCommand>();
44-
4530
constructor(
4631
private readonly gsSessionRepository: GameServerSessionRepository,
4732
@InjectRepository(GameServerSessionEntity)
@@ -51,16 +36,31 @@ export class FindGameServerHandler
5136
private readonly matchEntityRepository: Repository<MatchEntity>,
5237
private readonly qbus: QueryBus,
5338
@Inject("QueryCore") private readonly redisEventQueue: ClientProxy,
39+
@Inject("RMQ") private readonly rmq: ClientProxy,
40+
private readonly config: ConfigService,
5441
@InjectRepository(MatchmakingModeMappingEntity)
5542
private readonly matchmakingModeMappingEntityRepository: Repository<MatchmakingModeMappingEntity>,
5643
) {
57-
this.pendingGamesPool
58-
.pipe(asyncMap((cmd) => this.findServer(cmd), 1))
59-
.subscribe();
44+
6045
}
6146

6247
async execute(command: FindGameServerCommand) {
63-
this.pendingGamesPool.next(command);
48+
const gsInfo = await this.extendMatchInfo(command.info);
49+
50+
const m = new MatchEntity();
51+
m.server = MatchEntity.NOT_DECIDED_SERVER;
52+
m.mode = command.info.mode;
53+
m.started = false;
54+
m.finished = false;
55+
m.matchInfoJson = {
56+
...gsInfo,
57+
};
58+
59+
await this.matchEntityRepository.save(m);
60+
61+
this.logger.log("Created match stub");
62+
63+
await this.submitQueueTask(m.id, gsInfo);
6464
}
6565

6666
private async getMapForMatchMode(mode: MatchmakingMode): Promise<Dota_Map> {
@@ -126,113 +126,11 @@ export class FindGameServerHandler
126126
);
127127
}
128128

129-
private async findServer(command: FindGameServerCommand) {
130-
const freeServerPool = await this.gsSessionRepository.getAllFree(
131-
command.info.version,
132-
);
133-
134-
const gsInfo = await this.extendMatchInfo(command.info);
135-
136-
console.log("FindServer called, pool", freeServerPool);
137-
138-
const m = new MatchEntity();
139-
m.server = MatchEntity.NOT_DECIDED_SERVER;
140-
m.mode = command.info.mode;
141-
m.started = false;
142-
m.finished = false;
143-
m.matchInfoJson = {
144-
...gsInfo,
145-
};
146-
147-
await this.matchEntityRepository.save(m);
148-
149-
let i = 0;
150-
let foundServer: GameServerEntity | undefined;
151-
152-
console.log("Free pool:", freeServerPool.length);
153-
while (i < freeServerPool.length) {
154-
const candidate = freeServerPool[i];
155-
const stackUrl = candidate.url;
156-
try {
157-
const cmd = new LaunchGameServerCommand(candidate.url, m.id, gsInfo);
158-
console.log(JSON.stringify(cmd, null, 2));
159-
const req = await this.redisEventQueue
160-
.send<
161-
LaunchGameServerResponse,
162-
LaunchGameServerCommand
163-
>(LaunchGameServerCommand.name, cmd)
164-
.pipe(timeout(15000))
165-
.toPromise();
166-
167-
// we got req, now need to deci
168-
if (req.successful) {
169-
// server launcher
170-
foundServer = candidate;
171-
break;
172-
} else {
173-
console.log(req);
174-
// server not launched for some reason
175-
// i guess we skip? just try next server
176-
}
177-
} catch (e) {
178-
console.log("Sadkek?", e);
179-
console.error(e.stack);
180-
// timeout means server is DEAD
181-
this.ebus.publish(new ServerNotRespondingEvent(stackUrl));
182-
// just to make sure server is dead
183-
this.ebus.publish(new KillServerRequestedEvent(stackUrl));
184-
}
185-
186-
i++;
187-
}
188-
189-
console.log("So: ", inspect(foundServer));
190-
191-
if (foundServer) {
192-
m.server = foundServer.url;
193-
await this.matchEntityRepository.save(m);
194-
} else {
195-
// cancel match : no servers free :sadkek:
196-
this.ebus.publish(
197-
new MatchCancelledEvent(
198-
m.id,
199-
new MatchInfo(
200-
command.info.mode,
201-
command.info.roomId,
202-
command.info.players,
203-
0,
204-
command.info.version,
205-
),
206-
),
207-
);
208-
return;
209-
}
210-
// ok here we launch server
211-
212-
const session = new GameServerSessionEntity();
213-
session.url = foundServer.url;
214-
215-
session.matchId = m.id;
216-
session.matchInfoJson = {
217-
...gsInfo,
218-
};
219-
220-
await this.gameServerSessionModelRepository.save(session);
221-
222-
this.ebus.publish(
223-
new GameSessionCreatedEvent(
224-
session.url,
225-
session.matchId,
226-
session.matchInfoJson,
227-
),
228-
);
229-
230-
this.ebus.publish(
231-
new MatchStartedEvent(
232-
session.matchId,
233-
session.matchInfoJson,
234-
new GameServerInfo(session.url),
235-
),
129+
private async submitQueueTask(matchId: number, info: GSMatchInfo) {
130+
this.rmq.emit(
131+
LaunchGameServerCommand.name,
132+
new LaunchGameServerCommand(matchId, info),
236133
);
134+
this.logger.log("Submitted start server command to queue");
237135
}
238136
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { EventBus, EventsHandler, IEventHandler } from '@nestjs/cqrs';
2+
import { SrcdsServerStartedEvent } from 'gateway/events/srcds-server-started.event';
3+
import { MatchEntity } from 'gameserver/model/match.entity';
4+
import { InjectRepository } from '@nestjs/typeorm';
5+
import { Repository } from 'typeorm';
6+
import { GameServerSessionEntity } from 'gameserver/model/game-server-session.entity';
7+
import { GameSessionCreatedEvent } from 'gateway/events/game-session-created.event';
8+
import { MatchStartedEvent } from 'gateway/events/match-started.event';
9+
import { GameServerInfo } from 'gateway/shared-types/game-server-info';
10+
11+
@EventsHandler(SrcdsServerStartedEvent)
12+
export class SrcdsServerStartedHandler
13+
implements IEventHandler<SrcdsServerStartedEvent>
14+
{
15+
constructor(
16+
@InjectRepository(MatchEntity)
17+
private readonly matchEntityRepository: Repository<MatchEntity>,
18+
@InjectRepository(GameServerSessionEntity)
19+
private readonly gameServerSessionEntityRepository: Repository<GameServerSessionEntity>,
20+
private readonly ebus: EventBus,
21+
) {}
22+
23+
async handle(event: SrcdsServerStartedEvent) {
24+
const m = await this.matchEntityRepository.findOneOrFail({
25+
where: { id: event.matchId },
26+
});
27+
28+
m.server = event.server;
29+
await this.matchEntityRepository.save(m);
30+
31+
const session = new GameServerSessionEntity();
32+
session.url = event.server;
33+
34+
session.matchId = m.id;
35+
session.matchInfoJson = {
36+
...event.info,
37+
};
38+
39+
await this.gameServerSessionEntityRepository.save(session);
40+
41+
this.ebus.publish(
42+
new GameSessionCreatedEvent(
43+
session.url,
44+
session.matchId,
45+
session.matchInfoJson,
46+
),
47+
);
48+
49+
this.ebus.publish(
50+
new MatchStartedEvent(
51+
session.matchId,
52+
session.matchInfoJson,
53+
new GameServerInfo(session.url),
54+
),
55+
);
56+
}
57+
}

src/gameserver/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { MatchFailedHandler } from 'gameserver/event-handler/match-failed.handle
3232
import { PlayerAbandonedHandler } from 'gameserver/event-handler/player-abandoned.handler';
3333
import { PrepareGameHandler } from 'gameserver/command/PrepareGame/prepare-game.handler';
3434
import { LobbyReadyHandler } from 'gameserver/event-handler/lobby-ready.handler';
35+
import { SrcdsServerStartedHandler } from 'gameserver/event-handler/srcds-server-started.handler';
3536

3637
const CommandHandlers = [
3738
FindGameServerHandler,
@@ -43,6 +44,7 @@ const CommandHandlers = [
4344
const EventHandlers = [
4445
StartFakeMatchHandler,
4546
LiveMatchUpdateHandler,
47+
SrcdsServerStartedHandler,
4648

4749
GameServerStoppedHandler,
4850
GameServerDiscoveredHandler,

0 commit comments

Comments
 (0)