summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorMarie <marie@kaifa.ch>2024-01-22 19:58:43 +0100
committerMarie <marie@kaifa.ch>2024-01-22 19:58:43 +0100
commitfd69a2fbbdd6c0f9f1a77da8d8ed8b4e6a96bfa2 (patch)
tree4b33fbfff4fd7692eacd9ca93744fb7568604dd5 /packages/frontend/src
parentchore: rename "Misskey Games" to "Games" (diff)
parentfix of #13014 (misskey-js publish) (diff)
downloadsharkey-fd69a2fbbdd6c0f9f1a77da8d8ed8b4e6a96bfa2.tar.gz
sharkey-fd69a2fbbdd6c0f9f1a77da8d8ed8b4e6a96bfa2.tar.bz2
sharkey-fd69a2fbbdd6c0f9f1a77da8d8ed8b4e6a96bfa2.zip
merge: upstream
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/pages/reversi/game.board.vue96
-rw-r--r--packages/frontend/src/pages/reversi/game.vue38
-rw-r--r--packages/frontend/src/pages/timeline.vue20
-rw-r--r--packages/frontend/src/pizzax.ts19
4 files changed, 103 insertions, 70 deletions
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index 82d3a7f539..107da09f9f 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -163,7 +163,7 @@ const $i = signinRequired();
const props = defineProps<{
game: Misskey.entities.ReversiGameDetailed;
- connection: Misskey.ChannelConnection;
+ connection?: Misskey.ChannelConnection | null;
}>();
const showBoardLabels = ref<boolean>(false);
@@ -240,10 +240,10 @@ watch(logPos, (v) => {
if (game.value.isStarted && !game.value.isEnded) {
useInterval(() => {
- if (game.value.isEnded) return;
+ if (game.value.isEnded || props.connection == null) return;
const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString();
if (_DEV_) console.log('crc32', crc32);
- props.connection.send('checkState', {
+ props.connection.send('resync', {
crc32: crc32,
});
}, 10000, { immediate: false, afterMounted: true });
@@ -267,7 +267,7 @@ function putStone(pos) {
});
const id = Math.random().toString(36).slice(2);
- props.connection.send('putStone', {
+ props.connection!.send('putStone', {
pos: pos,
id,
});
@@ -283,22 +283,24 @@ const myTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
const opTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
const TIMER_INTERVAL_SEC = 3;
-useInterval(() => {
- if (myTurnTimerRmain.value > 0) {
- myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC);
- }
- if (opTurnTimerRmain.value > 0) {
- opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC);
- }
+if (!props.game.isEnded) {
+ useInterval(() => {
+ if (myTurnTimerRmain.value > 0) {
+ myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC);
+ }
+ if (opTurnTimerRmain.value > 0) {
+ opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC);
+ }
- if (iAmPlayer.value) {
- if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) {
- props.connection.send('claimTimeIsUp', {});
+ 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 });
+ }, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
+}
-function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
+async function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
game.value.logs = Reversi.Serializer.serializeLogs([
...Reversi.Serializer.deserializeLogs(game.value.logs),
log,
@@ -309,17 +311,25 @@ function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
if (log.id == null || !appliedOps.includes(log.id)) {
switch (log.operation) {
case 'put': {
+ sound.playUrl('/client-assets/reversi/put.mp3', {
+ volume: 1,
+ playbackRate: 1,
+ });
+
+ if (log.player !== engine.value.turn) { // = desyncが発生している
+ const _game = await misskeyApi('reversi/show-game', {
+ gameId: props.game.id,
+ });
+ restoreGame(_game);
+ return;
+ }
+
engine.value.putStone(log.pos);
triggerRef(engine);
myTurnTimerRmain.value = game.value.timeLimitForEachTurn;
opTurnTimerRmain.value = game.value.timeLimitForEachTurn;
- sound.playUrl('/client-assets/reversi/put.mp3', {
- volume: 1,
- playbackRate: 1,
- });
-
checkEnd();
break;
}
@@ -366,9 +376,7 @@ function checkEnd() {
}
}
-function onStreamRescue(_game) {
- console.log('rescue');
-
+function restoreGame(_game) {
game.value = deepClone(_game);
engine.value = Reversi.Serializer.restoreGame({
@@ -384,6 +392,12 @@ function onStreamRescue(_game) {
checkEnd();
}
+function onStreamResynced(_game) {
+ console.log('resynced');
+
+ restoreGame(_game);
+}
+
async function surrender() {
const { canceled } = await os.confirm({
type: 'warning',
@@ -434,27 +448,35 @@ function share() {
}
onMounted(() => {
- props.connection.on('log', onStreamLog);
- props.connection.on('rescue', onStreamRescue);
- props.connection.on('ended', onStreamEnded);
+ if (props.connection != null) {
+ props.connection.on('log', onStreamLog);
+ props.connection.on('resynced', onStreamResynced);
+ props.connection.on('ended', onStreamEnded);
+ }
});
onActivated(() => {
- props.connection.on('log', onStreamLog);
- props.connection.on('rescue', onStreamRescue);
- props.connection.on('ended', onStreamEnded);
+ if (props.connection != null) {
+ props.connection.on('log', onStreamLog);
+ props.connection.on('resynced', onStreamResynced);
+ props.connection.on('ended', onStreamEnded);
+ }
});
onDeactivated(() => {
- props.connection.off('log', onStreamLog);
- props.connection.off('rescue', onStreamRescue);
- props.connection.off('ended', onStreamEnded);
+ if (props.connection != null) {
+ props.connection.off('log', onStreamLog);
+ props.connection.off('resynced', onStreamResynced);
+ props.connection.off('ended', onStreamEnded);
+ }
});
onUnmounted(() => {
- props.connection.off('log', onStreamLog);
- props.connection.off('rescue', onStreamRescue);
- props.connection.off('ended', onStreamEnded);
+ if (props.connection != null) {
+ props.connection.off('log', onStreamLog);
+ props.connection.off('resynced', onStreamResynced);
+ props.connection.off('ended', onStreamEnded);
+ }
});
</script>
diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue
index 7d55ccbe54..7e918d01db 100644
--- a/packages/frontend/src/pages/reversi/game.vue
+++ b/packages/frontend/src/pages/reversi/game.vue
@@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div v-if="game == null || connection == null"><MkLoading/></div>
-<GameSetting v-else-if="!game.isStarted" :game="game" :connection="connection"/>
+<div v-if="game == null || (!game.isEnded && connection == null)"><MkLoading/></div>
+<GameSetting v-else-if="!game.isStarted" :game="game" :connection="connection!"/>
<GameBoard v-else :game="game" :connection="connection"/>
</template>
@@ -47,23 +47,25 @@ async function fetchGame() {
if (connection.value) {
connection.value.dispose();
}
- connection.value = useStream().useChannel('reversiGame', {
- gameId: game.value.id,
- });
- connection.value.on('started', x => {
- game.value = x.game;
- });
- connection.value.on('canceled', x => {
- connection.value?.dispose();
+ if (!game.value.isEnded) {
+ connection.value = useStream().useChannel('reversiGame', {
+ gameId: game.value.id,
+ });
+ connection.value.on('started', x => {
+ game.value = x.game;
+ });
+ connection.value.on('canceled', x => {
+ connection.value?.dispose();
- if (x.userId !== $i.id) {
- os.alert({
- type: 'warning',
- text: i18n.ts._reversi.gameCanceled,
- });
- router.push('/reversi');
- }
- });
+ if (x.userId !== $i.id) {
+ os.alert({
+ type: 'warning',
+ text: i18n.ts._reversi.gameCanceled,
+ });
+ router.push('/reversi');
+ }
+ });
+ }
}
onMounted(() => {
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 00e6ba59eb..0ffe5a6b97 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -73,9 +73,8 @@ const src = computed({
set: (x) => saveSrc(x),
});
const withRenotes = computed({
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- get: () => (defaultStore.reactiveState.tl.value.filter?.withRenotes ?? saveTlFilter('withRenotes', true)),
- set: (x) => saveTlFilter('withRenotes', x),
+ get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
+ set: (x: boolean) => saveTlFilter('withRenotes', x),
});
const withReplies = computed({
get: () => {
@@ -83,11 +82,10 @@ const withReplies = computed({
if (['local', 'social'].includes(src.value) && onlyFiles.value) {
return false;
} else {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- return defaultStore.reactiveState.tl.value.filter?.withReplies ?? saveTlFilter('withReplies', true);
+ return defaultStore.reactiveState.tl.value.filter.withReplies;
}
},
- set: (x) => saveTlFilter('withReplies', x),
+ set: (x: boolean) => saveTlFilter('withReplies', x),
});
const withBots = computed({
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -99,16 +97,14 @@ const onlyFiles = computed({
if (['local', 'social'].includes(src.value) && withReplies.value) {
return false;
} else {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- return defaultStore.reactiveState.tl.value.filter?.onlyFiles ?? saveTlFilter('onlyFiles', false);
+ return defaultStore.reactiveState.tl.value.filter.onlyFiles;
}
},
- set: (x) => saveTlFilter('onlyFiles', x),
+ set: (x: boolean) => saveTlFilter('onlyFiles', x),
});
const withSensitive = computed({
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- get: () => (defaultStore.reactiveState.tl.value.filter?.withSensitive ?? saveTlFilter('withSensitive', true)),
- set: (x) => {
+ get: () => defaultStore.reactiveState.tl.value.filter.withSensitive,
+ set: (x: boolean) => {
saveTlFilter('withSensitive', x);
// これだけはクライアント側で完結する処理なので手動でリロード
diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts
index 8723110b08..b3d2374899 100644
--- a/packages/frontend/src/pizzax.ts
+++ b/packages/frontend/src/pizzax.ts
@@ -7,6 +7,7 @@
import { onUnmounted, Ref, ref, watch } from 'vue';
import { BroadcastChannel } from 'broadcast-channel';
+import { defu } from 'defu';
import { $i } from '@/account.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { get, set } from '@/scripts/idb-proxy.js';
@@ -80,6 +81,18 @@ export class Storage<T extends StateDef> {
this.loaded = this.ready.then(() => this.load());
}
+ private isPureObject(value: unknown): value is Record<string, unknown> {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+ }
+
+ private mergeState<T>(value: T, def: T): T {
+ if (this.isPureObject(value) && this.isPureObject(def)) {
+ if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def);
+ return defu(value, def) as T;
+ }
+ return value;
+ }
+
private async init(): Promise<void> {
await this.migrate();
@@ -89,11 +102,11 @@ export class Storage<T extends StateDef> {
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
- this.reactiveState[k].value = this.state[k] = deviceState[k];
+ this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceState[k], v.default);
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
- this.reactiveState[k].value = this.state[k] = registryCache[k];
+ this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(registryCache[k], v.default);
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
- this.reactiveState[k].value = this.state[k] = deviceAccountState[k];
+ this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
} else {
this.reactiveState[k].value = this.state[k] = v.default;
if (_DEV_) console.log('Use default value', k, v.default);