Add Galaxy cloud saves support #75
8 changed files with 18 additions and 257 deletions
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -23,6 +23,7 @@
|
||||||
"is-plain-object": "^5.0.0",
|
"is-plain-object": "^5.0.0",
|
||||||
"lz-string": "^1.4.4",
|
"lz-string": "^1.4.4",
|
||||||
"nanoevents": "^6.0.2",
|
"nanoevents": "^6.0.2",
|
||||||
|
"unofficial-galaxy-sdk": "git+https://code.incremental.social/thepaperpilot/unofficial-galaxy-sdk.git#1.0.0",
|
||||||
"vite": "^2.9.12",
|
"vite": "^2.9.12",
|
||||||
"vite-plugin-pwa": "^0.12.0",
|
"vite-plugin-pwa": "^0.12.0",
|
||||||
"vite-tsconfig-paths": "^3.5.0",
|
"vite-tsconfig-paths": "^3.5.0",
|
||||||
|
@ -6878,6 +6879,10 @@
|
||||||
"node": ">= 4.0.0"
|
"node": ">= 4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unofficial-galaxy-sdk": {
|
||||||
|
"version": "1.0",
|
||||||
|
"resolved": "git+https://code.incremental.social/thepaperpilot/unofficial-galaxy-sdk.git#431b1f16bcbe9d9e5c9fd08c73c14e0f0fe4ebfd"
|
||||||
|
},
|
||||||
"node_modules/upath": {
|
"node_modules/upath": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"is-plain-object": "^5.0.0",
|
"is-plain-object": "^5.0.0",
|
||||||
"lz-string": "^1.4.4",
|
"lz-string": "^1.4.4",
|
||||||
"nanoevents": "^6.0.2",
|
"nanoevents": "^6.0.2",
|
||||||
|
"unofficial-galaxy-sdk": "git+https://code.incremental.social/thepaperpilot/unofficial-galaxy-sdk.git#1.0.0",
|
||||||
"vite": "^2.9.12",
|
"vite": "^2.9.12",
|
||||||
"vite-plugin-pwa": "^0.12.0",
|
"vite-plugin-pwa": "^0.12.0",
|
||||||
"vite-tsconfig-paths": "^3.5.0",
|
"vite-tsconfig-paths": "^3.5.0",
|
||||||
|
|
|
@ -121,7 +121,7 @@ function openDiscord() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const needsSync = computed(
|
const needsSync = computed(
|
||||||
() => galaxy.value?.loggedIn && !syncedSaves.value.includes(settings.active)
|
() => galaxy.value?.loggedIn === true && !syncedSaves.value.includes(settings.active)
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -135,7 +135,7 @@ function close() {
|
||||||
?.save(
|
?.save(
|
||||||
slot,
|
slot,
|
||||||
LZString.compressToUTF16(stringifySave(setupInitialStore(local))),
|
LZString.compressToUTF16(stringifySave(setupInitialStore(local))),
|
||||||
cloud.name ?? null
|
cloud.name
|
||||||
)
|
)
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
break;
|
break;
|
||||||
|
@ -152,7 +152,7 @@ function close() {
|
||||||
?.save(
|
?.save(
|
||||||
slot,
|
slot,
|
||||||
LZString.compressToUTF16(stringifySave(setupInitialStore(local))),
|
LZString.compressToUTF16(stringifySave(setupInitialStore(local))),
|
||||||
cloud.name ?? null
|
cloud.name
|
||||||
)
|
)
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -120,7 +120,10 @@ 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
|
||||||
);
|
);
|
||||||
const synced = computed(
|
const synced = computed(
|
||||||
() => !unref(readonly) && galaxy.value?.loggedIn && syncedSaves.value.includes(save.value.id)
|
() =>
|
||||||
|
!unref(readonly) &&
|
||||||
|
galaxy.value?.loggedIn === true &&
|
||||||
|
syncedSaves.value.includes(save.value.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
function changeName() {
|
function changeName() {
|
||||||
|
|
|
@ -159,7 +159,7 @@ const saves = computed(() =>
|
||||||
);
|
);
|
||||||
|
|
||||||
const showNotSyncedWarning = computed(
|
const showNotSyncedWarning = computed(
|
||||||
() => galaxy.value?.loggedIn && settings.saves.length < syncedSaves.value.length
|
() => galaxy.value?.loggedIn === true && settings.saves.length < syncedSaves.value.length
|
||||||
);
|
);
|
||||||
|
|
||||||
function exportSave(id: string) {
|
function exportSave(id: string) {
|
||||||
|
@ -204,7 +204,7 @@ function duplicateSave(id: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSave(id: string) {
|
function deleteSave(id: string) {
|
||||||
if (galaxy.value?.loggedIn) {
|
if (galaxy.value?.loggedIn === true) {
|
||||||
galaxy.value.getSaveList().then(list => {
|
galaxy.value.getSaveList().then(list => {
|
||||||
const slot = Object.keys(list).find(slot => {
|
const slot = Object.keys(list).find(slot => {
|
||||||
const content = list[slot as unknown as number].content;
|
const content = list[slot as unknown as number].content;
|
||||||
|
|
|
@ -1,248 +0,0 @@
|
||||||
/**
|
|
||||||
* The Galaxy API defines actions and responses for interacting with the Galaxy platform.
|
|
||||||
*
|
|
||||||
* @typedef {object} SupportsAction
|
|
||||||
* @property {"supports"} action - The action type.
|
|
||||||
* @property {boolean} saving - If your game auto-saves or allows the user to make/load game saves from within the UI.
|
|
||||||
* @property {boolean} save_manager - If your game has a complete save manager integrated into it.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The save list action sends a retrieval request to Galaxy to get the player's cloud save list.
|
|
||||||
*
|
|
||||||
* @typedef {object} SaveListAction
|
|
||||||
* @property {"save_list"} action - The action type.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The save action creates a cloud save and puts it into a certain save slot.
|
|
||||||
*
|
|
||||||
* @typedef {object} SaveAction
|
|
||||||
* @property {"save"} action - The action type.
|
|
||||||
* @property {number} slot - The save slot number. Must be an integer between 0 and 10, inclusive.
|
|
||||||
* @property {string} [label] - The optional label of the save file.
|
|
||||||
* @property {string} data - The actual save data.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The load action sends a retrieval request to Galaxy to get the cloud save data inside a certain save slot.
|
|
||||||
*
|
|
||||||
* @typedef {object} LoadAction
|
|
||||||
* @property {"load"} action - The action type.
|
|
||||||
* @property {number} slot - The save slot number.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Galaxy action can be one of SupportsAction, SaveListAction, SaveAction, or LoadAction.
|
|
||||||
*
|
|
||||||
* @typedef {SupportsAction | SaveListAction | SaveAction | LoadAction} GalaxyAction
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The info response is sent when the page loads.
|
|
||||||
*
|
|
||||||
* @typedef {object} InfoResponse
|
|
||||||
* @property {"info"} type - The response type.
|
|
||||||
* @property {boolean} galaxy - Whether you're talking to Galaxy.
|
|
||||||
* @property {number} api_version - The version of the API.
|
|
||||||
* @property {string} theme_preference - The player's theme preference.
|
|
||||||
* @property {boolean} logged_in - Whether the player is logged in.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The save list response is requested by the save_list action.
|
|
||||||
*
|
|
||||||
* @typedef {object} SaveListResponse
|
|
||||||
* @property {"save_list"} type - The response type.
|
|
||||||
* @property {Record<number, { label: string; content: string }>} list - A list of saves.
|
|
||||||
* @property {boolean} error - Whether the action encountered an error.
|
|
||||||
* @property {("no_account" | "server_error")} [message] - Reason for the error.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The save content response is requested by the load action.
|
|
||||||
*
|
|
||||||
* @typedef {object} SaveContentResponse
|
|
||||||
* @property {"save_content"} type - The response type.
|
|
||||||
* @property {boolean} error - Whether the action encountered an error.
|
|
||||||
* @property {("no_account" | "empty_slot" | "invalid_slot" | "server_error")} [message] - Reason for the error.
|
|
||||||
* @property {number} slot - The save slot number.
|
|
||||||
* @property {string} [label] - The save's label.
|
|
||||||
* @property {string} [content] - The save's actual data.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The saved response is requested by the save action.
|
|
||||||
*
|
|
||||||
* @typedef {object} SavedResponse
|
|
||||||
* @property {"saved"} type - The response type.
|
|
||||||
* @property {boolean} error - Whether the action encountered an error.
|
|
||||||
* @property {number} slot - The save slot number.
|
|
||||||
* @property {("no_account" | "too_big" | "invalid_slot" | "server_error")} [message] - Reason for the error.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The GalaxyResponse can be one of InfoResponse, SaveListResponse, SaveContentResponse, or SavedResponse.
|
|
||||||
*
|
|
||||||
* @typedef {InfoResponse | SaveListResponse | SaveContentResponse | SavedResponse} GalaxyResponse
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The GalaxyApi interface defines methods and properties for interacting with the Galaxy platform.
|
|
||||||
*
|
|
||||||
* @typedef {object} GalaxyApi
|
|
||||||
* @property {string[]} acceptedOrigins - Accepted origins.
|
|
||||||
* @property {boolean} [supportsSaving] - Whether saving is supported.
|
|
||||||
* @property {boolean} [supportsSaveManager] - Whether save manager is supported.
|
|
||||||
* @property {boolean} [ignoreApiVersion] - Whether to ignore API version.
|
|
||||||
* @property {function(GalaxyApi): void} [onLoggedInChanged] - Function to handle logged in changes.
|
|
||||||
* @property {string} origin - Origin of the API.
|
|
||||||
* @property {number} apiVersion - Version of the API.
|
|
||||||
* @property {boolean} loggedIn - Whether the player is logged in.
|
|
||||||
* @property {function(GalaxyAction): void} postMessage - Method to post a message.
|
|
||||||
* @property {function(): Promise<Record<number, { label: string; content: string }>>} getSaveList - Method to get the save list.
|
|
||||||
* @property {function(number, string, string?): Promise<number>} save - Method to save data.
|
|
||||||
* @property {function(number): Promise<{ content: string; label?: string; slot: number }>} load - Method to load data.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the Galaxy API.
|
|
||||||
* @param {Object} [options] - An object of options that configure the API
|
|
||||||
* @param {string[]} [options.acceptedOrigins] - A list of domains that the API trusts messages from. Defaults to `['https://galaxy.click']`.
|
|
||||||
* @param {boolean} [options.supportsSaving] - Indicates to Galaxy that this game supports saving. Defaults to false.
|
|
||||||
* @param {boolean} [options.supportsSaveManager] - Indicates to Galaxy that this game supports a saves manager. Defaults to false.
|
|
||||||
* @param {boolean} [options.ignoreApiVersion] - Ignores the api_version property received from Galaxy. By default this value is false, meaning if an unknown API version is encountered, the API will fail to initialize.
|
|
||||||
* @param {(galaxy: GalaxyApi) => void} [options.onLoggedInChanged] - A callback for when the logged in status of the player changes after the initialization.
|
|
||||||
* @returns {Promise<GalaxyApi>}
|
|
||||||
*/
|
|
||||||
export function initGalaxy({
|
|
||||||
acceptedOrigins,
|
|
||||||
supportsSaving,
|
|
||||||
supportsSaveManager,
|
|
||||||
ignoreApiVersion,
|
|
||||||
onLoggedInChanged
|
|
||||||
}) {
|
|
||||||
return new Promise((accept, reject) => {
|
|
||||||
acceptedOrigins = acceptedOrigins ?? ["https://galaxy.click"];
|
|
||||||
if (acceptedOrigins.includes(window.origin)) {
|
|
||||||
// Callbacks to resolve promises
|
|
||||||
/** @type function(SaveListResponse["list"]):void */
|
|
||||||
let saveListAccept,
|
|
||||||
/** @type function(string?):void */
|
|
||||||
saveListReject;
|
|
||||||
/** @type Record<number, { accept: function(number):void, reject: function(string?):void }> */
|
|
||||||
const saveCallbacks = {};
|
|
||||||
/** @type Record<number, { accept: function({ content: string; label?: string; slot: number }):void, reject: function(string?):void }> */
|
|
||||||
const loadCallbacks = {};
|
|
||||||
|
|
||||||
/** @type GalaxyApi */
|
|
||||||
const galaxy = {
|
|
||||||
acceptedOrigins,
|
|
||||||
supportsSaving,
|
|
||||||
supportsSaveManager,
|
|
||||||
ignoreApiVersion,
|
|
||||||
onLoggedInChanged,
|
|
||||||
origin: window.origin,
|
|
||||||
apiVersion: 0,
|
|
||||||
loggedIn: false,
|
|
||||||
postMessage: function (message) {
|
|
||||||
window.top?.postMessage(message, galaxy.origin);
|
|
||||||
},
|
|
||||||
getSaveList: function () {
|
|
||||||
if (saveListAccept != null || saveListReject != null) {
|
|
||||||
return Promise.reject("save_list action already in progress.");
|
|
||||||
}
|
|
||||||
galaxy.postMessage({ action: "save_list" });
|
|
||||||
return new Promise((accept, reject) => {
|
|
||||||
saveListAccept = accept;
|
|
||||||
saveListReject = reject;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
save: function (slot, content, label) {
|
|
||||||
if (slot in saveCallbacks) {
|
|
||||||
return Promise.reject(`save action for slot ${slot} already in progress.`);
|
|
||||||
}
|
|
||||||
galaxy.postMessage({ action: "save", slot, content, label });
|
|
||||||
return new Promise((accept, reject) => {
|
|
||||||
saveCallbacks[slot] = { accept, reject };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
load: function (slot) {
|
|
||||||
if (slot in loadCallbacks) {
|
|
||||||
return Promise.reject(`load action for slot ${slot} already in progress.`);
|
|
||||||
}
|
|
||||||
galaxy.postMessage({ action: "load", slot });
|
|
||||||
return new Promise((accept, reject) => {
|
|
||||||
loadCallbacks[slot] = { accept, reject };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("message", e => {
|
|
||||||
if (e.origin === galaxy.origin) {
|
|
||||||
console.log("Received message from Galaxy", e.data);
|
|
||||||
/** @type GalaxyResponse */
|
|
||||||
const data = e.data;
|
|
||||||
|
|
||||||
switch (data.type) {
|
|
||||||
case "info": {
|
|
||||||
const { galaxy: isGalaxy, api_version, logged_in } = data;
|
|
||||||
// Ignoring isGalaxy check in case other accepted origins send it as false
|
|
||||||
if (api_version !== 1 && galaxy.ignoreApiVersion !== true) {
|
|
||||||
reject(`API version not recognized: ${api_version}`);
|
|
||||||
} else {
|
|
||||||
// Info responses may be sent again if the information gets updated
|
|
||||||
// Specifically, we care if logged_in gets changed
|
|
||||||
// We can use the api_version to determine if this is the first
|
|
||||||
// info response or a new one.
|
|
||||||
const firstInfoResponse = galaxy.apiVersion === 0;
|
|
||||||
galaxy.apiVersion = api_version;
|
|
||||||
galaxy.loggedIn = logged_in;
|
|
||||||
galaxy.origin = e.origin;
|
|
||||||
if (firstInfoResponse) {
|
|
||||||
accept(galaxy);
|
|
||||||
} else {
|
|
||||||
galaxy.onLoggedInChanged?.(galaxy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "save_list": {
|
|
||||||
const { list, error, message } = data;
|
|
||||||
if (error === true) {
|
|
||||||
saveListReject(message);
|
|
||||||
} else {
|
|
||||||
saveListAccept(list);
|
|
||||||
}
|
|
||||||
saveListAccept = saveListReject = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "save_content": {
|
|
||||||
const { content, label, slot, error, message } = data;
|
|
||||||
if (error === true) {
|
|
||||||
loadCallbacks[slot]?.reject(message);
|
|
||||||
} else {
|
|
||||||
loadCallbacks[slot]?.accept({ slot, content, label });
|
|
||||||
}
|
|
||||||
delete loadCallbacks[slot];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "saved": {
|
|
||||||
const { slot, error, message } = data;
|
|
||||||
if (error === true) {
|
|
||||||
saveCallbacks[slot]?.reject(message);
|
|
||||||
} else {
|
|
||||||
saveCallbacks[slot]?.accept(slot);
|
|
||||||
}
|
|
||||||
delete saveCallbacks[slot];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reject(`Project is not running on an accepted origin: ${window.origin}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { LoadablePlayerData } from "components/saves/SavesManager.vue";
|
import { LoadablePlayerData } from "components/saves/SavesManager.vue";
|
||||||
import player, { Player, stringifySave } from "game/player";
|
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 "unofficial-galaxy-sdk";
|
||||||
import LZString from "lz-string";
|
import LZString from "lz-string";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { decodeSave, loadSave, save, setupInitialStore } from "./save";
|
import { decodeSave, loadSave, save, setupInitialStore } from "./save";
|
||||||
|
@ -13,7 +13,7 @@ export const conflictingSaves = ref<
|
||||||
export const syncedSaves = ref<string[]>([]);
|
export const syncedSaves = ref<string[]>([]);
|
||||||
|
|
||||||
export function sync() {
|
export function sync() {
|
||||||
if (galaxy.value == null || !galaxy.value.loggedIn) {
|
if (galaxy.value?.loggedIn !== true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (conflictingSaves.value.length > 0) {
|
if (conflictingSaves.value.length > 0) {
|
||||||
|
@ -138,7 +138,7 @@ function syncSaves(
|
||||||
LZString.compressToUTF16(
|
LZString.compressToUTF16(
|
||||||
stringifySave(setupInitialStore(cloudSave.content))
|
stringifySave(setupInitialStore(cloudSave.content))
|
||||||
),
|
),
|
||||||
cloudSave.label ?? null
|
cloudSave.label
|
||||||
)
|
)
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
// Update cloud save content for the return value
|
// Update cloud save content for the return value
|
||||||
|
|
Loading…
Reference in a new issue