Merge remote-branch 'misskey/develop'
This commit is contained in:
commit
120f583441
|
@ -1,7 +1,7 @@
|
|||
name: Check CherryPick JS autogen
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
@ -15,13 +15,14 @@ jobs:
|
|||
pull-requests: write
|
||||
|
||||
env:
|
||||
api_json_names: "api-base.json api-head.json"
|
||||
api_json_name: "api-head.json"
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
@ -87,22 +88,27 @@ jobs:
|
|||
find . -mindepth 1 -maxdepth 1 -type f -name '*.zip' -exec unzip {} -d . ';'
|
||||
ls -la
|
||||
|
||||
- name: get head checksum
|
||||
run: |-
|
||||
checksum=$(realpath head_checksum)
|
||||
|
||||
cd packages/cherrypick-js/src
|
||||
find autogen -type f -exec sh -c 'echo $(sed -E "s/^\s+\*\s+generatedAt:.+$//" {} | sha256sum | cut -d" " -f 1) {}' \; > $checksum
|
||||
cd ../../..
|
||||
|
||||
- name: build autogen
|
||||
run: |-
|
||||
for name in $(echo $api_json_names)
|
||||
do
|
||||
checksum=$(mktemp)
|
||||
mv $name packages/cherrypick-js/generator/api.json
|
||||
checksum=$(realpath ${api_json_name}_checksum)
|
||||
mv $api_json_name packages/cherrypick-js/generator/api.json
|
||||
|
||||
cd packages/cherrypick-js/generator
|
||||
pnpm run generate
|
||||
find built -type f -exec sh -c 'echo $(sed -E "s/^\s+\*\s+generatedAt:.+$//" {} | sha256sum | cut -d" " -f 1) {}' \; > $checksum
|
||||
cd ../../..
|
||||
cp $checksum ${name}_checksum
|
||||
done
|
||||
cd built
|
||||
find autogen -type f -exec sh -c 'echo $(sed -E "s/^\s+\*\s+generatedAt:.+$//" {} | sha256sum | cut -d" " -f 1) {}' \; > $checksum
|
||||
cd ../../../..
|
||||
|
||||
- name: check update for type definitions
|
||||
run: diff $(echo -n ${api_json_names} | awk -v RS=" " '{ printf "%s_checksum ", $0 }')
|
||||
run: diff head_checksum ${api_json_name}_checksum
|
||||
|
||||
- name: send message
|
||||
if: failure()
|
||||
|
@ -125,3 +131,4 @@ jobs:
|
|||
comment_tag: check-cherrypick-js-autogen
|
||||
mode: delete
|
||||
message: "Thank you!"
|
||||
create_if_not_exists: false
|
||||
|
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
@ -92,6 +92,6 @@ jobs:
|
|||
- run: pnpm i --frozen-lockfile
|
||||
- run: pnpm --filter cherrypick-js run build
|
||||
if: ${{ matrix.workspace == 'backend' }}
|
||||
- run: pnpm --filter misskey-reversi run build
|
||||
- run: pnpm --filter misskey-reversi run build:tsc
|
||||
if: ${{ matrix.workspace == 'backend' }}
|
||||
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
|
||||
|
|
45
.github/workflows/on-release-created.yml
vendored
Normal file
45
.github/workflows/on-release-created.yml
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
name: On Release Created (Publish cherrypick-js)
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish-cherrypick-js:
|
||||
name: Publish cherrypick-js
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.10.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Publish package
|
||||
run: |
|
||||
corepack enable
|
||||
pnpm i --frozen-lockfile
|
||||
pnpm build
|
||||
pnpm --filter cherrypick-js publish --access public --no-git-checks --provenance
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
14
.github/workflows/test-cherrypick-js.yml
vendored
14
.github/workflows/test-cherrypick-js.yml
vendored
|
@ -54,3 +54,17 @@ jobs:
|
|||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/cherrypick-js/coverage/coverage-final.json
|
||||
|
||||
check-version:
|
||||
# ルートの package.json と packages/cherrypick-js/package.json のバージョンが一致しているかを確認する
|
||||
name: Check version
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Check version
|
||||
run: |
|
||||
if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/cherrypick-js/package.json)" ]; then
|
||||
echo "Version mismatch!"
|
||||
exit 1
|
||||
fi
|
||||
|
|
12
locales/index.d.ts
vendored
12
locales/index.d.ts
vendored
|
@ -10837,6 +10837,18 @@ export interface Locale extends ILocale {
|
|||
* 対局がキャンセルされました
|
||||
*/
|
||||
"gameCanceled": string;
|
||||
/**
|
||||
* 開始時に対局をタイムラインに投稿
|
||||
*/
|
||||
"shareToTlTheGameWhenStart": string;
|
||||
/**
|
||||
* 対局を開始しました! #MisskeyReversi
|
||||
*/
|
||||
"iStartedAGame": string;
|
||||
/**
|
||||
* 相手が設定を変更しました
|
||||
*/
|
||||
"opponentHasSettingsChanged": string;
|
||||
};
|
||||
"_offlineScreen": {
|
||||
/**
|
||||
|
|
|
@ -2883,6 +2883,9 @@ _reversi:
|
|||
freeMatch: "フリーマッチ"
|
||||
lookingForPlayer: "対戦相手を探しています"
|
||||
gameCanceled: "対局がキャンセルされました"
|
||||
shareToTlTheGameWhenStart: "開始時に対局をタイムラインに投稿"
|
||||
iStartedAGame: "対局を開始しました! #MisskeyReversi"
|
||||
opponentHasSettingsChanged: "相手が設定を変更しました"
|
||||
|
||||
_offlineScreen:
|
||||
title: "オフライン - サーバーに接続できません"
|
||||
|
|
|
@ -380,8 +380,11 @@ hcaptcha: "hCaptcha(キャプチャ)"
|
|||
enableHcaptcha: "hCaptcha(キャプチャ)をつけとく"
|
||||
hcaptchaSiteKey: "サイトキー"
|
||||
hcaptchaSecretKey: "シークレットキー"
|
||||
mcaptcha: "mCaptcha"
|
||||
enableMcaptcha: "hCaptcha(キャプチャ)をつけとく"
|
||||
mcaptchaSiteKey: "サイトキー"
|
||||
mcaptchaSecretKey: "シークレットキー"
|
||||
mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "reCAPTCHA(リキャプチャ)を有効にする"
|
||||
recaptchaSiteKey: "サイトキー"
|
||||
|
@ -641,6 +644,7 @@ medium: "中"
|
|||
small: "小"
|
||||
generateAccessToken: "アクセストークンの発行"
|
||||
permission: "権限"
|
||||
adminPermission: "管理者権限"
|
||||
enableAll: "全部使えるようにする"
|
||||
disableAll: "全部使えへんようにする"
|
||||
tokenRequested: "アカウントへのアクセス許してやったらどうや"
|
||||
|
@ -1069,6 +1073,8 @@ limitWidthOfReaction: "ツッコミの最大横幅を制限して、ちっさく
|
|||
noteIdOrUrl: "ノートIDかURL"
|
||||
video: "動画"
|
||||
videos: "動画"
|
||||
audio: "音声"
|
||||
audioFiles: "音声"
|
||||
dataSaver: "データケチケチ"
|
||||
accountMigration: "アカウントのお引っ越し"
|
||||
accountMoved: "このユーザーはさらのアカウントに引っ越したで:"
|
||||
|
@ -1201,7 +1207,25 @@ seasonalScreenEffect: "季節にあった画面の動き"
|
|||
decorate: "デコる"
|
||||
addMfmFunction: "装飾つける"
|
||||
enableQuickAddMfmFunction: "ややこしいMFMのピッカーを出す"
|
||||
bubbleGame: "バブルゲーム"
|
||||
sfx: "効果音"
|
||||
soundWillBePlayed: "サウンドが再生されるで"
|
||||
showReplay: "リプレイ見る"
|
||||
replay: "リプレイ"
|
||||
replaying: "リプレイ中"
|
||||
ranking: "ランキング"
|
||||
lastNDays: "直近{n}日"
|
||||
backToTitle: "タイトルへ"
|
||||
hemisphere: "住んでる地域"
|
||||
withSensitive: "センシティブなファイルを含むノートを表示"
|
||||
userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿"
|
||||
enableHorizontalSwipe: "スワイプしてタブを切り替える"
|
||||
_bubbleGame:
|
||||
howToPlay: "遊び方"
|
||||
_howToPlay:
|
||||
section1: "位置を調整してハコにモノを落とすで。"
|
||||
section2: "同じもんがくっついたら別のやつになって、スコアがもらえるで。"
|
||||
section3: "モノがハコからあふれたらゲームオーバーや。ハコからあふれんようにしながらモノを融合させてハイスコアを目指しいや!"
|
||||
_announcement:
|
||||
forExistingUsers: "もうおるユーザーのみ"
|
||||
forExistingUsersDescription: "オンにしたらこのお知らせができた時点でおる人らにだけお知らせが行くで。切ったらこの知らせが行ったあとにアカウント作った人にもちゃんとお知らせが行くで。"
|
||||
|
@ -1575,6 +1599,13 @@ _achievements:
|
|||
_tutorialCompleted:
|
||||
title: "CherryPickひよっこ講座 修了証"
|
||||
description: "チュートリアル全部やった"
|
||||
_bubbleGameExplodingHead:
|
||||
title: "🤯"
|
||||
description: "バブルゲームで最も大きいモノを出した"
|
||||
_bubbleGameDoubleExplodingHead:
|
||||
title: "ダブル🤯"
|
||||
description: "バブルゲームで最も大きいモノを2つ同時に出した"
|
||||
flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
|
||||
_role:
|
||||
new: "ロールの作成"
|
||||
edit: "ロールの編集"
|
||||
|
@ -2499,6 +2530,51 @@ _dataSaver:
|
|||
_code:
|
||||
title: "コードハイライト"
|
||||
description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。"
|
||||
_hemisphere:
|
||||
N: "北半球"
|
||||
S: "南半球"
|
||||
caption: "一部のクライアント設定で、季節を判定するのに使用するで。"
|
||||
_reversi:
|
||||
reversi: "リバーシ"
|
||||
gameSettings: "対局の設定"
|
||||
chooseBoard: "ボードを選択"
|
||||
blackOrWhite: "先行/後攻"
|
||||
blackIs: "{name}が黒(先行)"
|
||||
rules: "ルール"
|
||||
thisGameIsStartedSoon: "対局、そろそろ開始されるで。"
|
||||
waitingForOther: "相手の準備が完了するのを待ってんで。"
|
||||
waitingForMe: "あんさんの準備が完了すんのを待ってんで"
|
||||
waitingBoth: "準備してなー"
|
||||
ready: "準備完了"
|
||||
cancelReady: "準備を再開"
|
||||
opponentTurn: "相手のターンやで"
|
||||
myTurn: "あんさんのターンや"
|
||||
turnOf: "{name}のターンやで"
|
||||
pastTurnOf: "{name}のターン"
|
||||
surrender: "投了"
|
||||
surrendered: "投了により"
|
||||
timeout: "時間切れ"
|
||||
drawn: "引き分け"
|
||||
won: "{name}の勝ち"
|
||||
black: "黒"
|
||||
white: "白"
|
||||
total: "合計"
|
||||
turnCount: "{count}ターン目"
|
||||
myGames: "自分の対局"
|
||||
allGames: "みんなの対局"
|
||||
ended: "終了"
|
||||
playing: "対局中"
|
||||
isLlotheo: "石の少ない方が勝ち(ロセオ)"
|
||||
loopedMap: "ループマップ"
|
||||
canPutEverywhere: "どこでも置けるモード"
|
||||
timeLimitForEachTurn: "1ターンの時間制限"
|
||||
freeMatch: "フリーマッチ"
|
||||
lookingForPlayer: "対戦相手を探してるで"
|
||||
gameCanceled: "対局がキャンセルされたわ"
|
||||
shareToTlTheGameWhenStart: "初めの時に対局をタイムラインに投稿するで"
|
||||
iStartedAGame: "対局し始めたで! #MisskeyReversi"
|
||||
opponentHasSettingsChanged: "相手が設定変えたで"
|
||||
_offlineScreen:
|
||||
title: "オフライン - サーバーに接続できひんで"
|
||||
header: "サーバーに接続できへんわ"
|
||||
|
||||
|
|
|
@ -461,9 +461,11 @@ hcaptcha: "hCaptcha"
|
|||
enableHcaptcha: "hCaptcha 활성화"
|
||||
hcaptchaSiteKey: "사이트 키"
|
||||
hcaptchaSecretKey: "시크릿 키"
|
||||
mcaptcha: "mCaptcha"
|
||||
enableMcaptcha: "mCaptcha 활성화"
|
||||
mcaptchaSiteKey: "사이트 키"
|
||||
mcaptchaSecretKey: "시크릿 키"
|
||||
mcaptchaInstanceUrl: "mCaptcha 인스턴스 URL"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "reCAPTCHA 활성화"
|
||||
recaptchaSiteKey: "사이트 키"
|
||||
|
@ -730,6 +732,7 @@ medium: "보통"
|
|||
small: "작게"
|
||||
generateAccessToken: "액세스 토큰 생성"
|
||||
permission: "권한"
|
||||
adminPermission: "관리자 권한"
|
||||
enableAll: "전체 선택"
|
||||
disableAll: "전체 해제"
|
||||
tokenRequested: "계정 접근 허용"
|
||||
|
@ -773,6 +776,7 @@ useGlobalSettingDesc: "활성화하면 계정의 알림 설정이 적용돼요.
|
|||
other: "기타"
|
||||
regenerateLoginToken: "로그인 토큰 재생성"
|
||||
regenerateLoginTokenDescription: "이 작업은 로그인할 때 사용되는 내부 토큰을 다시 생성해요. 일반적으로 이 작업을 실행할 필요는 없지만, 다른 사람이 계정에 대한 접근 권한을 가지고 있다고 생각되거나, 공공장소에서 접속한 후 로그아웃하지 않았을 때 사용할 수 있어요. 이 기능을 사용하면 이 계정으로 로그인한 모든 기기에서 로그아웃 되고, 다시 로그인 해야해요."
|
||||
theKeywordWhenSearchingForCustomEmoji: "커스텀 이모지를 검색할 때 키워드로 설정돼요."
|
||||
setMultipleBySeparatingWithSpace: "공백으로 구분해서 여러 개 설정할 수 있어요."
|
||||
fileIdOrUrl: "파일 ID 또는 URL"
|
||||
behavior: "동작"
|
||||
|
@ -1161,6 +1165,8 @@ limitWidthOfReaction: "리액션의 최대 폭을 제한하고 작게 표시"
|
|||
noteIdOrUrl: "노트 ID 및 URL"
|
||||
video: "동영상"
|
||||
videos: "동영상"
|
||||
audio: "오디오"
|
||||
audioFiles: "오디오 파일"
|
||||
dataSaver: "데이터 절약 모드"
|
||||
accountMigration: "계정 이동"
|
||||
accountMoved: "이 사용자는 다음 계정으로 이사했어요:"
|
||||
|
@ -1299,6 +1305,7 @@ seasonalScreenEffect: "계절에 따른 화면 연출"
|
|||
decorate: "장식하기"
|
||||
addMfmFunction: "장식 추가"
|
||||
enableQuickAddMfmFunction: "고급 MFM 선택기 표시하기"
|
||||
bubbleGame: "버블 게임"
|
||||
sfx: "효과음"
|
||||
soundWillBePlayed: "사운드가 재생돼요"
|
||||
showReplay: "리플레이 보기"
|
||||
|
@ -1306,7 +1313,7 @@ replay: "리플레이"
|
|||
replaying: "리플레이 중"
|
||||
ranking: "랭킹"
|
||||
lastNDays: "최근 {n}일"
|
||||
backToTitle: "타이틀로"
|
||||
backToTitle: "타이틀로 가기"
|
||||
hemisphere: "거주 지역"
|
||||
withSensitive: "민감한 파일이 포함된 노트 표시"
|
||||
userSaysSomethingSensitive: "{name}님의 민감한 파일이 포함된 게시물"
|
||||
|
@ -1335,6 +1342,10 @@ _messaging:
|
|||
direct: "다이렉트 메시지"
|
||||
_bubbleGame:
|
||||
howToPlay: "플레이 방법"
|
||||
_howToPlay:
|
||||
section1: "위치를 조정하여 상자에 물건을 떨어뜨려요."
|
||||
section2: "같은 종류의 물건이 붙으면 다른 물건으로 바뀌면서 점수를 얻을 수 있어요."
|
||||
section3: "상자에서 물건이 넘치면 게임 오버예요. 상자에서 물건이 넘치지 않도록 조심하면서 물건을 융합해 높은 점수를 획득하세요!"
|
||||
_announcement:
|
||||
forExistingUsers: "기존 사용자에게만 알리기"
|
||||
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 사용자에게만 표시해요. 비활성화하면 게시 후에 가입한 사용자에게도 표시해요."
|
||||
|
@ -1789,6 +1800,13 @@ _achievements:
|
|||
_tutorialCompleted:
|
||||
title: "CherryPick 입문자 과정 수료증"
|
||||
description: "튜토리얼을 완료했어요. 어서오세요!"
|
||||
_bubbleGameExplodingHead:
|
||||
title: "🤯"
|
||||
description: "버블 게임에서 가장 큰 물건을 내놓았어요"
|
||||
_bubbleGameDoubleExplodingHead:
|
||||
title: "더블🤯"
|
||||
description: "버블게임에서 가장 큰 물건 2개를 동시에 내놓았어요"
|
||||
flavor: "이 정도만 도시락통에 🤯🤯 조금만 더"
|
||||
_role:
|
||||
new: "새 역할 생성"
|
||||
edit: "역할 편집"
|
||||
|
@ -2741,7 +2759,7 @@ _dataSaver:
|
|||
description: "URL 미리보기의 썸네일 이미지를 불러오지 않아요."
|
||||
_code:
|
||||
title: "코드 문법 강조"
|
||||
description: "MFM 등에서 코드 문법 강조 기법을 사용할 때, 탭하기 전까지는 불러오지 않아요. 코드 문법 강조 기능은 강조할 언어마다 해당 정의 파일을 불러와야 하지만, 이를 자동으로 불러오지 않게 되어 데이터 사용량을 줄일 수 있어요."
|
||||
description: "MFM 등에서 코드 문법 강조 기법을 사용할 때, 클릭하기 전까지는 불러오지 않아요. 코드 문법 강조 기능은 강조할 언어마다 해당 정의 파일을 불러와야 하지만, 이를 자동으로 불러오지 않게 되어 데이터 사용량을 줄일 수 있어요."
|
||||
_hemisphere:
|
||||
N: "북반구"
|
||||
S: "남반구"
|
||||
|
@ -2751,7 +2769,7 @@ _reversi:
|
|||
gameSettings: "대국 설정"
|
||||
chooseBoard: "보드 선택"
|
||||
blackOrWhite: "선공/후공"
|
||||
blackIs: "{name}님이 흑(선행)"
|
||||
blackIs: "{name}님이 흑(선공)"
|
||||
rules: "규칙"
|
||||
thisGameIsStartedSoon: "대국이 곧 시작돼요. 준비해 주세요"
|
||||
waitingForOther: "상대방의 준비가 완료되기를 기다리고 있어요"
|
||||
|
@ -2763,9 +2781,9 @@ _reversi:
|
|||
myTurn: "내 차례에요"
|
||||
turnOf: "{name}님의 차례에요"
|
||||
pastTurnOf: "{name}님의 차례"
|
||||
surrender: "투료"
|
||||
surrendered: "투료에 의해"
|
||||
timeout: "마감 시간"
|
||||
surrender: "기권(투료)"
|
||||
surrendered: "기권(투료)에 의해"
|
||||
timeout: "시간 초과"
|
||||
drawn: "무승부"
|
||||
won: "{name}님의 승리"
|
||||
black: "흑"
|
||||
|
@ -2776,7 +2794,7 @@ _reversi:
|
|||
allGames: "모두의 대국"
|
||||
ended: "종료"
|
||||
playing: "대국 중"
|
||||
isLlotheo: "돌이 적은 쪽이 이김(오델로)"
|
||||
isLlotheo: "돌이 적은 사람이 승리(로세오)"
|
||||
loopedMap: "반복 맵"
|
||||
canPutEverywhere: "어디에나 둘 수 있는 모드"
|
||||
timeLimitForEachTurn: "1턴의 시간 제한"
|
||||
|
|
|
@ -2563,7 +2563,7 @@ _reversi:
|
|||
turnCount: "{count} 回合"
|
||||
myGames: "我的對弈"
|
||||
allGames: "所有對弈"
|
||||
ended: ""
|
||||
ended: "已結束"
|
||||
playing: "正在對弈"
|
||||
isLlotheo: "子較少的一方為勝(顛倒規則)"
|
||||
loopedMap: "循環棋盤"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "cherrypick",
|
||||
"version": "4.6.0",
|
||||
"basedMisskeyVersion": "2024.2.0-beta.3",
|
||||
"basedMisskeyVersion": "2024.2.0-beta.6",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -113,7 +113,6 @@
|
|||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"crc-32": "^1.2.2",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fastify": "4.25.2",
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import CRC32 from 'crc-32';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import * as Reversi from 'misskey-reversi';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { IsNull, LessThan, MoreThan } from 'typeorm';
|
||||
import type {
|
||||
MiReversiGame,
|
||||
ReversiGamesRepository,
|
||||
|
@ -25,7 +24,7 @@ import { Serialized } from '@/types.js';
|
|||
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
||||
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
const MATCHING_TIMEOUT_MS = 1000 * 15; // 15sec
|
||||
const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec
|
||||
|
||||
@Injectable()
|
||||
export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||
|
@ -90,14 +89,30 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
|
||||
public async matchSpecificUser(me: MiUser, targetUser: MiUser, multiple = false): Promise<MiReversiGame | null> {
|
||||
if (targetUser.id === me.id) {
|
||||
throw new Error('You cannot match yourself.');
|
||||
}
|
||||
|
||||
if (!multiple) {
|
||||
// 既にマッチしている対局が無いか探す(3分以内)
|
||||
const games = await this.reversiGamesRepository.find({
|
||||
where: [
|
||||
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, user2Id: targetUser.id, isStarted: false },
|
||||
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: targetUser.id, user2Id: me.id, isStarted: false },
|
||||
],
|
||||
relations: ['user1', 'user2'],
|
||||
order: { id: 'DESC' },
|
||||
});
|
||||
if (games.length > 0) {
|
||||
return games[0];
|
||||
}
|
||||
}
|
||||
|
||||
//#region 相手から既に招待されてないか確認
|
||||
const invitations = await this.redisClient.zrange(
|
||||
`reversi:matchSpecific:${me.id}`,
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
Date.now() - INVITATION_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
|
||||
|
@ -107,23 +122,42 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
const game = await this.matched(targetUser.id, me.id);
|
||||
|
||||
return game;
|
||||
} else {
|
||||
this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
|
||||
|
||||
this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
|
||||
user: await this.userEntityService.pack(me, targetUser),
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
|
||||
redisPipeline.expire(`reversi:matchSpecific:${targetUser.id}`, 120, 'NX');
|
||||
await redisPipeline.exec();
|
||||
|
||||
this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
|
||||
user: await this.userEntityService.pack(me, targetUser),
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async matchAnyUser(me: MiUser): Promise<MiReversiGame | null> {
|
||||
public async matchAnyUser(me: MiUser, multiple = false): Promise<MiReversiGame | null> {
|
||||
if (!multiple) {
|
||||
// 既にマッチしている対局が無いか探す(3分以内)
|
||||
const games = await this.reversiGamesRepository.find({
|
||||
where: [
|
||||
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, isStarted: false },
|
||||
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user2Id: me.id, isStarted: false },
|
||||
],
|
||||
relations: ['user1', 'user2'],
|
||||
order: { id: 'DESC' },
|
||||
});
|
||||
if (games.length > 0) {
|
||||
return games[0];
|
||||
}
|
||||
}
|
||||
|
||||
//#region まず自分宛ての招待を探す
|
||||
const invitations = await this.redisClient.zrange(
|
||||
`reversi:matchSpecific:${me.id}`,
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
Date.now() - INVITATION_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
|
||||
|
@ -139,15 +173,14 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
const matchings = await this.redisClient.zrange(
|
||||
'reversi:matchAny',
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
0,
|
||||
2, // 自分自身のIDが入っている場合もあるので2つ取得
|
||||
'REV');
|
||||
|
||||
const userIds = matchings.filter(id => id !== me.id);
|
||||
|
||||
if (userIds.length > 0) {
|
||||
// pick random
|
||||
const matchedUserId = userIds[Math.floor(Math.random() * userIds.length)];
|
||||
const matchedUserId = userIds[0];
|
||||
|
||||
await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId);
|
||||
|
||||
|
@ -155,7 +188,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
return game;
|
||||
} else {
|
||||
await this.redisClient.zadd('reversi:matchAny', Date.now(), me.id);
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.zadd('reversi:matchAny', Date.now(), me.id);
|
||||
redisPipeline.expire('reversi:matchAny', 15, 'NX');
|
||||
await redisPipeline.exec();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -170,6 +206,14 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
await this.redisClient.zrem('reversi:matchAny', user.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async cleanOutdatedGames() {
|
||||
await this.reversiGamesRepository.delete({
|
||||
id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 10)),
|
||||
isStarted: false,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async gameReady(gameId: MiReversiGame['id'], user: MiUser, ready: boolean) {
|
||||
const game = await this.get(gameId);
|
||||
|
@ -255,7 +299,13 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
bw = parseInt(game.bw, 10);
|
||||
}
|
||||
|
||||
const crc32 = CRC32.str(JSON.stringify(game.logs)).toString();
|
||||
const engine = new Reversi.Game(game.map, {
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
});
|
||||
|
||||
const crc32 = engine.calcCrc32().toString();
|
||||
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
|
@ -276,12 +326,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
this.cacheGame(updatedGame);
|
||||
|
||||
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
||||
const engine = new Reversi.Game(updatedGame.map, {
|
||||
isLlotheo: updatedGame.isLlotheo,
|
||||
canPutEverywhere: updatedGame.canPutEverywhere,
|
||||
loopedBoard: updatedGame.loopedBoard,
|
||||
});
|
||||
|
||||
if (engine.isEnded) {
|
||||
let winnerId;
|
||||
if (engine.winner === true) {
|
||||
|
@ -335,7 +379,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
public async getInvitations(user: MiUser): Promise<MiUser['id'][]> {
|
||||
const invitations = await this.redisClient.zrange(
|
||||
`reversi:matchSpecific:${user.id}`,
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
Date.now() - INVITATION_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
return invitations;
|
||||
|
@ -406,7 +450,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
|
||||
const serializeLogs = Reversi.Serializer.serializeLogs(logs);
|
||||
|
||||
const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString();
|
||||
const crc32 = engine.calcCrc32().toString();
|
||||
|
||||
const updatedGame = {
|
||||
...game,
|
||||
|
@ -536,7 +580,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
if (game == null) throw new Error('game not found');
|
||||
|
||||
if (crc32.toString() !== game.crc32) {
|
||||
return await this.reversiGameEntityService.packDetail(game);
|
||||
return game;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import type Logger from '@/logger.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
|
@ -32,6 +33,7 @@ export class CleanProcessorService {
|
|||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private reversiService: ReversiService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('clean');
|
||||
|
@ -65,6 +67,8 @@ export class CleanProcessorService {
|
|||
});
|
||||
}
|
||||
|
||||
this.reversiService.cleanOutdatedGames();
|
||||
|
||||
this.logger.succ('Cleaned.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -404,6 +404,7 @@ import * as ep___reversi_match from './endpoints/reversi/match.js';
|
|||
import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
|
||||
import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
|
||||
import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
|
||||
import * as ep___reversi_verify from './endpoints/reversi/verify.js';
|
||||
import { GetterService } from './GetterService.js';
|
||||
import { ApiLoggerService } from './ApiLoggerService.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
@ -805,6 +806,7 @@ const $reversi_match: Provider = { provide: 'ep:reversi/match', useClass: ep___r
|
|||
const $reversi_invitations: Provider = { provide: 'ep:reversi/invitations', useClass: ep___reversi_invitations.default };
|
||||
const $reversi_showGame: Provider = { provide: 'ep:reversi/show-game', useClass: ep___reversi_showGame.default };
|
||||
const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass: ep___reversi_surrender.default };
|
||||
const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep___reversi_verify.default };
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -1211,6 +1213,7 @@ const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass
|
|||
$reversi_invitations,
|
||||
$reversi_showGame,
|
||||
$reversi_surrender,
|
||||
$reversi_verify,
|
||||
],
|
||||
exports: [
|
||||
$admin_meta,
|
||||
|
@ -1606,6 +1609,7 @@ const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass
|
|||
$reversi_invitations,
|
||||
$reversi_showGame,
|
||||
$reversi_surrender,
|
||||
$reversi_verify,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule {}
|
||||
|
|
|
@ -404,6 +404,7 @@ import * as ep___reversi_match from './endpoints/reversi/match.js';
|
|||
import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
|
||||
import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
|
||||
import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
|
||||
import * as ep___reversi_verify from './endpoints/reversi/verify.js';
|
||||
|
||||
const eps = [
|
||||
['admin/meta', ep___admin_meta],
|
||||
|
@ -803,6 +804,7 @@ const eps = [
|
|||
['reversi/invitations', ep___reversi_invitations],
|
||||
['reversi/show-game', ep___reversi_showGame],
|
||||
['reversi/surrender', ep___reversi_surrender],
|
||||
['reversi/verify', ep___reversi_verify],
|
||||
];
|
||||
|
||||
interface IEndpointMetaBase {
|
||||
|
|
|
@ -43,7 +43,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId)
|
||||
.andWhere('game.isStarted = TRUE')
|
||||
.innerJoinAndSelect('game.user1', 'user1')
|
||||
.innerJoinAndSelect('game.user2', 'user2');
|
||||
|
||||
|
@ -53,6 +52,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.where('game.user1Id = :userId', { userId: me.id })
|
||||
.orWhere('game.user2Id = :userId', { userId: me.id });
|
||||
}));
|
||||
} else {
|
||||
query.andWhere('game.isStarted = TRUE');
|
||||
}
|
||||
|
||||
const games = await query.take(ps.limit).getMany();
|
||||
|
|
|
@ -37,6 +37,7 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
multiple: { type: 'boolean', default: false },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@ -56,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw err;
|
||||
}) : null;
|
||||
|
||||
const game = target ? await this.reversiService.matchSpecificUser(me, target) : await this.reversiService.matchAnyUser(me);
|
||||
const game = target ? await this.reversiService.matchSpecificUser(me, target, ps.multiple) : await this.reversiService.matchAnyUser(me, ps.multiple);
|
||||
|
||||
if (game == null) return;
|
||||
|
||||
|
|
64
packages/backend/src/server/api/endpoints/reversi/verify.ts
Normal file
64
packages/backend/src/server/api/endpoints/reversi/verify.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
errors: {
|
||||
noSuchGame: {
|
||||
message: 'No such game.',
|
||||
code: 'NO_SUCH_GAME',
|
||||
id: '8fb05624-b525-43dd-90f7-511852bdfeee',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
desynced: { type: 'boolean' },
|
||||
game: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'ReversiGameDetailed',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
gameId: { type: 'string', format: 'misskey:id' },
|
||||
crc32: { type: 'string' },
|
||||
},
|
||||
required: ['gameId', 'crc32'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private reversiService: ReversiService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const game = await this.reversiService.checkCrc(ps.gameId, ps.crc32);
|
||||
if (game) {
|
||||
return {
|
||||
desynced: true,
|
||||
game: await this.reversiGameEntityService.packDetail(game),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
desynced: false,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { MiReversiGame, ReversiGamesRepository } from '@/models/_.js';
|
||||
import type { MiReversiGame } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
|
@ -19,7 +19,6 @@ class ReversiGameChannel extends Channel {
|
|||
|
||||
constructor(
|
||||
private reversiService: ReversiService,
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
|
||||
id: string,
|
||||
|
@ -42,7 +41,6 @@ class ReversiGameChannel extends Channel {
|
|||
case 'updateSettings': this.updateSettings(body.key, body.value); break;
|
||||
case 'cancel': this.cancelGame(); break;
|
||||
case 'putStone': this.putStone(body.pos, body.id); break;
|
||||
case 'resync': this.resync(body.crc32); break;
|
||||
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
||||
}
|
||||
}
|
||||
|
@ -75,14 +73,6 @@ class ReversiGameChannel extends Channel {
|
|||
this.reversiService.putStoneToGame(this.gameId!, this.user, pos, id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async resync(crc32: string | number) {
|
||||
const game = await this.reversiService.checkCrc(this.gameId!, crc32);
|
||||
if (game) {
|
||||
this.send('resynced', game);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async claimTimeIsUp() {
|
||||
if (this.user == null) return;
|
||||
|
@ -104,9 +94,6 @@ export class ReversiGameChannelService implements MiChannelService<false> {
|
|||
public readonly kind = ReversiGameChannel.kind;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
private reversiService: ReversiService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
) {
|
||||
|
@ -116,7 +103,6 @@ export class ReversiGameChannelService implements MiChannelService<false> {
|
|||
public create(id: string, connection: Channel['connection']): ReversiGameChannel {
|
||||
return new ReversiGameChannel(
|
||||
this.reversiService,
|
||||
this.reversiGamesRepository,
|
||||
this.reversiGameEntityService,
|
||||
id,
|
||||
connection,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021-2022 syuilo and other contributors
|
||||
Copyright (c) 2021-2024 syuilo and noridev and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -1729,6 +1729,8 @@ declare namespace entities {
|
|||
ReversiShowGameRequest,
|
||||
ReversiShowGameResponse,
|
||||
ReversiSurrenderRequest,
|
||||
ReversiVerifyRequest,
|
||||
ReversiVerifyResponse,
|
||||
Error_2 as Error,
|
||||
UserLite,
|
||||
UserDetailedNotMeOnly,
|
||||
|
@ -2790,6 +2792,12 @@ type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200
|
|||
// @public (undocumented)
|
||||
type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type ReversiVerifyRequest = operations['reversi/verify']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type ReversiVerifyResponse = operations['reversi/verify']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type Role = components['schemas']['Role'];
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "cherrypick-js",
|
||||
"version": "0.0.16-cherrypick.1",
|
||||
"version": "4.6.0",
|
||||
"basedMisskeyVersion": "2024.2.0-beta.3",
|
||||
"description": "CherryPick SDK for JavaScript",
|
||||
"types": "./built/dts/index.d.ts",
|
||||
"exports": {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* version: 4.6.0
|
||||
* basedMisskeyVersion: 2024.2.0-beta.3
|
||||
* generatedAt: 2024-01-22T11:38:57.200Z
|
||||
* basedMisskeyVersion: 2024.2.0-beta.6
|
||||
* generatedAt: 2024-01-24T07:33:32.527Z
|
||||
*/
|
||||
|
||||
import type { SwitchCaseResponseType } from '../api.js';
|
||||
|
@ -4416,5 +4416,16 @@ declare module '../api.js' {
|
|||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
request<E extends 'reversi/verify', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* version: 4.6.0
|
||||
* basedMisskeyVersion: 2024.2.0-beta.3
|
||||
* generatedAt: 2024-01-22T11:38:57.197Z
|
||||
* basedMisskeyVersion: 2024.2.0-beta.6
|
||||
* generatedAt: 2024-01-24T07:33:32.525Z
|
||||
*/
|
||||
|
||||
import type {
|
||||
|
@ -599,6 +599,8 @@ import type {
|
|||
ReversiShowGameRequest,
|
||||
ReversiShowGameResponse,
|
||||
ReversiSurrenderRequest,
|
||||
ReversiVerifyRequest,
|
||||
ReversiVerifyResponse,
|
||||
} from './entities.js';
|
||||
|
||||
export type Endpoints = {
|
||||
|
@ -999,4 +1001,5 @@ export type Endpoints = {
|
|||
'reversi/invitations': { req: EmptyRequest; res: ReversiInvitationsResponse };
|
||||
'reversi/show-game': { req: ReversiShowGameRequest; res: ReversiShowGameResponse };
|
||||
'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse };
|
||||
'reversi/verify': { req: ReversiVerifyRequest; res: ReversiVerifyResponse };
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* version: 4.6.0
|
||||
* basedMisskeyVersion: 2024.2.0-beta.3
|
||||
* generatedAt: 2024-01-22T11:38:57.195Z
|
||||
* basedMisskeyVersion: 2024.2.0-beta.6
|
||||
* generatedAt: 2024-01-24T07:33:32.523Z
|
||||
*/
|
||||
|
||||
import { operations } from './types.js';
|
||||
|
@ -601,3 +601,5 @@ export type ReversiInvitationsResponse = operations['reversi/invitations']['resp
|
|||
export type ReversiShowGameRequest = operations['reversi/show-game']['requestBody']['content']['application/json'];
|
||||
export type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200']['content']['application/json'];
|
||||
export type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['content']['application/json'];
|
||||
export type ReversiVerifyRequest = operations['reversi/verify']['requestBody']['content']['application/json'];
|
||||
export type ReversiVerifyResponse = operations['reversi/verify']['responses']['200']['content']['application/json'];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* version: 4.6.0
|
||||
* basedMisskeyVersion: 2024.2.0-beta.3
|
||||
* generatedAt: 2024-01-22T11:38:57.194Z
|
||||
* basedMisskeyVersion: 2024.2.0-beta.6
|
||||
* generatedAt: 2024-01-24T07:33:32.522Z
|
||||
*/
|
||||
|
||||
import { components } from './types.js';
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
|
||||
/*
|
||||
* version: 4.6.0
|
||||
* basedMisskeyVersion: 2024.2.0-beta.3
|
||||
* generatedAt: 2024-01-22T11:38:57.091Z
|
||||
* basedMisskeyVersion: 2024.2.0-beta.6
|
||||
* generatedAt: 2024-01-24T07:33:32.436Z
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -3807,6 +3807,15 @@ export type paths = {
|
|||
*/
|
||||
post: operations['reversi/surrender'];
|
||||
};
|
||||
'/reversi/verify': {
|
||||
/**
|
||||
* reversi/verify
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
post: operations['reversi/verify'];
|
||||
};
|
||||
};
|
||||
|
||||
export type webhooks = Record<string, never>;
|
||||
|
@ -27982,6 +27991,8 @@ export type operations = {
|
|||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
userId?: string | null;
|
||||
/** @default false */
|
||||
multiple?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -28176,5 +28187,63 @@ export type operations = {
|
|||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* reversi/verify
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *No*
|
||||
*/
|
||||
'reversi/verify': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
gameId: string;
|
||||
crc32: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
desynced: boolean;
|
||||
game?: components['schemas']['ReversiGameDetailed'] | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 181 KiB |
|
@ -45,10 +45,8 @@
|
|||
"cherrypick-mfm-js": "0.24.0-cherrypick.4",
|
||||
"chromatic": "10.3.1",
|
||||
"compare-versions": "6.1.0",
|
||||
"crc-32": "^1.2.2",
|
||||
"cropperjs": "2.0.0-beta.4",
|
||||
"date-fns": "2.30.0",
|
||||
"defu": "^6.1.4",
|
||||
"escape-regexp": "0.0.1",
|
||||
"estree-walker": "3.0.3",
|
||||
"eventemitter3": "5.0.1",
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div
|
||||
ref="rootEl"
|
||||
:class="[$style.transitionRoot]"
|
||||
:class="[$style.transitionRoot, { [$style.enableAnimation]: shouldAnimate }]"
|
||||
@touchstart.passive="touchStart"
|
||||
@touchmove.passive="touchMove"
|
||||
@touchend.passive="touchEnd"
|
||||
|
@ -44,6 +44,8 @@ const emit = defineEmits<{
|
|||
(ev: 'swiped', newKey: string, direction: 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontalSwipe.value || defaultStore.reactiveState.animation.value);
|
||||
|
||||
// ▼ しきい値 ▼ //
|
||||
|
||||
// スワイプと判定される最小の距離
|
||||
|
@ -188,7 +190,9 @@ watch(tabModel, (newTab, oldTab) => {
|
|||
.transitionChildren {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
transform: translateX(var(--swipe));
|
||||
}
|
||||
|
||||
.enableAnimation .transitionChildren {
|
||||
&.swipeAnimation_enterActive,
|
||||
&.swipeAnimation_leaveActive {
|
||||
transition: transform .3s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
|
|
|
@ -143,7 +143,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue';
|
||||
import * as CRC32 from 'crc-32';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import * as Reversi from 'misskey-reversi';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
@ -240,11 +239,17 @@ watch(logPos, (v) => {
|
|||
|
||||
if (game.value.isStarted && !game.value.isEnded) {
|
||||
useInterval(() => {
|
||||
if (game.value.isEnded || props.connection == null) return;
|
||||
const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString();
|
||||
if (game.value.isEnded) return;
|
||||
const crc32 = engine.value.calcCrc32();
|
||||
if (_DEV_) console.log('crc32', crc32);
|
||||
props.connection.send('resync', {
|
||||
crc32: crc32,
|
||||
misskeyApi('reversi/verify', {
|
||||
gameId: game.value.id,
|
||||
crc32: crc32.toString(),
|
||||
}).then((res) => {
|
||||
if (res.desynced) {
|
||||
console.log('resynced');
|
||||
restoreGame(res.game!);
|
||||
}
|
||||
});
|
||||
}, 10000, { immediate: false, afterMounted: true });
|
||||
}
|
||||
|
@ -392,12 +397,6 @@ function restoreGame(_game) {
|
|||
checkEnd();
|
||||
}
|
||||
|
||||
function onStreamResynced(_game) {
|
||||
console.log('resynced');
|
||||
|
||||
restoreGame(_game);
|
||||
}
|
||||
|
||||
async function surrender() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
|
@ -450,7 +449,6 @@ function share() {
|
|||
onMounted(() => {
|
||||
if (props.connection != null) {
|
||||
props.connection.on('log', onStreamLog);
|
||||
props.connection.on('resynced', onStreamResynced);
|
||||
props.connection.on('ended', onStreamEnded);
|
||||
}
|
||||
});
|
||||
|
@ -458,7 +456,6 @@ onMounted(() => {
|
|||
onActivated(() => {
|
||||
if (props.connection != null) {
|
||||
props.connection.on('log', onStreamLog);
|
||||
props.connection.on('resynced', onStreamResynced);
|
||||
props.connection.on('ended', onStreamEnded);
|
||||
}
|
||||
});
|
||||
|
@ -466,7 +463,6 @@ onActivated(() => {
|
|||
onDeactivated(() => {
|
||||
if (props.connection != null) {
|
||||
props.connection.off('log', onStreamLog);
|
||||
props.connection.off('resynced', onStreamResynced);
|
||||
props.connection.off('ended', onStreamEnded);
|
||||
}
|
||||
});
|
||||
|
@ -474,7 +470,6 @@ onDeactivated(() => {
|
|||
onUnmounted(() => {
|
||||
if (props.connection != null) {
|
||||
props.connection.off('log', onStreamLog);
|
||||
props.connection.off('resynced', onStreamResynced);
|
||||
props.connection.off('ended', onStreamEnded);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -81,16 +81,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
|
||||
<div style="text-align: center; margin-bottom: 10px;">
|
||||
<template v-if="isReady && isOpReady">{{ i18n.ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
|
||||
<template v-if="isReady && !isOpReady">{{ i18n.ts._reversi.waitingForOther }}<MkEllipsis/></template>
|
||||
<template v-if="!isReady && isOpReady">{{ i18n.ts._reversi.waitingForMe }}</template>
|
||||
<template v-if="!isReady && !isOpReady">{{ i18n.ts._reversi.waitingBoth }}<MkEllipsis/></template>
|
||||
</div>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton rounded danger @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._reversi.ready }}</MkButton>
|
||||
<MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton>
|
||||
<div style="text-align: center;" class="_gaps_s">
|
||||
<div v-if="opponentHasSettingsChanged" style="color: var(--warn);">{{ i18n.ts._reversi.opponentHasSettingsChanged }}</div>
|
||||
<div>
|
||||
<template v-if="isReady && isOpReady">{{ i18n.ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
|
||||
<template v-if="isReady && !isOpReady">{{ i18n.ts._reversi.waitingForOther }}<MkEllipsis/></template>
|
||||
<template v-if="!isReady && isOpReady">{{ i18n.ts._reversi.waitingForMe }}</template>
|
||||
<template v-if="!isReady && !isOpReady">{{ i18n.ts._reversi.waitingBoth }}<MkEllipsis/></template>
|
||||
</div>
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton rounded danger @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._reversi.ready }}</MkButton>
|
||||
<MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton>
|
||||
</div>
|
||||
<div>
|
||||
<MkSwitch v-model="shareWhenStart">{{ i18n.ts._reversi.shareToTlTheGameWhenStart }}</MkSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
|
@ -124,6 +130,8 @@ const props = defineProps<{
|
|||
connection: Misskey.ChannelConnection;
|
||||
}>();
|
||||
|
||||
const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
|
||||
|
||||
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
|
||||
|
||||
const mapName = computed(() => {
|
||||
|
@ -142,6 +150,8 @@ const isOpReady = computed(() => {
|
|||
return false;
|
||||
});
|
||||
|
||||
const opponentHasSettingsChanged = ref(false);
|
||||
|
||||
watch(() => game.value.bw, () => {
|
||||
updateSettings('bw');
|
||||
});
|
||||
|
@ -190,6 +200,7 @@ async function cancel() {
|
|||
|
||||
function ready() {
|
||||
props.connection.send('ready', true);
|
||||
opponentHasSettingsChanged.value = false;
|
||||
}
|
||||
|
||||
function unready() {
|
||||
|
@ -212,6 +223,10 @@ function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof M
|
|||
if (userId === $i.id) return;
|
||||
if (game.value[key] === value) return;
|
||||
game.value[key] = value;
|
||||
if (isReady.value) {
|
||||
opponentHasSettingsChanged.value = true;
|
||||
unready();
|
||||
}
|
||||
}
|
||||
|
||||
function onMapCellClick(pos: number, pixel: string) {
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div v-if="game == null || (!game.isEnded && connection == null)"><MkLoading/></div>
|
||||
<GameSetting v-else-if="!game.isStarted" :game="game" :connection="connection!"/>
|
||||
<GameSetting v-else-if="!game.isStarted" v-model:shareWhenStart="shareWhenStart" :game="game" :connection="connection!"/>
|
||||
<GameBoard v-else :game="game" :connection="connection"/>
|
||||
</template>
|
||||
|
||||
|
@ -21,6 +21,7 @@ import { signinRequired } from '@/account.js';
|
|||
import { useRouter } from '@/global/router/supplier.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
|
@ -32,17 +33,32 @@ const props = defineProps<{
|
|||
|
||||
const game = shallowRef<Misskey.entities.ReversiGameDetailed | null>(null);
|
||||
const connection = shallowRef<Misskey.ChannelConnection | null>(null);
|
||||
const shareWhenStart = ref(false);
|
||||
|
||||
watch(() => props.gameId, () => {
|
||||
fetchGame();
|
||||
});
|
||||
|
||||
function start(_game: Misskey.entities.ReversiGameDetailed) {
|
||||
if (game.value?.isStarted) return;
|
||||
|
||||
if (shareWhenStart.value) {
|
||||
misskeyApi('notes/create', {
|
||||
text: i18n.ts._reversi.iStartedAGame + '\n' + location.href,
|
||||
visibility: 'home',
|
||||
});
|
||||
}
|
||||
|
||||
game.value = _game;
|
||||
}
|
||||
|
||||
async function fetchGame() {
|
||||
const _game = await misskeyApi('reversi/show-game', {
|
||||
gameId: props.gameId,
|
||||
});
|
||||
|
||||
game.value = _game;
|
||||
shareWhenStart.value = false;
|
||||
|
||||
if (connection.value) {
|
||||
connection.value.dispose();
|
||||
|
@ -52,7 +68,7 @@ async function fetchGame() {
|
|||
gameId: game.value.id,
|
||||
});
|
||||
connection.value.on('started', x => {
|
||||
game.value = x.game;
|
||||
start(x.game);
|
||||
});
|
||||
connection.value.on('canceled', x => {
|
||||
connection.value?.dispose();
|
||||
|
@ -68,6 +84,25 @@ async function fetchGame() {
|
|||
}
|
||||
}
|
||||
|
||||
// 通信を取りこぼした場合の救済
|
||||
useInterval(async () => {
|
||||
if (game.value == null) return;
|
||||
if (game.value.isStarted) return;
|
||||
|
||||
const _game = await misskeyApi('reversi/show-game', {
|
||||
gameId: props.gameId,
|
||||
});
|
||||
|
||||
if (_game.isStarted) {
|
||||
start(_game);
|
||||
} else {
|
||||
game.value = _game;
|
||||
}
|
||||
}, 1000 * 10, {
|
||||
immediate: false,
|
||||
afterMounted: true,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchGame();
|
||||
});
|
||||
|
@ -78,10 +113,6 @@ onUnmounted(() => {
|
|||
}
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: 'Reversi',
|
||||
icon: 'ti ti-device-gamepad',
|
||||
|
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkPagination :pagination="myGamesPagination" :disableAutoLoad="true">
|
||||
<template #default="{ items }">
|
||||
<div :class="$style.gamePreviews">
|
||||
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
|
||||
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
|
||||
<div :class="$style.gamePreviewPlayers">
|
||||
<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
|
||||
<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
|
||||
|
@ -45,7 +45,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
|
||||
</div>
|
||||
<div :class="$style.gamePreviewFooter">
|
||||
<span v-if="!g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
|
||||
<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
|
||||
<span v-else-if="!g.isEnded" :class="$style.gamePreviewStatusWaiting"><MkEllipsis/></span>
|
||||
<span v-else>{{ i18n.ts._reversi.ended }}</span>
|
||||
<MkTime style="margin-left: auto; opacity: 0.7;" :time="g.createdAt"/>
|
||||
</div>
|
||||
|
@ -60,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkPagination :pagination="gamesPagination" :disableAutoLoad="true">
|
||||
<template #default="{ items }">
|
||||
<div :class="$style.gamePreviews">
|
||||
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
|
||||
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
|
||||
<div :class="$style.gamePreviewPlayers">
|
||||
<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
|
||||
<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
|
||||
|
@ -71,7 +72,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
|
||||
</div>
|
||||
<div :class="$style.gamePreviewFooter">
|
||||
<span v-if="!g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
|
||||
<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
|
||||
<span v-else-if="!g.isEnded" :class="$style.gamePreviewStatusWaiting"><MkEllipsis/></span>
|
||||
<span v-else>{{ i18n.ts._reversi.ended }}</span>
|
||||
<MkTime style="margin-left: auto; opacity: 0.7;" :time="g.createdAt"/>
|
||||
</div>
|
||||
|
@ -137,7 +139,9 @@ if ($i) {
|
|||
const connection = useStream().useChannel('reversi');
|
||||
|
||||
connection.on('matched', x => {
|
||||
startGame(x.game);
|
||||
if (matchingUser.value != null || matchingAny.value) {
|
||||
startGame(x.game);
|
||||
}
|
||||
});
|
||||
|
||||
connection.on('invited', invitation => {
|
||||
|
@ -220,12 +224,14 @@ async function accept(user) {
|
|||
}
|
||||
}
|
||||
|
||||
useInterval(matchHeatbeat, 1000 * 10, { immediate: false, afterMounted: true });
|
||||
useInterval(matchHeatbeat, 1000 * 5, { immediate: false, afterMounted: true });
|
||||
|
||||
onMounted(() => {
|
||||
misskeyApi('reversi/invitations').then(_invitations => {
|
||||
invitations.value = _invitations;
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', cancelMatching);
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
|
@ -273,6 +279,10 @@ definePageMetadata(computed(() => ({
|
|||
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;
|
||||
|
@ -306,6 +316,12 @@ definePageMetadata(computed(() => ({
|
|||
animation: blink 2s infinite;
|
||||
}
|
||||
|
||||
.gamePreviewStatusWaiting {
|
||||
color: var(--warn);
|
||||
font-weight: bold;
|
||||
animation: blink 2s infinite;
|
||||
}
|
||||
|
||||
.waitingScreen {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import { BroadcastChannel } from 'broadcast-channel';
|
||||
import { defu } from 'defu';
|
||||
import { $i } from '@/account.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { get, set } from '@/scripts/idb-proxy.js';
|
||||
|
@ -81,14 +80,37 @@ export class Storage<T extends StateDef> {
|
|||
this.loaded = this.ready.then(() => this.load());
|
||||
}
|
||||
|
||||
private isPureObject(value: unknown): value is Record<string, unknown> {
|
||||
private isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
private mergeState<T>(value: T, def: T): T {
|
||||
/**
|
||||
* valueにないキーをdefからもらう(再帰的)\
|
||||
* nullはそのまま、undefinedはdefの値
|
||||
**/
|
||||
private mergeObject<X>(value: X, def: X): X {
|
||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
||||
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def);
|
||||
return defu(value, def) as T;
|
||||
const result = structuredClone(value) as X;
|
||||
for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
|
||||
if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
|
||||
result[k] = v;
|
||||
} else if (this.isPureObject(v) && this.isPureObject(result[k])) {
|
||||
const child = structuredClone(result[k]) as X[keyof X] & Record<string | number | symbol, unknown>;
|
||||
result[k] = this.mergeObject<typeof v>(child, v);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private mergeState<X>(value: X, def: X): X {
|
||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
||||
const merged = this.mergeObject(value, def);
|
||||
|
||||
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
|
||||
|
||||
return merged as X;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
|
31
packages/misskey-bubble-game/build.js
Normal file
31
packages/misskey-bubble-game/build.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { build } from "esbuild";
|
||||
import { globSync } from "glob";
|
||||
|
||||
const entryPoints = globSync("./src/**/**.{ts,tsx}");
|
||||
|
||||
/** @type {import('esbuild').BuildOptions} */
|
||||
const options = {
|
||||
entryPoints,
|
||||
minify: true,
|
||||
outdir: "./built/esm",
|
||||
target: "es2022",
|
||||
platform: "browser",
|
||||
format: "esm",
|
||||
};
|
||||
|
||||
if (process.env.WATCH === "true") {
|
||||
options.watch = {
|
||||
onRebuild(error, result) {
|
||||
if (error) {
|
||||
console.error("watch build failed:", error);
|
||||
} else {
|
||||
console.log("watch build succeeded:", result);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
build(options).catch((err) => {
|
||||
process.stderr.write(err.stderr);
|
||||
process.exit(1);
|
||||
});
|
|
@ -13,18 +13,21 @@
|
|||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run ts",
|
||||
"ts": "npm run ts-esm && npm run ts-dts",
|
||||
"ts-esm": "tsc --outDir built/esm",
|
||||
"ts-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
|
||||
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"",
|
||||
"build": "node ./build.js",
|
||||
"build:tsc": "npm run tsc",
|
||||
"tsc": "npm run ts-esm && npm run ts-dts",
|
||||
"tsc-esm": "tsc --outDir built/esm",
|
||||
"tsc-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
|
||||
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build:tsc\"",
|
||||
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@types/matter-js": "0.19.6",
|
||||
"@types/node": "20.11.5",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"eslint": "8.56.0",
|
||||
|
@ -35,9 +38,9 @@
|
|||
"built"
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/matter-js": "0.19.6",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"esbuild": "0.19.11",
|
||||
"eventemitter3": "5.0.1",
|
||||
"glob": "^10.3.10",
|
||||
"matter-js": "0.19.0",
|
||||
"seedrandom": "3.0.5"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
|
|
31
packages/misskey-reversi/build.js
Normal file
31
packages/misskey-reversi/build.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { build } from "esbuild";
|
||||
import { globSync } from "glob";
|
||||
|
||||
const entryPoints = globSync("./src/**/**.{ts,tsx}");
|
||||
|
||||
/** @type {import('esbuild').BuildOptions} */
|
||||
const options = {
|
||||
entryPoints,
|
||||
minify: true,
|
||||
outdir: "./built/esm",
|
||||
target: "es2022",
|
||||
platform: "browser",
|
||||
format: "esm",
|
||||
};
|
||||
|
||||
if (process.env.WATCH === "true") {
|
||||
options.watch = {
|
||||
onRebuild(error, result) {
|
||||
if (error) {
|
||||
console.error("watch build failed:", error);
|
||||
} else {
|
||||
console.log("watch build succeeded:", result);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
build(options).catch((err) => {
|
||||
process.stderr.write(err.stderr);
|
||||
process.exit(1);
|
||||
});
|
|
@ -13,11 +13,12 @@
|
|||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run ts",
|
||||
"ts": "npm run ts-esm && npm run ts-dts",
|
||||
"ts-esm": "tsc --outDir built/esm",
|
||||
"ts-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
|
||||
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"",
|
||||
"build": "node ./build.js",
|
||||
"build:tsc": "npm run tsc",
|
||||
"tsc": "npm run tsc-esm && npm run tsc-dts",
|
||||
"tsc-esm": "tsc --outDir built/esm",
|
||||
"tsc-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
|
||||
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build:tsc\"",
|
||||
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
|
@ -35,5 +36,8 @@
|
|||
"built"
|
||||
],
|
||||
"dependencies": {
|
||||
"crc-32": "1.2.2",
|
||||
"esbuild": "0.19.11",
|
||||
"glob": "10.3.10"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import CRC32 from 'crc-32';
|
||||
|
||||
/**
|
||||
* true ... 黒
|
||||
* false ... 白
|
||||
|
@ -204,6 +206,13 @@ export class Game {
|
|||
return ([] as number[]).concat(...diffVectors.map(effectsInLine));
|
||||
}
|
||||
|
||||
public calcCrc32(): number {
|
||||
return CRC32.str(JSON.stringify({
|
||||
board: this.board,
|
||||
turn: this.turn,
|
||||
}));
|
||||
}
|
||||
|
||||
public get isEnded(): boolean {
|
||||
return this.turn === null;
|
||||
}
|
||||
|
|
|
@ -200,9 +200,6 @@ importers:
|
|||
content-disposition:
|
||||
specifier: 0.5.4
|
||||
version: 0.5.4
|
||||
crc-32:
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
date-fns:
|
||||
specifier: 2.30.0
|
||||
version: 2.30.0
|
||||
|
@ -866,18 +863,12 @@ importers:
|
|||
compare-versions:
|
||||
specifier: 6.1.0
|
||||
version: 6.1.0
|
||||
crc-32:
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
cropperjs:
|
||||
specifier: 2.0.0-beta.4
|
||||
version: 2.0.0-beta.4
|
||||
date-fns:
|
||||
specifier: 2.30.0
|
||||
version: 2.30.0
|
||||
defu:
|
||||
specifier: ^6.1.4
|
||||
version: 6.1.4
|
||||
escape-regexp:
|
||||
specifier: 0.0.1
|
||||
version: 0.0.1
|
||||
|
@ -1176,15 +1167,15 @@ importers:
|
|||
|
||||
packages/misskey-bubble-game:
|
||||
dependencies:
|
||||
'@types/matter-js':
|
||||
specifier: 0.19.6
|
||||
version: 0.19.6
|
||||
'@types/seedrandom':
|
||||
specifier: 3.0.8
|
||||
version: 3.0.8
|
||||
esbuild:
|
||||
specifier: 0.19.11
|
||||
version: 0.19.11
|
||||
eventemitter3:
|
||||
specifier: 5.0.1
|
||||
version: 5.0.1
|
||||
glob:
|
||||
specifier: ^10.3.10
|
||||
version: 10.3.10
|
||||
matter-js:
|
||||
specifier: 0.19.0
|
||||
version: 0.19.0
|
||||
|
@ -1195,9 +1186,15 @@ importers:
|
|||
'@misskey-dev/eslint-plugin':
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0(@typescript-eslint/eslint-plugin@6.18.1)(@typescript-eslint/parser@6.18.1)(eslint-plugin-import@2.29.1)(eslint@8.56.0)
|
||||
'@types/matter-js':
|
||||
specifier: 0.19.6
|
||||
version: 0.19.6
|
||||
'@types/node':
|
||||
specifier: 20.11.5
|
||||
version: 20.11.5
|
||||
'@types/seedrandom':
|
||||
specifier: 3.0.8
|
||||
version: 3.0.8
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 6.18.1
|
||||
version: 6.18.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3)
|
||||
|
@ -1215,6 +1212,16 @@ importers:
|
|||
version: 5.3.3
|
||||
|
||||
packages/misskey-reversi:
|
||||
dependencies:
|
||||
crc-32:
|
||||
specifier: 1.2.2
|
||||
version: 1.2.2
|
||||
esbuild:
|
||||
specifier: 0.19.11
|
||||
version: 0.19.11
|
||||
glob:
|
||||
specifier: 10.3.10
|
||||
version: 10.3.10
|
||||
devDependencies:
|
||||
'@misskey-dev/eslint-plugin':
|
||||
specifier: 1.0.0
|
||||
|
@ -8095,6 +8102,7 @@ packages:
|
|||
|
||||
/@types/matter-js@0.19.6:
|
||||
resolution: {integrity: sha512-ffk6tqJM5scla+ThXmnox+mdfCo3qYk6yMjQsNcrbo6eQ5DqorVdtnaL+1agCoYzxUjmHeiNB7poBMAmhuLY7w==}
|
||||
dev: true
|
||||
|
||||
/@types/mdurl@1.0.2:
|
||||
resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==}
|
||||
|
@ -8306,7 +8314,7 @@ packages:
|
|||
|
||||
/@types/seedrandom@3.0.8:
|
||||
resolution: {integrity: sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==}
|
||||
dev: false
|
||||
dev: true
|
||||
|
||||
/@types/semver@7.5.6:
|
||||
resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==}
|
||||
|
@ -11150,6 +11158,7 @@ packages:
|
|||
|
||||
/defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
dev: true
|
||||
|
||||
/del@6.1.1:
|
||||
resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==}
|
||||
|
|
Loading…
Reference in a new issue