merge: upstream
This commit is contained in:
commit
fd69a2fbbd
74 changed files with 659 additions and 745 deletions
|
@ -161,11 +161,13 @@ describe('After user signed in', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successfully loads', () => {
|
it('successfully loads', () => {
|
||||||
cy.get('[data-cy-user-setup-continue]').should('be.visible');
|
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
|
||||||
|
cy.get('[data-cy-user-setup-continue]', { timeout: 12000 }).should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('account setup wizard', () => {
|
it('account setup wizard', () => {
|
||||||
cy.get('[data-cy-user-setup-continue]').click();
|
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
|
||||||
|
cy.get('[data-cy-user-setup-continue]', { timeout: 12000 }).click();
|
||||||
|
|
||||||
cy.get('[data-cy-user-setup-user-name] input').type('ありす');
|
cy.get('[data-cy-user-setup-user-name] input').type('ありす');
|
||||||
cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ');
|
cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ');
|
||||||
|
@ -202,7 +204,8 @@ describe('After user setup', () => {
|
||||||
cy.login('alice', 'alice1234');
|
cy.login('alice', 'alice1234');
|
||||||
|
|
||||||
// アカウント初期設定ウィザード
|
// アカウント初期設定ウィザード
|
||||||
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]').click();
|
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
|
||||||
|
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 12000 }).click();
|
||||||
cy.get('[data-cy-modal-dialog-ok]').click();
|
cy.get('[data-cy-modal-dialog-ok]').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1567,3 +1567,4 @@ _moderationLogTypes:
|
||||||
createInvitation: "ولِّد دعوة"
|
createInvitation: "ولِّد دعوة"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "المجموع"
|
total: "المجموع"
|
||||||
|
|
||||||
|
|
|
@ -1346,3 +1346,4 @@ _moderationLogTypes:
|
||||||
resetPassword: "পাসওয়ার্ড রিসেট করুন"
|
resetPassword: "পাসওয়ার্ড রিসেট করুন"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "মোট"
|
total: "মোট"
|
||||||
|
|
||||||
|
|
|
@ -1276,3 +1276,4 @@ _moderationLogTypes:
|
||||||
resetPassword: "Restableix la contrasenya"
|
resetPassword: "Restableix la contrasenya"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Total"
|
total: "Total"
|
||||||
|
|
||||||
|
|
|
@ -2022,3 +2022,4 @@ _moderationLogTypes:
|
||||||
createInvitation: "Vygenerovat pozvánku"
|
createInvitation: "Vygenerovat pozvánku"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Celkem"
|
total: "Celkem"
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
---
|
---
|
||||||
_lang_: "Dansk"
|
_lang_: "Dansk"
|
||||||
|
|
||||||
|
|
|
@ -2247,3 +2247,4 @@ _externalResourceInstaller:
|
||||||
description: "Während der Installation des Farbschemas ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden."
|
description: "Während der Installation des Farbschemas ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden."
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Gesamt"
|
total: "Gesamt"
|
||||||
|
|
||||||
|
|
|
@ -398,3 +398,4 @@ _moderationLogTypes:
|
||||||
suspend: "Αποβολή"
|
suspend: "Αποβολή"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Σύνολο"
|
total: "Σύνολο"
|
||||||
|
|
||||||
|
|
|
@ -2561,3 +2561,4 @@ _dataSaver:
|
||||||
description: "If code highlighting notations are used in MFM, etc., they will not load until tapped. Syntax highlighting requires downloading the highlight definition files for each programming language. Therefore, disabling the automatic loading of these files is expected to reduce the amount of communication data."
|
description: "If code highlighting notations are used in MFM, etc., they will not load until tapped. Syntax highlighting requires downloading the highlight definition files for each programming language. Therefore, disabling the automatic loading of these files is expected to reduce the amount of communication data."
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Total"
|
total: "Total"
|
||||||
|
|
||||||
|
|
|
@ -2429,3 +2429,4 @@ _dataSaver:
|
||||||
description: "Si se usa resaltado de código en MFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos."
|
description: "Si se usa resaltado de código en MFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos."
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Total"
|
total: "Total"
|
||||||
|
|
||||||
|
|
|
@ -2085,3 +2085,4 @@ _dataSaver:
|
||||||
description: "Si la notation de mise en évidence du code est utilisée, par exemple dans la MFM, elle ne sera pas chargée tant qu'elle n'aura pas été tapée. La mise en évidence du code nécessite le chargement du fichier de définition de chaque langue à mettre en évidence, mais comme ces fichiers ne sont plus chargés automatiquement, on peut s'attendre à une réduction du trafic de données."
|
description: "Si la notation de mise en évidence du code est utilisée, par exemple dans la MFM, elle ne sera pas chargée tant qu'elle n'aura pas été tapée. La mise en évidence du code nécessite le chargement du fichier de définition de chaque langue à mettre en évidence, mais comme ces fichiers ne sont plus chargés automatiquement, on peut s'attendre à une réduction du trafic de données."
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Total"
|
total: "Total"
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,4 @@ _lang_: "japanski"
|
||||||
ok: "OK"
|
ok: "OK"
|
||||||
gotIt: "Razumijem"
|
gotIt: "Razumijem"
|
||||||
cancel: "otkazati"
|
cancel: "otkazati"
|
||||||
|
|
||||||
|
|
|
@ -16,3 +16,4 @@ _2fa:
|
||||||
renewTOTPCancel: "Sispann"
|
renewTOTPCancel: "Sispann"
|
||||||
_widgets:
|
_widgets:
|
||||||
profile: "pwofil"
|
profile: "pwofil"
|
||||||
|
|
||||||
|
|
|
@ -102,3 +102,4 @@ _deck:
|
||||||
_columns:
|
_columns:
|
||||||
notifications: "Értesítések"
|
notifications: "Értesítések"
|
||||||
tl: "Idővonal"
|
tl: "Idővonal"
|
||||||
|
|
||||||
|
|
|
@ -2321,3 +2321,4 @@ _dataSaver:
|
||||||
description: "Jika notasi penyorotan kode digunakan di MFM, dll. Fungsi tersebut tidak akan dimuat apabila tidak diketuk. Penyorotan sintaks membutuhkan pengunduhan berkas definisi penyorotan untuk setiap bahasa pemrograman. Oleh sebab itu, menonaktifkan pemuatan otomatis dari berkas ini dilakukan untuk mengurangi jumlah komunikasi data."
|
description: "Jika notasi penyorotan kode digunakan di MFM, dll. Fungsi tersebut tidak akan dimuat apabila tidak diketuk. Penyorotan sintaks membutuhkan pengunduhan berkas definisi penyorotan untuk setiap bahasa pemrograman. Oleh sebab itu, menonaktifkan pemuatan otomatis dari berkas ini dilakukan untuk mengurangi jumlah komunikasi data."
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Jumlah"
|
total: "Jumlah"
|
||||||
|
|
||||||
|
|
|
@ -2361,3 +2361,4 @@ _dataSaver:
|
||||||
description: "Impedire che il codice sorgente sia automaticamente evidenziato. Evidenziare il codice richiede il caricamento di un file per ogni linguaggio. Puoi evidenziare soltanto il codice che intendi leggere e ridurre il traffico inutilizzato."
|
description: "Impedire che il codice sorgente sia automaticamente evidenziato. Evidenziare il codice richiede il caricamento di un file per ogni linguaggio. Puoi evidenziare soltanto il codice che intendi leggere e ridurre il traffico inutilizzato."
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Totale"
|
total: "Totale"
|
||||||
|
|
||||||
|
|
|
@ -2414,3 +2414,4 @@ _dataSaver:
|
||||||
description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。"
|
description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "合計"
|
total: "合計"
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
---
|
---
|
||||||
_lang_: "la .lojban."
|
_lang_: "la .lojban."
|
||||||
headlineMisskey: "lo se tcana noi jorne fi loi notci"
|
headlineMisskey: "lo se tcana noi jorne fi loi notci"
|
||||||
|
|
||||||
|
|
|
@ -104,3 +104,4 @@ _deck:
|
||||||
_columns:
|
_columns:
|
||||||
notifications: "Ilɣuyen"
|
notifications: "Ilɣuyen"
|
||||||
list: "Tibdarin"
|
list: "Tibdarin"
|
||||||
|
|
||||||
|
|
|
@ -84,3 +84,4 @@ _deck:
|
||||||
notifications: "ಅಧಿಸೂಚನೆಗಳು"
|
notifications: "ಅಧಿಸೂಚನೆಗಳು"
|
||||||
tl: "ಸಮಯಸಾಲು"
|
tl: "ಸಮಯಸಾಲು"
|
||||||
mentions: "ಹೆಸರಿಸಿದ"
|
mentions: "ಹೆಸರಿಸಿದ"
|
||||||
|
|
||||||
|
|
|
@ -726,3 +726,4 @@ _moderationLogTypes:
|
||||||
resolveAbuseReport: "신고 해겔하기"
|
resolveAbuseReport: "신고 해겔하기"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "합계"
|
total: "합계"
|
||||||
|
|
||||||
|
|
|
@ -2415,3 +2415,4 @@ _dataSaver:
|
||||||
description: "MFM 등으로 문자열 강조 기법을 사용할 때 누르기 전에는 불러오지 않습니다. 문자열 강조에서는 강조할 언어마다 그 정의 파일을 불러와야 하지만 이를 자동으로 불러오지 않으므로 데이터 사용량을 줄일 수 있습니다."
|
description: "MFM 등으로 문자열 강조 기법을 사용할 때 누르기 전에는 불러오지 않습니다. 문자열 강조에서는 강조할 언어마다 그 정의 파일을 불러와야 하지만 이를 자동으로 불러오지 않으므로 데이터 사용량을 줄일 수 있습니다."
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "합계"
|
total: "합계"
|
||||||
|
|
||||||
|
|
|
@ -466,3 +466,4 @@ _webhookSettings:
|
||||||
name: "ຊື່"
|
name: "ຊື່"
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
suspend: "ລະງັບ"
|
suspend: "ລະງັບ"
|
||||||
|
|
||||||
|
|
|
@ -497,3 +497,4 @@ _webhookSettings:
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
suspend: "Opschorten"
|
suspend: "Opschorten"
|
||||||
resetPassword: "Wachtwoord terugzetten"
|
resetPassword: "Wachtwoord terugzetten"
|
||||||
|
|
||||||
|
|
|
@ -720,3 +720,4 @@ _webhookSettings:
|
||||||
name: "Navn"
|
name: "Navn"
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
suspend: "Suspender"
|
suspend: "Suspender"
|
||||||
|
|
||||||
|
|
|
@ -1399,3 +1399,4 @@ _moderationLogTypes:
|
||||||
resetPassword: "Zresetuj hasło"
|
resetPassword: "Zresetuj hasło"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Łącznie"
|
total: "Łącznie"
|
||||||
|
|
||||||
|
|
|
@ -1500,3 +1500,4 @@ _moderationLogTypes:
|
||||||
resetPassword: "Redefinir senha"
|
resetPassword: "Redefinir senha"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Total"
|
total: "Total"
|
||||||
|
|
||||||
|
|
|
@ -729,3 +729,4 @@ _moderationLogTypes:
|
||||||
resetPassword: "Resetează parola"
|
resetPassword: "Resetează parola"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Total"
|
total: "Total"
|
||||||
|
|
||||||
|
|
|
@ -1972,3 +1972,4 @@ _moderationLogTypes:
|
||||||
resetPassword: "Сброс пароля:"
|
resetPassword: "Сброс пароля:"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Всего"
|
total: "Всего"
|
||||||
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -1447,3 +1447,4 @@ _moderationLogTypes:
|
||||||
resetPassword: "Resetovať heslo"
|
resetPassword: "Resetovať heslo"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Celkom"
|
total: "Celkom"
|
||||||
|
|
||||||
|
|
|
@ -576,3 +576,4 @@ _webhookSettings:
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
suspend: "Suspendera"
|
suspend: "Suspendera"
|
||||||
resetPassword: "Återställ Lösenord"
|
resetPassword: "Återställ Lösenord"
|
||||||
|
|
||||||
|
|
|
@ -2440,3 +2440,4 @@ _dataSaver:
|
||||||
description: "หากใช้สัญลักษณ์ไฮไลต์โค้ดใน MFM ฯลฯ สัญลักษณ์เหล่านั้นจะไม่โหลดจนกว่าจะแตะ การไฮไลต์ไวยากรณ์(syntax)จำเป็นต้องดาวน์โหลดไฟล์คำจำกัดความของไฮไลต์สำหรับแต่ละภาษา ดังนั้นการปิดใช้งานการโหลดไฟล์เหล่านี้โดยอัตโนมัติจึงคาดว่าจะช่วยลดปริมาณข้อมูลการสื่อสารได้"
|
description: "หากใช้สัญลักษณ์ไฮไลต์โค้ดใน MFM ฯลฯ สัญลักษณ์เหล่านั้นจะไม่โหลดจนกว่าจะแตะ การไฮไลต์ไวยากรณ์(syntax)จำเป็นต้องดาวน์โหลดไฟล์คำจำกัดความของไฮไลต์สำหรับแต่ละภาษา ดังนั้นการปิดใช้งานการโหลดไฟล์เหล่านี้โดยอัตโนมัติจึงคาดว่าจะช่วยลดปริมาณข้อมูลการสื่อสารได้"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "รวมทั้งหมด"
|
total: "รวมทั้งหมด"
|
||||||
|
|
||||||
|
|
|
@ -455,3 +455,4 @@ _deck:
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
suspend: "askıya al"
|
suspend: "askıya al"
|
||||||
resetPassword: "Şifre sıfırlama"
|
resetPassword: "Şifre sıfırlama"
|
||||||
|
|
||||||
|
|
|
@ -17,3 +17,4 @@ _2fa:
|
||||||
renewTOTPCancel: "ئۇنى توختىتىڭ"
|
renewTOTPCancel: "ئۇنى توختىتىڭ"
|
||||||
_widgets:
|
_widgets:
|
||||||
profile: "profile"
|
profile: "profile"
|
||||||
|
|
||||||
|
|
|
@ -1622,3 +1622,4 @@ _moderationLogTypes:
|
||||||
resetPassword: "Скинути пароль"
|
resetPassword: "Скинути пароль"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Всього"
|
total: "Всього"
|
||||||
|
|
||||||
|
|
|
@ -1090,3 +1090,4 @@ _moderationLogTypes:
|
||||||
resetPassword: "Parolni tiklash"
|
resetPassword: "Parolni tiklash"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Jami"
|
total: "Jami"
|
||||||
|
|
||||||
|
|
|
@ -1852,3 +1852,4 @@ _moderationLogTypes:
|
||||||
resetPassword: "Đặt lại mật khẩu"
|
resetPassword: "Đặt lại mật khẩu"
|
||||||
_reversi:
|
_reversi:
|
||||||
total: "Tổng cộng"
|
total: "Tổng cộng"
|
||||||
|
|
||||||
|
|
|
@ -1200,6 +1200,8 @@ replaying: "重播中"
|
||||||
ranking: "排行榜"
|
ranking: "排行榜"
|
||||||
lastNDays: "最近 {n} 天"
|
lastNDays: "最近 {n} 天"
|
||||||
backToTitle: "返回标题"
|
backToTitle: "返回标题"
|
||||||
|
hemisphere: "居住地区"
|
||||||
|
withSensitive: "显示包含敏感媒体的帖子"
|
||||||
enableHorizontalSwipe: "滑动切换标签页"
|
enableHorizontalSwipe: "滑动切换标签页"
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "游戏说明"
|
howToPlay: "游戏说明"
|
||||||
|
@ -2427,9 +2429,14 @@ _dataSaver:
|
||||||
_code:
|
_code:
|
||||||
title: "代码高亮"
|
title: "代码高亮"
|
||||||
description: "如果使用了代码高亮标记,例如在 MFM 中,则在点击之前不会加载。 代码高亮要求加载每种高亮语言的定义文件,由于这些文件不再自动加载,因此有望减少数据传输量。"
|
description: "如果使用了代码高亮标记,例如在 MFM 中,则在点击之前不会加载。 代码高亮要求加载每种高亮语言的定义文件,由于这些文件不再自动加载,因此有望减少数据传输量。"
|
||||||
|
_hemisphere:
|
||||||
|
N: "北半球"
|
||||||
|
S: "南半球"
|
||||||
|
caption: "在某些客户端设置中用来确定季节"
|
||||||
_reversi:
|
_reversi:
|
||||||
reversi: "黑白棋"
|
reversi: "黑白棋"
|
||||||
total: "总计"
|
total: "总计"
|
||||||
_offlineScreen:
|
_offlineScreen:
|
||||||
title: "离线——无法连接到服务器"
|
title: "离线——无法连接到服务器"
|
||||||
header: "无法连接到服务器"
|
header: "无法连接到服务器"
|
||||||
|
|
||||||
|
|
|
@ -1202,6 +1202,9 @@ replaying: "重播中"
|
||||||
ranking: "排行榜"
|
ranking: "排行榜"
|
||||||
lastNDays: "過去 {n} 天"
|
lastNDays: "過去 {n} 天"
|
||||||
backToTitle: "回到遊戲標題頁"
|
backToTitle: "回到遊戲標題頁"
|
||||||
|
hemisphere: "您居住的地區"
|
||||||
|
withSensitive: "顯示包含敏感檔案的貼文"
|
||||||
|
userSaysSomethingSensitive: "包含 {name} 敏感檔案的貼文"
|
||||||
enableHorizontalSwipe: "滑動切換時間軸"
|
enableHorizontalSwipe: "滑動切換時間軸"
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "玩法說明"
|
howToPlay: "玩法說明"
|
||||||
|
@ -2438,5 +2441,48 @@ _dataSaver:
|
||||||
_code:
|
_code:
|
||||||
title: "程式碼突出顯示"
|
title: "程式碼突出顯示"
|
||||||
description: "如果使用了 MFM 的程式碼突顯標記,則在點擊之前不會載入。程式碼突顯要求加載每種程式語言的突顯定義檔案,但由於這些檔案不再自動載入,因此有望減少資料流量。"
|
description: "如果使用了 MFM 的程式碼突顯標記,則在點擊之前不會載入。程式碼突顯要求加載每種程式語言的突顯定義檔案,但由於這些檔案不再自動載入,因此有望減少資料流量。"
|
||||||
|
_hemisphere:
|
||||||
|
N: "北半球"
|
||||||
|
S: "南半球"
|
||||||
|
caption: "在某些客戶端的設定中,用於判斷季節。"
|
||||||
_reversi:
|
_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: "合計"
|
total: "合計"
|
||||||
|
turnCount: "{count} 回合"
|
||||||
|
myGames: "我的對弈"
|
||||||
|
allGames: "所有對弈"
|
||||||
|
ended: ""
|
||||||
|
playing: "正在對弈"
|
||||||
|
isLlotheo: "子較少的一方為勝(顛倒規則)"
|
||||||
|
loopedMap: "循環棋盤"
|
||||||
|
canPutEverywhere: "隨意置放模式"
|
||||||
|
timeLimitForEachTurn: "每回合的時間限制"
|
||||||
|
freeMatch: "自由對戰"
|
||||||
|
lookingForPlayer: "正在搜尋對手"
|
||||||
|
gameCanceled: "對弈已被取消"
|
||||||
|
_offlineScreen:
|
||||||
|
title: "離線-無法連接伺服器"
|
||||||
|
header: "無法連接伺服器"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "sharkey",
|
"name": "sharkey",
|
||||||
"version": "2024.1.0.beta2",
|
"version": "2024.2.0-beta1",
|
||||||
"codename": "shonk",
|
"codename": "shonk",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -56,8 +56,8 @@
|
||||||
"typescript": "5.3.3"
|
"typescript": "5.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "6.19.0",
|
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||||
"@typescript-eslint/parser": "6.19.0",
|
"@typescript-eslint/parser": "6.18.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.6.3",
|
"cypress": "13.6.3",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./built/boot/entry.js",
|
"start": "node ./built/boot/entry.js",
|
||||||
"start:test": "NODE_ENV=test node ./built/boot/entry.js",
|
"start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||||
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
||||||
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||||
"check:connect": "node ./check_connect.js",
|
"check:connect": "node ./check_connect.js",
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||||
"test-and-coverage": "pnpm jest-and-coverage",
|
"test-and-coverage": "pnpm jest-and-coverage",
|
||||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||||
"generate-api-json": "node ./generate_api_json.js"
|
"generate-api-json": "pnpm build && node ./generate_api_json.js"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-android-arm64": "1.3.11",
|
"@swc/core-android-arm64": "1.3.11",
|
||||||
|
|
|
@ -55,23 +55,29 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'antennaCreated':
|
case 'antennaCreated':
|
||||||
this.antennas.push({
|
this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
...body,
|
...body,
|
||||||
lastUsedAt: new Date(body.lastUsedAt),
|
lastUsedAt: new Date(body.lastUsedAt),
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
|
userList: null, // joinなカラムは通常取ってこないので
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'antennaUpdated': {
|
case 'antennaUpdated': {
|
||||||
const idx = this.antennas.findIndex(a => a.id === body.id);
|
const idx = this.antennas.findIndex(a => a.id === body.id);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
this.antennas[idx] = {
|
this.antennas[idx] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
...body,
|
...body,
|
||||||
lastUsedAt: new Date(body.lastUsedAt),
|
lastUsedAt: new Date(body.lastUsedAt),
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
|
userList: null, // joinなカラムは通常取ってこないので
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり
|
// サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり
|
||||||
this.antennas.push({
|
this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
...body,
|
...body,
|
||||||
lastUsedAt: new Date(body.lastUsedAt),
|
lastUsedAt: new Date(body.lastUsedAt),
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
|
userList: null, // joinなカラムは通常取ってこないので
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,10 @@ export class MetaService implements OnApplicationShutdown {
|
||||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'metaUpdated': {
|
case 'metaUpdated': {
|
||||||
this.cache = body;
|
this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
|
...body,
|
||||||
|
proxyAccount: null, // joinなカラムは通常取ってこないので
|
||||||
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -12,18 +12,14 @@ import { IsNull } from 'typeorm';
|
||||||
import type {
|
import type {
|
||||||
MiReversiGame,
|
MiReversiGame,
|
||||||
ReversiGamesRepository,
|
ReversiGamesRepository,
|
||||||
UsersRepository,
|
|
||||||
} from '@/models/_.js';
|
} from '@/models/_.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { Serialized } from '@/types.js';
|
import { Serialized } from '@/types.js';
|
||||||
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
||||||
|
@ -58,7 +54,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async cacheGame(game: MiReversiGame) {
|
private async cacheGame(game: MiReversiGame) {
|
||||||
await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 3, JSON.stringify(game));
|
await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 60, JSON.stringify(game));
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -66,6 +62,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
await this.redisClient.del(`reversi:game:cache:${gameId}`);
|
await this.redisClient.del(`reversi:game:cache:${gameId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private getBakeProps(game: MiReversiGame) {
|
||||||
|
return {
|
||||||
|
startedAt: game.startedAt,
|
||||||
|
endedAt: game.endedAt,
|
||||||
|
// ゲームの途中からユーザーが変わることは無いので
|
||||||
|
//user1Id: game.user1Id,
|
||||||
|
//user2Id: game.user2Id,
|
||||||
|
user1Ready: game.user1Ready,
|
||||||
|
user2Ready: game.user2Ready,
|
||||||
|
black: game.black,
|
||||||
|
isStarted: game.isStarted,
|
||||||
|
isEnded: game.isEnded,
|
||||||
|
winnerId: game.winnerId,
|
||||||
|
surrenderedUserId: game.surrenderedUserId,
|
||||||
|
timeoutUserId: game.timeoutUserId,
|
||||||
|
isLlotheo: game.isLlotheo,
|
||||||
|
canPutEverywhere: game.canPutEverywhere,
|
||||||
|
loopedBoard: game.loopedBoard,
|
||||||
|
timeLimitForEachTurn: game.timeLimitForEachTurn,
|
||||||
|
logs: game.logs,
|
||||||
|
map: game.map,
|
||||||
|
bw: game.bw,
|
||||||
|
crc32: game.crc32,
|
||||||
|
} satisfies Partial<MiReversiGame>;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
|
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
|
||||||
if (targetUser.id === me.id) {
|
if (targetUser.id === me.id) {
|
||||||
|
@ -81,23 +104,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
if (invitations.includes(targetUser.id)) {
|
if (invitations.includes(targetUser.id)) {
|
||||||
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id);
|
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id);
|
||||||
|
|
||||||
const game = await this.reversiGamesRepository.insert({
|
const game = await this.matched(targetUser.id, me.id);
|
||||||
id: this.idService.gen(),
|
|
||||||
user1Id: targetUser.id,
|
|
||||||
user2Id: me.id,
|
|
||||||
user1Ready: false,
|
|
||||||
user2Ready: false,
|
|
||||||
isStarted: false,
|
|
||||||
isEnded: false,
|
|
||||||
logs: [],
|
|
||||||
map: Reversi.maps.eighteight.data,
|
|
||||||
bw: 'random',
|
|
||||||
isLlotheo: false,
|
|
||||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
|
||||||
this.cacheGame(game);
|
|
||||||
|
|
||||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: targetUser.id });
|
|
||||||
this.globalEventService.publishReversiStream(targetUser.id, 'matched', { game: packed });
|
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
} else {
|
} else {
|
||||||
|
@ -124,23 +131,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
const invitorId = invitations[Math.floor(Math.random() * invitations.length)];
|
const invitorId = invitations[Math.floor(Math.random() * invitations.length)];
|
||||||
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId);
|
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId);
|
||||||
|
|
||||||
const game = await this.reversiGamesRepository.insert({
|
const game = await this.matched(invitorId, me.id);
|
||||||
id: this.idService.gen(),
|
|
||||||
user1Id: invitorId,
|
|
||||||
user2Id: me.id,
|
|
||||||
user1Ready: false,
|
|
||||||
user2Ready: false,
|
|
||||||
isStarted: false,
|
|
||||||
isEnded: false,
|
|
||||||
logs: [],
|
|
||||||
map: Reversi.maps.eighteight.data,
|
|
||||||
bw: 'random',
|
|
||||||
isLlotheo: false,
|
|
||||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
|
||||||
this.cacheGame(game);
|
|
||||||
|
|
||||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: invitorId });
|
|
||||||
this.globalEventService.publishReversiStream(invitorId, 'matched', { game: packed });
|
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
@ -160,23 +151,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId);
|
await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId);
|
||||||
|
|
||||||
const game = await this.reversiGamesRepository.insert({
|
const game = await this.matched(matchedUserId, me.id);
|
||||||
id: this.idService.gen(),
|
|
||||||
user1Id: matchedUserId,
|
|
||||||
user2Id: me.id,
|
|
||||||
user1Ready: false,
|
|
||||||
user2Ready: false,
|
|
||||||
isStarted: false,
|
|
||||||
isEnded: false,
|
|
||||||
logs: [],
|
|
||||||
map: Reversi.maps.eighteight.data,
|
|
||||||
bw: 'random',
|
|
||||||
isLlotheo: false,
|
|
||||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
|
||||||
this.cacheGame(game);
|
|
||||||
|
|
||||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: matchedUserId });
|
|
||||||
this.globalEventService.publishReversiStream(matchedUserId, 'matched', { game: packed });
|
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
} else {
|
} else {
|
||||||
|
@ -204,14 +179,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
let isBothReady = false;
|
let isBothReady = false;
|
||||||
|
|
||||||
if (game.user1Id === user.id) {
|
if (game.user1Id === user.id) {
|
||||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
const updatedGame = {
|
||||||
.set({
|
...game,
|
||||||
user1Ready: ready,
|
user1Ready: ready,
|
||||||
})
|
};
|
||||||
.where('id = :id', { id: game.id })
|
|
||||||
.returning('*')
|
|
||||||
.execute()
|
|
||||||
.then((response) => response.raw[0]);
|
|
||||||
this.cacheGame(updatedGame);
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
||||||
|
@ -221,14 +192,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
if (ready && updatedGame.user2Ready) isBothReady = true;
|
if (ready && updatedGame.user2Ready) isBothReady = true;
|
||||||
} else if (game.user2Id === user.id) {
|
} else if (game.user2Id === user.id) {
|
||||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
const updatedGame = {
|
||||||
.set({
|
...game,
|
||||||
user2Ready: ready,
|
user2Ready: ready,
|
||||||
})
|
};
|
||||||
.where('id = :id', { id: game.id })
|
|
||||||
.returning('*')
|
|
||||||
.execute()
|
|
||||||
.then((response) => response.raw[0]);
|
|
||||||
this.cacheGame(updatedGame);
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
||||||
|
@ -253,6 +220,32 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async matched(parentId: MiUser['id'], childId: MiUser['id']): Promise<MiReversiGame> {
|
||||||
|
const game = await this.reversiGamesRepository.insert({
|
||||||
|
id: this.idService.gen(),
|
||||||
|
user1Id: parentId,
|
||||||
|
user2Id: childId,
|
||||||
|
user1Ready: false,
|
||||||
|
user2Ready: false,
|
||||||
|
isStarted: false,
|
||||||
|
isEnded: false,
|
||||||
|
logs: [],
|
||||||
|
map: Reversi.maps.eighteight.data,
|
||||||
|
bw: 'random',
|
||||||
|
isLlotheo: false,
|
||||||
|
}).then(x => this.reversiGamesRepository.findOneOrFail({
|
||||||
|
where: { id: x.identifiers[0].id },
|
||||||
|
relations: ['user1', 'user2'],
|
||||||
|
}));
|
||||||
|
this.cacheGame(game);
|
||||||
|
|
||||||
|
const packed = await this.reversiGameEntityService.packDetail(game);
|
||||||
|
this.globalEventService.publishReversiStream(parentId, 'matched', { game: packed });
|
||||||
|
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async startGame(game: MiReversiGame) {
|
private async startGame(game: MiReversiGame) {
|
||||||
let bw: number;
|
let bw: number;
|
||||||
|
@ -262,63 +255,44 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
bw = parseInt(game.bw, 10);
|
bw = parseInt(game.bw, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRandomMap() {
|
|
||||||
const mapCount = Object.entries(Reversi.maps).length;
|
|
||||||
const rnd = Math.floor(Math.random() * mapCount);
|
|
||||||
return Object.values(Reversi.maps)[rnd].data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const map = game.map != null ? game.map : getRandomMap();
|
|
||||||
|
|
||||||
const crc32 = CRC32.str(JSON.stringify(game.logs)).toString();
|
const crc32 = CRC32.str(JSON.stringify(game.logs)).toString();
|
||||||
|
|
||||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||||
.set({
|
.set({
|
||||||
|
...this.getBakeProps(game),
|
||||||
startedAt: new Date(),
|
startedAt: new Date(),
|
||||||
isStarted: true,
|
isStarted: true,
|
||||||
black: bw,
|
black: bw,
|
||||||
map: map,
|
map: game.map,
|
||||||
crc32,
|
crc32,
|
||||||
})
|
})
|
||||||
.where('id = :id', { id: game.id })
|
.where('id = :id', { id: game.id })
|
||||||
.returning('*')
|
.returning('*')
|
||||||
.execute()
|
.execute()
|
||||||
.then((response) => response.raw[0]);
|
.then((response) => response.raw[0]);
|
||||||
|
// キャッシュ効率化のためにユーザー情報は再利用
|
||||||
|
updatedGame.user1 = game.user1;
|
||||||
|
updatedGame.user2 = game.user2;
|
||||||
this.cacheGame(updatedGame);
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
||||||
const engine = new Reversi.Game(map, {
|
const engine = new Reversi.Game(updatedGame.map, {
|
||||||
isLlotheo: game.isLlotheo,
|
isLlotheo: updatedGame.isLlotheo,
|
||||||
canPutEverywhere: game.canPutEverywhere,
|
canPutEverywhere: updatedGame.canPutEverywhere,
|
||||||
loopedBoard: game.loopedBoard,
|
loopedBoard: updatedGame.loopedBoard,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (engine.isEnded) {
|
if (engine.isEnded) {
|
||||||
let winner;
|
let winnerId;
|
||||||
if (engine.winner === true) {
|
if (engine.winner === true) {
|
||||||
winner = bw === 1 ? game.user1Id : game.user2Id;
|
winnerId = bw === 1 ? updatedGame.user1Id : updatedGame.user2Id;
|
||||||
} else if (engine.winner === false) {
|
} else if (engine.winner === false) {
|
||||||
winner = bw === 1 ? game.user2Id : game.user1Id;
|
winnerId = bw === 1 ? updatedGame.user2Id : updatedGame.user1Id;
|
||||||
} else {
|
} else {
|
||||||
winner = null;
|
winnerId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
await this.endGame(updatedGame, winnerId, null);
|
||||||
.set({
|
|
||||||
isEnded: true,
|
|
||||||
endedAt: new Date(),
|
|
||||||
winnerId: winner,
|
|
||||||
})
|
|
||||||
.where('id = :id', { id: game.id })
|
|
||||||
.returning('*')
|
|
||||||
.execute()
|
|
||||||
.then((response) => response.raw[0]);
|
|
||||||
this.cacheGame(updatedGame);
|
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
|
||||||
winnerId: winner,
|
|
||||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -327,7 +301,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, '');
|
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, '');
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'started', {
|
this.globalEventService.publishReversiGameStream(game.id, 'started', {
|
||||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
game: await this.reversiGameEntityService.packDetail(updatedGame),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async endGame(game: MiReversiGame, winnerId: MiUser['id'] | null, reason: 'surrender' | 'timeout' | null) {
|
||||||
|
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||||
|
.set({
|
||||||
|
...this.getBakeProps(game),
|
||||||
|
isEnded: true,
|
||||||
|
endedAt: new Date(),
|
||||||
|
winnerId: winnerId,
|
||||||
|
surrenderedUserId: reason === 'surrender' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null,
|
||||||
|
timeoutUserId: reason === 'timeout' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null,
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: game.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => response.raw[0]);
|
||||||
|
// キャッシュ効率化のためにユーザー情報は再利用
|
||||||
|
updatedGame.user1 = game.user1;
|
||||||
|
updatedGame.user2 = game.user2;
|
||||||
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
|
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||||
|
winnerId: winnerId,
|
||||||
|
game: await this.reversiGameEntityService.packDetail(updatedGame),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,14 +354,10 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
// TODO: より厳格なバリデーション
|
// TODO: より厳格なバリデーション
|
||||||
|
|
||||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
const updatedGame = {
|
||||||
.set({
|
...game,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
})
|
};
|
||||||
.where('id = :id', { id: game.id })
|
|
||||||
.returning('*')
|
|
||||||
.execute()
|
|
||||||
.then((response) => response.raw[0]);
|
|
||||||
this.cacheGame(updatedGame);
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', {
|
this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', {
|
||||||
|
@ -397,17 +393,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
engine.putStone(pos);
|
engine.putStone(pos);
|
||||||
|
|
||||||
let winner;
|
|
||||||
if (engine.isEnded) {
|
|
||||||
if (engine.winner === true) {
|
|
||||||
winner = game.black === 1 ? game.user1Id : game.user2Id;
|
|
||||||
} else if (engine.winner === false) {
|
|
||||||
winner = game.black === 1 ? game.user2Id : game.user1Id;
|
|
||||||
} else {
|
|
||||||
winner = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logs = Reversi.Serializer.deserializeLogs(game.logs);
|
const logs = Reversi.Serializer.deserializeLogs(game.logs);
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
|
@ -423,17 +408,11 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString();
|
const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString();
|
||||||
|
|
||||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
const updatedGame = {
|
||||||
.set({
|
...game,
|
||||||
crc32,
|
crc32,
|
||||||
isEnded: engine.isEnded,
|
logs: serializeLogs,
|
||||||
winnerId: winner,
|
};
|
||||||
logs: serializeLogs,
|
|
||||||
})
|
|
||||||
.where('id = :id', { id: game.id })
|
|
||||||
.returning('*')
|
|
||||||
.execute()
|
|
||||||
.then((response) => response.raw[0]);
|
|
||||||
this.cacheGame(updatedGame);
|
this.cacheGame(updatedGame);
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'log', {
|
this.globalEventService.publishReversiGameStream(game.id, 'log', {
|
||||||
|
@ -442,10 +421,16 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (engine.isEnded) {
|
if (engine.isEnded) {
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
let winnerId;
|
||||||
winnerId: winner ?? null,
|
if (engine.winner === true) {
|
||||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
winnerId = game.black === 1 ? game.user1Id : game.user2Id;
|
||||||
});
|
} else if (engine.winner === false) {
|
||||||
|
winnerId = game.black === 1 ? game.user2Id : game.user1Id;
|
||||||
|
} else {
|
||||||
|
winnerId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.endGame(updatedGame, winnerId, null);
|
||||||
} else {
|
} else {
|
||||||
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`, updatedGame.timeLimitForEachTurn, '');
|
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`, updatedGame.timeLimitForEachTurn, '');
|
||||||
}
|
}
|
||||||
|
@ -460,23 +445,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
|
const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
|
||||||
|
|
||||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
await this.endGame(game, winnerId, 'surrender');
|
||||||
.set({
|
|
||||||
isEnded: true,
|
|
||||||
endedAt: new Date(),
|
|
||||||
winnerId: winnerId,
|
|
||||||
surrenderedUserId: user.id,
|
|
||||||
})
|
|
||||||
.where('id = :id', { id: game.id })
|
|
||||||
.returning('*')
|
|
||||||
.execute()
|
|
||||||
.then((response) => response.raw[0]);
|
|
||||||
this.cacheGame(updatedGame);
|
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
|
||||||
winnerId: winnerId,
|
|
||||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -500,23 +469,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
if (timer === 0) {
|
if (timer === 0) {
|
||||||
const winnerId = engine.turn ? (game.black === 1 ? game.user2Id : game.user1Id) : (game.black === 1 ? game.user1Id : game.user2Id);
|
const winnerId = engine.turn ? (game.black === 1 ? game.user2Id : game.user1Id) : (game.black === 1 ? game.user1Id : game.user2Id);
|
||||||
|
|
||||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
await this.endGame(game, winnerId, 'timeout');
|
||||||
.set({
|
|
||||||
isEnded: true,
|
|
||||||
endedAt: new Date(),
|
|
||||||
winnerId: winnerId,
|
|
||||||
timeoutUserId: engine.turn ? (game.black === 1 ? game.user1Id : game.user2Id) : (game.black === 1 ? game.user2Id : game.user1Id),
|
|
||||||
})
|
|
||||||
.where('id = :id', { id: game.id })
|
|
||||||
.returning('*')
|
|
||||||
.execute()
|
|
||||||
.then((response) => response.raw[0]);
|
|
||||||
this.cacheGame(updatedGame);
|
|
||||||
|
|
||||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
|
||||||
winnerId: winnerId,
|
|
||||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,14 +492,36 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||||
public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
|
public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
|
||||||
const cached = await this.redisClient.get(`reversi:game:cache:${id}`);
|
const cached = await this.redisClient.get(`reversi:game:cache:${id}`);
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
|
// TODO: この辺りのデシリアライズ処理をどこか別のサービスに切り出したい
|
||||||
const parsed = JSON.parse(cached) as Serialized<MiReversiGame>;
|
const parsed = JSON.parse(cached) as Serialized<MiReversiGame>;
|
||||||
return {
|
return {
|
||||||
...parsed,
|
...parsed,
|
||||||
startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null,
|
startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null,
|
||||||
endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : null,
|
endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : null,
|
||||||
|
user1: parsed.user1 != null ? {
|
||||||
|
...parsed.user1,
|
||||||
|
avatar: null,
|
||||||
|
banner: null,
|
||||||
|
updatedAt: parsed.user1.updatedAt != null ? new Date(parsed.user1.updatedAt) : null,
|
||||||
|
lastActiveDate: parsed.user1.lastActiveDate != null ? new Date(parsed.user1.lastActiveDate) : null,
|
||||||
|
lastFetchedAt: parsed.user1.lastFetchedAt != null ? new Date(parsed.user1.lastFetchedAt) : null,
|
||||||
|
movedAt: parsed.user1.movedAt != null ? new Date(parsed.user1.movedAt) : null,
|
||||||
|
} : null,
|
||||||
|
user2: parsed.user2 != null ? {
|
||||||
|
...parsed.user2,
|
||||||
|
avatar: null,
|
||||||
|
banner: null,
|
||||||
|
updatedAt: parsed.user2.updatedAt != null ? new Date(parsed.user2.updatedAt) : null,
|
||||||
|
lastActiveDate: parsed.user2.lastActiveDate != null ? new Date(parsed.user2.lastActiveDate) : null,
|
||||||
|
lastFetchedAt: parsed.user2.lastFetchedAt != null ? new Date(parsed.user2.lastFetchedAt) : null,
|
||||||
|
movedAt: parsed.user2.movedAt != null ? new Date(parsed.user2.movedAt) : null,
|
||||||
|
} : null,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const game = await this.reversiGamesRepository.findOneBy({ id });
|
const game = await this.reversiGamesRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: ['user1', 'user2'],
|
||||||
|
});
|
||||||
if (game == null) return null;
|
if (game == null) return null;
|
||||||
|
|
||||||
this.cacheGame(game);
|
this.cacheGame(game);
|
||||||
|
|
|
@ -181,9 +181,11 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
case 'userRoleAssigned': {
|
case 'userRoleAssigned': {
|
||||||
const cached = this.roleAssignmentByUserIdCache.get(body.userId);
|
const cached = this.roleAssignmentByUserIdCache.get(body.userId);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
cached.push({
|
cached.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
...body,
|
...body,
|
||||||
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
|
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
|
role: null, // joinなカラムは通常取ってこないので
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -49,9 +49,10 @@ export class WebhookService implements OnApplicationShutdown {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'webhookCreated':
|
case 'webhookCreated':
|
||||||
if (body.active) {
|
if (body.active) {
|
||||||
this.webhooks.push({
|
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
...body,
|
...body,
|
||||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -59,14 +60,16 @@ export class WebhookService implements OnApplicationShutdown {
|
||||||
if (body.active) {
|
if (body.active) {
|
||||||
const i = this.webhooks.findIndex(a => a.id === body.id);
|
const i = this.webhooks.findIndex(a => a.id === body.id);
|
||||||
if (i > -1) {
|
if (i > -1) {
|
||||||
this.webhooks[i] = {
|
this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
...body,
|
...body,
|
||||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.webhooks.push({
|
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||||
...body,
|
...body,
|
||||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||||
|
user: null, // joinなカラムは通常取ってこないので
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -9,7 +9,6 @@ import type { ReversiGamesRepository } from '@/models/_.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { } from '@/models/Blocking.js';
|
import type { } from '@/models/Blocking.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
|
||||||
import type { MiReversiGame } from '@/models/ReversiGame.js';
|
import type { MiReversiGame } from '@/models/ReversiGame.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
@ -29,10 +28,14 @@ export class ReversiGameEntityService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async packDetail(
|
public async packDetail(
|
||||||
src: MiReversiGame['id'] | MiReversiGame,
|
src: MiReversiGame['id'] | MiReversiGame,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
|
||||||
): Promise<Packed<'ReversiGameDetailed'>> {
|
): Promise<Packed<'ReversiGameDetailed'>> {
|
||||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
|
const users = await Promise.all([
|
||||||
|
this.userEntityService.pack(game.user1 ?? game.user1Id),
|
||||||
|
this.userEntityService.pack(game.user2 ?? game.user2Id),
|
||||||
|
]);
|
||||||
|
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
id: game.id,
|
id: game.id,
|
||||||
createdAt: this.idService.parse(game.id).date.toISOString(),
|
createdAt: this.idService.parse(game.id).date.toISOString(),
|
||||||
|
@ -46,10 +49,10 @@ export class ReversiGameEntityService {
|
||||||
user2Ready: game.user2Ready,
|
user2Ready: game.user2Ready,
|
||||||
user1Id: game.user1Id,
|
user1Id: game.user1Id,
|
||||||
user2Id: game.user2Id,
|
user2Id: game.user2Id,
|
||||||
user1: this.userEntityService.pack(game.user1Id, me),
|
user1: users[0],
|
||||||
user2: this.userEntityService.pack(game.user2Id, me),
|
user2: users[1],
|
||||||
winnerId: game.winnerId,
|
winnerId: game.winnerId,
|
||||||
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
|
winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
|
||||||
surrenderedUserId: game.surrenderedUserId,
|
surrenderedUserId: game.surrenderedUserId,
|
||||||
timeoutUserId: game.timeoutUserId,
|
timeoutUserId: game.timeoutUserId,
|
||||||
black: game.black,
|
black: game.black,
|
||||||
|
@ -66,18 +69,21 @@ export class ReversiGameEntityService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public packDetailMany(
|
public packDetailMany(
|
||||||
xs: MiReversiGame[],
|
xs: MiReversiGame[],
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
|
||||||
) {
|
) {
|
||||||
return Promise.all(xs.map(x => this.packDetail(x, me)));
|
return Promise.all(xs.map(x => this.packDetail(x)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async packLite(
|
public async packLite(
|
||||||
src: MiReversiGame['id'] | MiReversiGame,
|
src: MiReversiGame['id'] | MiReversiGame,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
|
||||||
): Promise<Packed<'ReversiGameLite'>> {
|
): Promise<Packed<'ReversiGameLite'>> {
|
||||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
|
const users = await Promise.all([
|
||||||
|
this.userEntityService.pack(game.user1 ?? game.user1Id),
|
||||||
|
this.userEntityService.pack(game.user2 ?? game.user2Id),
|
||||||
|
]);
|
||||||
|
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
id: game.id,
|
id: game.id,
|
||||||
createdAt: this.idService.parse(game.id).date.toISOString(),
|
createdAt: this.idService.parse(game.id).date.toISOString(),
|
||||||
|
@ -85,16 +91,12 @@ export class ReversiGameEntityService {
|
||||||
endedAt: game.endedAt && game.endedAt.toISOString(),
|
endedAt: game.endedAt && game.endedAt.toISOString(),
|
||||||
isStarted: game.isStarted,
|
isStarted: game.isStarted,
|
||||||
isEnded: game.isEnded,
|
isEnded: game.isEnded,
|
||||||
form1: game.form1,
|
|
||||||
form2: game.form2,
|
|
||||||
user1Ready: game.user1Ready,
|
|
||||||
user2Ready: game.user2Ready,
|
|
||||||
user1Id: game.user1Id,
|
user1Id: game.user1Id,
|
||||||
user2Id: game.user2Id,
|
user2Id: game.user2Id,
|
||||||
user1: this.userEntityService.pack(game.user1Id, me),
|
user1: users[0],
|
||||||
user2: this.userEntityService.pack(game.user2Id, me),
|
user2: users[1],
|
||||||
winnerId: game.winnerId,
|
winnerId: game.winnerId,
|
||||||
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
|
winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
|
||||||
surrenderedUserId: game.surrenderedUserId,
|
surrenderedUserId: game.surrenderedUserId,
|
||||||
timeoutUserId: game.timeoutUserId,
|
timeoutUserId: game.timeoutUserId,
|
||||||
black: game.black,
|
black: game.black,
|
||||||
|
@ -109,9 +111,8 @@ export class ReversiGameEntityService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public packLiteMany(
|
public packLiteMany(
|
||||||
xs: MiReversiGame[],
|
xs: MiReversiGame[],
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
|
||||||
) {
|
) {
|
||||||
return Promise.all(xs.map(x => this.packLite(x, me)));
|
return Promise.all(xs.map(x => this.packLite(x)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,22 +34,6 @@ export const packedReversiGameLiteSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
form1: {
|
|
||||||
type: 'any',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
form2: {
|
|
||||||
type: 'any',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
},
|
|
||||||
user1Ready: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
user2Ready: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
user1Id: {
|
user1Id: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -149,11 +133,11 @@ export const packedReversiGameDetailedSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
form1: {
|
form1: {
|
||||||
type: 'any',
|
type: 'object',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
form2: {
|
form2: {
|
||||||
type: 'any',
|
type: 'object',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
user1Ready: {
|
user1Ready: {
|
||||||
|
|
|
@ -43,7 +43,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId)
|
const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId)
|
||||||
.andWhere('game.isStarted = TRUE');
|
.andWhere('game.isStarted = TRUE')
|
||||||
|
.innerJoinAndSelect('game.user1', 'user1')
|
||||||
|
.innerJoinAndSelect('game.user2', 'user2');
|
||||||
|
|
||||||
if (ps.my && me) {
|
if (ps.my && me) {
|
||||||
query.andWhere(new Brackets(qb => {
|
query.andWhere(new Brackets(qb => {
|
||||||
|
@ -55,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
const games = await query.take(ps.limit).getMany();
|
const games = await query.take(ps.limit).getMany();
|
||||||
|
|
||||||
return await this.reversiGameEntityService.packLiteMany(games, me);
|
return await this.reversiGameEntityService.packLiteMany(games);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
if (game == null) return;
|
if (game == null) return;
|
||||||
|
|
||||||
return await this.reversiGameEntityService.packDetail(game, me);
|
return await this.reversiGameEntityService.packDetail(game);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.noSuchGame);
|
throw new ApiError(meta.errors.noSuchGame);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.reversiGameEntityService.packDetail(game, me);
|
return await this.reversiGameEntityService.packDetail(game);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ class ReversiGameChannel extends Channel {
|
||||||
case 'updateSettings': this.updateSettings(body.key, body.value); break;
|
case 'updateSettings': this.updateSettings(body.key, body.value); break;
|
||||||
case 'cancel': this.cancelGame(); break;
|
case 'cancel': this.cancelGame(); break;
|
||||||
case 'putStone': this.putStone(body.pos, body.id); break;
|
case 'putStone': this.putStone(body.pos, body.id); break;
|
||||||
case 'checkState': this.checkState(body.crc32); break;
|
case 'resync': this.resync(body.crc32); break;
|
||||||
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,12 +76,10 @@ class ReversiGameChannel extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async checkState(crc32: string | number) {
|
private async resync(crc32: string | number) {
|
||||||
if (crc32 != null) return;
|
|
||||||
|
|
||||||
const game = await this.reversiService.checkCrc(this.gameId!, crc32);
|
const game = await this.reversiService.checkCrc(this.gameId!, crc32);
|
||||||
if (game) {
|
if (game) {
|
||||||
this.send('rescue', game);
|
this.send('resynced', game);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,12 +31,13 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
||||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||||
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { deepClone } from '@/misc/clone.js';
|
import { deepClone } from '@/misc/clone.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||||
import { FeedService } from './FeedService.js';
|
import { FeedService } from './FeedService.js';
|
||||||
import { UrlPreviewService } from './UrlPreviewService.js';
|
import { UrlPreviewService } from './UrlPreviewService.js';
|
||||||
import { ClientLoggerService } from './ClientLoggerService.js';
|
import { ClientLoggerService } from './ClientLoggerService.js';
|
||||||
|
@ -83,6 +84,9 @@ export class ClientServerService {
|
||||||
@Inject(DI.flashsRepository)
|
@Inject(DI.flashsRepository)
|
||||||
private flashsRepository: FlashsRepository,
|
private flashsRepository: FlashsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.reversiGamesRepository)
|
||||||
|
private reversiGamesRepository: ReversiGamesRepository,
|
||||||
|
|
||||||
private flashEntityService: FlashEntityService,
|
private flashEntityService: FlashEntityService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
|
@ -90,6 +94,7 @@ export class ClientServerService {
|
||||||
private galleryPostEntityService: GalleryPostEntityService,
|
private galleryPostEntityService: GalleryPostEntityService,
|
||||||
private clipEntityService: ClipEntityService,
|
private clipEntityService: ClipEntityService,
|
||||||
private channelEntityService: ChannelEntityService,
|
private channelEntityService: ChannelEntityService,
|
||||||
|
private reversiGameEntityService: ReversiGameEntityService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private urlPreviewService: UrlPreviewService,
|
private urlPreviewService: UrlPreviewService,
|
||||||
private feedService: FeedService,
|
private feedService: FeedService,
|
||||||
|
@ -704,6 +709,25 @@ export class ClientServerService {
|
||||||
return await renderBase(reply);
|
return await renderBase(reply);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reversi game
|
||||||
|
fastify.get<{ Params: { game: string; } }>('/reversi/g/:game', async (request, reply) => {
|
||||||
|
const game = await this.reversiGamesRepository.findOneBy({
|
||||||
|
id: request.params.game,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (game) {
|
||||||
|
const _game = await this.reversiGameEntityService.packDetail(game);
|
||||||
|
const meta = await this.metaService.fetch();
|
||||||
|
reply.header('Cache-Control', 'public, max-age=3600');
|
||||||
|
return await reply.view('reversi-game', {
|
||||||
|
game: _game,
|
||||||
|
...this.generateCommonPugData(meta),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return await renderBase(reply);
|
||||||
|
}
|
||||||
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
fastify.get('/_info_card_', async (request, reply) => {
|
fastify.get('/_info_card_', async (request, reply) => {
|
||||||
|
|
20
packages/backend/src/server/web/views/reversi-game.pug
Normal file
20
packages/backend/src/server/web/views/reversi-game.pug
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
extends ./base
|
||||||
|
|
||||||
|
block vars
|
||||||
|
- const user1 = game.user1;
|
||||||
|
- const user2 = game.user2;
|
||||||
|
- const title = `${user1.username} vs ${user2.username}`;
|
||||||
|
- const url = `${config.url}/reversi/g/${game.id}`;
|
||||||
|
|
||||||
|
block title
|
||||||
|
= `${title} | ${instanceName}`
|
||||||
|
|
||||||
|
block desc
|
||||||
|
meta(name='description' content='⚫⚪Misskey Reversi⚪⚫')
|
||||||
|
|
||||||
|
block og
|
||||||
|
meta(property='og:type' content='article')
|
||||||
|
meta(property='og:title' content= title)
|
||||||
|
meta(property='og:description' content='⚫⚪Misskey Reversi⚪⚫')
|
||||||
|
meta(property='og:url' content= url)
|
||||||
|
meta(property='twitter:card' content='summary')
|
|
@ -283,7 +283,11 @@ export type Serialized<T> = {
|
||||||
? (string | null)
|
? (string | null)
|
||||||
: T[K] extends Record<string, any>
|
: T[K] extends Record<string, any>
|
||||||
? Serialized<T[K]>
|
? Serialized<T[K]>
|
||||||
: T[K];
|
: T[K] extends (Record<string, any> | null)
|
||||||
|
? (Serialized<T[K]> | null)
|
||||||
|
: T[K] extends (Record<string, any> | undefined)
|
||||||
|
? (Serialized<T[K]> | undefined)
|
||||||
|
: T[K];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FilterUnionByProperty<
|
export type FilterUnionByProperty<
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 139 KiB |
|
@ -45,6 +45,7 @@
|
||||||
"crc-32": "^1.2.2",
|
"crc-32": "^1.2.2",
|
||||||
"cropperjs": "2.0.0-beta.4",
|
"cropperjs": "2.0.0-beta.4",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
|
"defu": "^6.1.4",
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"estree-walker": "3.0.3",
|
"estree-walker": "3.0.3",
|
||||||
"eventemitter3": "5.0.1",
|
"eventemitter3": "5.0.1",
|
||||||
|
@ -54,9 +55,9 @@
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"katex": "0.16.9",
|
"katex": "0.16.9",
|
||||||
"matter-js": "0.19.0",
|
"matter-js": "0.19.0",
|
||||||
|
"misskey-bubble-game": "workspace:*",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"misskey-reversi": "workspace:*",
|
"misskey-reversi": "workspace:*",
|
||||||
"misskey-bubble-game": "workspace:*",
|
|
||||||
"photoswipe": "5.4.3",
|
"photoswipe": "5.4.3",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"rollup": "4.9.6",
|
"rollup": "4.9.6",
|
||||||
|
@ -112,7 +113,7 @@
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||||
"@typescript-eslint/parser": "6.18.1",
|
"@typescript-eslint/parser": "6.18.1",
|
||||||
"@vitest/coverage-v8": "1.2.1",
|
"@vitest/coverage-v8": "0.34.6",
|
||||||
"@vue/runtime-core": "3.4.15",
|
"@vue/runtime-core": "3.4.15",
|
||||||
"acorn": "8.11.3",
|
"acorn": "8.11.3",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
|
@ -134,7 +135,7 @@
|
||||||
"storybook": "7.6.10",
|
"storybook": "7.6.10",
|
||||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
"vitest": "1.2.1",
|
"vitest": "0.34.6",
|
||||||
"vitest-fetch-mock": "0.2.2",
|
"vitest-fetch-mock": "0.2.2",
|
||||||
"vue-eslint-parser": "9.4.0",
|
"vue-eslint-parser": "9.4.0",
|
||||||
"vue-tsc": "1.8.27"
|
"vue-tsc": "1.8.27"
|
||||||
|
|
|
@ -163,7 +163,7 @@ const $i = signinRequired();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
game: Misskey.entities.ReversiGameDetailed;
|
game: Misskey.entities.ReversiGameDetailed;
|
||||||
connection: Misskey.ChannelConnection;
|
connection?: Misskey.ChannelConnection | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const showBoardLabels = ref<boolean>(false);
|
const showBoardLabels = ref<boolean>(false);
|
||||||
|
@ -240,10 +240,10 @@ watch(logPos, (v) => {
|
||||||
|
|
||||||
if (game.value.isStarted && !game.value.isEnded) {
|
if (game.value.isStarted && !game.value.isEnded) {
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
if (game.value.isEnded) return;
|
if (game.value.isEnded || props.connection == null) return;
|
||||||
const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString();
|
const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString();
|
||||||
if (_DEV_) console.log('crc32', crc32);
|
if (_DEV_) console.log('crc32', crc32);
|
||||||
props.connection.send('checkState', {
|
props.connection.send('resync', {
|
||||||
crc32: crc32,
|
crc32: crc32,
|
||||||
});
|
});
|
||||||
}, 10000, { immediate: false, afterMounted: true });
|
}, 10000, { immediate: false, afterMounted: true });
|
||||||
|
@ -267,7 +267,7 @@ function putStone(pos) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const id = Math.random().toString(36).slice(2);
|
const id = Math.random().toString(36).slice(2);
|
||||||
props.connection.send('putStone', {
|
props.connection!.send('putStone', {
|
||||||
pos: pos,
|
pos: pos,
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
@ -283,22 +283,24 @@ const myTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
|
||||||
const opTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
|
const opTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
|
||||||
|
|
||||||
const TIMER_INTERVAL_SEC = 3;
|
const TIMER_INTERVAL_SEC = 3;
|
||||||
useInterval(() => {
|
if (!props.game.isEnded) {
|
||||||
if (myTurnTimerRmain.value > 0) {
|
useInterval(() => {
|
||||||
myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC);
|
if (myTurnTimerRmain.value > 0) {
|
||||||
}
|
myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC);
|
||||||
if (opTurnTimerRmain.value > 0) {
|
}
|
||||||
opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC);
|
if (opTurnTimerRmain.value > 0) {
|
||||||
}
|
opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC);
|
||||||
|
|
||||||
if (iAmPlayer.value) {
|
|
||||||
if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) {
|
|
||||||
props.connection.send('claimTimeIsUp', {});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
|
|
||||||
|
|
||||||
function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
if (iAmPlayer.value) {
|
||||||
|
if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) {
|
||||||
|
props.connection!.send('claimTimeIsUp', {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
||||||
game.value.logs = Reversi.Serializer.serializeLogs([
|
game.value.logs = Reversi.Serializer.serializeLogs([
|
||||||
...Reversi.Serializer.deserializeLogs(game.value.logs),
|
...Reversi.Serializer.deserializeLogs(game.value.logs),
|
||||||
log,
|
log,
|
||||||
|
@ -309,17 +311,25 @@ function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
||||||
if (log.id == null || !appliedOps.includes(log.id)) {
|
if (log.id == null || !appliedOps.includes(log.id)) {
|
||||||
switch (log.operation) {
|
switch (log.operation) {
|
||||||
case 'put': {
|
case 'put': {
|
||||||
|
sound.playUrl('/client-assets/reversi/put.mp3', {
|
||||||
|
volume: 1,
|
||||||
|
playbackRate: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (log.player !== engine.value.turn) { // = desyncが発生している
|
||||||
|
const _game = await misskeyApi('reversi/show-game', {
|
||||||
|
gameId: props.game.id,
|
||||||
|
});
|
||||||
|
restoreGame(_game);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
engine.value.putStone(log.pos);
|
engine.value.putStone(log.pos);
|
||||||
triggerRef(engine);
|
triggerRef(engine);
|
||||||
|
|
||||||
myTurnTimerRmain.value = game.value.timeLimitForEachTurn;
|
myTurnTimerRmain.value = game.value.timeLimitForEachTurn;
|
||||||
opTurnTimerRmain.value = game.value.timeLimitForEachTurn;
|
opTurnTimerRmain.value = game.value.timeLimitForEachTurn;
|
||||||
|
|
||||||
sound.playUrl('/client-assets/reversi/put.mp3', {
|
|
||||||
volume: 1,
|
|
||||||
playbackRate: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
checkEnd();
|
checkEnd();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -366,9 +376,7 @@ function checkEnd() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStreamRescue(_game) {
|
function restoreGame(_game) {
|
||||||
console.log('rescue');
|
|
||||||
|
|
||||||
game.value = deepClone(_game);
|
game.value = deepClone(_game);
|
||||||
|
|
||||||
engine.value = Reversi.Serializer.restoreGame({
|
engine.value = Reversi.Serializer.restoreGame({
|
||||||
|
@ -384,6 +392,12 @@ function onStreamRescue(_game) {
|
||||||
checkEnd();
|
checkEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onStreamResynced(_game) {
|
||||||
|
console.log('resynced');
|
||||||
|
|
||||||
|
restoreGame(_game);
|
||||||
|
}
|
||||||
|
|
||||||
async function surrender() {
|
async function surrender() {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
@ -434,27 +448,35 @@ function share() {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
props.connection.on('log', onStreamLog);
|
if (props.connection != null) {
|
||||||
props.connection.on('rescue', onStreamRescue);
|
props.connection.on('log', onStreamLog);
|
||||||
props.connection.on('ended', onStreamEnded);
|
props.connection.on('resynced', onStreamResynced);
|
||||||
|
props.connection.on('ended', onStreamEnded);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
props.connection.on('log', onStreamLog);
|
if (props.connection != null) {
|
||||||
props.connection.on('rescue', onStreamRescue);
|
props.connection.on('log', onStreamLog);
|
||||||
props.connection.on('ended', onStreamEnded);
|
props.connection.on('resynced', onStreamResynced);
|
||||||
|
props.connection.on('ended', onStreamEnded);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
props.connection.off('log', onStreamLog);
|
if (props.connection != null) {
|
||||||
props.connection.off('rescue', onStreamRescue);
|
props.connection.off('log', onStreamLog);
|
||||||
props.connection.off('ended', onStreamEnded);
|
props.connection.off('resynced', onStreamResynced);
|
||||||
|
props.connection.off('ended', onStreamEnded);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
props.connection.off('log', onStreamLog);
|
if (props.connection != null) {
|
||||||
props.connection.off('rescue', onStreamRescue);
|
props.connection.off('log', onStreamLog);
|
||||||
props.connection.off('ended', onStreamEnded);
|
props.connection.off('resynced', onStreamResynced);
|
||||||
|
props.connection.off('ended', onStreamEnded);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="game == null || connection == null"><MkLoading/></div>
|
<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" :game="game" :connection="connection!"/>
|
||||||
<GameBoard v-else :game="game" :connection="connection"/>
|
<GameBoard v-else :game="game" :connection="connection"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -47,23 +47,25 @@ async function fetchGame() {
|
||||||
if (connection.value) {
|
if (connection.value) {
|
||||||
connection.value.dispose();
|
connection.value.dispose();
|
||||||
}
|
}
|
||||||
connection.value = useStream().useChannel('reversiGame', {
|
if (!game.value.isEnded) {
|
||||||
gameId: game.value.id,
|
connection.value = useStream().useChannel('reversiGame', {
|
||||||
});
|
gameId: game.value.id,
|
||||||
connection.value.on('started', x => {
|
});
|
||||||
game.value = x.game;
|
connection.value.on('started', x => {
|
||||||
});
|
game.value = x.game;
|
||||||
connection.value.on('canceled', x => {
|
});
|
||||||
connection.value?.dispose();
|
connection.value.on('canceled', x => {
|
||||||
|
connection.value?.dispose();
|
||||||
|
|
||||||
if (x.userId !== $i.id) {
|
if (x.userId !== $i.id) {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: i18n.ts._reversi.gameCanceled,
|
text: i18n.ts._reversi.gameCanceled,
|
||||||
});
|
});
|
||||||
router.push('/reversi');
|
router.push('/reversi');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
|
@ -73,9 +73,8 @@ const src = computed({
|
||||||
set: (x) => saveSrc(x),
|
set: (x) => saveSrc(x),
|
||||||
});
|
});
|
||||||
const withRenotes = computed({
|
const withRenotes = computed({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
|
||||||
get: () => (defaultStore.reactiveState.tl.value.filter?.withRenotes ?? saveTlFilter('withRenotes', true)),
|
set: (x: boolean) => saveTlFilter('withRenotes', x),
|
||||||
set: (x) => saveTlFilter('withRenotes', x),
|
|
||||||
});
|
});
|
||||||
const withReplies = computed({
|
const withReplies = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
|
@ -83,11 +82,10 @@ const withReplies = computed({
|
||||||
if (['local', 'social'].includes(src.value) && onlyFiles.value) {
|
if (['local', 'social'].includes(src.value) && onlyFiles.value) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
return defaultStore.reactiveState.tl.value.filter.withReplies;
|
||||||
return defaultStore.reactiveState.tl.value.filter?.withReplies ?? saveTlFilter('withReplies', true);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set: (x) => saveTlFilter('withReplies', x),
|
set: (x: boolean) => saveTlFilter('withReplies', x),
|
||||||
});
|
});
|
||||||
const withBots = computed({
|
const withBots = computed({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
@ -99,16 +97,14 @@ const onlyFiles = computed({
|
||||||
if (['local', 'social'].includes(src.value) && withReplies.value) {
|
if (['local', 'social'].includes(src.value) && withReplies.value) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
return defaultStore.reactiveState.tl.value.filter.onlyFiles;
|
||||||
return defaultStore.reactiveState.tl.value.filter?.onlyFiles ?? saveTlFilter('onlyFiles', false);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set: (x) => saveTlFilter('onlyFiles', x),
|
set: (x: boolean) => saveTlFilter('onlyFiles', x),
|
||||||
});
|
});
|
||||||
const withSensitive = computed({
|
const withSensitive = computed({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
get: () => defaultStore.reactiveState.tl.value.filter.withSensitive,
|
||||||
get: () => (defaultStore.reactiveState.tl.value.filter?.withSensitive ?? saveTlFilter('withSensitive', true)),
|
set: (x: boolean) => {
|
||||||
set: (x) => {
|
|
||||||
saveTlFilter('withSensitive', x);
|
saveTlFilter('withSensitive', x);
|
||||||
|
|
||||||
// これだけはクライアント側で完結する処理なので手動でリロード
|
// これだけはクライアント側で完結する処理なので手動でリロード
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import { onUnmounted, Ref, ref, watch } from 'vue';
|
import { onUnmounted, Ref, ref, watch } from 'vue';
|
||||||
import { BroadcastChannel } from 'broadcast-channel';
|
import { BroadcastChannel } from 'broadcast-channel';
|
||||||
|
import { defu } from 'defu';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { get, set } from '@/scripts/idb-proxy.js';
|
import { get, set } from '@/scripts/idb-proxy.js';
|
||||||
|
@ -80,6 +81,18 @@ export class Storage<T extends StateDef> {
|
||||||
this.loaded = this.ready.then(() => this.load());
|
this.loaded = this.ready.then(() => this.load());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isPureObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeState<T>(value: T, def: T): T {
|
||||||
|
if (this.isPureObject(value) && this.isPureObject(def)) {
|
||||||
|
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def);
|
||||||
|
return defu(value, def) as T;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private async init(): Promise<void> {
|
private async init(): Promise<void> {
|
||||||
await this.migrate();
|
await this.migrate();
|
||||||
|
|
||||||
|
@ -89,11 +102,11 @@ export class Storage<T extends StateDef> {
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
|
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
|
||||||
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
|
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
|
||||||
this.reactiveState[k].value = this.state[k] = deviceState[k];
|
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceState[k], v.default);
|
||||||
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
|
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
|
||||||
this.reactiveState[k].value = this.state[k] = registryCache[k];
|
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(registryCache[k], v.default);
|
||||||
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
|
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
|
||||||
this.reactiveState[k].value = this.state[k] = deviceAccountState[k];
|
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
|
||||||
} else {
|
} else {
|
||||||
this.reactiveState[k].value = this.state[k] = v.default;
|
this.reactiveState[k].value = this.state[k] = v.default;
|
||||||
if (_DEV_) console.log('Use default value', k, v.default);
|
if (_DEV_) console.log('Use default value', k, v.default);
|
||||||
|
|
|
@ -24,11 +24,9 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||||
"@types/matter-js": "0.19.6",
|
|
||||||
"@types/node": "20.11.5",
|
"@types/node": "20.11.5",
|
||||||
"@types/seedrandom": "3.0.8",
|
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "6.19.0",
|
"@typescript-eslint/parser": "6.18.1",
|
||||||
"@typescript-eslint/parser": "6.19.0",
|
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"nodemon": "3.0.2",
|
"nodemon": "3.0.2",
|
||||||
"typescript": "5.3.3"
|
"typescript": "5.3.3"
|
||||||
|
@ -37,6 +35,8 @@
|
||||||
"built"
|
"built"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/matter-js": "0.19.6",
|
||||||
|
"@types/seedrandom": "3.0.8",
|
||||||
"eventemitter3": "5.0.1",
|
"eventemitter3": "5.0.1",
|
||||||
"matter-js": "0.19.0",
|
"matter-js": "0.19.0",
|
||||||
"seedrandom": "3.0.5"
|
"seedrandom": "3.0.5"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021-2022 syuilo and other contributors
|
Copyright (c) 2021-2024 syuilo and other contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
@ -81,7 +81,17 @@ module.exports = {
|
||||||
// ],
|
// ],
|
||||||
|
|
||||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||||
// moduleNameMapper: {},
|
moduleNameMapper: {
|
||||||
|
// Do not resolve .wasm.js to .wasm by the rule below
|
||||||
|
'^(.+)\\.wasm\\.js$': '$1.wasm.js',
|
||||||
|
// SWC converts @/foo/bar.js to `../../src/foo/bar.js`, and then this rule
|
||||||
|
// converts it again to `../../src/foo/bar` which then can be resolved to
|
||||||
|
// `.ts` files.
|
||||||
|
// See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225
|
||||||
|
// TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can
|
||||||
|
// directly import `.ts` files without this hack.
|
||||||
|
'^((?:\\.{1,2}|[A-Z:])*/.*)\\.js$': '$1',
|
||||||
|
},
|
||||||
|
|
||||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||||
// modulePathIgnorePatterns: [],
|
// modulePathIgnorePatterns: [],
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "0.0.16",
|
"version": "2024.2.0-beta.3",
|
||||||
"description": "Misskey SDK for JavaScript",
|
"description": "Misskey SDK for JavaScript",
|
||||||
|
"types": "./built/dts/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./built/esm/index.js",
|
"import": "./built/esm/index.js",
|
||||||
|
@ -39,8 +40,8 @@
|
||||||
"@swc/jest": "0.2.31",
|
"@swc/jest": "0.2.31",
|
||||||
"@types/jest": "29.5.11",
|
"@types/jest": "29.5.11",
|
||||||
"@types/node": "20.11.5",
|
"@types/node": "20.11.5",
|
||||||
"@typescript-eslint/eslint-plugin": "6.19.0",
|
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||||
"@typescript-eslint/parser": "6.19.0",
|
"@typescript-eslint/parser": "6.18.1",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-fetch-mock": "3.0.3",
|
"jest-fetch-mock": "3.0.3",
|
||||||
|
@ -52,7 +53,9 @@
|
||||||
"typescript": "5.3.3"
|
"typescript": "5.3.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"built"
|
"built",
|
||||||
|
"built/esm",
|
||||||
|
"built/dts"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/cli": "0.1.63",
|
"@swc/cli": "0.1.63",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2024.2.0-beta.2
|
||||||
* generatedAt: 2024-01-21T01:01:12.332Z
|
* generatedAt: 2024-01-22T07:11:08.412Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SwitchCaseResponseType } from '../api.js';
|
import type { SwitchCaseResponseType } from '../api.js';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2024.2.0-beta.2
|
||||||
* generatedAt: 2024-01-21T01:01:12.330Z
|
* generatedAt: 2024-01-22T07:11:08.410Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2024.2.0-beta.2
|
||||||
* generatedAt: 2024-01-21T01:01:12.328Z
|
* generatedAt: 2024-01-22T07:11:08.408Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { operations } from './types.js';
|
import { operations } from './types.js';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2024.2.0-beta.2
|
||||||
* generatedAt: 2024-01-21T01:01:12.327Z
|
* generatedAt: 2024-01-22T07:11:08.408Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { components } from './types.js';
|
import { components } from './types.js';
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
/* eslint @typescript-eslint/no-explicit-any: 0 */
|
/* eslint @typescript-eslint/no-explicit-any: 0 */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* version: 2023.12.2
|
* version: 2024.2.0-beta.2
|
||||||
* generatedAt: 2024-01-21T01:01:12.246Z
|
* generatedAt: 2024-01-22T07:11:08.327Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4602,10 +4602,6 @@ export type components = {
|
||||||
endedAt: string | null;
|
endedAt: string | null;
|
||||||
isStarted: boolean;
|
isStarted: boolean;
|
||||||
isEnded: boolean;
|
isEnded: boolean;
|
||||||
form1: Record<string, never> | null;
|
|
||||||
form2: Record<string, never> | null;
|
|
||||||
user1Ready: boolean;
|
|
||||||
user2Ready: boolean;
|
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
user1Id: string;
|
user1Id: string;
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
|
|
|
@ -25,8 +25,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||||
"@types/node": "20.11.5",
|
"@types/node": "20.11.5",
|
||||||
"@typescript-eslint/eslint-plugin": "6.19.0",
|
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||||
"@typescript-eslint/parser": "6.19.0",
|
"@typescript-eslint/parser": "6.18.1",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"nodemon": "3.0.2",
|
"nodemon": "3.0.2",
|
||||||
"typescript": "5.3.3"
|
"typescript": "5.3.3"
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||||
"@typescript-eslint/parser": "6.19.0",
|
"@typescript-eslint/parser": "6.18.1",
|
||||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.29.1",
|
||||||
|
|
570
pnpm-lock.yaml
570
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue