diff --git a/README.md b/README.md index 8193bad8..f7eb1409 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,7 @@ and does not intend to provide compatibility for older clients. ### Prerequisites - [ffmpeg](https://www.ffmpeg.org/download.html) -- [Node.js 16](https://nodejs.org/en/about/previous-releases) - (not 17+, see ) +- [Node.js](https://nodejs.org) - [Yarn 1](https://classic.yarnpkg.com/en/docs/install) (not 2, we haven't migrated yet) - [Java](https://www.oracle.com/java/technologies/downloads/) (or [OpenJDK](https://openjdk.org/)) to run the texture packer - [cURL](https://curl.se/download.html)[^1] to download the texture packer diff --git a/gulp/config.js b/gulp/config.js new file mode 100644 index 00000000..e54e1a0e --- /dev/null +++ b/gulp/config.js @@ -0,0 +1,44 @@ +import path from "path/posix"; +import BrowserSync from "browser-sync"; + +export const baseDir = path.resolve(".."); +export const buildFolder = path.join(baseDir, "build"); +export const buildOutputFolder = path.join(baseDir, "build_output"); + +// Globs for atlas resources +export const rawImageResourcesGlobs = ["../res_raw/atlas.json", "../res_raw/**/*.png"]; + +// Globs for non-ui resources +export const nonImageResourcesGlobs = ["../res/**/*.woff2", "../res/*.ico", "../res/**/*.webm"]; + +// Globs for ui resources +export const imageResourcesGlobs = [ + "../res/**/*.png", + "../res/**/*.svg", + "../res/**/*.jpg", + "../res/**/*.gif", +]; + +export const browserSync = BrowserSync.create(); + +// Check environment variables + +const envVars = [ + "SHAPEZ_CLI_SERVER_HOST", + "SHAPEZ_CLI_ALPHA_FTP_USER", + "SHAPEZ_CLI_ALPHA_FTP_PW", + "SHAPEZ_CLI_STAGING_FTP_USER", + "SHAPEZ_CLI_STAGING_FTP_PW", + "SHAPEZ_CLI_LIVE_FTP_USER", + "SHAPEZ_CLI_LIVE_FTP_PW", + "SHAPEZ_CLI_APPLE_ID", + "SHAPEZ_CLI_APPLE_CERT_NAME", + "SHAPEZ_CLI_GITHUB_USER", + "SHAPEZ_CLI_GITHUB_TOKEN", +]; + +for (let i = 0; i < envVars.length; ++i) { + if (!process.env[envVars[i]]) { + console.warn("Unset environment variable, might cause issues:", envVars[i]); + } +} diff --git a/gulp/css.js b/gulp/css.js index 288963b7..1b95f7bb 100644 --- a/gulp/css.js +++ b/gulp/css.js @@ -1,5 +1,7 @@ import path from "path/posix"; +import gulp from "gulp"; import { getRevision } from "./buildutils.js"; +import { buildFolder } from "./config.js"; import gulpPostcss from "gulp-postcss"; import postcssAssets from "postcss-assets"; @@ -13,112 +15,104 @@ import gulpDartSass from "gulp-dart-sass"; import gulpPlumber from "gulp-plumber"; import gulpRename from "gulp-rename"; -export default function gulptasksCSS(gulp, buildFolder, browserSync) { - // The assets plugin copies the files - const commitHash = getRevision(); - const postcssAssetsPlugin = postcssAssets({ - loadPaths: [path.join(buildFolder, "res", "ui")], - basePath: buildFolder, - baseUrl: ".", - }); +// The assets plugin copies the files +const commitHash = getRevision(); +const postcssAssetsPlugin = postcssAssets({ + loadPaths: [path.join(buildFolder, "res", "ui")], + basePath: buildFolder, + baseUrl: ".", +}); - // Postcss configuration - const postcssPlugins = prod => { - const plugins = [postcssAssetsPlugin]; - if (prod) { - plugins.unshift( - postcssPresetEnv({ - browsers: ["> 0.1%"], - }) - ); +// Postcss configuration +const postcssPlugins = prod => { + const plugins = [postcssAssetsPlugin]; + if (prod) { + plugins.unshift( + postcssPresetEnv({ + browsers: ["> 0.1%"], + }) + ); - plugins.push( - cssMqpacker({ - sort: true, - }), - cssnano({ - preset: [ - "advanced", - { - cssDeclarationSorter: false, - discardUnused: true, - mergeIdents: false, - reduceIdents: true, - zindex: true, - }, - ], - }), - postcssRoundSubpixels() - ); - } - return plugins; - }; - - // Performs linting on css - gulp.task("css.lint", () => { - return gulp - .src(["../src/css/**/*.scss"]) - .pipe(gulpSassLint({ configFile: ".sasslint.yml" })) - .pipe(gulpSassLint.format()) - .pipe(gulpSassLint.failOnError()); - }); - - function resourcesTask({ isProd }) { - return gulp - .src("../src/css/main.scss") - .pipe(gulpPlumber()) - .pipe(gulpDartSass.sync().on("error", gulpDartSass.logError)) - .pipe( - gulpPostcss([ - postcssCriticalSplit({ - blockTag: "@load-async", - }), - ]) - ) - .pipe(gulpRename("async-resources.css")) - .pipe(gulpPostcss(postcssPlugins(isProd))) - .pipe(gulp.dest(buildFolder)) - .pipe(browserSync.stream()); + plugins.push( + cssMqpacker({ + sort: true, + }), + cssnano({ + preset: [ + "advanced", + { + cssDeclarationSorter: false, + discardUnused: true, + mergeIdents: false, + reduceIdents: true, + zindex: true, + }, + ], + }), + postcssRoundSubpixels() + ); } + return plugins; +}; +// Performs linting on css +export function lint() { + return gulp + .src(["../src/css/**/*.scss"]) + .pipe(gulpSassLint({ configFile: ".sasslint.yml" })) + .pipe(gulpSassLint.format()) + .pipe(gulpSassLint.failOnError()); +} + +function resourcesTask({ isProd }) { + return gulp + .src("../src/css/main.scss") + .pipe(gulpPlumber()) + .pipe(gulpDartSass.sync().on("error", gulpDartSass.logError)) + .pipe( + gulpPostcss([ + postcssCriticalSplit({ + blockTag: "@load-async", + }), + ]) + ) + .pipe(gulpRename("async-resources.css")) + .pipe(gulpPostcss(postcssPlugins(isProd))) + .pipe(gulp.dest(buildFolder)); +} + +export const resources = { // Builds the css resources - gulp.task("css.resources.dev", () => { - return resourcesTask({ isProd: false }); - }); + dev: () => resourcesTask({ isProd: false }), // Builds the css resources in prod (=minified) - gulp.task("css.resources.prod", () => { - return resourcesTask({ isProd: true }); - }); + prod: () => resourcesTask({ isProd: true }), +}; - function mainTask({ isProd }) { - return gulp - .src("../src/css/main.scss") - .pipe(gulpPlumber()) - .pipe(gulpDartSass.sync().on("error", gulpDartSass.logError)) - .pipe( - gulpPostcss([ - postcssCriticalSplit({ - blockTag: "@load-async", - output: "rest", - }), - ]) - ) - .pipe(gulpPostcss(postcssPlugins(isProd))) - .pipe(gulp.dest(buildFolder)) - .pipe(browserSync.stream()); - } +function mainTask({ isProd }) { + return gulp + .src("../src/css/main.scss") + .pipe(gulpPlumber()) + .pipe(gulpDartSass.sync().on("error", gulpDartSass.logError)) + .pipe( + gulpPostcss([ + postcssCriticalSplit({ + blockTag: "@load-async", + output: "rest", + }), + ]) + ) + .pipe(gulpPostcss(postcssPlugins(isProd))) + .pipe(gulp.dest(buildFolder)); +} +export const main = { // Builds the css main - gulp.task("css.main.dev", () => { - return mainTask({ isProd: false }); - }); + dev: () => mainTask({ isProd: false }), // Builds the css main in prod (=minified) - gulp.task("css.main.prod", () => { - return mainTask({ isProd: true }); - }); + prod: () => mainTask({ isProd: true }), +}; - gulp.task("css.dev", gulp.parallel("css.main.dev", "css.resources.dev")); - gulp.task("css.prod", gulp.parallel("css.main.prod", "css.resources.prod")); -} +export const dev = gulp.parallel(main.dev, resources.dev); +export const prod = gulp.parallel(main.prod, resources.prod); diff --git a/gulp/docs.js b/gulp/docs.js index 3c11d451..e63f6483 100644 --- a/gulp/docs.js +++ b/gulp/docs.js @@ -1,38 +1,36 @@ import path from "path/posix"; -import fs from "fs"; +import fs from "fs/promises"; +import gulp from "gulp"; import gulpRename from "gulp-rename"; import stripJsonComments from "strip-json-comments"; -export default function gulptasksDocs(gulp, buildFolder) { - gulp.task("docs.convertJsToTs", () => { - return gulp - .src(path.join("..", "src", "js", "**", "*.js")) - .pipe( - gulpRename(path => { - path.extname = ".ts"; - }) - ) - .pipe(gulp.dest(path.join("..", "tsc_temp"))); - }); - - gulp.task("docs.copyTsconfigForHints", cb => { - const src = fs.readFileSync(path.join("..", "src", "js", "tsconfig.json")).toString(); - const baseConfig = JSON.parse(stripJsonComments(src)); - - baseConfig.allowJs = false; - baseConfig.checkJs = false; - baseConfig.declaration = true; - baseConfig.noEmit = false; - baseConfig.strict = false; - baseConfig.strictFunctionTypes = false; - baseConfig.strictBindCallApply = false; - baseConfig.alwaysStrict = false; - baseConfig.composite = true; - baseConfig.outFile = "bundled-ts.js"; - fs.writeFileSync(path.join("..", "tsc_temp", "tsconfig.json"), JSON.stringify(baseConfig)); - cb(); - }); - - gulp.task("main.prepareDocs", gulp.series("docs.convertJsToTs", "docs.copyTsconfigForHints")); +export function convertJsToTs() { + return gulp + .src(path.join("..", "src", "js", "**", "*.js")) + .pipe( + gulpRename(path => { + path.extname = ".ts"; + }) + ) + .pipe(gulp.dest(path.join("..", "tsc_temp"))); } + +export async function copyTsconfigForHints() { + const src = (await fs.readFile(path.join("..", "src", "js", "tsconfig.json"))).toString(); + const baseConfig = JSON.parse(stripJsonComments(src)); + + baseConfig.allowJs = false; + baseConfig.checkJs = false; + baseConfig.declaration = true; + baseConfig.noEmit = false; + baseConfig.strict = false; + baseConfig.strictFunctionTypes = false; + baseConfig.strictBindCallApply = false; + baseConfig.alwaysStrict = false; + baseConfig.composite = true; + baseConfig.outFile = "bundled-ts.js"; + await fs.writeFile(path.join("..", "tsc_temp", "tsconfig.json"), JSON.stringify(baseConfig)); +} + +export const prepareDocs = gulp.series(convertJsToTs, copyTsconfigForHints); diff --git a/gulp/ftp.js b/gulp/ftp.js index bce1d445..1a5bc1c4 100644 --- a/gulp/ftp.js +++ b/gulp/ftp.js @@ -1,68 +1,69 @@ import path from "path/posix"; -import fs from "fs"; +import fs from "fs/promises"; +import gulp from "gulp"; +import { buildFolder } from "./config.js"; import { getRevision, getVersion } from "./buildutils.js"; import gulpRename from "gulp-rename"; import gulpSftp from "gulp-sftp"; -export default function gulptasksFTP(gulp, buildFolder) { - const commitHash = getRevision(); +const commitHash = getRevision(); - const additionalFolder = path.join("additional_build_files"); +const additionalFolder = path.join("additional_build_files"); - const additionalFiles = [ - path.join(additionalFolder, "*"), - path.join(additionalFolder, "*.*"), - path.join(additionalFolder, ".*"), - ]; +const additionalGlobs = [ + path.join(additionalFolder, "*"), + path.join(additionalFolder, "*.*"), + path.join(additionalFolder, ".*"), +]; - const credentials = { - alpha: { - host: process.env.SHAPEZ_CLI_SERVER_HOST, - user: process.env.SHAPEZ_CLI_ALPHA_FTP_USER, - pass: process.env.SHAPEZ_CLI_ALPHA_FTP_PW, - }, - staging: { - host: process.env.SHAPEZ_CLI_SERVER_HOST, - user: process.env.SHAPEZ_CLI_STAGING_FTP_USER, - pass: process.env.SHAPEZ_CLI_STAGING_FTP_PW, - }, - prod: { - host: process.env.SHAPEZ_CLI_SERVER_HOST, - user: process.env.SHAPEZ_CLI_LIVE_FTP_USER, - pass: process.env.SHAPEZ_CLI_LIVE_FTP_PW, - }, - }; +const credentials = { + alpha: { + host: process.env.SHAPEZ_CLI_SERVER_HOST, + user: process.env.SHAPEZ_CLI_ALPHA_FTP_USER, + pass: process.env.SHAPEZ_CLI_ALPHA_FTP_PW, + }, + staging: { + host: process.env.SHAPEZ_CLI_SERVER_HOST, + user: process.env.SHAPEZ_CLI_STAGING_FTP_USER, + pass: process.env.SHAPEZ_CLI_STAGING_FTP_PW, + }, + prod: { + host: process.env.SHAPEZ_CLI_SERVER_HOST, + user: process.env.SHAPEZ_CLI_LIVE_FTP_USER, + pass: process.env.SHAPEZ_CLI_LIVE_FTP_PW, + }, +}; - // Write the "commit.txt" file - gulp.task("ftp.writeVersion", cb => { - fs.writeFileSync( - path.join(buildFolder, "version.json"), - JSON.stringify( - { - commit: getRevision(), - appVersion: getVersion(), - buildTime: new Date().getTime(), - }, - null, - 4 - ) - ); - cb(); - }); +// Write the "commit.txt" file +export async function writeVersion() { + await fs.writeFile( + path.join(buildFolder, "version.json"), + JSON.stringify( + { + commit: getRevision(), + appVersion: getVersion(), + buildTime: new Date().getTime(), + }, + null, + 4 + ) + ); +} - const gameSrcGlobs = [ - path.join(buildFolder, "**/*.*"), - path.join(buildFolder, "**/.*"), - path.join(buildFolder, "**/*"), - path.join(buildFolder, "!**/index.html"), - ]; +const gameSrcGlobs = [ + path.join(buildFolder, "**/*.*"), + path.join(buildFolder, "**/.*"), + path.join(buildFolder, "**/*"), + path.join(buildFolder, "!**/index.html"), +]; - for (const deployEnv of ["alpha", "prod", "staging"]) { +export const upload = Object.fromEntries( + ["alpha", "prod", "staging"].map(deployEnv => { const deployCredentials = credentials[deployEnv]; - gulp.task(`ftp.upload.${deployEnv}.game`, () => { + function game() { return gulp .src(gameSrcGlobs, { base: buildFolder }) .pipe( @@ -71,30 +72,28 @@ export default function gulptasksFTP(gulp, buildFolder) { }) ) .pipe(gulpSftp(deployCredentials)); - }); + } - gulp.task(`ftp.upload.${deployEnv}.indexHtml`, () => { + function indexHtml() { return gulp .src([path.join(buildFolder, "index.html"), path.join(buildFolder, "version.json")], { base: buildFolder, }) .pipe(gulpSftp(deployCredentials)); - }); + } - gulp.task(`ftp.upload.${deployEnv}.additionalFiles`, () => { - return gulp - .src(additionalFiles, { base: additionalFolder }) // - .pipe(gulpSftp(deployCredentials)); - }); + function additionalFiles() { + return gulp.src(additionalGlobs, { base: additionalFolder }).pipe(gulpSftp(deployCredentials)); + } - gulp.task( - `ftp.upload.${deployEnv}`, - gulp.series( - "ftp.writeVersion", - `ftp.upload.${deployEnv}.game`, - `ftp.upload.${deployEnv}.indexHtml`, - `ftp.upload.${deployEnv}.additionalFiles` - ) - ); - } -} + return [ + deployEnv, + { + game, + indexHtml, + additionalFiles, + all: gulp.series(writeVersion, game, indexHtml, additionalFiles), + }, + ]; + }) +); diff --git a/gulp/gulpfile.js b/gulp/gulpfile.js index ad9412df..4c4c8e4c 100644 --- a/gulp/gulpfile.js +++ b/gulp/gulpfile.js @@ -1,303 +1,23 @@ import gulp from "gulp"; -import BrowserSync from "browser-sync"; -const browserSync = BrowserSync.create({}); -import path from "path/posix"; -import pathNative from "path"; -import deleteEmpty from "delete-empty"; -import { execSync } from "child_process"; - -// Load other plugins -import gulpClean from "gulp-clean"; -import gulpWebserver from "gulp-webserver"; - -// Check environment variables - -const envVars = [ - "SHAPEZ_CLI_SERVER_HOST", - "SHAPEZ_CLI_ALPHA_FTP_USER", - "SHAPEZ_CLI_ALPHA_FTP_PW", - "SHAPEZ_CLI_STAGING_FTP_USER", - "SHAPEZ_CLI_STAGING_FTP_PW", - "SHAPEZ_CLI_LIVE_FTP_USER", - "SHAPEZ_CLI_LIVE_FTP_PW", - "SHAPEZ_CLI_APPLE_ID", - "SHAPEZ_CLI_APPLE_CERT_NAME", - "SHAPEZ_CLI_GITHUB_USER", - "SHAPEZ_CLI_GITHUB_TOKEN", -]; - -for (let i = 0; i < envVars.length; ++i) { - if (!process.env[envVars[i]]) { - console.warn("Unset environment variable, might cause issues:", envVars[i]); - } -} - -const baseDir = path.resolve(".."); -const buildFolder = path.join(baseDir, "build"); -const buildOuptutFolder = path.join(baseDir, "build_output"); - -import gulptasksImageResources, * as imgres from "./image-resources.js"; -gulptasksImageResources(gulp, buildFolder); - -import gulptasksCSS from "./css.js"; -gulptasksCSS(gulp, buildFolder, browserSync); - -import gulptasksSounds from "./sounds.js"; -gulptasksSounds(gulp, buildFolder); - -import gulptasksLocalConfig from "./local-config.js"; -gulptasksLocalConfig(gulp); - -import gulptasksJS from "./js.js"; -gulptasksJS(gulp, buildFolder, browserSync); - -import gulptasksHTML from "./html.js"; -gulptasksHTML(gulp, buildFolder); - -import gulptasksFTP from "./ftp.js"; -gulptasksFTP(gulp, buildFolder); - -import gulptasksDocs from "./docs.js"; -gulptasksDocs(gulp, buildFolder); - -import gulptasksStandalone from "./standalone.js"; -gulptasksStandalone(gulp); - -import gulptasksTranslations from "./translations.js"; -import { BUILD_VARIANTS } from "./build_variants.js"; -gulptasksTranslations(gulp); - -///////////////////// BUILD TASKS ///////////////////// - -// Cleans up everything -gulp.task("utils.cleanBuildFolder", () => { - return gulp.src(buildFolder, { read: false, allowEmpty: true }).pipe(gulpClean({ force: true })); -}); -gulp.task("utils.cleanBuildOutputFolder", () => { - return gulp.src(buildOuptutFolder, { read: false, allowEmpty: true }).pipe(gulpClean({ force: true })); -}); -gulp.task("utils.cleanBuildTempFolder", () => { - return gulp - .src(path.join("..", "src", "js", "built-temp"), { read: false, allowEmpty: true }) - .pipe(gulpClean({ force: true })); -}); -gulp.task("utils.cleanImageBuildFolder", () => { - return gulp - .src(path.join("res_built"), { read: false, allowEmpty: true }) - .pipe(gulpClean({ force: true })); -}); - -gulp.task( - "utils.cleanup", - gulp.series("utils.cleanBuildFolder", "utils.cleanImageBuildFolder", "utils.cleanBuildTempFolder") -); - -// Requires no uncomitted files -gulp.task("utils.requireCleanWorkingTree", cb => { - let output = execSync("git status -su").toString("ascii").trim().replace(/\r/gi, "").split("\n"); - - // Filter files which are OK to be untracked - output = output - .map(x => x.replace(/[\r\n]+/gi, "")) - .filter(x => x.indexOf(".local.js") < 0) - .filter(x => x.length > 0); - if (output.length > 0) { - console.error("\n\nYou have unstaged changes, please commit everything first!"); - console.error("Unstaged files:"); - console.error(output.map(x => "'" + x + "'").join("\n")); - process.exit(1); - } - cb(); -}); - -gulp.task("utils.copyAdditionalBuildFiles", cb => { - const additionalFolder = path.join("additional_build_files"); - const additionalSrcGlobs = [ - path.join(additionalFolder, "**/*.*"), - path.join(additionalFolder, "**/.*"), - path.join(additionalFolder, "**/*"), - ]; - - return gulp.src(additionalSrcGlobs).pipe(gulp.dest(buildFolder)); -}); - -// Starts a webserver on the built directory (useful for testing prod build) -gulp.task("main.webserver", () => { - return gulp.src(buildFolder).pipe( - gulpWebserver({ - livereload: { - enable: true, - }, - directoryListing: false, - open: true, - port: 3005, - }) - ); -}); +import * as tasks from "./tasks.js"; /** - * - * @param {object} param0 - * @param {keyof typeof BUILD_VARIANTS} param0.version + * @typedef {import("gulp").TaskFunction} TaskFunction + * @typedef {TaskFunction | { [k: string]: Tasks }} Tasks */ -function serveHTML({ version = "web-dev" }) { - browserSync.init({ - server: [buildFolder, path.join(baseDir, "mod_examples")], - port: 3005, - ghostMode: { - clicks: false, - scroll: false, - location: false, - forms: false, - }, - logLevel: "info", - logPrefix: "BS", - online: false, - xip: false, - notify: false, - reloadDebounce: 100, - reloadOnRestart: true, - watchEvents: ["add", "change"], - }); - // Watch .scss files, those trigger a css rebuild - gulp.watch(["../src/**/*.scss"], gulp.series("css.dev")); - - // Watch .html files, those trigger a html rebuild - gulp.watch("../src/**/*.html", gulp.series("html.dev")); - gulp.watch("./preloader/*.*", gulp.series("html.dev")); - - // Watch translations - gulp.watch("../translations/**/*.yaml", gulp.series("translations.convertToJson")); - - gulp.watch( - ["../res_raw/sounds/sfx/*.mp3", "../res_raw/sounds/sfx/*.wav"], - gulp.series("sounds.sfx", "sounds.copy") - ); - gulp.watch( - ["../res_raw/sounds/music/*.mp3", "../res_raw/sounds/music/*.wav"], - gulp.series("sounds.music", "sounds.copy") - ); - - // Watch resource files and copy them on change - gulp.watch(imgres.rawImageResourcesGlobs, gulp.series("imgres.buildAtlas")); - gulp.watch(imgres.nonImageResourcesGlobs, gulp.series("imgres.copyNonImageResources")); - gulp.watch(imgres.imageResourcesGlobs, gulp.series("imgres.copyImageResources")); - - // Watch .atlas files and recompile the atlas on change - gulp.watch("../res_built/atlas/*.atlas", gulp.series("imgres.atlasToJson")); - gulp.watch("../res_built/atlas/*.json", gulp.series("imgres.atlas")); - - // Watch the build folder and reload when anything changed - const extensions = ["html", "js", "png", "gif", "jpg", "svg", "mp3", "ico", "woff2", "json"]; - gulp.watch(extensions.map(ext => path.join(buildFolder, "**", "*." + ext))).on("change", function (p) { - return gulp - .src(pathNative.resolve(p).replaceAll(pathNative.sep, path.sep)) - .pipe(browserSync.reload({ stream: true })); - }); - - gulp.watch("../src/js/built-temp/*.json").on("change", function (p) { - return gulp - .src(pathNative.resolve(p).replaceAll(pathNative.sep, path.sep)) - .pipe(browserSync.reload({ stream: true })); - }); - - gulp.series("js." + version + ".dev.watch")(() => true); -} - -// Pre and postbuild -gulp.task("step.baseResources", gulp.series("imgres.allOptimized")); -gulp.task("step.deleteEmpty", cb => { - deleteEmpty.sync(buildFolder); - cb(); -}); - -gulp.task("step.postbuild", gulp.series("imgres.cleanupUnusedCssInlineImages", "step.deleteEmpty")); - -///////////////////// RUNNABLE TASKS ///////////////////// - -// Builds everything (dev) -gulp.task( - "build.prepare.dev", - gulp.series( - "utils.cleanup", - "utils.copyAdditionalBuildFiles", - "localConfig.findOrCreate", - "imgres.buildAtlas", - "imgres.atlasToJson", - "imgres.atlas", - "sounds.dev", - "imgres.copyImageResources", - "imgres.copyNonImageResources", - "translations.fullBuild", - "css.dev" - ) -); - -// Builds everything for every variant -for (const variant in BUILD_VARIANTS) { - const data = BUILD_VARIANTS[variant]; - const buildName = "build." + variant; - - // build - gulp.task( - buildName + ".code", - gulp.series( - data.standalone ? "sounds.fullbuildHQ" : "sounds.fullbuild", - "translations.fullBuild", - "js." + variant + ".prod" - ) - ); - - gulp.task(buildName + ".resourcesAndCode", gulp.parallel("step.baseResources", buildName + ".code")); - - gulp.task(buildName + ".all", gulp.series(buildName + ".resourcesAndCode", "css.prod", "html.prod")); - - gulp.task(buildName, gulp.series("utils.cleanup", buildName + ".all", "step.postbuild")); - - // Tasks for creating packages. These packages are already distributable, but usually can be further - // wrapped in a different format (an installer for Windows, tarball for Linux, DMG for macOS). - if (data.standalone) { - const packageTasks = [ - "win32-x64", - "win32-arm64", - "linux-x64", - "linux-arm64", - "darwin-x64", - "darwin-arm64", - "all", - ]; - - for (const task of packageTasks) { - gulp.task( - `package.${variant}.${task}`, - gulp.series( - "localConfig.findOrCreate", - `build.${variant}`, - "utils.cleanBuildOutputFolder", - `standalone.${variant}.prepare`, - `standalone.${variant}.package.${task}` - ) - ); - } +/** + * @param {Tasks} tasks + * @param {string=} prefix + */ +function register(tasks, prefix) { + if (tasks instanceof Function) { + gulp.task(prefix, tasks); + return; + } + for (const [k, v] of Object.entries(tasks)) { + register(v, prefix == null ? k : `${prefix}.${k}`); } - - // serve - gulp.task( - "serve." + variant, - gulp.series("build.prepare.dev", "html.dev", () => serveHTML({ version: variant })) - ); } -// Deploying! -gulp.task( - "deploy.staging", - gulp.series("utils.requireCleanWorkingTree", "build.web-shapezio-beta", "ftp.upload.staging") -); -gulp.task( - "deploy.prod", - gulp.series("utils.requireCleanWorkingTree", "build.web-shapezio", "ftp.upload.prod") -); - -// Default task (dev, localhost) -gulp.task("default", gulp.series("serve.standalone-steam")); +register(tasks); diff --git a/gulp/html.js b/gulp/html.js index d95e07e3..52b1685c 100644 --- a/gulp/html.js +++ b/gulp/html.js @@ -2,6 +2,8 @@ import { getRevision } from "./buildutils.js"; import fs from "fs"; import path from "path/posix"; import crypto from "crypto"; +import gulp from "gulp"; +import { buildFolder } from "./config.js"; import gulpDom from "gulp-dom"; import gulpHtmlmin from "gulp-htmlmin"; @@ -20,32 +22,31 @@ function computeIntegrityHash(fullPath, algorithm = "sha256") { * html.dev * html.prod */ -export default function gulptasksHTML(gulp, buildFolder) { - const commitHash = getRevision(); - async function buildHtml({ integrity = true }) { - return gulp - .src("../src/html/index.html") - .pipe( - gulpDom( - /** @this {Document} **/ function () { - const document = this; +const commitHash = getRevision(); +async function buildHtml({ integrity = true }) { + return gulp + .src("../src/html/index.html") + .pipe( + gulpDom( + /** @this {Document} **/ function () { + const document = this; - // Append css - const css = document.createElement("link"); - css.rel = "stylesheet"; - css.type = "text/css"; - css.media = "none"; - css.setAttribute("onload", "this.media='all'"); - css.href = "main.css"; - if (integrity) { - css.setAttribute( - "integrity", - computeIntegrityHash(path.join(buildFolder, "main.css")) - ); - } - document.head.appendChild(css); + // Append css + const css = document.createElement("link"); + css.rel = "stylesheet"; + css.type = "text/css"; + css.media = "none"; + css.setAttribute("onload", "this.media='all'"); + css.href = "main.css"; + if (integrity) { + css.setAttribute( + "integrity", + computeIntegrityHash(path.join(buildFolder, "main.css")) + ); + } + document.head.appendChild(css); - let fontCss = ` + let fontCss = ` @font-face { font-family: "GameFont"; font-style: normal; @@ -54,59 +55,54 @@ export default function gulptasksHTML(gulp, buildFolder) { src: url('res/fonts/GameFont.woff2') format("woff2"); } `; - let loadingCss = - fontCss + fs.readFileSync(path.join("preloader", "preloader.css")).toString(); + let loadingCss = + fontCss + fs.readFileSync(path.join("preloader", "preloader.css")).toString(); - const style = document.createElement("style"); - style.setAttribute("type", "text/css"); - style.textContent = loadingCss; - document.head.appendChild(style); + const style = document.createElement("style"); + style.setAttribute("type", "text/css"); + style.textContent = loadingCss; + document.head.appendChild(style); - let bodyContent = fs - .readFileSync(path.join("preloader", "preloader.html")) - .toString(); + let bodyContent = fs.readFileSync(path.join("preloader", "preloader.html")).toString(); - const bundleScript = document.createElement("script"); - bundleScript.type = "text/javascript"; - bundleScript.src = "bundle.js"; - if (integrity) { - bundleScript.setAttribute( - "integrity", - computeIntegrityHash(path.join(buildFolder, "bundle.js")) - ); - } - document.head.appendChild(bundleScript); - - document.body.innerHTML = bodyContent; + const bundleScript = document.createElement("script"); + bundleScript.type = "text/javascript"; + bundleScript.src = "bundle.js"; + if (integrity) { + bundleScript.setAttribute( + "integrity", + computeIntegrityHash(path.join(buildFolder, "bundle.js")) + ); } - ) - ) - .pipe( - gulpHtmlmin({ - caseSensitive: true, - collapseBooleanAttributes: true, - collapseInlineTagWhitespace: true, - collapseWhitespace: true, - preserveLineBreaks: true, - minifyJS: true, - minifyCSS: true, - quoteCharacter: '"', - useShortDoctype: true, - }) - ) - .pipe(gulpHtmlBeautify()) - .pipe(gulpRename("index.html")) - .pipe(gulp.dest(buildFolder)); - } + document.head.appendChild(bundleScript); - gulp.task("html.dev", () => { - return buildHtml({ - integrity: false, - }); - }); - gulp.task("html.prod", () => { - return buildHtml({ - integrity: true, - }); - }); + document.body.innerHTML = bodyContent; + } + ) + ) + .pipe( + gulpHtmlmin({ + caseSensitive: true, + collapseBooleanAttributes: true, + collapseInlineTagWhitespace: true, + collapseWhitespace: true, + preserveLineBreaks: true, + minifyJS: true, + minifyCSS: true, + quoteCharacter: '"', + useShortDoctype: true, + }) + ) + .pipe(gulpHtmlBeautify()) + .pipe(gulpRename("index.html")) + .pipe(gulp.dest(buildFolder)); } + +export const dev = () => + buildHtml({ + integrity: false, + }); +export const prod = () => + buildHtml({ + integrity: true, + }); diff --git a/gulp/image-resources.js b/gulp/image-resources.js index d247c231..22aa2860 100644 --- a/gulp/image-resources.js +++ b/gulp/image-resources.js @@ -1,12 +1,19 @@ -import fs from "fs"; +import fs from "fs/promises"; import path from "path/posix"; -import atlasToJson from "./atlas2json.js"; +import gulp from "gulp"; +import { buildFolder } from "./config.js"; +import atlas2Json from "./atlas2json.js"; -import { execSync } from "child_process"; -const execute = command => - execSync(command, { +import childProcess from "child_process"; +import { promisify } from "util"; +const exec = promisify(childProcess.exec); +const execute = command => { + const promise = exec(command, { encoding: "utf-8", }); + promise.child.stderr.pipe(process.stderr); + return promise; +}; import gulpImagemin from "gulp-imagemin"; import imageminJpegtran from "imagemin-jpegtran"; @@ -15,182 +22,161 @@ import imageminPngquant from "imagemin-pngquant"; import gulpIf from "gulp-if"; import gulpCached from "gulp-cached"; import gulpClean from "gulp-clean"; - -// Globs for atlas resources -export const rawImageResourcesGlobs = ["../res_raw/atlas.json", "../res_raw/**/*.png"]; - -// Globs for non-ui resources -export const nonImageResourcesGlobs = ["../res/**/*.woff2", "../res/*.ico", "../res/**/*.webm"]; - -// Globs for ui resources -export const imageResourcesGlobs = [ - "../res/**/*.png", - "../res/**/*.svg", - "../res/**/*.jpg", - "../res/**/*.gif", -]; +import { nonImageResourcesGlobs, imageResourcesGlobs } from "./config.js"; // Link to download LibGDX runnable-texturepacker.jar const runnableTPSource = "https://libgdx-nightlies.s3.eu-central-1.amazonaws.com/libgdx-runnables/runnable-texturepacker.jar"; -export default function gulptasksImageResources(gulp, buildFolder) { - // Lossless options - const minifyImagesOptsLossless = () => [ - imageminJpegtran({ - progressive: true, - }), - gulpImagemin.svgo({}), - gulpImagemin.optipng({ - optimizationLevel: 3, - }), - imageminGifsicle({ - optimizationLevel: 3, - colors: 128, - }), - ]; +// Lossless options +const minifyImagesOptsLossless = () => [ + imageminJpegtran({ + progressive: true, + }), + gulpImagemin.svgo({}), + gulpImagemin.optipng({ + optimizationLevel: 3, + }), + imageminGifsicle({ + optimizationLevel: 3, + colors: 128, + }), +]; - // Lossy options - const minifyImagesOpts = () => [ - gulpImagemin.mozjpeg({ - quality: 80, - maxMemory: 1024 * 1024 * 8, - }), - gulpImagemin.svgo({}), - imageminPngquant({ - speed: 1, - strip: true, - quality: [0.65, 0.9], - dithering: false, - verbose: false, - }), - gulpImagemin.optipng({ - optimizationLevel: 3, - }), - imageminGifsicle({ - optimizationLevel: 3, - colors: 128, - }), - ]; +// Lossy options +const minifyImagesOpts = () => [ + gulpImagemin.mozjpeg({ + quality: 80, + maxMemory: 1024 * 1024 * 8, + }), + gulpImagemin.svgo({}), + imageminPngquant({ + speed: 1, + strip: true, + quality: [0.65, 0.9], + dithering: false, + verbose: false, + }), + gulpImagemin.optipng({ + optimizationLevel: 3, + }), + imageminGifsicle({ + optimizationLevel: 3, + colors: 128, + }), +]; - // Where the resources folder are - const resourcesDestFolder = path.join(buildFolder, "res"); +// Where the resources folder are +const resourcesDestFolder = path.join(buildFolder, "res"); - /** - * Determines if an atlas must use lossless compression - * @param {string} fname - */ - function fileMustBeLossless(fname) { - return fname.indexOf("lossless") >= 0; - } - - /////////////// ATLAS ///////////////////// - - gulp.task("imgres.buildAtlas", cb => { - const config = JSON.stringify("../res_raw/atlas.json"); - const source = JSON.stringify("../res_raw"); - const dest = JSON.stringify("../res_built/atlas"); - - try { - // First check whether Java is installed - execute("java -version"); - // Now check and try downloading runnable-texturepacker.jar (22MB) - if (!fs.existsSync("./runnable-texturepacker.jar")) { - const escapedLink = JSON.stringify(runnableTPSource); - - try { - execute(`curl -o runnable-texturepacker.jar ${escapedLink}`); - } catch { - throw new Error("Failed to download runnable-texturepacker.jar!"); - } - } - - execute(`java -jar runnable-texturepacker.jar ${source} ${dest} atlas0 ${config}`); - } catch { - console.warn("Building atlas failed. Java not found / unsupported version?"); - } - cb(); - }); - - // Converts .atlas LibGDX files to JSON - gulp.task("imgres.atlasToJson", cb => { - atlasToJson("../res_built/atlas"); - cb(); - }); - - // Copies the atlas to the final destination - gulp.task("imgres.atlas", () => { - return gulp.src(["../res_built/atlas/*.png"]).pipe(gulp.dest(resourcesDestFolder)); - }); - - // Copies the atlas to the final destination after optimizing it (lossy compression) - gulp.task("imgres.atlasOptimized", () => { - return gulp - .src(["../res_built/atlas/*.png"]) - .pipe( - gulpIf( - fname => fileMustBeLossless(fname.history[0]), - gulpImagemin(minifyImagesOptsLossless()), - gulpImagemin(minifyImagesOpts()) - ) - ) - .pipe(gulp.dest(resourcesDestFolder)); - }); - - //////////////////// RESOURCES ////////////////////// - - // Copies all resources which are no ui resources - gulp.task("imgres.copyNonImageResources", () => { - return gulp.src(nonImageResourcesGlobs).pipe(gulp.dest(resourcesDestFolder)); - }); - - // Copies all ui resources - gulp.task("imgres.copyImageResources", () => { - return gulp - .src(imageResourcesGlobs) - - .pipe(gulpCached("imgres.copyImageResources")) - .pipe(gulp.dest(path.join(resourcesDestFolder))); - }); - - // Copies all ui resources and optimizes them - gulp.task("imgres.copyImageResourcesOptimized", () => { - return gulp - .src(imageResourcesGlobs) - .pipe( - gulpIf( - fname => fileMustBeLossless(fname.history[0]), - gulpImagemin(minifyImagesOptsLossless()), - gulpImagemin(minifyImagesOpts()) - ) - ) - .pipe(gulp.dest(path.join(resourcesDestFolder))); - }); - - // Copies all resources and optimizes them - gulp.task( - "imgres.allOptimized", - gulp.parallel( - "imgres.buildAtlas", - "imgres.atlasToJson", - "imgres.atlasOptimized", - "imgres.copyNonImageResources", - "imgres.copyImageResourcesOptimized" - ) - ); - - // Cleans up unused images which are instead inline into the css - gulp.task("imgres.cleanupUnusedCssInlineImages", () => { - return gulp - .src( - [ - path.join(buildFolder, "res", "ui", "**", "*.png"), - path.join(buildFolder, "res", "ui", "**", "*.jpg"), - path.join(buildFolder, "res", "ui", "**", "*.svg"), - path.join(buildFolder, "res", "ui", "**", "*.gif"), - ], - { read: false } - ) - .pipe(gulpIf(fname => fname.history[0].indexOf("noinline") < 0, gulpClean({ force: true }))); - }); +/** + * Determines if an atlas must use lossless compression + * @param {string} fname + */ +function fileMustBeLossless(fname) { + return fname.indexOf("lossless") >= 0; +} + +/////////////// ATLAS ///////////////////// + +export async function buildAtlas() { + const config = JSON.stringify("../res_raw/atlas.json"); + const source = JSON.stringify("../res_raw"); + const dest = JSON.stringify("../res_built/atlas"); + + try { + // First check whether Java is installed + await execute("java -version"); + // Now check and try downloading runnable-texturepacker.jar (22MB) + try { + await fs.access("./runnable-texturepacker.jar"); + } catch { + const escapedLink = JSON.stringify(runnableTPSource); + + try { + execute(`curl -o runnable-texturepacker.jar ${escapedLink}`); + } catch { + throw new Error("Failed to download runnable-texturepacker.jar!"); + } + } + + await execute(`java -jar runnable-texturepacker.jar ${source} ${dest} atlas0 ${config}`); + } catch { + console.warn("Building atlas failed. Java not found / unsupported version?"); + } +} + +// Converts .atlas LibGDX files to JSON +export async function atlasToJson() { + atlas2Json("../res_built/atlas"); +} + +// Copies the atlas to the final destination +export function atlas() { + return gulp.src(["../res_built/atlas/*.png"]).pipe(gulp.dest(resourcesDestFolder)); +} + +// Copies the atlas to the final destination after optimizing it (lossy compression) +export function atlasOptimized() { + return gulp + .src(["../res_built/atlas/*.png"]) + .pipe( + gulpIf( + fname => fileMustBeLossless(fname.history[0]), + gulpImagemin(minifyImagesOptsLossless()), + gulpImagemin(minifyImagesOpts()) + ) + ) + .pipe(gulp.dest(resourcesDestFolder)); +} + +//////////////////// RESOURCES ////////////////////// + +// Copies all resources which are no ui resources +export function copyNonImageResources() { + return gulp.src(nonImageResourcesGlobs).pipe(gulp.dest(resourcesDestFolder)); +} + +// Copies all ui resources +export function copyImageResources() { + return gulp + .src(imageResourcesGlobs) + .pipe(gulpCached("imgres.copyImageResources")) + .pipe(gulp.dest(path.join(resourcesDestFolder))); +} + +// Copies all ui resources and optimizes them +export function copyImageResourcesOptimized() { + return gulp + .src(imageResourcesGlobs) + .pipe( + gulpIf( + fname => fileMustBeLossless(fname.history[0]), + gulpImagemin(minifyImagesOptsLossless()), + gulpImagemin(minifyImagesOpts()) + ) + ) + .pipe(gulp.dest(path.join(resourcesDestFolder))); +} + +// Copies all resources and optimizes them +export const allOptimized = gulp.parallel( + gulp.series(buildAtlas, atlasToJson, atlasOptimized), + copyNonImageResources, + copyImageResourcesOptimized +); + +// Cleans up unused images which are instead inline into the css +export function cleanupUnusedCssInlineImages() { + return gulp + .src( + [ + path.join(buildFolder, "res", "ui", "**", "*.png"), + path.join(buildFolder, "res", "ui", "**", "*.jpg"), + path.join(buildFolder, "res", "ui", "**", "*.svg"), + path.join(buildFolder, "res", "ui", "**", "*.gif"), + ], + { read: false } + ) + .pipe(gulpIf(fname => fname.history[0].indexOf("noinline") < 0, gulpClean({ force: true }))); } diff --git a/gulp/js.js b/gulp/js.js index 3ce86305..197bda39 100644 --- a/gulp/js.js +++ b/gulp/js.js @@ -1,4 +1,7 @@ +import gulp from "gulp"; +import webpack from "webpack"; import { BUILD_VARIANTS } from "./build_variants.js"; +import { buildFolder } from "./config.js"; import webpackConfig from "./webpack.config.js"; import webpackProductionConfig from "./webpack.production.config.js"; @@ -15,64 +18,60 @@ import gulpRename from "gulp-rename"; * */ -export default function gulptasksJS(gulp, buildFolder, browserSync) { - //// DEV +//// DEV - for (const variant in BUILD_VARIANTS) { - const data = BUILD_VARIANTS[variant]; +export default Object.fromEntries( + Object.entries(BUILD_VARIANTS).map(([variant, data]) => { + function build() { + return gulp + .src("../src/js/main.js") + .pipe(webpackStream(webpackConfig, webpack)) + .pipe(gulp.dest(buildFolder)); + } - gulp.task("js." + variant + ".dev.watch", () => { - gulp.src("../src/js/main.js") - .pipe(webpackStream(webpackConfig)) - .pipe(gulp.dest(buildFolder)) - .pipe(browserSync.stream()); - }); + const dev = { + build, + }; + let prod; if (!data.standalone) { // WEB - gulp.task("js." + variant + ".dev", () => { + function transpiled() { return gulp .src("../src/js/main.js") - .pipe(webpackStream(webpackConfig)) - .pipe(gulp.dest(buildFolder)); - }); - - gulp.task("js." + variant + ".prod.transpiled", () => { - return gulp - .src("../src/js/main.js") - .pipe(webpackStream(webpackProductionConfig)) + .pipe(webpackStream(webpackProductionConfig, webpack)) .pipe(gulpRename("bundle-transpiled.js")) .pipe(gulp.dest(buildFolder)); - }); + } - gulp.task("js." + variant + ".prod.es6", () => { + function es6() { return gulp .src("../src/js/main.js") - .pipe(webpackStream(webpackProductionConfig)) + .pipe(webpackStream(webpackProductionConfig, webpack)) .pipe(gulp.dest(buildFolder)); - }); - gulp.task( - "js." + variant + ".prod", + } - // transpiled currently not used - // gulp.parallel("js." + variant + ".prod.transpiled", "js." + variant + ".prod.es6") - gulp.parallel("js." + variant + ".prod.es6") - ); + prod = { + transpiled, + es6, + build: + // transpiled currently not used + // gulp.parallel("js." + variant + ".prod.transpiled", "js." + variant + ".prod.es6") + es6, + }; } else { // STANDALONE - gulp.task("js." + variant + ".dev", () => { + function build() { return gulp .src("../src/js/main.js") - .pipe(webpackStream(webpackConfig)) + .pipe(webpackStream(webpackProductionConfig, webpack)) .pipe(gulp.dest(buildFolder)); - }); - gulp.task("js." + variant + ".prod", () => { - return gulp - .src("../src/js/main.js") - .pipe(webpackStream(webpackProductionConfig)) - .pipe(gulp.dest(buildFolder)); - }); + } + + prod = { build }; } - } -} + + return [variant, { dev, prod }]; + }) +); diff --git a/gulp/local-config.js b/gulp/local-config.js index 5660beda..365abce8 100644 --- a/gulp/local-config.js +++ b/gulp/local-config.js @@ -1,14 +1,10 @@ -import fs from "fs"; +import fs from "fs/promises"; const configTemplatePath = "../src/js/core/config.local.template.js"; const configPath = "../src/js/core/config.local.js"; -export default function gulptasksLocalConfig(gulp) { - gulp.task("localConfig.findOrCreate", cb => { - if (!fs.existsSync(configPath)) { - fs.copyFileSync(configTemplatePath, configPath); - } - - cb(); - }); +export async function findOrCreate() { + try { + await fs.copyFile(configTemplatePath, configPath, fs.constants.COPYFILE_EXCL); + } catch {} } diff --git a/gulp/sounds.js b/gulp/sounds.js index aba13937..184a424a 100644 --- a/gulp/sounds.js +++ b/gulp/sounds.js @@ -1,135 +1,132 @@ import path from "path/posix"; -import audiosprite from "gulp-audiosprite"; +import gulp from "gulp"; +import { buildFolder } from "./config.js"; +import gulpAudiosprite from "gulp-audiosprite"; import gulpClean from "gulp-clean"; import gulpCache from "gulp-cache"; import gulpPlumber from "gulp-plumber"; import gulpFluentFfmpeg from "gulp-fluent-ffmpeg"; -export default function gulptasksSounds(gulp, buildFolder) { - // Gather some basic infos - const soundsDir = path.join("..", "res_raw", "sounds"); - const builtSoundsDir = path.join("..", "res_built", "sounds"); +// Gather some basic infos +const soundsDir = path.join("..", "res_raw", "sounds"); +const builtSoundsDir = path.join("..", "res_built", "sounds"); - gulp.task("sounds.clear", () => { - return gulp.src(builtSoundsDir, { read: false, allowEmpty: true }).pipe(gulpClean({ force: true })); - }); +export function clear() { + return gulp.src(builtSoundsDir, { read: false, allowEmpty: true }).pipe(gulpClean({ force: true })); +} - const filters = ["volume=0.2"]; +const filters = ["volume=0.2"]; - const fileCache = new gulpCache.Cache({ - cacheDirName: "shapezio-precompiled-sounds", - }); +const fileCache = new gulpCache.Cache({ + cacheDirName: "shapezio-precompiled-sounds", +}); - function getFileCacheValue(file) { - const { _isVinyl, base, cwd, contents, history, stat, path } = file; - const encodedContents = Buffer.from(contents).toString("base64"); - return { _isVinyl, base, cwd, contents: encodedContents, history, stat, path }; - } +function getFileCacheValue(file) { + const { _isVinyl, base, cwd, contents, history, stat, path } = file; + const encodedContents = Buffer.from(contents).toString("base64"); + return { _isVinyl, base, cwd, contents: encodedContents, history, stat, path }; +} - // Encodes the game music - gulp.task("sounds.music", () => { - return gulp - .src([path.join(soundsDir, "music", "**", "*.wav"), path.join(soundsDir, "music", "**", "*.mp3")]) - .pipe(gulpPlumber()) - .pipe( - gulpCache( - gulpFluentFfmpeg("mp3", function (cmd) { - return cmd - .audioBitrate(48) - .audioChannels(1) - .audioFrequency(22050) - .audioCodec("libmp3lame") - .audioFilters(["volume=0.15"]); - }), - { - name: "music", - fileCache, - value: getFileCacheValue, - } - ) - ) - .pipe(gulp.dest(path.join(builtSoundsDir, "music"))); - }); - - // Encodes the game music in high quality for the standalone - gulp.task("sounds.musicHQ", () => { - return gulp - .src([path.join(soundsDir, "music", "**", "*.wav"), path.join(soundsDir, "music", "**", "*.mp3")]) - .pipe(gulpPlumber()) - .pipe( - gulpCache( - gulpFluentFfmpeg("mp3", function (cmd) { - return cmd - .audioBitrate(256) - .audioChannels(2) - .audioFrequency(44100) - .audioCodec("libmp3lame") - .audioFilters(["volume=0.15"]); - }), - { - name: "music-high-quality", - fileCache, - value: getFileCacheValue, - } - ) - ) - .pipe(gulp.dest(path.join(builtSoundsDir, "music"))); - }); - - // Encodes the ui sounds - gulp.task("sounds.sfxGenerateSprites", () => { - return gulp - .src([path.join(soundsDir, "sfx", "**", "*.wav"), path.join(soundsDir, "sfx", "**", "*.mp3")]) - .pipe(gulpPlumber()) - .pipe( - audiosprite({ - format: "howler", - output: "sfx", - gap: 0.1, - export: "mp3", - }) - ) - .pipe(gulp.dest(path.join(builtSoundsDir))); - }); - gulp.task("sounds.sfxOptimize", () => { - return gulp - .src([path.join(builtSoundsDir, "sfx.mp3")]) - .pipe(gulpPlumber()) - .pipe( +// Encodes the game music +export function music() { + return gulp + .src([path.join(soundsDir, "music", "**", "*.wav"), path.join(soundsDir, "music", "**", "*.mp3")]) + .pipe(gulpPlumber()) + .pipe( + gulpCache( gulpFluentFfmpeg("mp3", function (cmd) { return cmd - .audioBitrate(128) + .audioBitrate(48) .audioChannels(1) .audioFrequency(22050) .audioCodec("libmp3lame") - .audioFilters(filters); - }) + .audioFilters(["volume=0.15"]); + }), + { + name: "music", + fileCache, + value: getFileCacheValue, + } ) - .pipe(gulp.dest(path.join(builtSoundsDir))); - }); - gulp.task("sounds.sfxCopyAtlas", () => { - return gulp - .src([path.join(builtSoundsDir, "sfx.json")]) - .pipe(gulp.dest(path.join("..", "src", "js", "built-temp"))); - }); - - gulp.task( - "sounds.sfx", - gulp.series("sounds.sfxGenerateSprites", "sounds.sfxOptimize", "sounds.sfxCopyAtlas") - ); - - gulp.task("sounds.copy", () => { - return gulp - .src(path.join(builtSoundsDir, "**", "*.mp3")) - .pipe(gulpPlumber()) - .pipe(gulp.dest(path.join(buildFolder, "res", "sounds"))); - }); - - gulp.task("sounds.buildall", gulp.parallel("sounds.music", "sounds.sfx")); - gulp.task("sounds.buildallHQ", gulp.parallel("sounds.musicHQ", "sounds.sfx")); - - gulp.task("sounds.fullbuild", gulp.series("sounds.clear", "sounds.buildall", "sounds.copy")); - gulp.task("sounds.fullbuildHQ", gulp.series("sounds.clear", "sounds.buildallHQ", "sounds.copy")); - gulp.task("sounds.dev", gulp.series("sounds.buildall", "sounds.copy")); + ) + .pipe(gulp.dest(path.join(builtSoundsDir, "music"))); } + +// Encodes the game music in high quality for the standalone +export function musicHQ() { + return gulp + .src([path.join(soundsDir, "music", "**", "*.wav"), path.join(soundsDir, "music", "**", "*.mp3")]) + .pipe(gulpPlumber()) + .pipe( + gulpCache( + gulpFluentFfmpeg("mp3", function (cmd) { + return cmd + .audioBitrate(256) + .audioChannels(2) + .audioFrequency(44100) + .audioCodec("libmp3lame") + .audioFilters(["volume=0.15"]); + }), + { + name: "music-high-quality", + fileCache, + value: getFileCacheValue, + } + ) + ) + .pipe(gulp.dest(path.join(builtSoundsDir, "music"))); +} + +// Encodes the ui sounds +export function sfxGenerateSprites() { + return gulp + .src([path.join(soundsDir, "sfx", "**", "*.wav"), path.join(soundsDir, "sfx", "**", "*.mp3")]) + .pipe(gulpPlumber()) + .pipe( + gulpAudiosprite({ + format: "howler", + output: "sfx", + gap: 0.1, + export: "mp3", + }) + ) + .pipe(gulp.dest(path.join(builtSoundsDir))); +} +export function sfxOptimize() { + return gulp + .src([path.join(builtSoundsDir, "sfx.mp3")]) + .pipe(gulpPlumber()) + .pipe( + gulpFluentFfmpeg("mp3", function (cmd) { + return cmd + .audioBitrate(128) + .audioChannels(1) + .audioFrequency(22050) + .audioCodec("libmp3lame") + .audioFilters(filters); + }) + ) + .pipe(gulp.dest(path.join(builtSoundsDir))); +} +export function sfxCopyAtlas() { + return gulp + .src([path.join(builtSoundsDir, "sfx.json")]) + .pipe(gulp.dest(path.join("..", "src", "js", "built-temp"))); +} + +export const sfx = gulp.series(sfxGenerateSprites, sfxOptimize, sfxCopyAtlas); + +export function copy() { + return gulp + .src(path.join(builtSoundsDir, "**", "*.mp3")) + .pipe(gulpPlumber()) + .pipe(gulp.dest(path.join(buildFolder, "res", "sounds"))); +} + +export const buildall = gulp.parallel(music, sfx); +export const buildallHQ = gulp.parallel(musicHQ, sfx); + +export const fullbuild = gulp.series(clear, buildall, copy); +export const fullbuildHQ = gulp.series(clear, buildallHQ, copy); +export const dev = gulp.series(buildall, copy); diff --git a/gulp/standalone.js b/gulp/standalone.js index ac8b5908..cdcc883d 100644 --- a/gulp/standalone.js +++ b/gulp/standalone.js @@ -2,8 +2,11 @@ import packager from "electron-packager"; import pj from "../electron/package.json" assert { type: "json" }; import path from "path/posix"; import { getVersion } from "./buildutils.js"; -import fs from "fs"; -import { execSync } from "child_process"; +import fs from "fs/promises"; +import childProcess from "child_process"; +import { promisify } from "util"; +const exec = promisify(childProcess.exec); +import gulp from "gulp"; import { BUILD_VARIANTS } from "./build_variants.js"; import gulpClean from "gulp-clean"; @@ -11,121 +14,132 @@ import gulpClean from "gulp-clean"; const platforms = /** @type {const} */ (["win32", "linux", "darwin"]); const architectures = /** @type {const} */ (["x64", "arm64"]); -export default function gulptasksStandalone(gulp) { - for (const variant in BUILD_VARIANTS) { - const variantData = BUILD_VARIANTS[variant]; - if (!variantData.standalone) { - continue; - } - const tempDestDir = path.join("..", "build_output", variant); - const taskPrefix = "standalone." + variant; - const electronBaseDir = path.join("..", "electron"); - const tempDestBuildDir = path.join(tempDestDir, "built"); +export default Object.fromEntries( + Object.entries(BUILD_VARIANTS) + .filter(([variant, variantData]) => variantData.standalone) + .map(([variant, variantData]) => { + const tempDestDir = path.join("..", "build_output", variant); + const electronBaseDir = path.join("..", "electron"); + const tempDestBuildDir = path.join(tempDestDir, "built"); - gulp.task(taskPrefix + ".prepare.cleanup", () => { - return gulp.src(tempDestDir, { read: false, allowEmpty: true }).pipe(gulpClean({ force: true })); - }); + function cleanup() { + return gulp + .src(tempDestDir, { read: false, allowEmpty: true }) + .pipe(gulpClean({ force: true })); + } - gulp.task(taskPrefix + ".prepare.copyPrefab", () => { - const requiredFiles = [ - path.join(electronBaseDir, "node_modules", "**", "*.*"), - path.join(electronBaseDir, "node_modules", "**", ".*"), - path.join(electronBaseDir, "favicon*"), - ]; - return gulp.src(requiredFiles, { base: electronBaseDir }).pipe(gulp.dest(tempDestBuildDir)); - }); + function copyPrefab() { + const requiredFiles = [ + path.join(electronBaseDir, "node_modules", "**", "*.*"), + path.join(electronBaseDir, "node_modules", "**", ".*"), + path.join(electronBaseDir, "favicon*"), + ]; + return gulp.src(requiredFiles, { base: electronBaseDir }).pipe(gulp.dest(tempDestBuildDir)); + } - gulp.task(taskPrefix + ".prepare.writePackageJson", cb => { - const packageJsonString = JSON.stringify( - { - scripts: { - start: pj.scripts.start, + async function writePackageJson() { + const packageJsonString = JSON.stringify( + { + scripts: { + start: pj.scripts.start, + }, + devDependencies: pj.devDependencies, + dependencies: pj.dependencies, + optionalDependencies: pj.optionalDependencies, }, - devDependencies: pj.devDependencies, - dependencies: pj.dependencies, - optionalDependencies: pj.optionalDependencies, - }, - null, - 4 - ); + null, + 4 + ); - fs.writeFileSync(path.join(tempDestBuildDir, "package.json"), packageJsonString); - - cb(); - }); - - gulp.task(taskPrefix + ".prepare.minifyCode", () => { - return gulp.src(path.join(electronBaseDir, "*.js")).pipe(gulp.dest(tempDestBuildDir)); - }); - - gulp.task(taskPrefix + ".prepare.copyGamefiles", () => { - return gulp.src("../build/**/*.*", { base: "../build" }).pipe(gulp.dest(tempDestBuildDir)); - }); - - gulp.task(taskPrefix + ".killRunningInstances", cb => { - try { - execSync("taskkill /F /IM shapezio.exe"); - } catch (ex) { - console.warn("Failed to kill running instances, maybe none are up."); - } - cb(); - }); - - gulp.task( - taskPrefix + ".prepare", - gulp.series( - taskPrefix + ".killRunningInstances", - taskPrefix + ".prepare.cleanup", - taskPrefix + ".prepare.copyPrefab", - taskPrefix + ".prepare.writePackageJson", - taskPrefix + ".prepare.minifyCode", - taskPrefix + ".prepare.copyGamefiles" - ) - ); - - /** - * - * @param {typeof platforms[number] | (typeof platforms[number])[]} platform - * @param {typeof architectures[number] | (typeof architectures[number])[]} arch - * @param {function():void} cb - */ - async function packageStandalone(platform, arch, cb) { - const appPaths = await packager({ - dir: tempDestBuildDir, - appCopyright: "tobspr Games", - appVersion: getVersion(), - buildVersion: "1.0.0", - arch, - platform, - asar: true, - executableName: "shapezio", - icon: path.join(electronBaseDir, "favicon"), - name: "shapez", - out: tempDestDir, - overwrite: true, - appBundleId: "tobspr.shapezio." + variant, - appCategoryType: "public.app-category.games", - }); - - console.log("Packages created:", appPaths); - for (const appPath of appPaths) { - fs.writeFileSync(path.join(appPath, "LICENSE"), fs.readFileSync(path.join("..", "LICENSE"))); + await fs.writeFile(path.join(tempDestBuildDir, "package.json"), packageJsonString); } - cb(); - } + function minifyCode() { + return gulp.src(path.join(electronBaseDir, "*.js")).pipe(gulp.dest(tempDestBuildDir)); + } - for (const platform of platforms) { - for (const arch of architectures) { - gulp.task(taskPrefix + `.package.${platform}-${arch}`, cb => - packageStandalone(platform, arch, cb) + function copyGamefiles() { + return gulp.src("../build/**/*.*", { base: "../build" }).pipe(gulp.dest(tempDestBuildDir)); + } + + async function killRunningInstances() { + try { + await exec("taskkill /F /IM shapezio.exe"); + } catch (ex) { + console.warn("Failed to kill running instances, maybe none are up."); + } + } + + const prepare = { + cleanup, + copyPrefab, + writePackageJson, + minifyCode, + copyGamefiles, + all: gulp.series( + killRunningInstances, + cleanup, + copyPrefab, + writePackageJson, + minifyCode, + copyGamefiles + ), + }; + + /** + * + * @param {typeof platforms[number] | (typeof platforms[number])[]} platform + * @param {typeof architectures[number] | (typeof architectures[number])[]} arch + */ + async function packageStandalone(platform, arch) { + const appPaths = await packager({ + dir: tempDestBuildDir, + appCopyright: "tobspr Games", + appVersion: getVersion(), + buildVersion: "1.0.0", + arch, + platform, + asar: true, + executableName: "shapezio", + icon: path.join(electronBaseDir, "favicon"), + name: "shapez", + out: tempDestDir, + overwrite: true, + appBundleId: "tobspr.shapezio." + variant, + appCategoryType: "public.app-category.games", + }); + + console.log("Packages created:", appPaths); + await Promise.all( + appPaths.map(async appPath => { + await fs.writeFile( + path.join(appPath, "LICENSE"), + await fs.readFile(path.join("..", "LICENSE")) + ); + }) ); } - } - // TODO: Review this hack forced by readonly types - gulp.task(taskPrefix + ".package.all", cb => - packageStandalone([...platforms], [...architectures], cb) - ); - } -} + const pack = { + ...Object.fromEntries( + platforms.flatMap(platform => + architectures.map(arch => [ + `${platform}-${arch}`, + () => packageStandalone(platform, arch), + ]) + ) + ), + // TODO: Review this hack forced by readonly types + all: () => packageStandalone([...platforms], [...architectures]), + }; + + return [ + variant, + { + killRunningInstances, + prepare, + package: pack, + }, + ]; + }) +); diff --git a/gulp/tasks.js b/gulp/tasks.js new file mode 100644 index 00000000..c6266c16 --- /dev/null +++ b/gulp/tasks.js @@ -0,0 +1,289 @@ +import gulp from "gulp"; +import path from "path/posix"; +import pathNative from "path"; +import delEmpty from "delete-empty"; +import childProcess from "child_process"; +import { promisify } from "util"; +const exec = promisify(childProcess.exec); +import { BUILD_VARIANTS } from "./build_variants.js"; +import { + baseDir, + buildFolder, + buildOutputFolder, + browserSync, + rawImageResourcesGlobs, + nonImageResourcesGlobs, + imageResourcesGlobs, +} from "./config.js"; + +// Load other plugins +import gulpClean from "gulp-clean"; +import gulpWebserver from "gulp-webserver"; + +import * as imgres from "./image-resources.js"; +import * as css from "./css.js"; +import * as sounds from "./sounds.js"; +import * as localConfig from "./local-config.js"; +import js from "./js.js"; +import * as html from "./html.js"; +import * as ftp from "./ftp.js"; +import * as docs from "./docs.js"; +import standalone from "./standalone.js"; +import * as translations from "./translations.js"; + +export { imgres, css, sounds, localConfig, js, html, ftp, docs, standalone, translations }; + +///////////////////// BUILD TASKS ///////////////////// + +// Cleans up everything +function cleanBuildFolder() { + return gulp.src(buildFolder, { read: false, allowEmpty: true }).pipe(gulpClean({ force: true })); +} +function cleanBuildOutputFolder() { + return gulp.src(buildOutputFolder, { read: false, allowEmpty: true }).pipe(gulpClean({ force: true })); +} +function cleanBuildTempFolder() { + return gulp + .src(path.join("..", "src", "js", "built-temp"), { read: false, allowEmpty: true }) + .pipe(gulpClean({ force: true })); +} +function cleanImageBuildFolder() { + return gulp + .src(path.join("res_built"), { read: false, allowEmpty: true }) + .pipe(gulpClean({ force: true })); +} + +const cleanup = gulp.parallel(cleanBuildFolder, cleanImageBuildFolder, cleanBuildTempFolder); + +// Requires no uncomitted files +async function requireCleanWorkingTree() { + let output = (await exec("git status -su", { encoding: "buffer" })).stdout + .toString("ascii") + .trim() + .replace(/\r/gi, "") + .split("\n"); + + // Filter files which are OK to be untracked + output = output + .map(x => x.replace(/[\r\n]+/gi, "")) + .filter(x => x.indexOf(".local.js") < 0) + .filter(x => x.length > 0); + if (output.length > 0) { + console.error("\n\nYou have unstaged changes, please commit everything first!"); + console.error("Unstaged files:"); + console.error(output.map(x => "'" + x + "'").join("\n")); + process.exit(1); + } +} + +function copyAdditionalBuildFiles() { + const additionalFolder = path.join("additional_build_files"); + const additionalSrcGlobs = [ + path.join(additionalFolder, "**/*.*"), + path.join(additionalFolder, "**/.*"), + path.join(additionalFolder, "**/*"), + ]; + + return gulp.src(additionalSrcGlobs).pipe(gulp.dest(buildFolder)); +} + +export const utils = { + cleanBuildFolder, + cleanBuildOutputFolder, + cleanBuildTempFolder, + cleanImageBuildFolder, + cleanup, + requireCleanWorkingTree, + copyAdditionalBuildFiles, +}; + +// Starts a webserver on the built directory (useful for testing prod build) +function webserver() { + return gulp.src(buildFolder).pipe( + gulpWebserver({ + livereload: { + enable: true, + }, + directoryListing: false, + open: true, + port: 3005, + }) + ); +} + +/** + * + * @param {object} param0 + * @param {keyof typeof BUILD_VARIANTS} param0.version + */ +async function serveHTML({ version = "web-dev" }) { + browserSync.init({ + server: [buildFolder, path.join(baseDir, "mod_examples")], + port: 3005, + ghostMode: false, + logLevel: "info", + logPrefix: "BS", + online: false, + notify: false, + reloadDebounce: 100, + watchEvents: ["add", "change"], + open: false, + }); + + gulp.watch("../src/js/**", js[version].dev.build); + + // Watch .scss files, those trigger a css rebuild + gulp.watch("../src/css/**", css.dev); + + // Watch .html files, those trigger a html rebuild + gulp.watch(["../src/html/**", "./preloader/*"], html.dev); + + // Watch translations + gulp.watch("../translations/*.yaml", translations.convertToJson); + + gulp.watch( + ["../res_raw/sounds/sfx/**/*.mp3", "../res_raw/sounds/sfx/**/*.wav"], + gulp.series(sounds.sfx, sounds.copy) + ); + gulp.watch( + ["../res_raw/sounds/music/**/*.mp3", "../res_raw/sounds/music/**/*.wav"], + gulp.series(sounds.music, sounds.copy) + ); + + // Watch resource files and copy them on change + gulp.watch(rawImageResourcesGlobs, imgres.buildAtlas); + gulp.watch(nonImageResourcesGlobs, imgres.copyNonImageResources); + gulp.watch(imageResourcesGlobs, imgres.copyImageResources); + + // Watch .atlas files and recompile the atlas on change + gulp.watch("../res_built/atlas/*.atlas", imgres.atlasToJson); + gulp.watch("../res_built/atlas/*.json", imgres.atlas); + + // Watch the build folder and reload when anything changed + gulp.watch(path.join(buildFolder, "**")).on("change", p => + gulp.src(pathNative.resolve(p).replaceAll(pathNative.sep, path.sep)).pipe(browserSync.stream()) + ); +} + +// Pre and postbuild +const baseResources = imgres.allOptimized; +async function deleteEmpty() { + await delEmpty(buildFolder); +} + +const postbuild = gulp.series(imgres.cleanupUnusedCssInlineImages, deleteEmpty); + +export const step = { + baseResources, + deleteEmpty, + postbuild, +}; + +///////////////////// RUNNABLE TASKS ///////////////////// + +// Builds everything (dev) +const prepare = { + dev: variant => + gulp.series( + utils.cleanup, + localConfig.findOrCreate, + gulp.parallel( + utils.copyAdditionalBuildFiles, + gulp.series(imgres.buildAtlas, gulp.parallel(imgres.atlasToJson, imgres.atlas)), + gulp.series(imgres.copyImageResources, css.dev), + imgres.copyNonImageResources, + html.dev, + gulp.series(gulp.parallel(sounds.dev, translations.fullBuild), js[variant].dev.build) + ) + ), +}; + +/** + * @typedef {import("gulp").TaskFunction} TaskFunction + */ + +export const build = + /** + * @type {Record & { prepare: typeof prepare }} + */ + ({ + prepare, + }); +/** + * @type {Record>} + */ +const pack = {}; +export { pack as package }; +/** @type {Record} */ +export const serve = {}; + +// Builds everything for every variant +for (const variant in BUILD_VARIANTS) { + const data = BUILD_VARIANTS[variant]; + + // build + const code = gulp.series( + data.standalone ? sounds.fullbuildHQ : sounds.fullbuild, + translations.fullBuild, + js[variant].prod.build + ); + + const resourcesAndCode = gulp.parallel(step.baseResources, code); + + const all = gulp.series(resourcesAndCode, css.prod, html.prod); + + const full = gulp.series(utils.cleanup, all, step.postbuild); + + build[variant] = { code, resourcesAndCode, all, full }; + + // Tasks for creating packages. These packages are already distributable, but usually can be further + // wrapped in a different format (an installer for Windows, tarball for Linux, DMG for macOS). + if (data.standalone) { + const packageTasks = [ + "win32-x64", + "win32-arm64", + "linux-x64", + "linux-arm64", + "darwin-x64", + "darwin-arm64", + "all", + ]; + + pack[variant] = {}; + for (const task of packageTasks) { + pack[variant][task] = gulp.series( + localConfig.findOrCreate, + full, + utils.cleanBuildOutputFolder, + standalone[variant].prepare.all, + standalone[variant].package[task] + ); + } + } + + // serve + serve[variant] = gulp.series(build.prepare.dev(variant), () => serveHTML({ version: variant })); +} + +// Deploying! +export const deploy = { + staging: gulp.series( + utils.requireCleanWorkingTree, + build["web-shapezio-beta"].full, + ftp.upload.staging.all + ), + prod: gulp.series(utils.requireCleanWorkingTree, build["web-shapezio"].full, ftp.upload.prod.all), +}; + +export const main = { + prepareDocs: docs.prepareDocs, + webserver, +}; + +// Default task (dev, localhost) +export default gulp.series(serve["standalone-steam"]); diff --git a/gulp/translations.js b/gulp/translations.js index 257ca885..9f7239e9 100644 --- a/gulp/translations.js +++ b/gulp/translations.js @@ -1,21 +1,20 @@ import path from "path/posix"; -import fs from "fs"; -import gulpYaml from "gulp-yaml"; +import fs from "fs/promises"; import YAML from "yaml"; +import gulp from "gulp"; import gulpPlumber from "gulp-plumber"; +import gulpYaml from "gulp-yaml"; const translationsSourceDir = path.join("..", "translations"); const translationsJsonDir = path.join("..", "src", "js", "built-temp"); -export default function gulptasksTranslations(gulp) { - gulp.task("translations.convertToJson", () => { - return gulp - .src(path.join(translationsSourceDir, "*.yaml")) - .pipe(gulpPlumber()) - .pipe(gulpYaml({ space: 2, safe: true })) - .pipe(gulp.dest(translationsJsonDir)); - }); - - gulp.task("translations.fullBuild", gulp.series("translations.convertToJson")); +export function convertToJson() { + return gulp + .src(path.join(translationsSourceDir, "*.yaml")) + .pipe(gulpPlumber()) + .pipe(gulpYaml({ space: 2, safe: true })) + .pipe(gulp.dest(translationsJsonDir)); } + +export const fullBuild = convertToJson; diff --git a/gulp/webpack.config.js b/gulp/webpack.config.js index a911d2af..581dfd98 100644 --- a/gulp/webpack.config.js +++ b/gulp/webpack.config.js @@ -95,7 +95,6 @@ export default { extensions: [".ts", ".js", ".tsx", ".jsx"], }, devtool: "cheap-source-map", - watch: true, cache: false, plugins: [ new webpack.DefinePlugin(globalDefs),