Skip to content

Commit

Permalink
refactor: assets.ts, assets/index.js
Browse files Browse the repository at this point in the history
  • Loading branch information
andretchen0 committed Jul 15, 2023
1 parent 0fdc3b9 commit d2d911f
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 97 deletions.
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
### Ancient Beast ###
# Generated files
deploy/
# List of assets
src/assets.js

# Generated list of assets
assets/index.js

# Dotenv
.env

Expand Down
39 changes: 39 additions & 0 deletions assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# assets/autoload/phaser

Images placed anywhere under assets/autoload/phaser will be loaded into Phaser when the game begins.

## autoloaded images: Phaser keys

Phaser requires a 'key' to access images, e.g.,

```
this.display.loadTexture("myKey");
```

When using an autoloaded image, the basename of the file will be used as the key in Phaser.

### Example

If /assets/autoload/phaser/apples/apple1.png is a valid path, it will be loaded **for you** into Phaser as follows:

```
// NOTE: This is done FOR YOU when using autoloading.
phaser.load.image('apple1', '/assets/autoload/phaser/apple/apple1.png');
```

The basename of the file - "apple1" - is the Phaser key, so you'll use the image like this:

```
this.display.loadTexture("apple1");
```

### Troubleshooting

Because the file basenames are used as Phaser keys, all basenames under assets/autoload/phaser must be unique.

For example, including these two images in the project will cause an error to be thrown, because they have the same basename.

```
/assets/autoload/phaser/monster/scary.png
/assets/autoload/phaser/head/hair/morning/scary.png
```
7 changes: 0 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"@babel/preset-typescript": "^7.21.4",
"@types/jest": "^29.5.0",
"@types/jquery": "^3.5.9",
"@types/webpack-env": "^1.18.1",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"babel-jest": "^29.5.0",
Expand Down
228 changes: 141 additions & 87 deletions src/assets.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,98 @@
import { DEBUG } from './debug';
import { phaserAutoloadAssetPaths, assetPaths } from '../assets/index';

/**
* TODO: Simplify
*
* Ancient Beast used to use a custom script to list assets as part of the build process:
* https://github.com/FreezingMoon/AncientBeast/blob/76692ad039d7a0530fa30ceef367bb0ef68c5015/assetLister.js
* Load all assets in phaserAutoloadAssetsPaths into Phaser Game instance, using URL basename as Phaser key.
*
* But Webpack can list assets using require.context, so we're using it here instead.
* @param {Phaser.Game} e.g., units/shouts/Chimera
* @returns {void}
* @throws Throws an error if two files have the same basename.
*/
export function use(phaser: Phaser.Game): void {
// NOTE: currently only accepts images.
const urls = Object.values(phaserAutoloadAssetPaths);

const hasDuplicateBasenames = new Set(urls.map((url) => getBasename(url))).size < urls.length;
if (DEBUG && hasDuplicateBasenames) throwDuplicateBasenamesError();

for (const url of urls) {
phaser.load.image(getBasename(url), url);
}
}

/**
* Extract basename from a file path.
*
* The assetLister was removed, but its format is duplicated here for compatibility reasons.
* It should probably be rethought and simplified.
* @param {string} path a file path
* @returns {string} the basename from the path
* @example getBasename('./assets/a/b/myFile.png') === 'myFile'
*/
function getBasename(path: string): string {
const base = new String(path).substring(path.lastIndexOf('/') + 1);
let i = base.lastIndexOf('.');
if (base.lastIndexOf('.') === -1) {
return base;
}
while (i > 0 && base[i] === '.') {
i--;
}
return base.substring(0, i + 1);
}

function throwDuplicateBasenamesError() {
// NOTE: Throw a useful message listing duplicate basenames.
const paths = assetPaths
.map(({ path }) => path)
.filter((path) => path.indexOf('/autoload/phaser/') !== -1);

const basenameToPaths: { [basename: string]: string[] } = paths.reduce((obj, path) => {
const bn = getBasename(path);
if (obj.hasOwnProperty(bn)) {
obj[bn].push(path);
} else {
obj[bn] = [path];
}
return obj;
}, {});

const duplicates: { [basename: string]: string[] } = Object.entries(basenameToPaths).reduce(
(obj, [basename, paths]) => {
if (paths.length > 1) {
obj[basename] = paths;
}
return obj;
},
{},
);

const formatted = [];
for (const [basename, paths] of Object.entries(duplicates)) {
for (const path of paths) {
formatted.push(basename + '\t' + path);
}
}

throw new Error(`[Ancient Beast]
Some files under assets/autoload/phaser/ have the same basename.
Basenames are used as keys by Phaser and must be unique.
Please make each basename unique.
Duplicate basenames:
${formatted.join('\n')}
`);
}

/**
* Legacy asset system
* /////////////////////////////////////////////////////////////////////////////////////////////////
*
* TODO: Simplify legacy assets
*
* Ancient Beast used to use a custom asset list format in `assetLister`.
* assetLister.ts was removed, but its format is duplicated here for compatibility reasons.
* It should probably be rethought and simplified.
*
*
* NOTE: Assemble the legacy Assets format.
*
* For theses local paths ...
Expand Down Expand Up @@ -61,61 +143,34 @@
*/

type AssetEntry = { id: string; url?: string; children?: AssetEntry[] };
const dirs: AssetEntry[] = [];
const urls: { [key: string]: string } = {};

{
const importAll = (require: __WebpackModuleApi.RequireContext) =>
require.keys().map((localPath) => ({ localPath, url: require(localPath) }));

const localPaths_urls = importAll(require.context('../assets', true));

{
// NOTE: Add entries to URLs.
/**
* Receives the "local path" and returns the "key".
* E.g., getKey('./units/sprites/Gumble - Royal Seal.png') === 'units/sprites/Gumble - Royal Seal;
*/
const getKey = (localPath: string): string => {
const parts = localPath.split('/');
parts.shift();
const filename = parts.pop().split('.');
filename.pop();
parts.push(filename.join('.'));
return parts.join('/');
};

for (const { localPath, url } of localPaths_urls) {
urls[getKey(localPath)] = url;
}
}

{
// NOTE: Add entries to dirs.
for (const { localPath, url } of localPaths_urls) {
const parts = localPath.split('/');
parts.shift();

const id = parts[parts.length - 1].split('.')[0];

const directories = [...parts];
directories.pop();

let currDir = dirs;
for (const dir of directories) {
const matches = currDir.filter((entry) => entry.id === dir);
if (matches.length && matches[0].children) {
currDir = matches[0].children;
} else {
const entry = { id: dir, children: [] };
currDir.push(entry);
currDir = entry.children;
}
const dirs: AssetEntry[] = (() => {
// NOTE: Add entries to dirs.
const result = [];
for (const [path, url] of Object.entries(assetPaths)) {
const parts = path.split('/');
parts.shift();

const id = parts[parts.length - 1].split('.')[0];

const directories = [...parts];
directories.pop();

let currDir = result;
for (const dir of directories) {
const matches = currDir.filter((entry) => entry.id === dir);
if (matches.length && matches[0].children) {
currDir = matches[0].children;
} else {
const entry = { id: dir, children: [] };
currDir.push(entry);
currDir = entry.children;
}
currDir.push({ id, url });
}
currDir.push({ id, url });
}
}
return result;
})();

function getAssetEntry(pathStr: string): string | AssetEntry[] {
// Convert path to an array if it is a string
Expand Down Expand Up @@ -147,18 +202,6 @@ function getAssetEntry(pathStr: string): string | AssetEntry[] {
return result;
}

/**
* Accepts a key and returns the absolute path to the resource.
*
* @param key {string} e.g., units/shouts/Chimera
* @returns {string} e.g., http://0.0.0.0:8080/assets/units/shouts/Chimera..ogg
* @throws Throws an error if the key is not found.
*/
export function getUrl(key: string): string {
if (urls.hasOwnProperty(key)) return urls[key];
throw new Error('assets.getUrl(key) is not available for the key: ' + key);
}

const dirCache: Record<string, AssetEntry[]> = {};

/**
Expand Down Expand Up @@ -193,24 +236,35 @@ export function getDirectory(path: string): AssetEntry[] {
return entry;
}

export function use(phaser: Phaser.Game) {
const importAll = (require: __WebpackModuleApi.RequireContext) =>
require.keys().map((localPath) => require(localPath));

const basename = (path: string) => {
const base = new String(path).substring(path.lastIndexOf('/') + 1);
let i = base.lastIndexOf('.');
if (base.lastIndexOf('.') === -1) {
return base;
}
while (i > 0 && base[i] === '.') {
i--;
}
return base.substring(0, i + 1);
const urls: { [key: string]: string } = (() => {
/**
* Receives the "local path" and returns the "key".
* E.g., getKey('./units/sprites/Gumble - Royal Seal.png') === 'units/sprites/Gumble - Royal Seal;
*/
const getKey = (path: string): string => {
const parts = path.split('/');
parts.shift();
const filename = parts.pop().split('.');
filename.pop();
parts.push(filename.join('.'));
return parts.join('/');
};

const urls = importAll(require.context('../assets/autoload/phaser', true));
for (const url of urls) {
phaser.load.image(basename(url), url);
const result = {};
for (const [path, url] of Object.entries(assetPaths)) {
result[getKey(path)] = url;
}
return result;
})();

/**
* Accepts a key and returns the absolute path to the resource.
*
* @param key {string} e.g., units/shouts/Chimera
* @returns {string} e.g., http://0.0.0.0:8080/deploy/assets/0acb67b5fb51207b6b23..ogg
* @throws Throws an error if the key is not found.
*/
export function getUrl(key: string): string {
if (urls.hasOwnProperty(key)) return urls[key];
throw new Error('assets.getUrl(key) is not available for the key: ' + key);
}
32 changes: 32 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,38 @@ const pixi = path.join(phaserModule, 'build/custom/pixi.js');
const p2 = path.join(phaserModule, 'build/custom/p2.js');
const Dotenv = require('dotenv-webpack');

{
// NOTE: Generate asset lists.
// Was using Webpack's require.context to generate asset lists, but
// it appears to have a memory leak that causes the dev server to crash after
// a few consecutive builds.
const fs = require('fs');
const glob = require('glob');
const toObjString = (paths) =>
'{' + paths.map((path) => `"${path}":require("${path}")`).join(',') + '}';
const globOptions = { ignore: ['**/*.js', '**/*.ts', '**/*.md'], posix: true };
const phaserAutoloadAssets = toObjString(glob.sync('assets/autoload/phaser/**/*.*', globOptions));
const allAssets = toObjString(glob.sync('assets/**/*.*', globOptions));

fs.writeFileSync(
'assets/index.js',
`// NOTE: Generated at build time by ${path.basename(__filename)}.
// Do not add to this file.
// Any changes to this file will be overwritten when the project is rebuilt.
// ---------------------------------------------------------------------------
// Webpack's require.context would make this unnecessary,
// however, it has performance issues https://github.com/webpack/webpack/issues/13636
// and it appears to have a memory leak that makes the webpack dev server crash after a few builds.
// require.context was last tested July 15, 2023.
export const phaserAutoloadAssetPaths=${phaserAutoloadAssets}
export const assetPaths=${allAssets}
`,
);
}

// Expose mode argument to unify our config options
module.exports = (env, argv) => {
const production = (argv && argv.mode === 'production') || process.env.NODE_ENV === 'production';
Expand Down

0 comments on commit d2d911f

Please sign in to comment.