diff options
| author | Marie <marie@kaifa.ch> | 2024-01-25 14:21:42 +0100 |
|---|---|---|
| committer | Marie <marie@kaifa.ch> | 2024-01-25 14:21:42 +0100 |
| commit | 913dd581efa3e0afe6da57131d213965a376f90e (patch) | |
| tree | 7d6241b02e299d86c02db9305c23a5142390e044 /packages/backend/src | |
| parent | fix: notes/versions not checking visibility (diff) | |
| parent | 2024.2.0-beta.7 (diff) | |
| download | sharkey-913dd581efa3e0afe6da57131d213965a376f90e.tar.gz sharkey-913dd581efa3e0afe6da57131d213965a376f90e.tar.bz2 sharkey-913dd581efa3e0afe6da57131d213965a376f90e.zip | |
merge: upstream
Diffstat (limited to 'packages/backend/src')
11 files changed, 195 insertions, 55 deletions
diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 4f68fabc4c..50bddb0c2b 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -5,10 +5,9 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import CRC32 from 'crc-32'; import { ModuleRef } from '@nestjs/core'; import * as Reversi from 'misskey-reversi'; -import { IsNull } from 'typeorm'; +import { IsNull, LessThan, MoreThan } from 'typeorm'; import type { MiReversiGame, ReversiGamesRepository, @@ -25,7 +24,7 @@ import { Serialized } from '@/types.js'; import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; -const MATCHING_TIMEOUT_MS = 1000 * 15; // 15sec +const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec @Injectable() export class ReversiService implements OnApplicationShutdown, OnModuleInit { @@ -86,44 +85,82 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { map: game.map, bw: game.bw, crc32: game.crc32, + noIrregularRules: game.noIrregularRules, } satisfies Partial<MiReversiGame>; } @bindThis - public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> { + public async matchSpecificUser(me: MiUser, targetUser: MiUser, multiple = false): Promise<MiReversiGame | null> { if (targetUser.id === me.id) { throw new Error('You cannot match yourself.'); } + if (!multiple) { + // 既にマッチしている対局が無いか探す(3分以内) + const games = await this.reversiGamesRepository.find({ + where: [ + { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, user2Id: targetUser.id, isStarted: false }, + { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: targetUser.id, user2Id: me.id, isStarted: false }, + ], + relations: ['user1', 'user2'], + order: { id: 'DESC' }, + }); + if (games.length > 0) { + return games[0]; + } + } + + //#region 相手から既に招待されてないか確認 const invitations = await this.redisClient.zrange( `reversi:matchSpecific:${me.id}`, - Date.now() - MATCHING_TIMEOUT_MS, + Date.now() - INVITATION_TIMEOUT_MS, '+inf', 'BYSCORE'); if (invitations.includes(targetUser.id)) { await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id); - const game = await this.matched(targetUser.id, me.id); + const game = await this.matched(targetUser.id, me.id, { + noIrregularRules: false, + }); return game; - } else { - this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id); + } + //#endregion - this.globalEventService.publishReversiStream(targetUser.id, 'invited', { - user: await this.userEntityService.pack(me, targetUser), - }); + const redisPipeline = this.redisClient.pipeline(); + redisPipeline.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id); + redisPipeline.expire(`reversi:matchSpecific:${targetUser.id}`, 120, 'NX'); + await redisPipeline.exec(); - return null; - } + this.globalEventService.publishReversiStream(targetUser.id, 'invited', { + user: await this.userEntityService.pack(me, targetUser), + }); + + return null; } @bindThis - public async matchAnyUser(me: MiUser): Promise<MiReversiGame | null> { + public async matchAnyUser(me: MiUser, options: { noIrregularRules: boolean }, multiple = false): Promise<MiReversiGame | null> { + if (!multiple) { + // 既にマッチしている対局が無いか探す(3分以内) + const games = await this.reversiGamesRepository.find({ + where: [ + { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, isStarted: false }, + { id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user2Id: me.id, isStarted: false }, + ], + relations: ['user1', 'user2'], + order: { id: 'DESC' }, + }); + if (games.length > 0) { + return games[0]; + } + } + //#region まず自分宛ての招待を探す const invitations = await this.redisClient.zrange( `reversi:matchSpecific:${me.id}`, - Date.now() - MATCHING_TIMEOUT_MS, + Date.now() - INVITATION_TIMEOUT_MS, '+inf', 'BYSCORE'); @@ -131,7 +168,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const invitorId = invitations[Math.floor(Math.random() * invitations.length)]; await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId); - const game = await this.matched(invitorId, me.id); + const game = await this.matched(invitorId, me.id, { + noIrregularRules: false, + }); return game; } @@ -139,23 +178,35 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const matchings = await this.redisClient.zrange( 'reversi:matchAny', - Date.now() - MATCHING_TIMEOUT_MS, - '+inf', - 'BYSCORE'); + 0, + 2, // 自分自身のIDが入っている場合もあるので2つ取得 + 'REV'); - const userIds = matchings.filter(id => id !== me.id); + const items = matchings.filter(id => !id.startsWith(me.id)); - if (userIds.length > 0) { - // pick random - const matchedUserId = userIds[Math.floor(Math.random() * userIds.length)]; + if (items.length > 0) { + const [matchedUserId, option] = items[0].split(':'); - await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId); + await this.redisClient.zrem('reversi:matchAny', + me.id, + matchedUserId, + me.id + ':noIrregularRules', + matchedUserId + ':noIrregularRules'); - const game = await this.matched(matchedUserId, me.id); + const game = await this.matched(matchedUserId, me.id, { + noIrregularRules: options.noIrregularRules || option === 'noIrregularRules', + }); return game; } else { - await this.redisClient.zadd('reversi:matchAny', Date.now(), me.id); + const redisPipeline = this.redisClient.pipeline(); + if (options.noIrregularRules) { + redisPipeline.zadd('reversi:matchAny', Date.now(), me.id + ':noIrregularRules'); + } else { + redisPipeline.zadd('reversi:matchAny', Date.now(), me.id); + } + redisPipeline.expire('reversi:matchAny', 15, 'NX'); + await redisPipeline.exec(); return null; } } @@ -167,7 +218,15 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { @bindThis public async matchAnyUserCancel(user: MiUser) { - await this.redisClient.zrem('reversi:matchAny', user.id); + await this.redisClient.zrem('reversi:matchAny', user.id, user.id + ':noIrregularRules'); + } + + @bindThis + public async cleanOutdatedGames() { + await this.reversiGamesRepository.delete({ + id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 10)), + isStarted: false, + }); } @bindThis @@ -221,7 +280,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } @bindThis - private async matched(parentId: MiUser['id'], childId: MiUser['id']): Promise<MiReversiGame> { + private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> { const game = await this.reversiGamesRepository.insert({ id: this.idService.gen(), user1Id: parentId, @@ -234,6 +293,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { map: Reversi.maps.eighteight.data, bw: 'random', isLlotheo: false, + noIrregularRules: options.noIrregularRules, }).then(x => this.reversiGamesRepository.findOneOrFail({ where: { id: x.identifiers[0].id }, relations: ['user1', 'user2'], @@ -255,7 +315,13 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { bw = parseInt(game.bw, 10); } - const crc32 = CRC32.str(JSON.stringify(game.logs)).toString(); + const engine = new Reversi.Game(game.map, { + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + }); + + const crc32 = engine.calcCrc32().toString(); const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() .set({ @@ -276,12 +342,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { this.cacheGame(updatedGame); //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 - const engine = new Reversi.Game(updatedGame.map, { - isLlotheo: updatedGame.isLlotheo, - canPutEverywhere: updatedGame.canPutEverywhere, - loopedBoard: updatedGame.loopedBoard, - }); - if (engine.isEnded) { let winnerId; if (engine.winner === true) { @@ -335,7 +395,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { public async getInvitations(user: MiUser): Promise<MiUser['id'][]> { const invitations = await this.redisClient.zrange( `reversi:matchSpecific:${user.id}`, - Date.now() - MATCHING_TIMEOUT_MS, + Date.now() - INVITATION_TIMEOUT_MS, '+inf', 'BYSCORE'); return invitations; @@ -406,7 +466,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const serializeLogs = Reversi.Serializer.serializeLogs(logs); - const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString(); + const crc32 = engine.calcCrc32().toString(); const updatedGame = { ...game, @@ -538,7 +598,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { if (game == null) throw new Error('game not found'); if (crc32.toString() !== game.crc32) { - return await this.reversiGameEntityService.packDetail(game); + return game; } else { return null; } diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts index 6c89a70599..1a689a7b53 100644 --- a/packages/backend/src/core/entities/ReversiGameEntityService.ts +++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts @@ -61,6 +61,7 @@ export class ReversiGameEntityService { canPutEverywhere: game.canPutEverywhere, loopedBoard: game.loopedBoard, timeLimitForEachTurn: game.timeLimitForEachTurn, + noIrregularRules: game.noIrregularRules, logs: game.logs, map: game.map, }); @@ -105,6 +106,7 @@ export class ReversiGameEntityService { canPutEverywhere: game.canPutEverywhere, loopedBoard: game.loopedBoard, timeLimitForEachTurn: game.timeLimitForEachTurn, + noIrregularRules: game.noIrregularRules, }); } diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts index 11d236e458..c03335dd63 100644 --- a/packages/backend/src/models/ReversiGame.ts +++ b/packages/backend/src/models/ReversiGame.ts @@ -109,6 +109,11 @@ export class MiReversiGame { @Column('boolean', { default: false, }) + public noIrregularRules: boolean; + + @Column('boolean', { + default: false, + }) public isLlotheo: boolean; @Column('boolean', { diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts index f8a5e7451c..ff4c78eeb0 100644 --- a/packages/backend/src/models/json-schema/reversi-game.ts +++ b/packages/backend/src/models/json-schema/reversi-game.ts @@ -82,6 +82,10 @@ export const packedReversiGameLiteSchema = { type: 'string', optional: false, nullable: false, }, + noIrregularRules: { + type: 'boolean', + optional: false, nullable: false, + }, isLlotheo: { type: 'boolean', optional: false, nullable: false, @@ -196,6 +200,10 @@ export const packedReversiGameDetailedSchema = { type: 'string', optional: false, nullable: false, }, + noIrregularRules: { + type: 'boolean', + optional: false, nullable: false, + }, isLlotheo: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index e252c5d8a1..17b6c8ba0c 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -11,6 +11,7 @@ import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; import type { Config } from '@/config.js'; +import { ReversiService } from '@/core/ReversiService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -32,6 +33,7 @@ export class CleanProcessorService { private roleAssignmentsRepository: RoleAssignmentsRepository, private queueLoggerService: QueueLoggerService, + private reversiService: ReversiService, private idService: IdService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('clean'); @@ -65,6 +67,8 @@ export class CleanProcessorService { }); } + this.reversiService.cleanOutdatedGames(); + this.logger.succ('Cleaned.'); } } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 5b4ba9ddcd..35aceeaa66 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -385,6 +385,7 @@ import * as ep___reversi_match from './endpoints/reversi/match.js'; import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; +import * as ep___reversi_verify from './endpoints/reversi/verify.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; @@ -768,6 +769,7 @@ const $reversi_match: Provider = { provide: 'ep:reversi/match', useClass: ep___r const $reversi_invitations: Provider = { provide: 'ep:reversi/invitations', useClass: ep___reversi_invitations.default }; const $reversi_showGame: Provider = { provide: 'ep:reversi/show-game', useClass: ep___reversi_showGame.default }; const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass: ep___reversi_surrender.default }; +const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep___reversi_verify.default }; @Module({ imports: [ @@ -1155,6 +1157,7 @@ const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass $reversi_invitations, $reversi_showGame, $reversi_surrender, + $reversi_verify, ], exports: [ $admin_meta, @@ -1533,6 +1536,7 @@ const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass $reversi_invitations, $reversi_showGame, $reversi_surrender, + $reversi_verify, ], }) export class EndpointsModule {} diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index b5d8c6d678..8de5171475 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -386,6 +386,7 @@ import * as ep___reversi_match from './endpoints/reversi/match.js'; import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; +import * as ep___reversi_verify from './endpoints/reversi/verify.js'; const eps = [ ['admin/meta', ep___admin_meta], @@ -767,6 +768,7 @@ const eps = [ ['reversi/invitations', ep___reversi_invitations], ['reversi/show-game', ep___reversi_showGame], ['reversi/surrender', ep___reversi_surrender], + ['reversi/verify', ep___reversi_verify], ]; interface IEndpointMetaBase { diff --git a/packages/backend/src/server/api/endpoints/reversi/games.ts b/packages/backend/src/server/api/endpoints/reversi/games.ts index f28fe5d987..c1b2ff1702 100644 --- a/packages/backend/src/server/api/endpoints/reversi/games.ts +++ b/packages/backend/src/server/api/endpoints/reversi/games.ts @@ -43,7 +43,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId) - .andWhere('game.isStarted = TRUE') .innerJoinAndSelect('game.user1', 'user1') .innerJoinAndSelect('game.user2', 'user2'); @@ -53,6 +52,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .where('game.user1Id = :userId', { userId: me.id }) .orWhere('game.user2Id = :userId', { userId: me.id }); })); + } else { + query.andWhere('game.isStarted = TRUE'); } const games = await query.take(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts index 1065ce5a89..f8dee21c4c 100644 --- a/packages/backend/src/server/api/endpoints/reversi/match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -37,6 +37,8 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id', nullable: true }, + noIrregularRules: { type: 'boolean', default: false }, + multiple: { type: 'boolean', default: false }, }, required: [], } as const; @@ -56,7 +58,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw err; }) : null; - const game = target ? await this.reversiService.matchSpecificUser(me, target) : await this.reversiService.matchAnyUser(me); + const game = target + ? await this.reversiService.matchSpecificUser(me, target, ps.multiple) + : await this.reversiService.matchAnyUser(me, { noIrregularRules: ps.noIrregularRules }, ps.multiple); if (game == null) return; diff --git a/packages/backend/src/server/api/endpoints/reversi/verify.ts b/packages/backend/src/server/api/endpoints/reversi/verify.ts new file mode 100644 index 0000000000..5f5af6ce67 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/verify.ts @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ReversiService } from '@/core/ReversiService.js'; +import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + errors: { + noSuchGame: { + message: 'No such game.', + code: 'NO_SUCH_GAME', + id: '8fb05624-b525-43dd-90f7-511852bdfeee', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + desynced: { type: 'boolean' }, + game: { + type: 'object', + optional: true, nullable: true, + ref: 'ReversiGameDetailed', + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + gameId: { type: 'string', format: 'misskey:id' }, + crc32: { type: 'string' }, + }, + required: ['gameId', 'crc32'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private reversiService: ReversiService, + private reversiGameEntityService: ReversiGameEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const game = await this.reversiService.checkCrc(ps.gameId, ps.crc32); + if (game) { + return { + desynced: true, + game: await this.reversiGameEntityService.packDetail(game), + }; + } else { + return { + desynced: false, + }; + } + }); + } +} diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts index 820c80006b..fb24a29b75 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { MiReversiGame, ReversiGamesRepository } from '@/models/_.js'; +import type { MiReversiGame } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { ReversiService } from '@/core/ReversiService.js'; @@ -19,7 +19,6 @@ class ReversiGameChannel extends Channel { constructor( private reversiService: ReversiService, - private reversiGamesRepository: ReversiGamesRepository, private reversiGameEntityService: ReversiGameEntityService, id: string, @@ -42,7 +41,6 @@ class ReversiGameChannel extends Channel { case 'updateSettings': this.updateSettings(body.key, body.value); break; case 'cancel': this.cancelGame(); break; case 'putStone': this.putStone(body.pos, body.id); break; - case 'resync': this.resync(body.crc32); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break; } } @@ -76,14 +74,6 @@ class ReversiGameChannel extends Channel { } @bindThis - private async resync(crc32: string | number) { - const game = await this.reversiService.checkCrc(this.gameId!, crc32); - if (game) { - this.send('resynced', game); - } - } - - @bindThis private async claimTimeIsUp() { if (this.user == null) return; @@ -104,9 +94,6 @@ export class ReversiGameChannelService implements MiChannelService<false> { public readonly kind = ReversiGameChannel.kind; constructor( - @Inject(DI.reversiGamesRepository) - private reversiGamesRepository: ReversiGamesRepository, - private reversiService: ReversiService, private reversiGameEntityService: ReversiGameEntityService, ) { @@ -116,7 +103,6 @@ export class ReversiGameChannelService implements MiChannelService<false> { public create(id: string, connection: Channel['connection']): ReversiGameChannel { return new ReversiGameChannel( this.reversiService, - this.reversiGamesRepository, this.reversiGameEntityService, id, connection, |