merge: upstream
This commit is contained in:
commit
fd69a2fbbd
74 changed files with 659 additions and 745 deletions
|
@ -8,7 +8,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"start": "node ./built/boot/entry.js",
|
||||
"start:test": "NODE_ENV=test node ./built/boot/entry.js",
|
||||
"start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
||||
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||
"check:connect": "node ./check_connect.js",
|
||||
|
@ -31,7 +31,7 @@
|
|||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||
"generate-api-json": "node ./generate_api_json.js"
|
||||
"generate-api-json": "pnpm build && node ./generate_api_json.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
|
|
|
@ -55,23 +55,29 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'antennaCreated':
|
||||
this.antennas.push({
|
||||
this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
lastUsedAt: new Date(body.lastUsedAt),
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
userList: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
break;
|
||||
case 'antennaUpdated': {
|
||||
const idx = this.antennas.findIndex(a => a.id === body.id);
|
||||
if (idx >= 0) {
|
||||
this.antennas[idx] = {
|
||||
this.antennas[idx] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
lastUsedAt: new Date(body.lastUsedAt),
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
userList: null, // joinなカラムは通常取ってこないので
|
||||
};
|
||||
} else {
|
||||
// サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり
|
||||
this.antennas.push({
|
||||
this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
lastUsedAt: new Date(body.lastUsedAt),
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
userList: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,10 @@ export class MetaService implements OnApplicationShutdown {
|
|||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'metaUpdated': {
|
||||
this.cache = body;
|
||||
this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
proxyAccount: null, // joinなカラムは通常取ってこないので
|
||||
};
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -12,18 +12,14 @@ import { IsNull } from 'typeorm';
|
|||
import type {
|
||||
MiReversiGame,
|
||||
ReversiGamesRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { Serialized } from '@/types.js';
|
||||
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
||||
|
@ -58,7 +54,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
@bindThis
|
||||
private async cacheGame(game: MiReversiGame) {
|
||||
await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 3, JSON.stringify(game));
|
||||
await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 60, JSON.stringify(game));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -66,6 +62,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
await this.redisClient.del(`reversi:game:cache:${gameId}`);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private getBakeProps(game: MiReversiGame) {
|
||||
return {
|
||||
startedAt: game.startedAt,
|
||||
endedAt: game.endedAt,
|
||||
// ゲームの途中からユーザーが変わることは無いので
|
||||
//user1Id: game.user1Id,
|
||||
//user2Id: game.user2Id,
|
||||
user1Ready: game.user1Ready,
|
||||
user2Ready: game.user2Ready,
|
||||
black: game.black,
|
||||
isStarted: game.isStarted,
|
||||
isEnded: game.isEnded,
|
||||
winnerId: game.winnerId,
|
||||
surrenderedUserId: game.surrenderedUserId,
|
||||
timeoutUserId: game.timeoutUserId,
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
timeLimitForEachTurn: game.timeLimitForEachTurn,
|
||||
logs: game.logs,
|
||||
map: game.map,
|
||||
bw: game.bw,
|
||||
crc32: game.crc32,
|
||||
} satisfies Partial<MiReversiGame>;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
|
||||
if (targetUser.id === me.id) {
|
||||
|
@ -81,23 +104,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
if (invitations.includes(targetUser.id)) {
|
||||
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id);
|
||||
|
||||
const game = await this.reversiGamesRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
user1Id: targetUser.id,
|
||||
user2Id: me.id,
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
isStarted: false,
|
||||
isEnded: false,
|
||||
logs: [],
|
||||
map: Reversi.maps.eighteight.data,
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
this.cacheGame(game);
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: targetUser.id });
|
||||
this.globalEventService.publishReversiStream(targetUser.id, 'matched', { game: packed });
|
||||
const game = await this.matched(targetUser.id, me.id);
|
||||
|
||||
return game;
|
||||
} else {
|
||||
|
@ -124,23 +131,7 @@ 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.reversiGamesRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
user1Id: invitorId,
|
||||
user2Id: me.id,
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
isStarted: false,
|
||||
isEnded: false,
|
||||
logs: [],
|
||||
map: Reversi.maps.eighteight.data,
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
this.cacheGame(game);
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: invitorId });
|
||||
this.globalEventService.publishReversiStream(invitorId, 'matched', { game: packed });
|
||||
const game = await this.matched(invitorId, me.id);
|
||||
|
||||
return game;
|
||||
}
|
||||
|
@ -160,23 +151,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId);
|
||||
|
||||
const game = await this.reversiGamesRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
user1Id: matchedUserId,
|
||||
user2Id: me.id,
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
isStarted: false,
|
||||
isEnded: false,
|
||||
logs: [],
|
||||
map: Reversi.maps.eighteight.data,
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
this.cacheGame(game);
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: matchedUserId });
|
||||
this.globalEventService.publishReversiStream(matchedUserId, 'matched', { game: packed });
|
||||
const game = await this.matched(matchedUserId, me.id);
|
||||
|
||||
return game;
|
||||
} else {
|
||||
|
@ -204,14 +179,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
let isBothReady = false;
|
||||
|
||||
if (game.user1Id === user.id) {
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
user1Ready: ready,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
const updatedGame = {
|
||||
...game,
|
||||
user1Ready: ready,
|
||||
};
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
||||
|
@ -221,14 +192,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
if (ready && updatedGame.user2Ready) isBothReady = true;
|
||||
} else if (game.user2Id === user.id) {
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
user2Ready: ready,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
const updatedGame = {
|
||||
...game,
|
||||
user2Ready: ready,
|
||||
};
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
||||
|
@ -253,6 +220,32 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async matched(parentId: MiUser['id'], childId: MiUser['id']): Promise<MiReversiGame> {
|
||||
const game = await this.reversiGamesRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
user1Id: parentId,
|
||||
user2Id: childId,
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
isStarted: false,
|
||||
isEnded: false,
|
||||
logs: [],
|
||||
map: Reversi.maps.eighteight.data,
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
}).then(x => this.reversiGamesRepository.findOneOrFail({
|
||||
where: { id: x.identifiers[0].id },
|
||||
relations: ['user1', 'user2'],
|
||||
}));
|
||||
this.cacheGame(game);
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game);
|
||||
this.globalEventService.publishReversiStream(parentId, 'matched', { game: packed });
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async startGame(game: MiReversiGame) {
|
||||
let bw: number;
|
||||
|
@ -262,63 +255,44 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
bw = parseInt(game.bw, 10);
|
||||
}
|
||||
|
||||
function getRandomMap() {
|
||||
const mapCount = Object.entries(Reversi.maps).length;
|
||||
const rnd = Math.floor(Math.random() * mapCount);
|
||||
return Object.values(Reversi.maps)[rnd].data;
|
||||
}
|
||||
|
||||
const map = game.map != null ? game.map : getRandomMap();
|
||||
|
||||
const crc32 = CRC32.str(JSON.stringify(game.logs)).toString();
|
||||
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
...this.getBakeProps(game),
|
||||
startedAt: new Date(),
|
||||
isStarted: true,
|
||||
black: bw,
|
||||
map: map,
|
||||
map: game.map,
|
||||
crc32,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
// キャッシュ効率化のためにユーザー情報は再利用
|
||||
updatedGame.user1 = game.user1;
|
||||
updatedGame.user2 = game.user2;
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
||||
const engine = new Reversi.Game(map, {
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
const engine = new Reversi.Game(updatedGame.map, {
|
||||
isLlotheo: updatedGame.isLlotheo,
|
||||
canPutEverywhere: updatedGame.canPutEverywhere,
|
||||
loopedBoard: updatedGame.loopedBoard,
|
||||
});
|
||||
|
||||
if (engine.isEnded) {
|
||||
let winner;
|
||||
let winnerId;
|
||||
if (engine.winner === true) {
|
||||
winner = bw === 1 ? game.user1Id : game.user2Id;
|
||||
winnerId = bw === 1 ? updatedGame.user1Id : updatedGame.user2Id;
|
||||
} else if (engine.winner === false) {
|
||||
winner = bw === 1 ? game.user2Id : game.user1Id;
|
||||
winnerId = bw === 1 ? updatedGame.user2Id : updatedGame.user1Id;
|
||||
} else {
|
||||
winner = null;
|
||||
winnerId = null;
|
||||
}
|
||||
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
isEnded: true,
|
||||
endedAt: new Date(),
|
||||
winnerId: winner,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winner,
|
||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||
});
|
||||
await this.endGame(updatedGame, winnerId, null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -327,7 +301,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, '');
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'started', {
|
||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||
game: await this.reversiGameEntityService.packDetail(updatedGame),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async endGame(game: MiReversiGame, winnerId: MiUser['id'] | null, reason: 'surrender' | 'timeout' | null) {
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
...this.getBakeProps(game),
|
||||
isEnded: true,
|
||||
endedAt: new Date(),
|
||||
winnerId: winnerId,
|
||||
surrenderedUserId: reason === 'surrender' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null,
|
||||
timeoutUserId: reason === 'timeout' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
// キャッシュ効率化のためにユーザー情報は再利用
|
||||
updatedGame.user1 = game.user1;
|
||||
updatedGame.user2 = game.user2;
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winnerId,
|
||||
game: await this.reversiGameEntityService.packDetail(updatedGame),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -354,14 +354,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
// TODO: より厳格なバリデーション
|
||||
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
[key]: value,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
const updatedGame = {
|
||||
...game,
|
||||
[key]: value,
|
||||
};
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', {
|
||||
|
@ -397,17 +393,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
engine.putStone(pos);
|
||||
|
||||
let winner;
|
||||
if (engine.isEnded) {
|
||||
if (engine.winner === true) {
|
||||
winner = game.black === 1 ? game.user1Id : game.user2Id;
|
||||
} else if (engine.winner === false) {
|
||||
winner = game.black === 1 ? game.user2Id : game.user1Id;
|
||||
} else {
|
||||
winner = null;
|
||||
}
|
||||
}
|
||||
|
||||
const logs = Reversi.Serializer.deserializeLogs(game.logs);
|
||||
|
||||
const log = {
|
||||
|
@ -423,17 +408,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString();
|
||||
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
crc32,
|
||||
isEnded: engine.isEnded,
|
||||
winnerId: winner,
|
||||
logs: serializeLogs,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
const updatedGame = {
|
||||
...game,
|
||||
crc32,
|
||||
logs: serializeLogs,
|
||||
};
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'log', {
|
||||
|
@ -442,10 +421,16 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
});
|
||||
|
||||
if (engine.isEnded) {
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winner ?? null,
|
||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||
});
|
||||
let winnerId;
|
||||
if (engine.winner === true) {
|
||||
winnerId = game.black === 1 ? game.user1Id : game.user2Id;
|
||||
} else if (engine.winner === false) {
|
||||
winnerId = game.black === 1 ? game.user2Id : game.user1Id;
|
||||
} else {
|
||||
winnerId = null;
|
||||
}
|
||||
|
||||
await this.endGame(updatedGame, winnerId, null);
|
||||
} else {
|
||||
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`, updatedGame.timeLimitForEachTurn, '');
|
||||
}
|
||||
|
@ -460,23 +445,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
|
||||
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
isEnded: true,
|
||||
endedAt: new Date(),
|
||||
winnerId: winnerId,
|
||||
surrenderedUserId: user.id,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winnerId,
|
||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||
});
|
||||
await this.endGame(game, winnerId, 'surrender');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -500,23 +469,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
if (timer === 0) {
|
||||
const winnerId = engine.turn ? (game.black === 1 ? game.user2Id : game.user1Id) : (game.black === 1 ? game.user1Id : game.user2Id);
|
||||
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
isEnded: true,
|
||||
endedAt: new Date(),
|
||||
winnerId: winnerId,
|
||||
timeoutUserId: engine.turn ? (game.black === 1 ? game.user1Id : game.user2Id) : (game.black === 1 ? game.user2Id : game.user1Id),
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winnerId,
|
||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||
});
|
||||
await this.endGame(game, winnerId, 'timeout');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -539,14 +492,36 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
|
||||
const cached = await this.redisClient.get(`reversi:game:cache:${id}`);
|
||||
if (cached != null) {
|
||||
// TODO: この辺りのデシリアライズ処理をどこか別のサービスに切り出したい
|
||||
const parsed = JSON.parse(cached) as Serialized<MiReversiGame>;
|
||||
return {
|
||||
...parsed,
|
||||
startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null,
|
||||
endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : null,
|
||||
user1: parsed.user1 != null ? {
|
||||
...parsed.user1,
|
||||
avatar: null,
|
||||
banner: null,
|
||||
updatedAt: parsed.user1.updatedAt != null ? new Date(parsed.user1.updatedAt) : null,
|
||||
lastActiveDate: parsed.user1.lastActiveDate != null ? new Date(parsed.user1.lastActiveDate) : null,
|
||||
lastFetchedAt: parsed.user1.lastFetchedAt != null ? new Date(parsed.user1.lastFetchedAt) : null,
|
||||
movedAt: parsed.user1.movedAt != null ? new Date(parsed.user1.movedAt) : null,
|
||||
} : null,
|
||||
user2: parsed.user2 != null ? {
|
||||
...parsed.user2,
|
||||
avatar: null,
|
||||
banner: null,
|
||||
updatedAt: parsed.user2.updatedAt != null ? new Date(parsed.user2.updatedAt) : null,
|
||||
lastActiveDate: parsed.user2.lastActiveDate != null ? new Date(parsed.user2.lastActiveDate) : null,
|
||||
lastFetchedAt: parsed.user2.lastFetchedAt != null ? new Date(parsed.user2.lastFetchedAt) : null,
|
||||
movedAt: parsed.user2.movedAt != null ? new Date(parsed.user2.movedAt) : null,
|
||||
} : null,
|
||||
};
|
||||
} else {
|
||||
const game = await this.reversiGamesRepository.findOneBy({ id });
|
||||
const game = await this.reversiGamesRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['user1', 'user2'],
|
||||
});
|
||||
if (game == null) return null;
|
||||
|
||||
this.cacheGame(game);
|
||||
|
|
|
@ -181,9 +181,11 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
case 'userRoleAssigned': {
|
||||
const cached = this.roleAssignmentByUserIdCache.get(body.userId);
|
||||
if (cached) {
|
||||
cached.push({
|
||||
cached.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
role: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -49,9 +49,10 @@ export class WebhookService implements OnApplicationShutdown {
|
|||
switch (type) {
|
||||
case 'webhookCreated':
|
||||
if (body.active) {
|
||||
this.webhooks.push({
|
||||
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
@ -59,14 +60,16 @@ export class WebhookService implements OnApplicationShutdown {
|
|||
if (body.active) {
|
||||
const i = this.webhooks.findIndex(a => a.id === body.id);
|
||||
if (i > -1) {
|
||||
this.webhooks[i] = {
|
||||
this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
};
|
||||
} else {
|
||||
this.webhooks.push({
|
||||
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -9,7 +9,6 @@ import type { ReversiGamesRepository } from '@/models/_.js';
|
|||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
@ -29,10 +28,14 @@ export class ReversiGameEntityService {
|
|||
@bindThis
|
||||
public async packDetail(
|
||||
src: MiReversiGame['id'] | MiReversiGame,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
): Promise<Packed<'ReversiGameDetailed'>> {
|
||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const users = await Promise.all([
|
||||
this.userEntityService.pack(game.user1 ?? game.user1Id),
|
||||
this.userEntityService.pack(game.user2 ?? game.user2Id),
|
||||
]);
|
||||
|
||||
return await awaitAll({
|
||||
id: game.id,
|
||||
createdAt: this.idService.parse(game.id).date.toISOString(),
|
||||
|
@ -46,10 +49,10 @@ export class ReversiGameEntityService {
|
|||
user2Ready: game.user2Ready,
|
||||
user1Id: game.user1Id,
|
||||
user2Id: game.user2Id,
|
||||
user1: this.userEntityService.pack(game.user1Id, me),
|
||||
user2: this.userEntityService.pack(game.user2Id, me),
|
||||
user1: users[0],
|
||||
user2: users[1],
|
||||
winnerId: game.winnerId,
|
||||
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
|
||||
winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
|
||||
surrenderedUserId: game.surrenderedUserId,
|
||||
timeoutUserId: game.timeoutUserId,
|
||||
black: game.black,
|
||||
|
@ -66,18 +69,21 @@ export class ReversiGameEntityService {
|
|||
@bindThis
|
||||
public packDetailMany(
|
||||
xs: MiReversiGame[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(xs.map(x => this.packDetail(x, me)));
|
||||
return Promise.all(xs.map(x => this.packDetail(x)));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiReversiGame['id'] | MiReversiGame,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
): Promise<Packed<'ReversiGameLite'>> {
|
||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const users = await Promise.all([
|
||||
this.userEntityService.pack(game.user1 ?? game.user1Id),
|
||||
this.userEntityService.pack(game.user2 ?? game.user2Id),
|
||||
]);
|
||||
|
||||
return await awaitAll({
|
||||
id: game.id,
|
||||
createdAt: this.idService.parse(game.id).date.toISOString(),
|
||||
|
@ -85,16 +91,12 @@ export class ReversiGameEntityService {
|
|||
endedAt: game.endedAt && game.endedAt.toISOString(),
|
||||
isStarted: game.isStarted,
|
||||
isEnded: game.isEnded,
|
||||
form1: game.form1,
|
||||
form2: game.form2,
|
||||
user1Ready: game.user1Ready,
|
||||
user2Ready: game.user2Ready,
|
||||
user1Id: game.user1Id,
|
||||
user2Id: game.user2Id,
|
||||
user1: this.userEntityService.pack(game.user1Id, me),
|
||||
user2: this.userEntityService.pack(game.user2Id, me),
|
||||
user1: users[0],
|
||||
user2: users[1],
|
||||
winnerId: game.winnerId,
|
||||
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
|
||||
winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
|
||||
surrenderedUserId: game.surrenderedUserId,
|
||||
timeoutUserId: game.timeoutUserId,
|
||||
black: game.black,
|
||||
|
@ -109,9 +111,8 @@ export class ReversiGameEntityService {
|
|||
@bindThis
|
||||
public packLiteMany(
|
||||
xs: MiReversiGame[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(xs.map(x => this.packLite(x, me)));
|
||||
return Promise.all(xs.map(x => this.packLite(x)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,22 +34,6 @@ export const packedReversiGameLiteSchema = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
form1: {
|
||||
type: 'any',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
form2: {
|
||||
type: 'any',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
user1Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user2Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user1Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
@ -149,11 +133,11 @@ export const packedReversiGameDetailedSchema = {
|
|||
optional: false, nullable: false,
|
||||
},
|
||||
form1: {
|
||||
type: 'any',
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
form2: {
|
||||
type: 'any',
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
user1Ready: {
|
||||
|
|
|
@ -43,7 +43,9 @@ 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');
|
||||
.andWhere('game.isStarted = TRUE')
|
||||
.innerJoinAndSelect('game.user1', 'user1')
|
||||
.innerJoinAndSelect('game.user2', 'user2');
|
||||
|
||||
if (ps.my && me) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
|
@ -55,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const games = await query.take(ps.limit).getMany();
|
||||
|
||||
return await this.reversiGameEntityService.packLiteMany(games, me);
|
||||
return await this.reversiGameEntityService.packLiteMany(games);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
if (game == null) return;
|
||||
|
||||
return await this.reversiGameEntityService.packDetail(game, me);
|
||||
return await this.reversiGameEntityService.packDetail(game);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchGame);
|
||||
}
|
||||
|
||||
return await this.reversiGameEntityService.packDetail(game, me);
|
||||
return await this.reversiGameEntityService.packDetail(game);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ 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 'checkState': this.checkState(body.crc32); break;
|
||||
case 'resync': this.resync(body.crc32); break;
|
||||
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
||||
}
|
||||
}
|
||||
|
@ -76,12 +76,10 @@ class ReversiGameChannel extends Channel {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async checkState(crc32: string | number) {
|
||||
if (crc32 != null) return;
|
||||
|
||||
private async resync(crc32: string | number) {
|
||||
const game = await this.reversiService.checkCrc(this.gameId!, crc32);
|
||||
if (game) {
|
||||
this.send('rescue', game);
|
||||
this.send('resynced', game);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,12 +31,13 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
|||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { FeedService } from './FeedService.js';
|
||||
import { UrlPreviewService } from './UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './ClientLoggerService.js';
|
||||
|
@ -83,6 +84,9 @@ export class ClientServerService {
|
|||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
private flashEntityService: FlashEntityService,
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
|
@ -90,6 +94,7 @@ export class ClientServerService {
|
|||
private galleryPostEntityService: GalleryPostEntityService,
|
||||
private clipEntityService: ClipEntityService,
|
||||
private channelEntityService: ChannelEntityService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
private metaService: MetaService,
|
||||
private urlPreviewService: UrlPreviewService,
|
||||
private feedService: FeedService,
|
||||
|
@ -704,6 +709,25 @@ export class ClientServerService {
|
|||
return await renderBase(reply);
|
||||
}
|
||||
});
|
||||
|
||||
// Reversi game
|
||||
fastify.get<{ Params: { game: string; } }>('/reversi/g/:game', async (request, reply) => {
|
||||
const game = await this.reversiGamesRepository.findOneBy({
|
||||
id: request.params.game,
|
||||
});
|
||||
|
||||
if (game) {
|
||||
const _game = await this.reversiGameEntityService.packDetail(game);
|
||||
const meta = await this.metaService.fetch();
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await reply.view('reversi-game', {
|
||||
game: _game,
|
||||
...this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
fastify.get('/_info_card_', async (request, reply) => {
|
||||
|
|
20
packages/backend/src/server/web/views/reversi-game.pug
Normal file
20
packages/backend/src/server/web/views/reversi-game.pug
Normal file
|
@ -0,0 +1,20 @@
|
|||
extends ./base
|
||||
|
||||
block vars
|
||||
- const user1 = game.user1;
|
||||
- const user2 = game.user2;
|
||||
- const title = `${user1.username} vs ${user2.username}`;
|
||||
- const url = `${config.url}/reversi/g/${game.id}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content='⚫⚪Misskey Reversi⚪⚫')
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content='⚫⚪Misskey Reversi⚪⚫')
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='twitter:card' content='summary')
|
|
@ -283,7 +283,11 @@ export type Serialized<T> = {
|
|||
? (string | null)
|
||||
: T[K] extends Record<string, any>
|
||||
? Serialized<T[K]>
|
||||
: T[K];
|
||||
: T[K] extends (Record<string, any> | null)
|
||||
? (Serialized<T[K]> | null)
|
||||
: T[K] extends (Record<string, any> | undefined)
|
||||
? (Serialized<T[K]> | undefined)
|
||||
: T[K];
|
||||
};
|
||||
|
||||
export type FilterUnionByProperty<
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 139 KiB |
|
@ -45,6 +45,7 @@
|
|||
"crc-32": "^1.2.2",
|
||||
"cropperjs": "2.0.0-beta.4",
|
||||
"date-fns": "2.30.0",
|
||||
"defu": "^6.1.4",
|
||||
"escape-regexp": "0.0.1",
|
||||
"estree-walker": "3.0.3",
|
||||
"eventemitter3": "5.0.1",
|
||||
|
@ -54,9 +55,9 @@
|
|||
"json5": "2.2.3",
|
||||
"katex": "0.16.9",
|
||||
"matter-js": "0.19.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"photoswipe": "5.4.3",
|
||||
"punycode": "2.3.1",
|
||||
"rollup": "4.9.6",
|
||||
|
@ -112,7 +113,7 @@
|
|||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"@vitest/coverage-v8": "1.2.1",
|
||||
"@vitest/coverage-v8": "0.34.6",
|
||||
"@vue/runtime-core": "3.4.15",
|
||||
"acorn": "8.11.3",
|
||||
"cross-env": "7.0.3",
|
||||
|
@ -134,7 +135,7 @@
|
|||
"storybook": "7.6.10",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "1.2.1",
|
||||
"vitest": "0.34.6",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-eslint-parser": "9.4.0",
|
||||
"vue-tsc": "1.8.27"
|
||||
|
|
|
@ -163,7 +163,7 @@ const $i = signinRequired();
|
|||
|
||||
const props = defineProps<{
|
||||
game: Misskey.entities.ReversiGameDetailed;
|
||||
connection: Misskey.ChannelConnection;
|
||||
connection?: Misskey.ChannelConnection | null;
|
||||
}>();
|
||||
|
||||
const showBoardLabels = ref<boolean>(false);
|
||||
|
@ -240,10 +240,10 @@ watch(logPos, (v) => {
|
|||
|
||||
if (game.value.isStarted && !game.value.isEnded) {
|
||||
useInterval(() => {
|
||||
if (game.value.isEnded) return;
|
||||
if (game.value.isEnded || props.connection == null) return;
|
||||
const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString();
|
||||
if (_DEV_) console.log('crc32', crc32);
|
||||
props.connection.send('checkState', {
|
||||
props.connection.send('resync', {
|
||||
crc32: crc32,
|
||||
});
|
||||
}, 10000, { immediate: false, afterMounted: true });
|
||||
|
@ -267,7 +267,7 @@ function putStone(pos) {
|
|||
});
|
||||
|
||||
const id = Math.random().toString(36).slice(2);
|
||||
props.connection.send('putStone', {
|
||||
props.connection!.send('putStone', {
|
||||
pos: pos,
|
||||
id,
|
||||
});
|
||||
|
@ -283,22 +283,24 @@ const myTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
|
|||
const opTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
|
||||
|
||||
const TIMER_INTERVAL_SEC = 3;
|
||||
useInterval(() => {
|
||||
if (myTurnTimerRmain.value > 0) {
|
||||
myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC);
|
||||
}
|
||||
if (opTurnTimerRmain.value > 0) {
|
||||
opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC);
|
||||
}
|
||||
|
||||
if (iAmPlayer.value) {
|
||||
if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) {
|
||||
props.connection.send('claimTimeIsUp', {});
|
||||
if (!props.game.isEnded) {
|
||||
useInterval(() => {
|
||||
if (myTurnTimerRmain.value > 0) {
|
||||
myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC);
|
||||
}
|
||||
if (opTurnTimerRmain.value > 0) {
|
||||
opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC);
|
||||
}
|
||||
}
|
||||
}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
|
||||
|
||||
function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
||||
if (iAmPlayer.value) {
|
||||
if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) {
|
||||
props.connection!.send('claimTimeIsUp', {});
|
||||
}
|
||||
}
|
||||
}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
|
||||
}
|
||||
|
||||
async function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
||||
game.value.logs = Reversi.Serializer.serializeLogs([
|
||||
...Reversi.Serializer.deserializeLogs(game.value.logs),
|
||||
log,
|
||||
|
@ -309,17 +311,25 @@ function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
|||
if (log.id == null || !appliedOps.includes(log.id)) {
|
||||
switch (log.operation) {
|
||||
case 'put': {
|
||||
sound.playUrl('/client-assets/reversi/put.mp3', {
|
||||
volume: 1,
|
||||
playbackRate: 1,
|
||||
});
|
||||
|
||||
if (log.player !== engine.value.turn) { // = desyncが発生している
|
||||
const _game = await misskeyApi('reversi/show-game', {
|
||||
gameId: props.game.id,
|
||||
});
|
||||
restoreGame(_game);
|
||||
return;
|
||||
}
|
||||
|
||||
engine.value.putStone(log.pos);
|
||||
triggerRef(engine);
|
||||
|
||||
myTurnTimerRmain.value = game.value.timeLimitForEachTurn;
|
||||
opTurnTimerRmain.value = game.value.timeLimitForEachTurn;
|
||||
|
||||
sound.playUrl('/client-assets/reversi/put.mp3', {
|
||||
volume: 1,
|
||||
playbackRate: 1,
|
||||
});
|
||||
|
||||
checkEnd();
|
||||
break;
|
||||
}
|
||||
|
@ -366,9 +376,7 @@ function checkEnd() {
|
|||
}
|
||||
}
|
||||
|
||||
function onStreamRescue(_game) {
|
||||
console.log('rescue');
|
||||
|
||||
function restoreGame(_game) {
|
||||
game.value = deepClone(_game);
|
||||
|
||||
engine.value = Reversi.Serializer.restoreGame({
|
||||
|
@ -384,6 +392,12 @@ function onStreamRescue(_game) {
|
|||
checkEnd();
|
||||
}
|
||||
|
||||
function onStreamResynced(_game) {
|
||||
console.log('resynced');
|
||||
|
||||
restoreGame(_game);
|
||||
}
|
||||
|
||||
async function surrender() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
|
@ -434,27 +448,35 @@ function share() {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
props.connection.on('log', onStreamLog);
|
||||
props.connection.on('rescue', onStreamRescue);
|
||||
props.connection.on('ended', onStreamEnded);
|
||||
if (props.connection != null) {
|
||||
props.connection.on('log', onStreamLog);
|
||||
props.connection.on('resynced', onStreamResynced);
|
||||
props.connection.on('ended', onStreamEnded);
|
||||
}
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
props.connection.on('log', onStreamLog);
|
||||
props.connection.on('rescue', onStreamRescue);
|
||||
props.connection.on('ended', onStreamEnded);
|
||||
if (props.connection != null) {
|
||||
props.connection.on('log', onStreamLog);
|
||||
props.connection.on('resynced', onStreamResynced);
|
||||
props.connection.on('ended', onStreamEnded);
|
||||
}
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
props.connection.off('log', onStreamLog);
|
||||
props.connection.off('rescue', onStreamRescue);
|
||||
props.connection.off('ended', onStreamEnded);
|
||||
if (props.connection != null) {
|
||||
props.connection.off('log', onStreamLog);
|
||||
props.connection.off('resynced', onStreamResynced);
|
||||
props.connection.off('ended', onStreamEnded);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
props.connection.off('log', onStreamLog);
|
||||
props.connection.off('rescue', onStreamRescue);
|
||||
props.connection.off('ended', onStreamEnded);
|
||||
if (props.connection != null) {
|
||||
props.connection.off('log', onStreamLog);
|
||||
props.connection.off('resynced', onStreamResynced);
|
||||
props.connection.off('ended', onStreamEnded);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="game == null || connection == null"><MkLoading/></div>
|
||||
<GameSetting v-else-if="!game.isStarted" :game="game" :connection="connection"/>
|
||||
<div v-if="game == null || (!game.isEnded && connection == null)"><MkLoading/></div>
|
||||
<GameSetting v-else-if="!game.isStarted" :game="game" :connection="connection!"/>
|
||||
<GameBoard v-else :game="game" :connection="connection"/>
|
||||
</template>
|
||||
|
||||
|
@ -47,23 +47,25 @@ async function fetchGame() {
|
|||
if (connection.value) {
|
||||
connection.value.dispose();
|
||||
}
|
||||
connection.value = useStream().useChannel('reversiGame', {
|
||||
gameId: game.value.id,
|
||||
});
|
||||
connection.value.on('started', x => {
|
||||
game.value = x.game;
|
||||
});
|
||||
connection.value.on('canceled', x => {
|
||||
connection.value?.dispose();
|
||||
if (!game.value.isEnded) {
|
||||
connection.value = useStream().useChannel('reversiGame', {
|
||||
gameId: game.value.id,
|
||||
});
|
||||
connection.value.on('started', x => {
|
||||
game.value = x.game;
|
||||
});
|
||||
connection.value.on('canceled', x => {
|
||||
connection.value?.dispose();
|
||||
|
||||
if (x.userId !== $i.id) {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
text: i18n.ts._reversi.gameCanceled,
|
||||
});
|
||||
router.push('/reversi');
|
||||
}
|
||||
});
|
||||
if (x.userId !== $i.id) {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
text: i18n.ts._reversi.gameCanceled,
|
||||
});
|
||||
router.push('/reversi');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
@ -73,9 +73,8 @@ const src = computed({
|
|||
set: (x) => saveSrc(x),
|
||||
});
|
||||
const withRenotes = computed({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
get: () => (defaultStore.reactiveState.tl.value.filter?.withRenotes ?? saveTlFilter('withRenotes', true)),
|
||||
set: (x) => saveTlFilter('withRenotes', x),
|
||||
get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
|
||||
set: (x: boolean) => saveTlFilter('withRenotes', x),
|
||||
});
|
||||
const withReplies = computed({
|
||||
get: () => {
|
||||
|
@ -83,11 +82,10 @@ const withReplies = computed({
|
|||
if (['local', 'social'].includes(src.value) && onlyFiles.value) {
|
||||
return false;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
return defaultStore.reactiveState.tl.value.filter?.withReplies ?? saveTlFilter('withReplies', true);
|
||||
return defaultStore.reactiveState.tl.value.filter.withReplies;
|
||||
}
|
||||
},
|
||||
set: (x) => saveTlFilter('withReplies', x),
|
||||
set: (x: boolean) => saveTlFilter('withReplies', x),
|
||||
});
|
||||
const withBots = computed({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
|
@ -99,16 +97,14 @@ const onlyFiles = computed({
|
|||
if (['local', 'social'].includes(src.value) && withReplies.value) {
|
||||
return false;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
return defaultStore.reactiveState.tl.value.filter?.onlyFiles ?? saveTlFilter('onlyFiles', false);
|
||||
return defaultStore.reactiveState.tl.value.filter.onlyFiles;
|
||||
}
|
||||
},
|
||||
set: (x) => saveTlFilter('onlyFiles', x),
|
||||
set: (x: boolean) => saveTlFilter('onlyFiles', x),
|
||||
});
|
||||
const withSensitive = computed({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
get: () => (defaultStore.reactiveState.tl.value.filter?.withSensitive ?? saveTlFilter('withSensitive', true)),
|
||||
set: (x) => {
|
||||
get: () => defaultStore.reactiveState.tl.value.filter.withSensitive,
|
||||
set: (x: boolean) => {
|
||||
saveTlFilter('withSensitive', x);
|
||||
|
||||
// これだけはクライアント側で完結する処理なので手動でリロード
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import { BroadcastChannel } from 'broadcast-channel';
|
||||
import { defu } from 'defu';
|
||||
import { $i } from '@/account.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { get, set } from '@/scripts/idb-proxy.js';
|
||||
|
@ -80,6 +81,18 @@ export class Storage<T extends StateDef> {
|
|||
this.loaded = this.ready.then(() => this.load());
|
||||
}
|
||||
|
||||
private isPureObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
private mergeState<T>(value: T, def: T): T {
|
||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
||||
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def);
|
||||
return defu(value, def) as T;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private async init(): Promise<void> {
|
||||
await this.migrate();
|
||||
|
||||
|
@ -89,11 +102,11 @@ export class Storage<T extends StateDef> {
|
|||
|
||||
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
|
||||
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
|
||||
this.reactiveState[k].value = this.state[k] = deviceState[k];
|
||||
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceState[k], v.default);
|
||||
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
|
||||
this.reactiveState[k].value = this.state[k] = registryCache[k];
|
||||
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(registryCache[k], v.default);
|
||||
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
|
||||
this.reactiveState[k].value = this.state[k] = deviceAccountState[k];
|
||||
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
|
||||
} else {
|
||||
this.reactiveState[k].value = this.state[k] = v.default;
|
||||
if (_DEV_) console.log('Use default value', k, v.default);
|
||||
|
|
|
@ -24,11 +24,9 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@types/matter-js": "0.19.6",
|
||||
"@types/node": "20.11.5",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "6.19.0",
|
||||
"@typescript-eslint/parser": "6.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"eslint": "8.56.0",
|
||||
"nodemon": "3.0.2",
|
||||
"typescript": "5.3.3"
|
||||
|
@ -37,6 +35,8 @@
|
|||
"built"
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/matter-js": "0.19.6",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"eventemitter3": "5.0.1",
|
||||
"matter-js": "0.19.0",
|
||||
"seedrandom": "3.0.5"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021-2022 syuilo and other contributors
|
||||
Copyright (c) 2021-2024 syuilo and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -81,7 +81,17 @@ module.exports = {
|
|||
// ],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
moduleNameMapper: {
|
||||
// Do not resolve .wasm.js to .wasm by the rule below
|
||||
'^(.+)\\.wasm\\.js$': '$1.wasm.js',
|
||||
// SWC converts @/foo/bar.js to `../../src/foo/bar.js`, and then this rule
|
||||
// converts it again to `../../src/foo/bar` which then can be resolved to
|
||||
// `.ts` files.
|
||||
// See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225
|
||||
// TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can
|
||||
// directly import `.ts` files without this hack.
|
||||
'^((?:\\.{1,2}|[A-Z:])*/.*)\\.js$': '$1',
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "0.0.16",
|
||||
"version": "2024.2.0-beta.3",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"types": "./built/dts/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./built/esm/index.js",
|
||||
|
@ -39,8 +40,8 @@
|
|||
"@swc/jest": "0.2.31",
|
||||
"@types/jest": "29.5.11",
|
||||
"@types/node": "20.11.5",
|
||||
"@typescript-eslint/eslint-plugin": "6.19.0",
|
||||
"@typescript-eslint/parser": "6.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"eslint": "8.56.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
|
@ -52,7 +53,9 @@
|
|||
"typescript": "5.3.3"
|
||||
},
|
||||
"files": [
|
||||
"built"
|
||||
"built",
|
||||
"built/esm",
|
||||
"built/dts"
|
||||
],
|
||||
"dependencies": {
|
||||
"@swc/cli": "0.1.63",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2023.12.2
|
||||
* generatedAt: 2024-01-21T01:01:12.332Z
|
||||
* version: 2024.2.0-beta.2
|
||||
* generatedAt: 2024-01-22T07:11:08.412Z
|
||||
*/
|
||||
|
||||
import type { SwitchCaseResponseType } from '../api.js';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2023.12.2
|
||||
* generatedAt: 2024-01-21T01:01:12.330Z
|
||||
* version: 2024.2.0-beta.2
|
||||
* generatedAt: 2024-01-22T07:11:08.410Z
|
||||
*/
|
||||
|
||||
import type {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2023.12.2
|
||||
* generatedAt: 2024-01-21T01:01:12.328Z
|
||||
* version: 2024.2.0-beta.2
|
||||
* generatedAt: 2024-01-22T07:11:08.408Z
|
||||
*/
|
||||
|
||||
import { operations } from './types.js';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* version: 2023.12.2
|
||||
* generatedAt: 2024-01-21T01:01:12.327Z
|
||||
* version: 2024.2.0-beta.2
|
||||
* generatedAt: 2024-01-22T07:11:08.408Z
|
||||
*/
|
||||
|
||||
import { components } from './types.js';
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
/* eslint @typescript-eslint/no-explicit-any: 0 */
|
||||
|
||||
/*
|
||||
* version: 2023.12.2
|
||||
* generatedAt: 2024-01-21T01:01:12.246Z
|
||||
* version: 2024.2.0-beta.2
|
||||
* generatedAt: 2024-01-22T07:11:08.327Z
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -4602,10 +4602,6 @@ export type components = {
|
|||
endedAt: string | null;
|
||||
isStarted: boolean;
|
||||
isEnded: boolean;
|
||||
form1: Record<string, never> | null;
|
||||
form2: Record<string, never> | null;
|
||||
user1Ready: boolean;
|
||||
user2Ready: boolean;
|
||||
/** Format: id */
|
||||
user1Id: string;
|
||||
/** Format: id */
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@types/node": "20.11.5",
|
||||
"@typescript-eslint/eslint-plugin": "6.19.0",
|
||||
"@typescript-eslint/parser": "6.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"eslint": "8.56.0",
|
||||
"nodemon": "3.0.2",
|
||||
"typescript": "5.3.3"
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@typescript-eslint/parser": "6.19.0",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue