Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31186262fa | ||
|
|
99fb6e864e | ||
|
|
7a41cfe28b | ||
|
|
0a604285a0 |
5
.github/workflows/lint.yml
vendored
@@ -13,7 +13,6 @@ on:
|
||||
- packages/sw/**
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- packages/misskey-mahjong/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/shared/eslint.config.js
|
||||
- .github/workflows/lint.yml
|
||||
@@ -26,7 +25,6 @@ on:
|
||||
- packages/sw/**
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- packages/misskey-mahjong/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/shared/eslint.config.js
|
||||
- .github/workflows/lint.yml
|
||||
@@ -60,7 +58,6 @@ jobs:
|
||||
- sw
|
||||
- misskey-js
|
||||
- misskey-bubble-game
|
||||
- misskey-mahjong
|
||||
- misskey-reversi
|
||||
env:
|
||||
eslint-cache-version: v1
|
||||
@@ -109,6 +106,6 @@ jobs:
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: pnpm --filter misskey-js run build
|
||||
if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'sw' }}
|
||||
- run: pnpm --filter misskey-mahjong --filter misskey-reversi run build
|
||||
- run: pnpm --filter misskey-reversi run build
|
||||
if: ${{ matrix.workspace == 'backend' }}
|
||||
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
|
||||
|
||||
2
.github/workflows/storybook.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
- name: Check pnpm-lock.yaml
|
||||
run: git diff --exit-code pnpm-lock.yaml
|
||||
- name: Build dependent packages
|
||||
run: pnpm -F misskey-js -F misskey-bubble-game -F misskey-mahjong -F misskey-reversi build
|
||||
run: pnpm -F misskey-js -F misskey-bubble-game -F misskey-reversi build
|
||||
- name: Build storybook
|
||||
run: pnpm --filter frontend build-storybook
|
||||
- name: Publish to Chromatic
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
- Enhance: ジョブキューの成功/失敗したジョブも一定数・一定期間保存するようにし、後から問題を調査することを容易に
|
||||
- Enhance: フォローしているユーザーならフォロワー限定投稿のノートでもアンテナで検知できるように
|
||||
(Cherry-picked from https://github.com/yojo-art/cherrypick/pull/568 and https://github.com/team-shahu/misskey/pull/38)
|
||||
- Enhance: ユーザーごとにノートの表示が高速化するように
|
||||
- Fix: システムアカウントの名前がサーバー名と同期されない問題を修正
|
||||
- Fix: 大文字を含むユーザの URL で紹介された場合に 404 エラーを返す問題 #15813
|
||||
- Fix: リードレプリカ設定時にレコードの追加・更新・削除を伴うクエリを発行した際はmasterノードで実行されるように調整( #10897 )
|
||||
|
||||
@@ -26,7 +26,6 @@ COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
|
||||
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
|
||||
COPY --link ["packages/misskey-mahjong/package.json", "./packages/misskey-mahjong/"]
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -57,7 +56,6 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
|
||||
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
|
||||
COPY --link ["packages/misskey-mahjong/package.json", "./packages/misskey-mahjong/"]
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -94,12 +92,10 @@ COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/nod
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-reversi/node_modules ./packages/misskey-reversi/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-bubble-game/node_modules ./packages/misskey-bubble-game/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-mahjong/node_modules ./packages/misskey-mahjong/node_modules
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/built ./built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/built ./packages/misskey-js/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-mahjong/built ./packages/misskey-mahjong/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
|
||||
COPY --chown=misskey:misskey . ./
|
||||
|
||||
282
locales/index.d.ts
vendored
@@ -11002,288 +11002,6 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"useAvatarAsStone": string;
|
||||
};
|
||||
"_mahjong": {
|
||||
/**
|
||||
* 麻雀
|
||||
*/
|
||||
"mahjong": string;
|
||||
/**
|
||||
* ルームに参加
|
||||
*/
|
||||
"joinRoom": string;
|
||||
/**
|
||||
* ルームを作成
|
||||
*/
|
||||
"createRoom": string;
|
||||
/**
|
||||
* 準備完了
|
||||
*/
|
||||
"ready": string;
|
||||
/**
|
||||
* 準備を再開
|
||||
*/
|
||||
"cancelReady": string;
|
||||
/**
|
||||
* 退室
|
||||
*/
|
||||
"leave": string;
|
||||
/**
|
||||
* CPUを追加
|
||||
*/
|
||||
"addCpu": string;
|
||||
/**
|
||||
* 東
|
||||
*/
|
||||
"east": string;
|
||||
/**
|
||||
* 南
|
||||
*/
|
||||
"south": string;
|
||||
/**
|
||||
* 西
|
||||
*/
|
||||
"west": string;
|
||||
/**
|
||||
* 北
|
||||
*/
|
||||
"north": string;
|
||||
/**
|
||||
* ドラ
|
||||
*/
|
||||
"dora": string;
|
||||
/**
|
||||
* 赤ドラ
|
||||
*/
|
||||
"redDora": string;
|
||||
/**
|
||||
* 飜
|
||||
*/
|
||||
"fan": string;
|
||||
"_fanNames": {
|
||||
/**
|
||||
* 満貫
|
||||
*/
|
||||
"mangan": string;
|
||||
/**
|
||||
* 跳満
|
||||
*/
|
||||
"haneman": string;
|
||||
/**
|
||||
* 倍満
|
||||
*/
|
||||
"baiman": string;
|
||||
/**
|
||||
* 三倍満
|
||||
*/
|
||||
"sanbaiman": string;
|
||||
/**
|
||||
* 役満
|
||||
*/
|
||||
"yakuman": string;
|
||||
/**
|
||||
* 数え役満
|
||||
*/
|
||||
"kazoeyakuman": string;
|
||||
};
|
||||
"_yakus": {
|
||||
/**
|
||||
* 立直
|
||||
*/
|
||||
"riichi": string;
|
||||
/**
|
||||
* 一発
|
||||
*/
|
||||
"ippatsu": string;
|
||||
/**
|
||||
* 門前清自摸和
|
||||
*/
|
||||
"tsumo": string;
|
||||
/**
|
||||
* 断么
|
||||
*/
|
||||
"tanyao": string;
|
||||
/**
|
||||
* 平和
|
||||
*/
|
||||
"pinfu": string;
|
||||
/**
|
||||
* 一盃口
|
||||
*/
|
||||
"iipeko": string;
|
||||
/**
|
||||
* 東
|
||||
*/
|
||||
"field-wind-e": string;
|
||||
/**
|
||||
* 南
|
||||
*/
|
||||
"field-wind-s": string;
|
||||
/**
|
||||
* 東
|
||||
*/
|
||||
"seat-wind-e": string;
|
||||
/**
|
||||
* 南
|
||||
*/
|
||||
"seat-wind-s": string;
|
||||
/**
|
||||
* 西
|
||||
*/
|
||||
"seat-wind-w": string;
|
||||
/**
|
||||
* 北
|
||||
*/
|
||||
"seat-wind-n": string;
|
||||
/**
|
||||
* 白
|
||||
*/
|
||||
"white": string;
|
||||
/**
|
||||
* 發
|
||||
*/
|
||||
"green": string;
|
||||
/**
|
||||
* 中
|
||||
*/
|
||||
"red": string;
|
||||
/**
|
||||
* 嶺上開花
|
||||
*/
|
||||
"rinshan": string;
|
||||
/**
|
||||
* 搶槓
|
||||
*/
|
||||
"chankan": string;
|
||||
/**
|
||||
* 海底摸月
|
||||
*/
|
||||
"haitei": string;
|
||||
/**
|
||||
* 河底撈魚
|
||||
*/
|
||||
"hotei": string;
|
||||
/**
|
||||
* 三色同順
|
||||
*/
|
||||
"sanshoku-dojun": string;
|
||||
/**
|
||||
* 三色同刻
|
||||
*/
|
||||
"sanshoku-doko": string;
|
||||
/**
|
||||
* 一気通貫
|
||||
*/
|
||||
"ittsu": string;
|
||||
/**
|
||||
* 混全帯么九
|
||||
*/
|
||||
"chanta": string;
|
||||
/**
|
||||
* 七対子
|
||||
*/
|
||||
"chitoitsu": string;
|
||||
/**
|
||||
* 対々
|
||||
*/
|
||||
"toitoi": string;
|
||||
/**
|
||||
* 三暗刻
|
||||
*/
|
||||
"sananko": string;
|
||||
/**
|
||||
* 混老頭
|
||||
*/
|
||||
"honroto": string;
|
||||
/**
|
||||
* 三槓子
|
||||
*/
|
||||
"sankantsu": string;
|
||||
/**
|
||||
* 小三元
|
||||
*/
|
||||
"shosangen": string;
|
||||
/**
|
||||
* ダブル立直
|
||||
*/
|
||||
"double-riichi": string;
|
||||
/**
|
||||
* 混一色
|
||||
*/
|
||||
"honitsu": string;
|
||||
/**
|
||||
* 清全帯么九
|
||||
*/
|
||||
"junchan": string;
|
||||
/**
|
||||
* ニ盃口
|
||||
*/
|
||||
"ryampeko": string;
|
||||
/**
|
||||
* 清一色
|
||||
*/
|
||||
"chinitsu": string;
|
||||
/**
|
||||
* 国士無双
|
||||
*/
|
||||
"kokushi": string;
|
||||
/**
|
||||
* 国士無双十三面待
|
||||
*/
|
||||
"kokushi-13": string;
|
||||
/**
|
||||
* 四暗刻
|
||||
*/
|
||||
"suanko": string;
|
||||
/**
|
||||
* 四暗刻単騎待
|
||||
*/
|
||||
"suanko-tanki": string;
|
||||
/**
|
||||
* 大三元
|
||||
*/
|
||||
"daisangen": string;
|
||||
/**
|
||||
* 字一色
|
||||
*/
|
||||
"tsuiso": string;
|
||||
/**
|
||||
* 小四喜
|
||||
*/
|
||||
"shosushi": string;
|
||||
/**
|
||||
* 大四喜
|
||||
*/
|
||||
"daisushi": string;
|
||||
/**
|
||||
* 緑一色
|
||||
*/
|
||||
"ryuiso": string;
|
||||
/**
|
||||
* 清老頭
|
||||
*/
|
||||
"chinroto": string;
|
||||
/**
|
||||
* 四槓子
|
||||
*/
|
||||
"sukantsu": string;
|
||||
/**
|
||||
* 九蓮宝燈
|
||||
*/
|
||||
"churen": string;
|
||||
/**
|
||||
* 九連宝灯九面待
|
||||
*/
|
||||
"churen-9": string;
|
||||
/**
|
||||
* 天和
|
||||
*/
|
||||
"tenho": string;
|
||||
/**
|
||||
* 地和
|
||||
*/
|
||||
"chiho": string;
|
||||
};
|
||||
};
|
||||
"_offlineScreen": {
|
||||
/**
|
||||
* オフライン - サーバーに接続できません
|
||||
|
||||
@@ -2926,79 +2926,6 @@ _reversi:
|
||||
showBoardLabels: "盤面に行・列番号を表示"
|
||||
useAvatarAsStone: "石をアイコンにする"
|
||||
|
||||
_mahjong:
|
||||
mahjong: "麻雀"
|
||||
joinRoom: "ルームに参加"
|
||||
createRoom: "ルームを作成"
|
||||
ready: "準備完了"
|
||||
cancelReady: "準備を再開"
|
||||
leave: "退室"
|
||||
addCpu: "CPUを追加"
|
||||
east: "東"
|
||||
south: "南"
|
||||
west: "西"
|
||||
north: "北"
|
||||
dora: "ドラ"
|
||||
redDora: "赤ドラ"
|
||||
fan: "飜"
|
||||
_fanNames:
|
||||
mangan: "満貫"
|
||||
haneman: "跳満"
|
||||
baiman: "倍満"
|
||||
sanbaiman: "三倍満"
|
||||
yakuman: "役満"
|
||||
kazoeyakuman: "数え役満"
|
||||
_yakus:
|
||||
"riichi": "立直"
|
||||
"ippatsu": "一発"
|
||||
"tsumo": "門前清自摸和"
|
||||
"tanyao": "断么"
|
||||
"pinfu": "平和"
|
||||
"iipeko": "一盃口"
|
||||
"field-wind-e": "東"
|
||||
"field-wind-s": "南"
|
||||
"seat-wind-e": "東"
|
||||
"seat-wind-s": "南"
|
||||
"seat-wind-w": "西"
|
||||
"seat-wind-n": "北"
|
||||
"white": "白"
|
||||
"green": "發"
|
||||
"red": "中"
|
||||
"rinshan": "嶺上開花"
|
||||
"chankan": "搶槓"
|
||||
"haitei": "海底摸月"
|
||||
"hotei": "河底撈魚"
|
||||
"sanshoku-dojun": "三色同順"
|
||||
"sanshoku-doko": "三色同刻"
|
||||
"ittsu": "一気通貫"
|
||||
"chanta": "混全帯么九"
|
||||
"chitoitsu": "七対子"
|
||||
"toitoi": "対々"
|
||||
"sananko": "三暗刻"
|
||||
"honroto": "混老頭"
|
||||
"sankantsu": "三槓子"
|
||||
"shosangen": "小三元"
|
||||
"double-riichi": "ダブル立直"
|
||||
"honitsu": "混一色"
|
||||
"junchan": "清全帯么九"
|
||||
"ryampeko": "ニ盃口"
|
||||
"chinitsu": "清一色"
|
||||
"kokushi": "国士無双"
|
||||
"kokushi-13": "国士無双十三面待"
|
||||
"suanko": "四暗刻"
|
||||
"suanko-tanki": "四暗刻単騎待"
|
||||
"daisangen": "大三元"
|
||||
"tsuiso": "字一色"
|
||||
"shosushi": "小四喜"
|
||||
"daisushi": "大四喜"
|
||||
"ryuiso": "緑一色"
|
||||
"chinroto": "清老頭"
|
||||
"sukantsu": "四槓子"
|
||||
"churen": "九蓮宝燈"
|
||||
"churen-9": "九連宝灯九面待"
|
||||
"tenho": "天和"
|
||||
"chiho": "地和"
|
||||
|
||||
_offlineScreen:
|
||||
title: "オフライン - サーバーに接続できません"
|
||||
header: "サーバーに接続できません"
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
"packages/sw",
|
||||
"packages/misskey-js",
|
||||
"packages/misskey-reversi",
|
||||
"packages/misskey-bubble-game",
|
||||
"packages/misskey-mahjong"
|
||||
"packages/misskey-bubble-game"
|
||||
],
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -27,6 +26,7 @@
|
||||
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
|
||||
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
|
||||
"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||
"cli": "cd packages/backend && pnpm cli",
|
||||
"init": "pnpm migrate",
|
||||
"migrate": "cd packages/backend && pnpm migrate",
|
||||
"revert": "cd packages/backend && pnpm revert",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Mahjong1706234054207 {
|
||||
name = 'Mahjong1706234054207'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "mahjong_game" ("id" character varying(32) NOT NULL, "startedAt" TIMESTAMP WITH TIME ZONE, "endedAt" TIMESTAMP WITH TIME ZONE, "user1Id" character varying(32), "user2Id" character varying(32), "user3Id" character varying(32), "user4Id" character varying(32), "isEnded" boolean NOT NULL DEFAULT false, "winnerId" character varying(32), "timeLimitForEachTurn" smallint NOT NULL DEFAULT '90', "logs" jsonb NOT NULL DEFAULT '[]', CONSTRAINT "PK_77db54c0a9785d387e3fbbdd2f0" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" ADD CONSTRAINT "FK_b98c78761a845b69e6540401264" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" ADD CONSTRAINT "FK_f17b0ba519ae28f188a7915ad6f" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" ADD CONSTRAINT "FK_64314ffd3cb59475b0d06330058" FOREIGN KEY ("user3Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" ADD CONSTRAINT "FK_58a75f1ea2a810ae3986f72a0e3" FOREIGN KEY ("user4Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" DROP CONSTRAINT "FK_58a75f1ea2a810ae3986f72a0e3"`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" DROP CONSTRAINT "FK_64314ffd3cb59475b0d06330058"`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" DROP CONSTRAINT "FK_f17b0ba519ae28f188a7915ad6f"`);
|
||||
await queryRunner.query(`ALTER TABLE "mahjong_game" DROP CONSTRAINT "FK_b98c78761a845b69e6540401264"`);
|
||||
await queryRunner.query(`DROP TABLE "mahjong_game"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class CompositeNoteIndex1745378064470 {
|
||||
name = 'CompositeNoteIndex1745378064470';
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE INDEX "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`);
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_5b87d9d19127bd5d92026017a7"`);
|
||||
// Flush all cached Linear Scan Plans and redo statistics for composite index
|
||||
// this is important for Postgres to learn that even in highly complex queries, using this index first can reduce the result set significantly
|
||||
await queryRunner.query(`ANALYZE "user", "note"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
"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",
|
||||
"cli": "node ./built/boot/cli.js",
|
||||
"check:connect": "node ./scripts/check_connect.js",
|
||||
"build": "swc src -d built -D --strip-leading-paths",
|
||||
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
|
||||
@@ -112,7 +113,7 @@
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fastify": "5.2.2",
|
||||
"fastify": "5.3.2",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.6.0",
|
||||
@@ -139,7 +140,6 @@
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"misskey-mahjong": "workspace:*",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"nanoid": "5.1.5",
|
||||
"nested-property": "4.0.0",
|
||||
|
||||
0
packages/backend/scripts/cli.mjs
Normal file
23
packages/backend/src/boot/cli.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { CommandModule } from '@/cli/CommandModule.js';
|
||||
import { NestLogger } from '@/NestLogger.js';
|
||||
import { CommandService } from '@/cli/CommandService.js';
|
||||
|
||||
process.title = 'Misskey Cli';
|
||||
|
||||
Error.stackTraceLimit = Infinity;
|
||||
EventEmitter.defaultMaxListeners = 128;
|
||||
|
||||
const app = await NestFactory.createApplicationContext(CommandModule, {
|
||||
logger: new NestLogger(),
|
||||
});
|
||||
|
||||
const commandService = app.get(CommandService);
|
||||
commandService.ping();
|
||||
23
packages/backend/src/cli/CommandModule.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { CommandService } from './CommandService.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
GlobalModule,
|
||||
CoreModule,
|
||||
],
|
||||
providers: [
|
||||
CommandService,
|
||||
],
|
||||
exports: [
|
||||
CommandService,
|
||||
],
|
||||
})
|
||||
export class CommandModule {}
|
||||
26
packages/backend/src/cli/CommandService.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class CommandService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async ping() {
|
||||
console.log('pong');
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,6 @@ import { ChannelFollowingService } from './ChannelFollowingService.js';
|
||||
import { ChatService } from './ChatService.js';
|
||||
import { RegistryApiService } from './RegistryApiService.js';
|
||||
import { ReversiService } from './ReversiService.js';
|
||||
import { MahjongService } from './MahjongService.js';
|
||||
|
||||
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
||||
import FederationChart from './chart/charts/federation.js';
|
||||
@@ -225,7 +224,6 @@ const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService',
|
||||
const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatService };
|
||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
||||
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
|
||||
const $MahjongService: Provider = { provide: 'MahjongService', useExisting: MahjongService };
|
||||
|
||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||
@@ -376,7 +374,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ChatService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
MahjongService,
|
||||
|
||||
ChartLoggerService,
|
||||
FederationChart,
|
||||
@@ -523,7 +520,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ChatService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$MahjongService,
|
||||
|
||||
$ChartLoggerService,
|
||||
$FederationChart,
|
||||
@@ -671,7 +667,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ChatService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
MahjongService,
|
||||
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
@@ -816,7 +811,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ChatService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$MahjongService,
|
||||
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import * as Reversi from 'misskey-reversi';
|
||||
import * as Mmj from 'misskey-mahjong';
|
||||
import type { MiChannel } from '@/models/Channel.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
@@ -206,78 +205,6 @@ export interface ReversiGameEventTypes {
|
||||
userId: MiUser['id'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface MahjongRoomEventTypes {
|
||||
joined: {
|
||||
index: number;
|
||||
user: Packed<'UserLite'> | null;
|
||||
};
|
||||
changeReadyStates: {
|
||||
user1: boolean;
|
||||
user2: boolean;
|
||||
user3: boolean;
|
||||
user4: boolean;
|
||||
};
|
||||
started: {
|
||||
room: Packed<'MahjongRoomDetailed'>;
|
||||
};
|
||||
nextKyoku: {
|
||||
room: Packed<'MahjongRoomDetailed'>;
|
||||
};
|
||||
tsumo: {
|
||||
house: Mmj.House;
|
||||
tile: number;
|
||||
};
|
||||
dahai: {
|
||||
house: Mmj.House;
|
||||
tile: number;
|
||||
riichi: boolean;
|
||||
};
|
||||
dahaiAndTsumo: {
|
||||
dahaiHouse: Mmj.House;
|
||||
dahaiTile: number;
|
||||
tsumoTile: number;
|
||||
riichi: boolean;
|
||||
};
|
||||
ponned: {
|
||||
caller: Mmj.House;
|
||||
callee: Mmj.House;
|
||||
tiles: readonly [number, number, number];
|
||||
};
|
||||
kanned: {
|
||||
caller: Mmj.House;
|
||||
callee: Mmj.House;
|
||||
tiles: readonly [number, number, number, number];
|
||||
rinsyan: number;
|
||||
};
|
||||
ciied: {
|
||||
caller: Mmj.House;
|
||||
callee: Mmj.House;
|
||||
tiles: readonly [number, number, number];
|
||||
};
|
||||
ronned: {
|
||||
callers: Mmj.House[];
|
||||
callee: Mmj.House;
|
||||
handTiles: Record<Mmj.House, number[]>;
|
||||
};
|
||||
ryuukyoku: object;
|
||||
ankanned: {
|
||||
house: Mmj.House;
|
||||
tiles: readonly [number, number, number, number];
|
||||
rinsyan: number;
|
||||
};
|
||||
kakanned: {
|
||||
house: Mmj.House;
|
||||
tiles: readonly [number, number, number, number];
|
||||
rinsyan: number;
|
||||
from: Mmj.House;
|
||||
};
|
||||
tsumoHora: {
|
||||
house: Mmj.House;
|
||||
handTiles: number[];
|
||||
tsumoTile: number;
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// 辞書(interface or type)から{ type, body }ユニオンを定義
|
||||
@@ -395,10 +322,6 @@ export type GlobalEvents = {
|
||||
name: `reversiGameStream:${MiReversiGame['id']}`;
|
||||
payload: EventTypesToEventPayload<ReversiGameEventTypes>;
|
||||
};
|
||||
mahjongRoom: {
|
||||
name: `mahjongRoomStream:${string}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<MahjongRoomEventTypes>>;
|
||||
};
|
||||
};
|
||||
|
||||
// API event definitions
|
||||
@@ -508,9 +431,4 @@ export class GlobalEventService {
|
||||
public publishReversiGameStream<K extends keyof ReversiGameEventTypes>(gameId: MiReversiGame['id'], type: K, value?: ReversiGameEventTypes[K]): void {
|
||||
this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishMahjongRoomStream<K extends keyof MahjongRoomEventTypes>(roomId: string, type: K, value?: MahjongRoomEventTypes[K]): void {
|
||||
this.publish(`mahjongRoomStream:${roomId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,734 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { IsNull, LessThan, MoreThan } from 'typeorm';
|
||||
import * as Mmj from 'misskey-mahjong';
|
||||
import type {
|
||||
MiMahjongGame,
|
||||
MahjongGamesRepository,
|
||||
} from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { Serialized } from '@/types.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
||||
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec
|
||||
const CALL_AND_RON_ASKING_TIMEOUT_MS = 1000 * 10; // 10sec
|
||||
const TURN_TIMEOUT_MS = 1000 * 30; // 30sec
|
||||
const NEXT_KYOKU_CONFIRMATION_TIMEOUT_MS = 1000 * 15; // 15sec
|
||||
|
||||
type Room = {
|
||||
id: string;
|
||||
user1Id: MiUser['id'];
|
||||
user2Id: MiUser['id'] | null;
|
||||
user3Id: MiUser['id'] | null;
|
||||
user4Id: MiUser['id'] | null;
|
||||
user1: Packed<'UserLite'> | null;
|
||||
user2: Packed<'UserLite'> | null;
|
||||
user3: Packed<'UserLite'> | null;
|
||||
user4: Packed<'UserLite'> | null;
|
||||
user1Ai?: boolean;
|
||||
user2Ai?: boolean;
|
||||
user3Ai?: boolean;
|
||||
user4Ai?: boolean;
|
||||
user1Ready: boolean;
|
||||
user2Ready: boolean;
|
||||
user3Ready: boolean;
|
||||
user4Ready: boolean;
|
||||
user1Offline?: boolean;
|
||||
user2Offline?: boolean;
|
||||
user3Offline?: boolean;
|
||||
user4Offline?: boolean;
|
||||
isStarted?: boolean;
|
||||
timeLimitForEachTurn: number;
|
||||
|
||||
gameState?: Mmj.MasterState;
|
||||
};
|
||||
|
||||
type CallingAnswers = {
|
||||
pon: null | boolean;
|
||||
cii: null | false | 'x__' | '_x_' | '__x';
|
||||
kan: null | boolean;
|
||||
ron: {
|
||||
e: null | boolean;
|
||||
s: null | boolean;
|
||||
w: null | boolean;
|
||||
n: null | boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type NextKyokuConfirmation = {
|
||||
user1: boolean;
|
||||
user2: boolean;
|
||||
user3: boolean;
|
||||
user4: boolean;
|
||||
};
|
||||
|
||||
function getUserIdOfHouse(room: Room, mj: Mmj.MasterGameEngine, house: Mmj.House): MiUser['id'] {
|
||||
return mj.user1House === house ? room.user1Id : mj.user2House === house ? room.user2Id : mj.user3House === house ? room.user3Id : room.user4Id;
|
||||
}
|
||||
|
||||
function getHouseOfUserId(room: Room, mj: Mmj.MasterGameEngine, userId: MiUser['id']): Mmj.House {
|
||||
return userId === room.user1Id ? mj.user1House : userId === room.user2Id ? mj.user2House : userId === room.user3Id ? mj.user3House : mj.user4House;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MahjongService implements OnApplicationShutdown, OnModuleInit {
|
||||
private notificationService: NotificationService;
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
//@Inject(DI.mahjongGamesRepository)
|
||||
//private mahjongGamesRepository: MahjongGamesRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
async onModuleInit() {
|
||||
this.notificationService = this.moduleRef.get(NotificationService.name);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async saveRoom(room: Room) {
|
||||
await this.redisClient.set(`mahjong:room:${room.id}`, JSON.stringify(room), 'EX', 60 * 30);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createRoom(user: MiUser): Promise<Room> {
|
||||
const room: Room = {
|
||||
id: this.idService.gen(),
|
||||
user1Id: user.id,
|
||||
user2Id: null,
|
||||
user3Id: null,
|
||||
user4Id: null,
|
||||
user1: await this.userEntityService.pack(user),
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
user3Ready: false,
|
||||
user4Ready: false,
|
||||
timeLimitForEachTurn: 30,
|
||||
};
|
||||
await this.saveRoom(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getRoom(id: Room['id']): Promise<Room | null> {
|
||||
const room = await this.redisClient.get(`mahjong:room:${id}`);
|
||||
if (!room) return null;
|
||||
const parsed = JSON.parse(room);
|
||||
return {
|
||||
...parsed,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async joinRoom(roomId: Room['id'], user: MiUser): Promise<Room | null> {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
if (room.user1Id === user.id) return room;
|
||||
if (room.user2Id === user.id) return room;
|
||||
if (room.user3Id === user.id) return room;
|
||||
if (room.user4Id === user.id) return room;
|
||||
if (room.user2Id === null) {
|
||||
room.user2Id = user.id;
|
||||
room.user2 = await this.userEntityService.pack(user);
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 2, user: room.user2 });
|
||||
return room;
|
||||
}
|
||||
if (room.user3Id === null) {
|
||||
room.user3Id = user.id;
|
||||
room.user3 = await this.userEntityService.pack(user);
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 3, user: room.user3 });
|
||||
return room;
|
||||
}
|
||||
if (room.user4Id === null) {
|
||||
room.user4Id = user.id;
|
||||
room.user4 = await this.userEntityService.pack(user);
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 4, user: room.user4 });
|
||||
return room;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async addAi(roomId: Room['id'], user: MiUser): Promise<Room | null> {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
if (room.user1Id !== user.id) throw new Error('access denied');
|
||||
|
||||
if (room.user2Id == null && !room.user2Ai) {
|
||||
room.user2Ai = true;
|
||||
room.user2Ready = true;
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 2, user: null });
|
||||
return room;
|
||||
}
|
||||
if (room.user3Id == null && !room.user3Ai) {
|
||||
room.user3Ai = true;
|
||||
room.user3Ready = true;
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 3, user: null });
|
||||
return room;
|
||||
}
|
||||
if (room.user4Id == null && !room.user4Ai) {
|
||||
room.user4Ai = true;
|
||||
room.user4Ready = true;
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'joined', { index: 4, user: null });
|
||||
return room;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async leaveRoom(roomId: Room['id'], user: MiUser): Promise<Room | null> {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
if (room.user1Id === user.id) {
|
||||
room.user1Id = null;
|
||||
room.user1 = null;
|
||||
await this.saveRoom(room);
|
||||
return room;
|
||||
}
|
||||
if (room.user2Id === user.id) {
|
||||
room.user2Id = null;
|
||||
room.user2 = null;
|
||||
await this.saveRoom(room);
|
||||
return room;
|
||||
}
|
||||
if (room.user3Id === user.id) {
|
||||
room.user3Id = null;
|
||||
room.user3 = null;
|
||||
await this.saveRoom(room);
|
||||
return room;
|
||||
}
|
||||
if (room.user4Id === user.id) {
|
||||
room.user4Id = null;
|
||||
room.user4 = null;
|
||||
await this.saveRoom(room);
|
||||
return room;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async changeReadyState(roomId: Room['id'], user: MiUser, ready: boolean): Promise<void> {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
if (room.user1Id === user.id) {
|
||||
room.user1Ready = ready;
|
||||
await this.saveRoom(room);
|
||||
}
|
||||
if (room.user2Id === user.id) {
|
||||
room.user2Ready = ready;
|
||||
await this.saveRoom(room);
|
||||
}
|
||||
if (room.user3Id === user.id) {
|
||||
room.user3Ready = ready;
|
||||
await this.saveRoom(room);
|
||||
}
|
||||
if (room.user4Id === user.id) {
|
||||
room.user4Ready = ready;
|
||||
await this.saveRoom(room);
|
||||
}
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'changeReadyStates', {
|
||||
user1: room.user1Ready,
|
||||
user2: room.user2Ready,
|
||||
user3: room.user3Ready,
|
||||
user4: room.user4Ready,
|
||||
});
|
||||
|
||||
if (room.user1Ready && room.user2Ready && room.user3Ready && room.user4Ready) {
|
||||
await this.startGame(room);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async startGame(room: Room) {
|
||||
if (!room.user1Ready || !room.user2Ready || !room.user3Ready || !room.user4Ready) {
|
||||
throw new Error('Not ready');
|
||||
}
|
||||
|
||||
room.gameState = Mmj.MasterGameEngine.createInitialState();
|
||||
room.isStarted = true;
|
||||
await this.saveRoom(room);
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'started', { room: room });
|
||||
|
||||
this.kyokuStarted(room);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private kyokuStarted(room: Room) {
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
|
||||
this.waitForTurn(room, mj.turn, mj);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async answer(room: Room, mj: Mmj.MasterGameEngine, answers: CallingAnswers) {
|
||||
const res = mj.commit_resolveCallingInterruption({
|
||||
pon: answers.pon ?? false,
|
||||
cii: answers.cii ?? false,
|
||||
kan: answers.kan ?? false,
|
||||
ron: [...(answers.ron.e ? ['e'] : []), ...(answers.ron.s ? ['s'] : []), ...(answers.ron.w ? ['w'] : []), ...(answers.ron.n ? ['n'] : [])] as Mmj.House[],
|
||||
});
|
||||
room.gameState = mj.getState();
|
||||
await this.saveRoom(room);
|
||||
|
||||
switch (res.type) {
|
||||
case 'tsumo':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'tsumo', { house: res.house, tile: res.tile });
|
||||
this.waitForTurn(room, res.turn, mj);
|
||||
break;
|
||||
case 'ponned':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ponned', { caller: res.caller, callee: res.callee, tiles: res.tiles });
|
||||
this.waitForTurn(room, res.turn, mj);
|
||||
break;
|
||||
case 'kanned':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'kanned', { caller: res.caller, callee: res.callee, tiles: res.tiles, rinsyan: res.rinsyan });
|
||||
this.waitForTurn(room, res.turn, mj);
|
||||
break;
|
||||
case 'ciied':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ciied', { caller: res.caller, callee: res.callee, tiles: res.tiles });
|
||||
this.waitForTurn(room, res.turn, mj);
|
||||
break;
|
||||
case 'ronned':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ronned', {
|
||||
callers: res.callers,
|
||||
callee: res.callee,
|
||||
handTiles: {
|
||||
e: mj.handTiles.e,
|
||||
s: mj.handTiles.s,
|
||||
w: mj.handTiles.w,
|
||||
n: mj.handTiles.n,
|
||||
},
|
||||
});
|
||||
this.endKyoku(room, mj);
|
||||
break;
|
||||
case 'ryuukyoku':
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ryuukyoku', {
|
||||
});
|
||||
this.endKyoku(room, mj);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async endKyoku(room: Room, mj: Mmj.MasterGameEngine) {
|
||||
const confirmation: NextKyokuConfirmation = {
|
||||
user1: false,
|
||||
user2: false,
|
||||
user3: false,
|
||||
user4: false,
|
||||
};
|
||||
this.redisClient.set(`mahjong:gameNextKyokuConfirmation:${room.id}`, JSON.stringify(confirmation));
|
||||
const waitingStartedAt = Date.now();
|
||||
const interval = setInterval(async () => {
|
||||
const confirmationRaw = await this.redisClient.get(`mahjong:gameNextKyokuConfirmation:${room.id}`);
|
||||
if (confirmationRaw == null) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
const confirmation = JSON.parse(confirmationRaw) as NextKyokuConfirmation;
|
||||
const allConfirmed = confirmation.user1 && confirmation.user2 && confirmation.user3 && confirmation.user4;
|
||||
if (allConfirmed || (Date.now() - waitingStartedAt > NEXT_KYOKU_CONFIRMATION_TIMEOUT_MS)) {
|
||||
await this.redisClient.del(`mahjong:gameNextKyokuConfirmation:${room.id}`);
|
||||
clearInterval(interval);
|
||||
this.nextKyoku(room, mj);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async nextKyoku(room: Room, mj: Mmj.MasterGameEngine) {
|
||||
const res = mj.commit_nextKyoku();
|
||||
room.gameState = mj.getState();
|
||||
await this.saveRoom(room);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'nextKyoku', {
|
||||
room: room,
|
||||
});
|
||||
this.kyokuStarted(room);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async dahai(room: Room, mj: Mmj.MasterGameEngine, house: Mmj.House, tile: Mmj.TileId, riichi = false) {
|
||||
const res = mj.commit_dahai(house, tile, riichi);
|
||||
room.gameState = mj.getState();
|
||||
await this.saveRoom(room);
|
||||
|
||||
const aiHouses = [[1, room.user1Ai], [2, room.user2Ai], [3, room.user3Ai], [4, room.user4Ai]].filter(([id, ai]) => ai).map(([id, ai]) => mj.getHouse(id));
|
||||
|
||||
if (res.ryuukyoku) {
|
||||
this.endKyoku(room, mj);
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ryuukyoku', {
|
||||
});
|
||||
} else if (res.asking) {
|
||||
const answers: CallingAnswers = {
|
||||
pon: null,
|
||||
cii: null,
|
||||
kan: null,
|
||||
ron: {
|
||||
e: null,
|
||||
s: null,
|
||||
w: null,
|
||||
n: null,
|
||||
},
|
||||
};
|
||||
|
||||
// リーチ中はポン、チー、カンできない
|
||||
if (res.canPonHouse != null && mj.riichis[res.canPonHouse]) {
|
||||
answers.pon = false;
|
||||
}
|
||||
if (res.canCiiHouse != null && mj.riichis[res.canCiiHouse]) {
|
||||
answers.cii = false;
|
||||
}
|
||||
if (res.canKanHouse != null && mj.riichis[res.canKanHouse]) {
|
||||
answers.kan = false;
|
||||
}
|
||||
|
||||
if (aiHouses.includes(res.canPonHouse)) {
|
||||
// TODO: ちゃんと思考するようにする
|
||||
answers.pon = Math.random() < 0.25;
|
||||
}
|
||||
if (aiHouses.includes(res.canCiiHouse)) {
|
||||
// TODO: ちゃんと思考するようにする
|
||||
//answers.cii = Math.random() < 0.25;
|
||||
answers.cii = false;
|
||||
}
|
||||
if (aiHouses.includes(res.canKanHouse)) {
|
||||
// TODO: ちゃんと思考するようにする
|
||||
answers.kan = Math.random() < 0.25;
|
||||
}
|
||||
for (const h of res.canRonHouses) {
|
||||
if (aiHouses.includes(h)) {
|
||||
// TODO: ちゃんと思考するようにする
|
||||
}
|
||||
}
|
||||
|
||||
this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(answers));
|
||||
const waitingStartedAt = Date.now();
|
||||
const interval = setInterval(async () => {
|
||||
const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`);
|
||||
if (current == null) throw new Error('arienai (gameCallingAsking)');
|
||||
const currentAnswers = JSON.parse(current) as CallingAnswers;
|
||||
const allAnswered = !(
|
||||
(res.canPonHouse != null && currentAnswers.pon == null) ||
|
||||
(res.canCiiHouse != null && currentAnswers.cii == null) ||
|
||||
(res.canKanHouse != null && currentAnswers.kan == null) ||
|
||||
(res.canRonHouses.includes('e') && currentAnswers.ron.e == null) ||
|
||||
(res.canRonHouses.includes('s') && currentAnswers.ron.s == null) ||
|
||||
(res.canRonHouses.includes('w') && currentAnswers.ron.w == null) ||
|
||||
(res.canRonHouses.includes('n') && currentAnswers.ron.n == null)
|
||||
);
|
||||
if (allAnswered || (Date.now() - waitingStartedAt > CALL_AND_RON_ASKING_TIMEOUT_MS)) {
|
||||
console.log(allAnswered ? 'ask all answerd' : 'ask timeout');
|
||||
await this.redisClient.del(`mahjong:gameCallingAsking:${room.id}`);
|
||||
clearInterval(interval);
|
||||
this.answer(room, mj, currentAnswers);
|
||||
return;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'dahai', { house: house, tile, riichi });
|
||||
} else {
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'dahaiAndTsumo', { dahaiHouse: house, dahaiTile: tile, tsumoTile: res.tsumoTile, riichi });
|
||||
|
||||
this.waitForTurn(room, res.next, mj);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async confirmNextKyoku(roomId: Room['id'], user: MiUser) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||
const confirmationRaw = await this.redisClient.get(`mahjong:gameNextKyokuConfirmation:${room.id}`);
|
||||
if (confirmationRaw == null) return;
|
||||
const confirmation = JSON.parse(confirmationRaw) as NextKyokuConfirmation;
|
||||
if (user.id === room.user1Id) confirmation.user1 = true;
|
||||
if (user.id === room.user2Id) confirmation.user2 = true;
|
||||
if (user.id === room.user3Id) confirmation.user3 = true;
|
||||
if (user.id === room.user4Id) confirmation.user4 = true;
|
||||
await this.redisClient.set(`mahjong:gameNextKyokuConfirmation:${room.id}`, JSON.stringify(confirmation));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_dahai(roomId: MiMahjongGame['id'], user: MiUser, tile: Mmj.TileId, riichi = false) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myHouse = getHouseOfUserId(room, mj, user.id);
|
||||
|
||||
await this.clearTurnWaitingTimer(room.id);
|
||||
|
||||
await this.dahai(room, mj, myHouse, tile, riichi);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_ankan(roomId: MiMahjongGame['id'], user: MiUser, tile: Mmj.TileId) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myHouse = getHouseOfUserId(room, mj, user.id);
|
||||
|
||||
await this.clearTurnWaitingTimer(room.id);
|
||||
|
||||
const res = mj.commit_ankan(myHouse, tile);
|
||||
room.gameState = mj.getState();
|
||||
await this.saveRoom(room);
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'ankanned', { house: myHouse, tiles: res.tiles, rinsyan: res.rinsyan });
|
||||
|
||||
this.waitForTurn(room, myHouse, mj);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_kakan(roomId: MiMahjongGame['id'], user: MiUser, tile: Mmj.TileId) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myHouse = getHouseOfUserId(room, mj, user.id);
|
||||
|
||||
await this.clearTurnWaitingTimer(room.id);
|
||||
|
||||
const res = mj.commit_kakan(myHouse, tile);
|
||||
room.gameState = mj.getState();
|
||||
await this.saveRoom(room);
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'kakanned', { house: myHouse, tiles: res.tiles, rinsyan: res.rinsyan, from: res.from });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_tsumoHora(roomId: MiMahjongGame['id'], user: MiUser) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myHouse = getHouseOfUserId(room, mj, user.id);
|
||||
|
||||
await this.clearTurnWaitingTimer(room.id);
|
||||
|
||||
const res = mj.commit_tsumoHora(myHouse);
|
||||
room.gameState = mj.getState();
|
||||
await this.saveRoom(room);
|
||||
|
||||
this.globalEventService.publishMahjongRoomStream(room.id, 'tsumoHora', { house: myHouse, handTiles: res.handTiles, tsumoTile: res.tsumoTile });
|
||||
|
||||
this.endKyoku(room, mj);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_ronHora(roomId: MiMahjongGame['id'], user: MiUser) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myHouse = getHouseOfUserId(room, mj, user.id);
|
||||
|
||||
// TODO: 自分に回答する権利がある状態かバリデーション
|
||||
|
||||
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||
const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`);
|
||||
if (current == null) throw new Error('no asking found');
|
||||
const currentAnswers = JSON.parse(current) as CallingAnswers;
|
||||
currentAnswers.ron[myHouse] = true;
|
||||
await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_pon(roomId: MiMahjongGame['id'], user: MiUser) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
// TODO: 自分に回答する権利がある状態かバリデーション
|
||||
|
||||
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||
const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`);
|
||||
if (current == null) throw new Error('no asking found');
|
||||
const currentAnswers = JSON.parse(current) as CallingAnswers;
|
||||
currentAnswers.pon = true;
|
||||
await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_kan(roomId: MiMahjongGame['id'], user: MiUser) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
// TODO: 自分に回答する権利がある状態かバリデーション
|
||||
|
||||
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||
const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`);
|
||||
if (current == null) throw new Error('no asking found');
|
||||
const currentAnswers = JSON.parse(current) as CallingAnswers;
|
||||
currentAnswers.kan = true;
|
||||
await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_cii(roomId: MiMahjongGame['id'], user: MiUser, pattern: 'x__' | '_x_' | '__x') {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
// TODO: 自分に回答する権利がある状態かバリデーション
|
||||
|
||||
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||
const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`);
|
||||
if (current == null) throw new Error('no asking found');
|
||||
const currentAnswers = JSON.parse(current) as CallingAnswers;
|
||||
currentAnswers.cii = pattern;
|
||||
await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async commit_nop(roomId: MiMahjongGame['id'], user: MiUser) {
|
||||
const room = await this.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
if (room.gameState == null) return;
|
||||
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myHouse = getHouseOfUserId(room, mj, user.id);
|
||||
|
||||
// TODO: この辺の処理はアトミックに行いたいけどJSONサポートはRedis Stackが必要
|
||||
const current = await this.redisClient.get(`mahjong:gameCallingAsking:${room.id}`);
|
||||
if (current == null) throw new Error('no asking found');
|
||||
const currentAnswers = JSON.parse(current) as CallingAnswers;
|
||||
if (mj.askings.pon?.caller === myHouse) currentAnswers.pon = false;
|
||||
if (mj.askings.cii?.caller === myHouse) currentAnswers.cii = false;
|
||||
if (mj.askings.kan?.caller === myHouse) currentAnswers.kan = false;
|
||||
if (mj.askings.ron != null && mj.askings.ron.callers.includes(myHouse)) currentAnswers.ron[myHouse] = false;
|
||||
await this.redisClient.set(`mahjong:gameCallingAsking:${room.id}`, JSON.stringify(currentAnswers));
|
||||
}
|
||||
|
||||
/**
|
||||
* プレイヤーの行動(打牌、加槓、暗槓、ツモ和了)を待つ
|
||||
* 制限時間が過ぎたらツモ切り
|
||||
* NOTE: 時間切れチェックが行われたときにタイミングによっては次のwaitingが始まっている場合があることを考慮し、Setに一意のIDを格納する構造としている
|
||||
* @param room
|
||||
* @param house
|
||||
* @param mj
|
||||
*/
|
||||
@bindThis
|
||||
private async waitForTurn(room: Room, house: Mmj.House, mj: Mmj.MasterGameEngine) {
|
||||
const aiHouses = [[1, room.user1Ai], [2, room.user2Ai], [3, room.user3Ai], [4, room.user4Ai]].filter(([id, ai]) => ai).map(([id, ai]) => mj.getHouse(id));
|
||||
|
||||
if (mj.riichis[house]) {
|
||||
// リーチ時はアガリ牌でない限りツモ切り
|
||||
if (!Mmj.isAgarikei(mj.handTileTypes[house])) {
|
||||
setTimeout(() => {
|
||||
this.dahai(room, mj, house, mj.handTiles[house].at(-1));
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (aiHouses.includes(house)) {
|
||||
setTimeout(() => {
|
||||
this.dahai(room, mj, house, mj.handTiles[house].at(-1));
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = Math.random().toString(36).slice(2);
|
||||
console.log('waitForTurn', house, id);
|
||||
this.redisClient.sadd(`mahjong:gameTurnWaiting:${room.id}`, id);
|
||||
const waitingStartedAt = Date.now();
|
||||
const interval = setInterval(async () => {
|
||||
const waiting = await this.redisClient.sismember(`mahjong:gameTurnWaiting:${room.id}`, id);
|
||||
if (waiting === 0) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
if (Date.now() - waitingStartedAt > TURN_TIMEOUT_MS) {
|
||||
await this.redisClient.srem(`mahjong:gameTurnWaiting:${room.id}`, id);
|
||||
console.log('turn timeout', house, id);
|
||||
clearInterval(interval);
|
||||
const handTiles = mj.handTiles[house];
|
||||
await this.dahai(room, mj, house, handTiles.at(-1));
|
||||
return;
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* プレイヤーが行動(打牌、加槓、暗槓、ツモ和了)したら呼ぶ
|
||||
* @param roomId
|
||||
*/
|
||||
@bindThis
|
||||
private async clearTurnWaitingTimer(roomId: Room['id']) {
|
||||
await this.redisClient.del(`mahjong:gameTurnWaiting:${roomId}`);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packState(room: Room, me: MiUser) {
|
||||
const mj = new Mmj.MasterGameEngine(room.gameState);
|
||||
const myIndex = room.user1Id === me.id ? 1 : room.user2Id === me.id ? 2 : room.user3Id === me.id ? 3 : 4;
|
||||
return mj.createPlayerState(myIndex);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packRoom(room: Room, me: MiUser) {
|
||||
if (room.gameState) {
|
||||
return {
|
||||
...room,
|
||||
gameState: this.packState(room, me),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...room,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,5 @@ export const DI = {
|
||||
chatRoomInvitationsRepository: Symbol('chatRoomInvitationsRepository'),
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
mahjongGamesRepository: Symbol('mahjongGamesRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
||||
@@ -67,7 +67,6 @@ import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessage
|
||||
import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
|
||||
import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js';
|
||||
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
|
||||
import { packedMahjongRoomDetailedSchema } from '@/models/json-schema/mahjong-room.js';
|
||||
|
||||
export const refs = {
|
||||
UserLite: packedUserLiteSchema,
|
||||
@@ -132,7 +131,6 @@ export const refs = {
|
||||
ChatRoom: packedChatRoomSchema,
|
||||
ChatRoomInvitation: packedChatRoomInvitationSchema,
|
||||
ChatRoomMembership: packedChatRoomMembershipSchema,
|
||||
MahjongRoomDetailed: packedMahjongRoomDetailedSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('mahjong_game')
|
||||
export class MiMahjongGame {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public startedAt: Date | null;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public endedAt: Date | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public user1Id: MiUser['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user1: MiUser | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public user2Id: MiUser['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user2: MiUser | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public user3Id: MiUser['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user3: MiUser | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public user4Id: MiUser['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user4: MiUser | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isEnded: boolean;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public winnerId: MiUser['id'] | null;
|
||||
|
||||
// in sec
|
||||
@Column('smallint', {
|
||||
default: 90,
|
||||
})
|
||||
public timeLimitForEachTurn: number;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public logs: number[][];
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { MiUser } from './User.js';
|
||||
import { MiChannel } from './Channel.js';
|
||||
import type { MiDriveFile } from './DriveFile.js';
|
||||
|
||||
@Index(['userId', 'id'])
|
||||
@Entity('note')
|
||||
export class MiNote {
|
||||
@PrimaryColumn(id())
|
||||
@@ -65,7 +66,6 @@ export class MiNote {
|
||||
})
|
||||
public cw: string | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The ID of author.',
|
||||
|
||||
@@ -82,7 +82,6 @@ import {
|
||||
MiChatRoomMembership,
|
||||
MiChatRoomInvitation,
|
||||
MiChatApproval,
|
||||
MiMahjongGame,
|
||||
} from './_.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DataSource } from 'typeorm';
|
||||
@@ -531,12 +530,6 @@ const $reversiGamesRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $mahjongGamesRepository: Provider = {
|
||||
provide: DI.mahjongGamesRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiMahjongGame),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [
|
||||
@@ -614,7 +607,6 @@ const $mahjongGamesRepository: Provider = {
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$mahjongGamesRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
@@ -691,7 +683,6 @@ const $mahjongGamesRepository: Provider = {
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$mahjongGamesRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {
|
||||
|
||||
@@ -51,7 +51,6 @@ import { MiGalleryLike } from '@/models/GalleryLike.js';
|
||||
import { MiGalleryPost } from '@/models/GalleryPost.js';
|
||||
import { MiHashtag } from '@/models/Hashtag.js';
|
||||
import { MiInstance } from '@/models/Instance.js';
|
||||
import { MiMahjongGame } from '@/models/MahjongGame.js';
|
||||
import { MiMeta } from '@/models/Meta.js';
|
||||
import { MiModerationLog } from '@/models/ModerationLog.js';
|
||||
import { MiMuting } from '@/models/Muting.js';
|
||||
@@ -233,7 +232,6 @@ export {
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiMahjongGame,
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
|
||||
@@ -312,4 +310,3 @@ export type ChatRoomInvitationsRepository = Repository<MiChatRoomInvitation> & M
|
||||
export type ChatApprovalsRepository = Repository<MiChatApproval> & MiRepository<MiChatApproval>;
|
||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
|
||||
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
|
||||
export type MahjongGamesRepository = Repository<MiMahjongGame> & MiRepository<MiMahjongGame>;
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedMahjongRoomDetailedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
startedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
endedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
isStarted: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isEnded: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user1Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user2Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
user3Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
user4Id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
user1: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'User',
|
||||
},
|
||||
user2: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'User',
|
||||
},
|
||||
user3: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'User',
|
||||
},
|
||||
user4: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'User',
|
||||
},
|
||||
user1Ai: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user2Ai: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user3Ai: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user4Ai: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user1Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user2Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user3Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user4Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
timeLimitForEachTurn: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -84,7 +84,6 @@ import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
|
||||
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { MiMahjongGame } from '@/models/MahjongGame.js';
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiSystemAccount } from '@/models/SystemAccount.js';
|
||||
|
||||
@@ -258,7 +257,6 @@ export const entities = [
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiMahjongGame,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ import { ChatUserChannelService } from './api/stream/channels/chat-user.js';
|
||||
import { ChatRoomChannelService } from './api/stream/channels/chat-room.js';
|
||||
import { ReversiChannelService } from './api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
|
||||
import { MahjongRoomChannelService } from './api/stream/channels/mahjong-room.js';
|
||||
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
||||
|
||||
@Module({
|
||||
@@ -91,7 +90,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
||||
ChatRoomChannelService,
|
||||
ReversiChannelService,
|
||||
ReversiGameChannelService,
|
||||
MahjongRoomChannelService,
|
||||
HomeTimelineChannelService,
|
||||
HybridTimelineChannelService,
|
||||
LocalTimelineChannelService,
|
||||
|
||||
@@ -294,12 +294,6 @@ export * as 'invite/create' from './endpoints/invite/create.js';
|
||||
export * as 'invite/delete' from './endpoints/invite/delete.js';
|
||||
export * as 'invite/limit' from './endpoints/invite/limit.js';
|
||||
export * as 'invite/list' from './endpoints/invite/list.js';
|
||||
export * as 'mahjong/cancel-match' from './endpoints/mahjong/cancel-match.js';
|
||||
export * as 'mahjong/create-room' from './endpoints/mahjong/create-room.js';
|
||||
export * as 'mahjong/games' from './endpoints/mahjong/games.js';
|
||||
export * as 'mahjong/join-room' from './endpoints/mahjong/join-room.js';
|
||||
export * as 'mahjong/show-room' from './endpoints/mahjong/show-room.js';
|
||||
export * as 'mahjong/verify' from './endpoints/mahjong/verify.js';
|
||||
export * as 'meta' from './endpoints/meta.js';
|
||||
export * as 'miauth/gen-token' from './endpoints/miauth/gen-token.js';
|
||||
export * as 'mute/create' from './endpoints/mute/create.js';
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* 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';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private reversiService: ReversiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.userId) {
|
||||
await this.reversiService.matchSpecificUserCancel(me, ps.userId);
|
||||
return;
|
||||
} else {
|
||||
await this.reversiService.matchAnyUserCancel(me);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { MahjongService } from '@/core/MahjongService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'MahjongRoomDetailed',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private mahjongService: MahjongService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.mahjongService.createRoom(me);
|
||||
return await this.mahjongService.packRoom(room, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { ReversiGamesRepository } from '@/models/_.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: { ref: 'ReversiGameLite' },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
my: { type: 'boolean', default: false },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId)
|
||||
.innerJoinAndSelect('game.user1', 'user1')
|
||||
.innerJoinAndSelect('game.user2', 'user2');
|
||||
|
||||
if (ps.my && me) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.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();
|
||||
|
||||
return await this.reversiGameEntityService.packLiteMany(games);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { MahjongService } from '@/core/MahjongService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: '370e42b0-2a67-4306-9328-51c5f568f110',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'MahjongRoomDetailed',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private mahjongService: MahjongService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.mahjongService.getRoom(ps.roomId);
|
||||
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
await this.mahjongService.joinRoom(room.id, me);
|
||||
|
||||
return await this.mahjongService.packRoom(room, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { MahjongService } from '@/core/MahjongService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'd77df68f-06f3-492b-9078-e6f72f4acf23',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'MahjongRoomDetailed',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private mahjongService: MahjongService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.mahjongService.getRoom(ps.roomId);
|
||||
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
return await this.mahjongService.packRoom(room, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import { ChatUserChannelService } from './channels/chat-user.js';
|
||||
import { ChatRoomChannelService } from './channels/chat-room.js';
|
||||
import { ReversiChannelService } from './channels/reversi.js';
|
||||
import { ReversiGameChannelService } from './channels/reversi-game.js';
|
||||
import { MahjongRoomChannelService } from './channels/mahjong-room.js';
|
||||
import { type MiChannelService } from './channel.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -47,7 +46,6 @@ export class ChannelsService {
|
||||
private chatRoomChannelService: ChatRoomChannelService,
|
||||
private reversiChannelService: ReversiChannelService,
|
||||
private reversiGameChannelService: ReversiGameChannelService,
|
||||
private mahjongRoomChannelService: MahjongRoomChannelService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -72,7 +70,6 @@ export class ChannelsService {
|
||||
case 'chatRoom': return this.chatRoomChannelService;
|
||||
case 'reversi': return this.reversiChannelService;
|
||||
case 'reversiGame': return this.reversiGameChannelService;
|
||||
case 'mahjongRoom': return this.mahjongRoomChannelService;
|
||||
|
||||
default:
|
||||
throw new Error(`no such channel: ${name}`);
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MahjongService } from '@/core/MahjongService.js';
|
||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class MahjongRoomChannel extends Channel {
|
||||
public readonly chName = 'mahjongRoom';
|
||||
public static shouldShare = false;
|
||||
public static requireCredential = true as const;
|
||||
public static kind = 'read:account';
|
||||
private roomId: string | null = null;
|
||||
|
||||
constructor(
|
||||
private mahjongService: MahjongService,
|
||||
|
||||
id: string,
|
||||
connection: Channel['connection'],
|
||||
) {
|
||||
super(id, connection);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.roomId = params.roomId as string;
|
||||
|
||||
this.subscriber.on(`mahjongRoomStream:${this.roomId}`, this.onMahjongRoomStreamMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMahjongRoomStreamMessage(message: GlobalEvents['mahjongRoom']['payload']) {
|
||||
if (message.type === 'started') {
|
||||
const packed = await this.mahjongService.packRoom(message.body.room, this.user!);
|
||||
this.send('started', {
|
||||
room: packed,
|
||||
});
|
||||
} else if (message.type === 'nextKyoku') {
|
||||
const packed = this.mahjongService.packState(message.body.room, this.user!);
|
||||
this.send('nextKyoku', {
|
||||
state: packed,
|
||||
});
|
||||
} else {
|
||||
this.send(message.type, message.body);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
switch (type) {
|
||||
case 'ready': this.ready(body); break;
|
||||
case 'updateSettings': this.updateSettings(body.key, body.value); break;
|
||||
case 'addAi': this.addAi(); break;
|
||||
case 'leave': this.leaveRoom(); break;
|
||||
case 'confirmNextKyoku': this.confirmNextKyoku(); break;
|
||||
case 'dahai': this.dahai(body.tile, body.riichi); break;
|
||||
case 'tsumoHora': this.tsumoHora(); break;
|
||||
case 'ronHora': this.ronHora(); break;
|
||||
case 'pon': this.pon(); break;
|
||||
case 'cii': this.cii(body.pattern); break;
|
||||
case 'kan': this.kan(); break;
|
||||
case 'ankan': this.ankan(body.tile); break;
|
||||
case 'kakan': this.kakan(body.tile); break;
|
||||
case 'nop': this.nop(); break;
|
||||
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async updateSettings(key: string, value: any) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.updateSettings(this.roomId!, this.user, key, value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async ready(ready: boolean) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.changeReadyState(this.roomId!, this.user, ready);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async confirmNextKyoku() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.confirmNextKyoku(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async addAi() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.addAi(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async leaveRoom() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.leaveRoom(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async dahai(tile: number, riichi = false) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_dahai(this.roomId!, this.user, tile, riichi);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async tsumoHora() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_tsumoHora(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async ronHora() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_ronHora(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async pon() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_pon(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async cii(pattern: string) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_cii(this.roomId!, this.user, pattern);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async kan() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_kan(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async ankan(tile: number) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_ankan(this.roomId!, this.user, tile);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async kakan(tile: number) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_kakan(this.roomId!, this.user, tile);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async nop() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.commit_nop(this.roomId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async claimTimeIsUp() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.mahjongService.checkTimeout(this.roomId!);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose() {
|
||||
// Unsubscribe events
|
||||
this.subscriber.off(`mahjongRoomStream:${this.roomId}`, this.onMahjongRoomStreamMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MahjongRoomChannelService implements MiChannelService<true> {
|
||||
public readonly shouldShare = MahjongRoomChannel.shouldShare;
|
||||
public readonly requireCredential = MahjongRoomChannel.requireCredential;
|
||||
public readonly kind = MahjongRoomChannel.kind;
|
||||
|
||||
constructor(
|
||||
private mahjongService: MahjongService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public create(id: string, connection: Channel['connection']): MahjongRoomChannel {
|
||||
return new MahjongRoomChannel(
|
||||
this.mahjongService,
|
||||
id,
|
||||
connection,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -50,14 +50,6 @@ services:
|
||||
source: ../../misskey-js/package.json
|
||||
target: /misskey/packages/misskey-js/package.json
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../misskey-mahjong/built
|
||||
target: /misskey/packages/misskey-mahjong/built
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../misskey-mahjong/package.json
|
||||
target: /misskey/packages/misskey-mahjong/package.json
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../misskey-reversi/built
|
||||
target: /misskey/packages/misskey-reversi/built
|
||||
|
||||
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 57 KiB |
@@ -56,7 +56,6 @@
|
||||
"matter-js": "0.20.0",
|
||||
"mfm-js": "0.24.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-mahjong": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"photoswipe": "5.4.4",
|
||||
|
||||
@@ -17,11 +17,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||
</MkA>
|
||||
</div>
|
||||
<div class="_panel">
|
||||
<MkA to="/mahjong">
|
||||
<img src="/client-assets/mahjong/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||
</MkA>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</PageWithHeader>
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root]">
|
||||
<div
|
||||
v-for="tile in Mmj.sortTiles(separateLast ? tiles.slice(0, tiles.length - 1) : tiles)"
|
||||
:class="[$style.tile, { [$style.tileNonSelectable]: selectableTiles != null && !selectableTiles.includes(mj$type(tile)), [$style.tileDora]: doras.includes(mj$type(tile)) }]"
|
||||
@click="chooseTile(tile, $event)"
|
||||
>
|
||||
<div :class="$style.tileInner">
|
||||
<div :class="$style.tileBg1"></div>
|
||||
<div :class="$style.tileBg2"></div>
|
||||
<div :class="$style.tileBg3"></div>
|
||||
<img :src="`/client-assets/mahjong/tiles/${mj$(tile).red ? mj$type(tile) + 'r' : mj$type(tile)}.png`" :class="$style.tileFg1"/>
|
||||
<div :class="$style.tileFg2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="separateLast"
|
||||
style="display: inline-block; margin-left: 5px;"
|
||||
:class="[$style.tile, { [$style.tileNonSelectable]: selectableTiles != null && !selectableTiles.includes(mj$type(tiles.at(-1))), [$style.tileDora]: doras.includes(mj$type(tiles.at(-1))) }]"
|
||||
@click="chooseTile(tiles.at(-1), $event)"
|
||||
>
|
||||
<div :class="$style.tileInner">
|
||||
<div :class="$style.tileBg1"></div>
|
||||
<div :class="$style.tileBg2"></div>
|
||||
<div :class="$style.tileBg3"></div>
|
||||
<img :src="`/client-assets/mahjong/tiles/${mj$(tiles.at(-1)).red ? mj$type(tiles.at(-1)) + 'r' : mj$type(tiles.at(-1))}.png`" :class="$style.tileFg1"/>
|
||||
<div :class="$style.tileFg2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import * as Mmj from 'misskey-mahjong';
|
||||
|
||||
//#region syntax suger
|
||||
function mj$(tid: Mmj.TileId): Mmj.TileInstance {
|
||||
return Mmj.findTileByIdOrFail(tid);
|
||||
}
|
||||
|
||||
function mj$type(tid: Mmj.TileId): Mmj.TileType {
|
||||
return mj$(tid).t;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const props = defineProps<{
|
||||
tiles: Mmj.TileId[];
|
||||
doras: Mmj.TileType[];
|
||||
selectableTiles: Mmj.TileType[] | null;
|
||||
separateLast: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'choose', tile: Mmj.TileId): void;
|
||||
}>();
|
||||
|
||||
function chooseTile(tile: Mmj.TileId, event: MouseEvent) {
|
||||
if (props.selectableTiles != null && !props.selectableTiles.includes(mj$type(tile))) return;
|
||||
emit('choose', tile);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@keyframes shine {
|
||||
0% { translate: -20%; }
|
||||
100% { translate: -70%; }
|
||||
}
|
||||
|
||||
.root {
|
||||
|
||||
}
|
||||
|
||||
.tile {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
position: relative;
|
||||
width: 35px;
|
||||
aspect-ratio: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tileInner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: clip;
|
||||
border-radius: 4px;
|
||||
transition: translate 0.1s ease;
|
||||
}
|
||||
.tile:hover > .tileInner {
|
||||
translate: 0 -10px;
|
||||
}
|
||||
.tileNonSelectable {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tileDora > .tileInner {
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
width: 200px;
|
||||
height: 8px;
|
||||
rotate: -45deg;
|
||||
translate: -30px;
|
||||
background: #ffffffee;
|
||||
animation: shine 2s infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tileBg1 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
background: #E38A45;
|
||||
}
|
||||
.tileBg2 {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 95%;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
background: #DFDEDD;
|
||||
border-radius: 3px 3px 0 0;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 78%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 6%;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
background: linear-gradient(0deg, #fff 0%, #fff0 100%);
|
||||
}
|
||||
}
|
||||
.tileBg3 {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 75%;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
background: #fff;
|
||||
}
|
||||
.tileFg1 {
|
||||
position: absolute;
|
||||
bottom: 5%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 65%;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
.tileFg2 {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 1px #000 inset;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,46 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="huro.type === 'ankan'" :class="[$style.root]">
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[0])" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[1])" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[2])" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[3])" :variation="variation" :doras="doras"/>
|
||||
</div>
|
||||
<div v-else-if="huro.type === 'minkan'" :class="[$style.root]">
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[0])" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[1])" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[2])" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[3])" :variation="variation" :doras="doras"/>
|
||||
</div>
|
||||
<div v-else-if="huro.type === 'cii'" :class="[$style.root]">
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[0])" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[1])" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[2])" :variation="variation" :doras="doras"/>
|
||||
</div>
|
||||
<div v-else :class="[$style.root]">
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[0])" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[1])" :variation="variation" :doras="doras"/>
|
||||
<XTile :tile="Mmj.findTileByIdOrFail(huro.tiles[2])" :variation="variation" :doras="doras"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import * as Mmj from 'misskey-mahjong';
|
||||
import XTile from './tile.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
huro: Mmj.Huro;
|
||||
variation: string;
|
||||
doras: Mmj.TileType[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
}
|
||||
</style>
|
||||
@@ -1,166 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkSpacer :contentMax="600">
|
||||
<div class="_gaps">
|
||||
<div>
|
||||
<img src="/client-assets/mahjong/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||
</div>
|
||||
|
||||
<div class="_panel _gaps" style="padding: 16px;">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton primary gradate rounded @click="joinRoom">{{ i18n.ts._mahjong.joinRoom }}</MkButton>
|
||||
<MkButton primary gradate rounded @click="createRoom">{{ i18n.ts._mahjong.createRoom }}</MkButton>
|
||||
</div>
|
||||
<div style="font-size: 90%; opacity: 0.7; text-align: center;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onDeactivated, onMounted, onUnmounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/i.js';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
import * as os from '@/os.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
|
||||
const myGamesPagination = {
|
||||
endpoint: 'mahjong/games' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
my: true,
|
||||
},
|
||||
};
|
||||
|
||||
const gamesPagination = {
|
||||
endpoint: 'mahjong/games' as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const invitations = ref<Misskey.entities.UserLite[]>([]);
|
||||
const matchingUser = ref<Misskey.entities.UserLite | null>(null);
|
||||
const matchingAny = ref<boolean>(false);
|
||||
const noIrregularRules = ref<boolean>(false);
|
||||
|
||||
async function joinRoom() {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'roomId',
|
||||
});
|
||||
if (canceled) return;
|
||||
const room = await misskeyApi('mahjong/join-room', {
|
||||
roomId: result,
|
||||
});
|
||||
router.push(`/mahjong/g/${room.id}`);
|
||||
}
|
||||
|
||||
async function createRoom(ev: MouseEvent) {
|
||||
const room = await misskeyApi('mahjong/create-room', {
|
||||
});
|
||||
router.push(`/mahjong/g/${room.id}`);
|
||||
}
|
||||
|
||||
definePage(computed(() => ({
|
||||
title: i18n.ts._mahjong.mahjong,
|
||||
icon: 'ti ti-device-gamepad',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
@keyframes blink {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.2; }
|
||||
}
|
||||
|
||||
.invitation {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
line-height: 32px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.gamePreviews {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
grid-gap: var(--margin);
|
||||
}
|
||||
|
||||
.gamePreview {
|
||||
font-size: 90%;
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.gamePreviewActive {
|
||||
box-shadow: inset 0 0 8px 0px var(--accent);
|
||||
}
|
||||
|
||||
.gamePreviewWaiting {
|
||||
box-shadow: inset 0 0 8px 0px var(--warn);
|
||||
}
|
||||
|
||||
.gamePreviewPlayers {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.gamePreviewPlayersAvatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.gamePreviewFooter {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
padding: 6px 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.gamePreviewStatusActive {
|
||||
color: var(--accent);
|
||||
font-weight: bold;
|
||||
animation: blink 2s infinite;
|
||||
}
|
||||
|
||||
.gamePreviewStatusWaiting {
|
||||
color: var(--warn);
|
||||
font-weight: bold;
|
||||
animation: blink 2s infinite;
|
||||
}
|
||||
|
||||
.waitingScreen {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.waitingScreenTitle {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,165 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<MkSpacer :contentMax="600">
|
||||
<div class="_gaps">
|
||||
<div class="_panel">
|
||||
<MkAvatar v-if="room.user1" :user="room.user1" :class="$style.userAvatar"/>
|
||||
<div v-else-if="room.user1Ai">AI</div>
|
||||
<div v-if="room.user1Ready">OK</div>
|
||||
</div>
|
||||
<div class="_panel">
|
||||
<MkAvatar v-if="room.user2" :user="room.user2" :class="$style.userAvatar"/>
|
||||
<div v-else-if="room.user2Ai">AI</div>
|
||||
<div v-if="room.user2Ready">OK</div>
|
||||
</div>
|
||||
<div class="_panel">
|
||||
<MkAvatar v-if="room.user3" :user="room.user3" :class="$style.userAvatar"/>
|
||||
<div v-else-if="room.user3Ai">AI</div>
|
||||
<div v-if="room.user3Ready">OK</div>
|
||||
</div>
|
||||
<div class="_panel">
|
||||
<MkAvatar v-if="room.user4" :user="room.user4" :class="$style.userAvatar"/>
|
||||
<div v-else-if="room.user4Ai">AI</div>
|
||||
<div v-if="room.user4Ready">OK</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<MkButton rounded primary @click="addCpu">{{ i18n.ts._mahjong.addCpu }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
|
||||
<div style="text-align: center;" class="_gaps_s">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton rounded danger @click="leave">{{ i18n.ts._mahjong.leave }}</MkButton>
|
||||
<MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._mahjong.ready }}</MkButton>
|
||||
<MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._mahjong.cancelReady }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as Mmj from 'misskey-mahjong';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
room: Misskey.entities.MahjongRoomDetailed;
|
||||
connection: Misskey.ChannelConnection<Misskey.Channels['mahjongRoom']>;
|
||||
}>();
|
||||
|
||||
const room = ref<Misskey.entities.MahjongRoomDetailed>(deepClone(props.room));
|
||||
|
||||
const isReady = computed(() => {
|
||||
if (room.value.user1Id === $i.id && room.value.user1Ready) return true;
|
||||
if (room.value.user2Id === $i.id && room.value.user2Ready) return true;
|
||||
if (room.value.user3Id === $i.id && room.value.user3Ready) return true;
|
||||
if (room.value.user4Id === $i.id && room.value.user4Ready) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
async function leave() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.areYouSure,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
props.connection.send('leave', {});
|
||||
|
||||
router.push('/mahjong');
|
||||
}
|
||||
|
||||
function ready() {
|
||||
props.connection.send('ready', true);
|
||||
}
|
||||
|
||||
function unready() {
|
||||
props.connection.send('ready', false);
|
||||
}
|
||||
|
||||
function addCpu() {
|
||||
props.connection.send('addAi', {});
|
||||
}
|
||||
|
||||
function onChangeReadyStates(states) {
|
||||
room.value.user1Ready = states.user1;
|
||||
room.value.user2Ready = states.user2;
|
||||
room.value.user3Ready = states.user3;
|
||||
room.value.user4Ready = states.user4;
|
||||
}
|
||||
|
||||
function onJoined(x) {
|
||||
switch (x.index) {
|
||||
case 1:
|
||||
room.value.user1 = x.user;
|
||||
room.value.user1Ai = x.user == null;
|
||||
room.value.user1Ready = room.value.user1Ai;
|
||||
break;
|
||||
case 2:
|
||||
room.value.user2 = x.user;
|
||||
room.value.user2Ai = x.user == null;
|
||||
room.value.user2Ready = room.value.user2Ai;
|
||||
break;
|
||||
case 3:
|
||||
room.value.user3 = x.user;
|
||||
room.value.user3Ai = x.user == null;
|
||||
room.value.user3Ready = room.value.user3Ai;
|
||||
break;
|
||||
case 4:
|
||||
room.value.user4 = x.user;
|
||||
room.value.user4Ai = x.user == null;
|
||||
room.value.user4Ready = room.value.user4Ai;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
props.connection.on('changeReadyStates', onChangeReadyStates);
|
||||
props.connection.on('joined', onJoined);
|
||||
|
||||
onUnmounted(() => {
|
||||
props.connection.off('changeReadyStates', onChangeReadyStates);
|
||||
props.connection.off('joined', onJoined);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.userAvatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
background: var(--acrylicBg);
|
||||
border-top: solid 0.5px var(--divider);
|
||||
}
|
||||
</style>
|
||||
@@ -1,113 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="room == null || (!room.isEnded && connection == null)"><MkLoading/></div>
|
||||
<RoomSetting v-else-if="!room.isStarted" :room="room" :connection="connection!"/>
|
||||
<RoomGame v-else :room="room" :connection="connection"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import RoomSetting from './room.setting.vue';
|
||||
import RoomGame from './room.game.vue';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
roomId: string;
|
||||
}>();
|
||||
|
||||
const room = shallowRef<Misskey.entities.MahjongRoomDetailed | null>(null);
|
||||
const connection = shallowRef<Misskey.ChannelConnection<Misskey.Channels['mahjongRoom']> | null>(null);
|
||||
const shareWhenStart = ref(false);
|
||||
|
||||
watch(() => props.roomId, () => {
|
||||
fetchGame();
|
||||
});
|
||||
|
||||
function start(_room: Misskey.entities.MahjongRoomDetailed) {
|
||||
if (room.value?.isStarted) return;
|
||||
|
||||
room.value = _room;
|
||||
}
|
||||
|
||||
async function fetchGame() {
|
||||
const _room = await misskeyApi('mahjong/show-room', {
|
||||
roomId: props.roomId,
|
||||
});
|
||||
|
||||
room.value = _room;
|
||||
shareWhenStart.value = false;
|
||||
|
||||
if (connection.value) {
|
||||
connection.value.dispose();
|
||||
}
|
||||
if (!room.value.isEnded) {
|
||||
connection.value = useStream().useChannel('mahjongRoom', {
|
||||
roomId: room.value.id,
|
||||
});
|
||||
connection.value.on('started', x => {
|
||||
start(x.room);
|
||||
});
|
||||
connection.value.on('canceled', x => {
|
||||
connection.value?.dispose();
|
||||
|
||||
if (x.userId !== $i.id) {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
text: i18n.ts._mahjong.roomCanceled,
|
||||
});
|
||||
router.push('/mahjong');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 通信を取りこぼした場合の救済
|
||||
useInterval(async () => {
|
||||
if (room.value == null) return;
|
||||
if (room.value.isStarted) return;
|
||||
|
||||
const _room = await misskeyApi('mahjong/show-room', {
|
||||
roomId: props.roomId,
|
||||
});
|
||||
|
||||
if (_room.isStarted) {
|
||||
start(_room);
|
||||
} else {
|
||||
room.value = _room;
|
||||
}
|
||||
}, 1000 * 10, {
|
||||
immediate: false,
|
||||
afterMounted: true,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchGame();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (connection.value) {
|
||||
connection.value.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
definePage(computed(() => ({
|
||||
title: i18n.ts._mahjong.mahjong,
|
||||
icon: 'ti ti-device-roompad',
|
||||
})));
|
||||
</script>
|
||||