forked from profectus/Profectus
Add save conflict resolver
This commit is contained in:
parent
cfba55d2c6
commit
ece7ed2923
9 changed files with 291 additions and 46 deletions
|
@ -8,6 +8,7 @@
|
||||||
<TPS v-if="unref(showTPS)" />
|
<TPS v-if="unref(showTPS)" />
|
||||||
<GameOverScreen />
|
<GameOverScreen />
|
||||||
<NaNScreen />
|
<NaNScreen />
|
||||||
|
<CloudSaveResolver />
|
||||||
<component :is="gameComponent" />
|
<component :is="gameComponent" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -16,10 +17,11 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import "@fontsource/roboto-mono";
|
import "@fontsource/roboto-mono";
|
||||||
import Error from "components/Error.vue";
|
import Error from "components/Error.vue";
|
||||||
|
import CloudSaveResolver from "components/saves/CloudSaveResolver.vue";
|
||||||
import { jsx } from "features/feature";
|
import { jsx } from "features/feature";
|
||||||
import state from "game/state";
|
import state from "game/state";
|
||||||
import { coerceComponent, render } from "util/vue";
|
import { coerceComponent, render } from "util/vue";
|
||||||
import { CSSProperties } from "vue";
|
import type { CSSProperties } from "vue";
|
||||||
import { computed, toRef, unref } from "vue";
|
import { computed, toRef, unref } from "vue";
|
||||||
import Game from "./components/Game.vue";
|
import Game from "./components/Game.vue";
|
||||||
import GameOverScreen from "./components/GameOverScreen.vue";
|
import GameOverScreen from "./components/GameOverScreen.vue";
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
name="modal"
|
name="modal"
|
||||||
@before-enter="isAnimating = true"
|
@before-enter="isAnimating = true"
|
||||||
@after-leave="isAnimating = false"
|
@after-leave="isAnimating = false"
|
||||||
|
appear
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="modal-mask"
|
class="modal-mask"
|
||||||
|
@ -12,7 +13,7 @@
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<div class="modal-wrapper">
|
<div class="modal-wrapper">
|
||||||
<div class="modal-container">
|
<div class="modal-container" :width="width">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<slot name="header" :shown="isOpen"> default header </slot>
|
<slot name="header" :shown="isOpen"> default header </slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,6 +46,8 @@ import Context from "./Context.vue";
|
||||||
|
|
||||||
const _props = defineProps<{
|
const _props = defineProps<{
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
|
closable?: boolean;
|
||||||
|
width?: string;
|
||||||
}>();
|
}>();
|
||||||
const props = toRefs(_props);
|
const props = toRefs(_props);
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -53,7 +56,9 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const isOpen = computed(() => unref(props.modelValue) || isAnimating.value);
|
const isOpen = computed(() => unref(props.modelValue) || isAnimating.value);
|
||||||
function close() {
|
function close() {
|
||||||
|
if (unref(props.closable) !== false) {
|
||||||
emit("update:modelValue", false);
|
emit("update:modelValue", false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAnimating = ref(false);
|
const isAnimating = ref(false);
|
||||||
|
|
|
@ -55,7 +55,7 @@ import Decimal, { format } from "util/bignum";
|
||||||
import type { ComponentPublicInstance } from "vue";
|
import type { ComponentPublicInstance } from "vue";
|
||||||
import { computed, ref, toRef, watch } from "vue";
|
import { computed, ref, toRef, watch } from "vue";
|
||||||
import Toggle from "./fields/Toggle.vue";
|
import Toggle from "./fields/Toggle.vue";
|
||||||
import SavesManager from "./SavesManager.vue";
|
import SavesManager from "./saves/SavesManager.vue";
|
||||||
|
|
||||||
const { discordName, discordLink } = projInfo;
|
const { discordName, discordLink } = projInfo;
|
||||||
const autosave = ref(true);
|
const autosave = ref(true);
|
||||||
|
|
|
@ -103,7 +103,7 @@ import type { ComponentPublicInstance } from "vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import Info from "./Info.vue";
|
import Info from "./Info.vue";
|
||||||
import Options from "./Options.vue";
|
import Options from "./Options.vue";
|
||||||
import SavesManager from "./SavesManager.vue";
|
import SavesManager from "./saves/SavesManager.vue";
|
||||||
|
|
||||||
const info = ref<ComponentPublicInstance<typeof Info> | null>(null);
|
const info = ref<ComponentPublicInstance<typeof Info> | null>(null);
|
||||||
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);
|
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);
|
||||||
|
|
186
src/components/saves/CloudSaveResolver.vue
Normal file
186
src/components/saves/CloudSaveResolver.vue
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
<template>
|
||||||
|
<Modal v-model="isOpen" width="960px" ref="modal">
|
||||||
|
<template v-slot:header>
|
||||||
|
<div class="cloud-saves-modal-header">
|
||||||
|
<h2>Cloud {{ pluralizedSave }} loaded!</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body>
|
||||||
|
<div>
|
||||||
|
Upon loading, your cloud {{ pluralizedSave }}
|
||||||
|
{{ conflictingSaves.length > 1 ? "appear" : "appears" }} to be out of sync with your
|
||||||
|
local {{ pluralizedSave }}. Which
|
||||||
|
{{ pluralizedSave }}
|
||||||
|
do you want to keep?
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div
|
||||||
|
v-for="(conflict, i) in unref(conflictingSaves)"
|
||||||
|
:key="conflict.id"
|
||||||
|
class="conflict-container"
|
||||||
|
>
|
||||||
|
<div @click="selectCloud(i)" :class="{ selected: selectedSaves[i] }">
|
||||||
|
<h2>
|
||||||
|
Cloud
|
||||||
|
<span
|
||||||
|
v-if="(conflict.cloud.time ?? 0) > (conflict.local.time ?? 0)"
|
||||||
|
class="note"
|
||||||
|
>(more recent)</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
(conflict.cloud.timePlayed ?? 0) > (conflict.local.timePlayed ?? 0)
|
||||||
|
"
|
||||||
|
class="note"
|
||||||
|
>(more playtime)</span
|
||||||
|
>
|
||||||
|
</h2>
|
||||||
|
<Save :save="conflict.cloud" :readonly="true" />
|
||||||
|
</div>
|
||||||
|
<div @click="selectLocal(i)" :class="{ selected: !selectedSaves[i] }">
|
||||||
|
<h2>
|
||||||
|
Local
|
||||||
|
<span
|
||||||
|
v-if="(conflict.cloud.time ?? 0) <= (conflict.local.time ?? 0)"
|
||||||
|
class="note"
|
||||||
|
>(more recent)</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
(conflict.cloud.timePlayed ?? 0) <= (conflict.local.timePlayed ?? 0)
|
||||||
|
"
|
||||||
|
class="note"
|
||||||
|
>(more playtime)</span
|
||||||
|
>
|
||||||
|
</h2>
|
||||||
|
<Save :save="conflict.local" :readonly="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:footer>
|
||||||
|
<div class="cloud-saves-footer">
|
||||||
|
<button @click="close" class="button">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Modal from "components/Modal.vue";
|
||||||
|
import { stringifySave } from "game/player";
|
||||||
|
import LZString from "lz-string";
|
||||||
|
import { conflictingSaves, galaxy } from "util/galaxy";
|
||||||
|
import { save, setupInitialStore } from "util/save";
|
||||||
|
import { ComponentPublicInstance, computed, ref, unref, watch } from "vue";
|
||||||
|
import Save from "./Save.vue";
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
// True means replacing local save with cloud save
|
||||||
|
const selectedSaves = ref<boolean[]>([]);
|
||||||
|
|
||||||
|
const pluralizedSave = computed(() => (conflictingSaves.value.length > 1 ? "saves" : "save"));
|
||||||
|
|
||||||
|
const modal = ref<ComponentPublicInstance<typeof Modal> | null>(null);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => conflictingSaves.value.length > 0,
|
||||||
|
shouldOpen => {
|
||||||
|
if (shouldOpen) {
|
||||||
|
selectedSaves.value = conflictingSaves.value.map(({ local, cloud }) => {
|
||||||
|
return (local.time ?? 0) < (cloud.time ?? 0);
|
||||||
|
});
|
||||||
|
isOpen.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modal.value?.isOpen,
|
||||||
|
open => {
|
||||||
|
console.log("!!", open);
|
||||||
|
if (open === false) {
|
||||||
|
conflictingSaves.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modal.value?.isAnimating,
|
||||||
|
open => {
|
||||||
|
console.log("!! anim", open);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function selectLocal(index: number) {
|
||||||
|
selectedSaves.value[index] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCloud(index: number) {
|
||||||
|
selectedSaves.value[index] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
for (let i = 0; i < selectedSaves.value.length; i++) {
|
||||||
|
const { slot, local, cloud } = conflictingSaves.value[i];
|
||||||
|
if (selectedSaves.value[i]) {
|
||||||
|
// Replace local save with cloud
|
||||||
|
save(setupInitialStore(cloud));
|
||||||
|
} else {
|
||||||
|
// Replace cloud save with cloud
|
||||||
|
galaxy.value
|
||||||
|
?.save(
|
||||||
|
slot,
|
||||||
|
LZString.compressToUTF16(stringifySave(setupInitialStore(local))),
|
||||||
|
cloud.name ?? null
|
||||||
|
)
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isOpen.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cloud-saves-modal-header {
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloud-saves-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloud-saves-footer button {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-container > * {
|
||||||
|
flex-basis: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-container + .conflict-container {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
font-size: x-small;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.conflict-container .save {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-container .selected .save {
|
||||||
|
border-color: var(--bought);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="save" :class="{ active: isActive }">
|
<div class="save" :class="{ active: isActive, readonly }">
|
||||||
<div class="handle material-icons">drag_handle</div>
|
<div class="handle material-icons" v-if="readonly !== true">drag_handle</div>
|
||||||
<div class="actions" v-if="!isEditing">
|
<div class="actions" v-if="!isEditing && readonly !== true">
|
||||||
<FeedbackButton
|
<FeedbackButton
|
||||||
@click="emit('export')"
|
@click="emit('export')"
|
||||||
class="button"
|
class="button"
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</DangerButton>
|
</DangerButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions" v-else>
|
<div class="actions" v-else-if="readonly !== true">
|
||||||
<button @click="changeName" class="button">
|
<button @click="changeName" class="button">
|
||||||
<Tooltip display="Save" :direction="Direction.Left" class="info">
|
<Tooltip display="Save" :direction="Direction.Left" class="info">
|
||||||
<span class="material-icons">check</span>
|
<span class="material-icons">check</span>
|
||||||
|
@ -53,12 +53,14 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="details" v-if="save.error == undefined && !isEditing">
|
<div class="details" v-if="save.error == undefined && !isEditing">
|
||||||
<button class="button open" @click="emit('open')">
|
<button class="button open" @click="emit('open')" :disabled="readonly">
|
||||||
<h3>{{ save.name }}</h3>
|
<h3>{{ save.name }}</h3>
|
||||||
</button>
|
</button>
|
||||||
<span class="save-version">v{{ save.modVersion }}</span
|
<span class="save-version">v{{ save.modVersion }}</span
|
||||||
><br />
|
><br />
|
||||||
<div v-if="currentTime">Last played {{ dateFormat.format(currentTime) }}</div>
|
<div v-if="currentTime" class="time">
|
||||||
|
Last played {{ dateFormat.format(currentTime) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="details" v-else-if="save.error == undefined && isEditing">
|
<div class="details" v-else-if="save.error == undefined && isEditing">
|
||||||
<Text v-model="newName" class="editname" @submit="changeName" />
|
<Text v-model="newName" class="editname" @submit="changeName" />
|
||||||
|
@ -73,16 +75,17 @@
|
||||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||||
import player from "game/player";
|
import player from "game/player";
|
||||||
import { Direction } from "util/common";
|
import { Direction } from "util/common";
|
||||||
import { computed, ref, toRefs, watch } from "vue";
|
import { computed, ref, toRefs, unref, watch } from "vue";
|
||||||
import DangerButton from "./fields/DangerButton.vue";
|
import DangerButton from "../fields/DangerButton.vue";
|
||||||
import FeedbackButton from "./fields/FeedbackButton.vue";
|
import FeedbackButton from "../fields/FeedbackButton.vue";
|
||||||
import Text from "./fields/Text.vue";
|
import Text from "../fields/Text.vue";
|
||||||
import type { LoadablePlayerData } from "./SavesManager.vue";
|
import type { LoadablePlayerData } from "./SavesManager.vue";
|
||||||
|
|
||||||
const _props = defineProps<{
|
const _props = defineProps<{
|
||||||
save: LoadablePlayerData;
|
save: LoadablePlayerData;
|
||||||
|
readonly?: boolean;
|
||||||
}>();
|
}>();
|
||||||
const { save } = toRefs(_props);
|
const { save, readonly } = toRefs(_props);
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "export"): void;
|
(e: "export"): void;
|
||||||
(e: "open"): void;
|
(e: "open"): void;
|
||||||
|
@ -106,7 +109,9 @@ const newName = ref("");
|
||||||
|
|
||||||
watch(isEditing, () => (newName.value = save.value.name ?? ""));
|
watch(isEditing, () => (newName.value = save.value.name ?? ""));
|
||||||
|
|
||||||
const isActive = computed(() => save.value != null && save.value.id === player.id);
|
const isActive = computed(
|
||||||
|
() => save.value != null && save.value.id === player.id && !unref(readonly)
|
||||||
|
);
|
||||||
const currentTime = computed(() =>
|
const currentTime = computed(() =>
|
||||||
isActive.value ? player.time : (save.value != null && save.value.time) ?? 0
|
isActive.value ? player.time : (save.value != null && save.value.time) ?? 0
|
||||||
);
|
);
|
||||||
|
@ -139,6 +144,13 @@ function changeName() {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.open:disabled {
|
||||||
|
cursor: inherit;
|
||||||
|
color: var(--foreground);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.handle {
|
.handle {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
@ -152,6 +164,10 @@ function changeName() {
|
||||||
margin-right: 80px;
|
margin-right: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save.readonly .details {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
|
@ -176,6 +192,10 @@ function changeName() {
|
||||||
.editname {
|
.editname {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
|
@ -76,8 +76,8 @@ import {
|
||||||
import type { ComponentPublicInstance } from "vue";
|
import type { ComponentPublicInstance } from "vue";
|
||||||
import { computed, nextTick, ref, watch } from "vue";
|
import { computed, nextTick, ref, watch } from "vue";
|
||||||
import Draggable from "vuedraggable";
|
import Draggable from "vuedraggable";
|
||||||
import Select from "./fields/Select.vue";
|
import Select from "../fields/Select.vue";
|
||||||
import Text from "./fields/Text.vue";
|
import Text from "../fields/Text.vue";
|
||||||
import Save from "./Save.vue";
|
import Save from "./Save.vue";
|
||||||
|
|
||||||
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
|
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
|
|
@ -1,12 +1,15 @@
|
||||||
import player, { Player } from "game/player";
|
import { LoadablePlayerData } from "components/saves/SavesManager.vue";
|
||||||
|
import player, { Player, stringifySave } from "game/player";
|
||||||
import settings from "game/settings";
|
import settings from "game/settings";
|
||||||
import { GalaxyApi, initGalaxy } from "lib/galaxy";
|
import { GalaxyApi, initGalaxy } from "lib/galaxy";
|
||||||
import { decodeSave, loadSave, save } from "./save";
|
import LZString from "lz-string";
|
||||||
import { setupInitialStore } from "./save";
|
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import { decodeSave, loadSave, save, setupInitialStore } from "./save";
|
||||||
|
|
||||||
export const galaxy = ref<GalaxyApi>();
|
export const galaxy = ref<GalaxyApi>();
|
||||||
export const conflictingSaves = ref<string[]>([]);
|
export const conflictingSaves = ref<
|
||||||
|
{ id: string; local: LoadablePlayerData; cloud: LoadablePlayerData; slot: number }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
export function sync() {
|
export function sync() {
|
||||||
if (galaxy.value == null || !galaxy.value.loggedIn) {
|
if (galaxy.value == null || !galaxy.value.loggedIn) {
|
||||||
|
@ -16,7 +19,7 @@ export function sync() {
|
||||||
// Pause syncing while resolving conflicted saves
|
// Pause syncing while resolving conflicted saves
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
galaxy.value.getSaveList().then(syncSaves);
|
galaxy.value.getSaveList().then(syncSaves).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup Galaxy API
|
// Setup Galaxy API
|
||||||
|
@ -24,10 +27,12 @@ initGalaxy({
|
||||||
supportsSaving: true,
|
supportsSaving: true,
|
||||||
supportsSaveManager: true,
|
supportsSaveManager: true,
|
||||||
onLoggedInChanged
|
onLoggedInChanged
|
||||||
}).then(g => {
|
})
|
||||||
|
.then(g => {
|
||||||
galaxy.value = g;
|
galaxy.value = g;
|
||||||
onLoggedInChanged(g);
|
onLoggedInChanged(g);
|
||||||
});
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
function onLoggedInChanged(g: GalaxyApi) {
|
function onLoggedInChanged(g: GalaxyApi) {
|
||||||
if (g.loggedIn !== true) {
|
if (g.loggedIn !== true) {
|
||||||
|
@ -38,7 +43,8 @@ function onLoggedInChanged(g: GalaxyApi) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
g.getSaveList().then(list => {
|
g.getSaveList()
|
||||||
|
.then(list => {
|
||||||
const saves = syncSaves(list);
|
const saves = syncSaves(list);
|
||||||
|
|
||||||
// If our current save has under a minute of playtime, load the cloud save with the most recent time.
|
// If our current save has under a minute of playtime, load the cloud save with the most recent time.
|
||||||
|
@ -48,7 +54,8 @@ function onLoggedInChanged(g: GalaxyApi) {
|
||||||
);
|
);
|
||||||
loadSave(longestSave.content);
|
loadSave(longestSave.content);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncSaves(
|
function syncSaves(
|
||||||
|
@ -60,7 +67,9 @@ function syncSaves(
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
) {
|
) {
|
||||||
return (
|
const savesToUpload = new Set(settings.saves.slice());
|
||||||
|
const availableSlots = new Set(new Array(11).fill(0).map((_, i) => i));
|
||||||
|
const saves = (
|
||||||
Object.keys(list)
|
Object.keys(list)
|
||||||
.map(slot => {
|
.map(slot => {
|
||||||
const { label, content } = list[slot as unknown as number];
|
const { label, content } = list[slot as unknown as number];
|
||||||
|
@ -85,11 +94,13 @@ function syncSaves(
|
||||||
if (cloudSave.label != null) {
|
if (cloudSave.label != null) {
|
||||||
cloudSave.content.name = cloudSave.label;
|
cloudSave.content.name = cloudSave.label;
|
||||||
}
|
}
|
||||||
|
availableSlots.delete(cloudSave.slot);
|
||||||
const localSaveId = settings.saves.find(id => id === cloudSave.content.id);
|
const localSaveId = settings.saves.find(id => id === cloudSave.content.id);
|
||||||
if (localSaveId == undefined) {
|
if (localSaveId == undefined) {
|
||||||
settings.saves.push(cloudSave.content.id);
|
settings.saves.push(cloudSave.content.id);
|
||||||
save(setupInitialStore(cloudSave.content));
|
save(setupInitialStore(cloudSave.content));
|
||||||
} else {
|
} else {
|
||||||
|
savesToUpload.delete(localSaveId);
|
||||||
try {
|
try {
|
||||||
const localSave = JSON.parse(
|
const localSave = JSON.parse(
|
||||||
decodeSave(localStorage.getItem(localSaveId) ?? "") ?? ""
|
decodeSave(localStorage.getItem(localSaveId) ?? "") ?? ""
|
||||||
|
@ -114,16 +125,25 @@ function syncSaves(
|
||||||
loadSave(cloudSave.content);
|
loadSave(cloudSave.content);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
galaxy.value?.save(
|
galaxy.value
|
||||||
|
?.save(
|
||||||
cloudSave.slot,
|
cloudSave.slot,
|
||||||
JSON.stringify(cloudSave.content),
|
LZString.compressToUTF16(
|
||||||
|
stringifySave(setupInitialStore(cloudSave.content))
|
||||||
|
),
|
||||||
cloudSave.label ?? null
|
cloudSave.label ?? null
|
||||||
);
|
)
|
||||||
|
.catch(console.error);
|
||||||
// Update cloud save content for the return value
|
// Update cloud save content for the return value
|
||||||
cloudSave.content = localSave as Player;
|
cloudSave.content = localSave as Player;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
conflictingSaves.value.push(localSaveId);
|
conflictingSaves.value.push({
|
||||||
|
id: localSaveId,
|
||||||
|
cloud: cloudSave.content,
|
||||||
|
local: localSave as LoadablePlayerData,
|
||||||
|
slot: cloudSave.slot
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -131,4 +151,16 @@ function syncSaves(
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
savesToUpload.forEach(id => {
|
||||||
|
const localSave = decodeSave(localStorage.getItem(id) ?? "");
|
||||||
|
if (localSave != null && availableSlots.size > 0) {
|
||||||
|
const parsedLocalSave = JSON.parse(localSave);
|
||||||
|
const slot = parseInt(Object.keys(availableSlots)[0]);
|
||||||
|
galaxy.value?.save(slot, localSave, parsedLocalSave.name).catch(console.error);
|
||||||
|
availableSlots.delete(slot);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return saves;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { LoadablePlayerData } from "components/SavesManager.vue";
|
import { LoadablePlayerData } from "components/saves/SavesManager.vue";
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import { globalBus } from "game/events";
|
import { globalBus } from "game/events";
|
||||||
import type { Player } from "game/player";
|
import type { Player } from "game/player";
|
||||||
|
|
Loading…
Reference in a new issue