Skip to content

Commit

Permalink
⚡ Change ct.fs to use app data directories for Linux, Windows, macOS (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
JulianWebb authored Aug 23, 2020
1 parent 6697a21 commit 7f04e30
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 13 deletions.
3 changes: 3 additions & 0 deletions app/data/ct.libs/fs/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>`

Saves an object/array to a file.
Expand Down
17 changes: 13 additions & 4 deletions app/data/ct.libs/fs/README.md
Original file line number Diff line number Diff line change
@@ -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:

Expand Down
72 changes: 63 additions & 9 deletions app/data/ct.libs/fs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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 !== '') {
Expand Down
10 changes: 10 additions & 0 deletions app/data/ct.libs/fs/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;

Expand Down

0 comments on commit 7f04e30

Please sign in to comment.