From 7f04e301a95d52f7e0f0d6936ea95e686524aed6 Mon Sep 17 00:00:00 2001 From: Julian Webb Date: Sun, 23 Aug 2020 14:39:45 -0700 Subject: [PATCH] :zap: Change ct.fs to use app data directories for Linux, Windows, macOS (#226 by @JulianWebb) --- app/data/ct.libs/fs/DOCS.md | 3 ++ app/data/ct.libs/fs/README.md | 17 ++++++-- app/data/ct.libs/fs/index.js | 72 +++++++++++++++++++++++++++++----- app/data/ct.libs/fs/types.d.ts | 10 +++++ 4 files changed, 89 insertions(+), 13 deletions(-) diff --git a/app/data/ct.libs/fs/DOCS.md b/app/data/ct.libs/fs/DOCS.md index c30bd3db3..5a6428916 100644 --- a/app/data/ct.libs/fs/DOCS.md +++ b/app/data/ct.libs/fs/DOCS.md @@ -3,6 +3,9 @@ When set to `true`, any operations towards files outside the game's save directory will fail. Set to `true` by default. +## `ct.fs.isAvailable: boolean` +When set to `false`, the game is running in a way that disallows access to the filesystem (such as a web release) + ## `ct.fs.save(filename: string, data: object|Array): Promise` Saves an object/array to a file. diff --git a/app/data/ct.libs/fs/README.md b/app/data/ct.libs/fs/README.md index 3dc9785bf..5216f29a4 100644 --- a/app/data/ct.libs/fs/README.md +++ b/app/data/ct.libs/fs/README.md @@ -1,12 +1,21 @@ -A module that provides a uniform API for storing and loading data for your desktop games. +A module that provides a uniform API for storing and loading data for games exported for desktop. It allows you to easily save and load JSON objects, as well as plain text data. -JSON objects are regular JavaScript objects, but without functions, Date objects, RegExps, circular references, and some other advanced stuff. If your variable consists of other objects, arrays, strings, numbers and boolean, it can be safely stored, and loaded later in the same form. Thus, they are great for saving your game state. +[JSON objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) are regular JavaScript objects, but without functions, Date objects, RegExps, circular references, and some other advanced stuff. If your variable consists of other objects, arrays, strings, numbers and boolean, it can be safely stored, and loaded later in the same form. Thus, they are great for saving your game state. -By default, all the file operations will be performed relative to a special directory created for your game, which is templated as a `${User's home directory}/${Author name from the settings tab}/${Project's name from the same tab}`, e.g. `/home/comigo/Cosmo Myzrail Gorynych/Platformer complete tutorial` on a Linux machine. You can inspect this behavior by calling `ct.fs.getPath(...)`, or simply by reading ct.fs.gameFolder parameter. +By default, all file operations will be done under the application data directory on the player's system. For Windows this will be `%AppData%`, Linux will be `$XDG_DATA_HOME` (or `$HOME/.local/share` if unset), and macOS will be `$HOME/Library/Application Support`. For other operating systems, the player's home directory will be used instead. -This behavior can be changed by setting `ct.fs.gameFolder`, but it's not recommended unless you changed your meta fields and need to preserved user's data. +Within the application directory, ct.js will create a path using the defined Author Name and Project Name. If both are set, then the path would be `${Application Data Path}/${Author Name}/${Project Name}`, if only the Project Name is set, it would be `${Application Data Path}/${Project Name}`. + +> **For Example**: If a player with the name `naturecodevoid` was running the game `jettyCat` by `comigo` on Linux, the default directory would be: + `/home/naturecodevoid/.local/share/comigo/jettyCat` + + You can verify this by calling `ct.fs.getPath('')` or by checking the variable `ct.fs.gameFolder`. + +It is not recommended, but you can set `ct.fs.gameFolder` to a different directory. This is useful if your meta fields (Author Name, Project Name) have changed, but you wish to preserve user data. + +Also to note, operations outside of the game folder are not recommended and by default are not allowed, causing an error to appear in the game's console. To allow operations outside of the game folder set `ct.fs.forceLocal` to `false` first. Every action in `ct.fs` is asynchronous so that a game stays responsive even on heavy loads, and thus you have to use JS Promises. This is not hard, though: diff --git a/app/data/ct.libs/fs/index.js b/app/data/ct.libs/fs/index.js index cac14b7e7..9611a5480 100644 --- a/app/data/ct.libs/fs/index.js +++ b/app/data/ct.libs/fs/index.js @@ -2,21 +2,74 @@ /* eslint-disable no-console */ try { + // This will fail when in a browser so no browser checking required const fs = require('fs').promises; const path = require('path'); + // Like an enum, but not. + const operatingSystems = { + Windows: 'win', + macOS: 'mac', + ChromeOS: 'cros', + Linux: 'linux', + iOS: 'ios', + Android: 'android' + }; + // The `HOME` variable is not always available in ct.js on Windows const home = process.env.HOME || ((process.env.HOMEDRIVE || '') + process.env.HOMEPATH); + // Borrowed from keyboard.polyfill + const contains = function contains(s, ss) { + return String(s).indexOf(ss) !== -1; + }; + + const operatingSystem = (function getOperatingSystem() { + if (contains(navigator.platform, 'Win')) { + return operatingSystems.Windows; + } + if (contains(navigator.platform, 'Mac')) { + return operatingSystems.macOS; + } + if (contains(navigator.platform, 'CrOS')) { + return operatingSystems.ChromeOS; + } + if (contains(navigator.platform, 'Linux')) { + return operatingSystems.Linux; + } + if (contains(navigator.userAgent, 'iPad') || contains(navigator.platform, 'iPod') || contains(navigator.platform, 'iPhone')) { + return operatingSystems.iOS; + } + return ''; + }()); + + + const getAppData = (home, operatingSystem) => { + switch (operatingSystem) { + case operatingSystems.Windows: + return process.env.AppData; + case operatingSystems.macOS: + return `${home}/Library/Application Support`; + case operatingSystems.Linux: + return process.env.XDG_DATA_HOME || `${home}/.local/share`; + // Don't know what to do for ChromeOS or iOS, do they use AppData or + // Should those default to LocalStorage? + default: + return home; + } + }; + + const appData = getAppData(home, operatingSystem); + const getPath = dest => { - const d = path.isAbsolute(dest)? dest : path.join(ct.fs.gameFolder, dest); + const absoluteDest = path.isAbsolute(dest) ? dest : path.join(ct.fs.gameFolder, dest); if (ct.fs.forceLocal) { - if (d.indexOf(ct.fs.gameFolder) !== 0) { + if (absoluteDest.indexOf(ct.fs.gameFolder) !== 0) { throw new Error('[ct.fs] Operations outside the save directory are not permitted by default due to safety concerns. If you do need to work outside the save directory, change `ct.fs.forceLocal` to `false`. ' + - `The save directory: "${ct.fs.gameFolder}", the target directory: "${dest}", which resolves into "${d}".`); + `The save directory: "${ct.fs.gameFolder}", the target directory: "${dest}", which resolves into "${absoluteDest}".`); } } - return d; + return absoluteDest; }; const ensureParents = async dest => { const parents = path.dirname(getPath(dest)); @@ -25,19 +78,20 @@ try { }); }; + ct.fs = { isAvailable: true, - gameFolder: path.join(home, ct.meta.author || '', ct.meta.name || 'Ct.js game'), + gameFolder: path.join(appData, ct.meta.author || '', ct.meta.name || 'Ct.js game'), forceLocal: true, - async save(filename, data) { + async save(filename, jsonData) { await ensureParents(filename); - await fs.writeFile(getPath(filename), JSON.stringify(data), 'utf8'); + await fs.writeFile(getPath(filename), JSON.stringify(jsonData), 'utf8'); return void 0; }, async load(filename) { - const data = await fs.readFile(getPath(filename), 'utf8'); - return JSON.parse(data); + const textData = await fs.readFile(getPath(filename), 'utf8'); + return JSON.parse(textData); }, async saveText(filename, text) { if (!text && text !== '') { diff --git a/app/data/ct.libs/fs/types.d.ts b/app/data/ct.libs/fs/types.d.ts index 9e0f20f19..cb396a608 100644 --- a/app/data/ct.libs/fs/types.d.ts +++ b/app/data/ct.libs/fs/types.d.ts @@ -36,6 +36,16 @@ declare namespace ct { */ var forceLocal: boolean; + /** + * When set to `false`, the game is running in a way that disallows access to the filesystem (such as a web release) + */ + var isAvailable: boolean; + + /** + * The base location for application data. Not for normal usage. + */ + var gameFolder: string; + /** Saves an object/array to a file. */ function save(filename: string, data: object|any[]): Promise;