1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-14 02:31:51 +00:00

Merge pull request #27 from tobspr-games/refactor/gulp

Refactor gulp setup; switch from webpack watch to gulp.watch exclusively
This commit is contained in:
Даниїл Григор'єв 2024-04-26 22:42:00 +03:00 committed by GitHub
commit 7aeb687d14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1059 additions and 1030 deletions

View File

@ -57,8 +57,7 @@ and does not intend to provide compatibility for older clients.
### Prerequisites ### Prerequisites
- [ffmpeg](https://www.ffmpeg.org/download.html) - [ffmpeg](https://www.ffmpeg.org/download.html)
- [Node.js 16](https://nodejs.org/en/about/previous-releases) - [Node.js](https://nodejs.org)
(not 17+, see <https://github.com/tobspr-games/shapez.io/issues/1473>)
- [Yarn 1](https://classic.yarnpkg.com/en/docs/install) (not 2, we haven't migrated yet) - [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 - [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 - [cURL](https://curl.se/download.html)[^1] to download the texture packer

44
gulp/config.js Normal file
View File

@ -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]);
}
}

View File

@ -1,5 +1,7 @@
import path from "path/posix"; import path from "path/posix";
import gulp from "gulp";
import { getRevision } from "./buildutils.js"; import { getRevision } from "./buildutils.js";
import { buildFolder } from "./config.js";
import gulpPostcss from "gulp-postcss"; import gulpPostcss from "gulp-postcss";
import postcssAssets from "postcss-assets"; import postcssAssets from "postcss-assets";
@ -13,112 +15,104 @@ import gulpDartSass from "gulp-dart-sass";
import gulpPlumber from "gulp-plumber"; import gulpPlumber from "gulp-plumber";
import gulpRename from "gulp-rename"; import gulpRename from "gulp-rename";
export default function gulptasksCSS(gulp, buildFolder, browserSync) { // The assets plugin copies the files
// The assets plugin copies the files const commitHash = getRevision();
const commitHash = getRevision(); const postcssAssetsPlugin = postcssAssets({
const postcssAssetsPlugin = postcssAssets({ loadPaths: [path.join(buildFolder, "res", "ui")],
loadPaths: [path.join(buildFolder, "res", "ui")], basePath: buildFolder,
basePath: buildFolder, baseUrl: ".",
baseUrl: ".", });
});
// Postcss configuration // Postcss configuration
const postcssPlugins = prod => { const postcssPlugins = prod => {
const plugins = [postcssAssetsPlugin]; const plugins = [postcssAssetsPlugin];
if (prod) { if (prod) {
plugins.unshift( plugins.unshift(
postcssPresetEnv({ postcssPresetEnv({
browsers: ["> 0.1%"], browsers: ["> 0.1%"],
}) })
); );
plugins.push( plugins.push(
cssMqpacker({ cssMqpacker({
sort: true, sort: true,
}), }),
cssnano({ cssnano({
preset: [ preset: [
"advanced", "advanced",
{ {
cssDeclarationSorter: false, cssDeclarationSorter: false,
discardUnused: true, discardUnused: true,
mergeIdents: false, mergeIdents: false,
reduceIdents: true, reduceIdents: true,
zindex: true, zindex: true,
}, },
], ],
}), }),
postcssRoundSubpixels() 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());
} }
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 // Builds the css resources
gulp.task("css.resources.dev", () => { dev: () => resourcesTask({ isProd: false }),
return resourcesTask({ isProd: false });
});
// Builds the css resources in prod (=minified) // Builds the css resources in prod (=minified)
gulp.task("css.resources.prod", () => { prod: () => resourcesTask({ isProd: true }),
return resourcesTask({ isProd: true }); };
});
function mainTask({ isProd }) { function mainTask({ isProd }) {
return gulp return gulp
.src("../src/css/main.scss") .src("../src/css/main.scss")
.pipe(gulpPlumber()) .pipe(gulpPlumber())
.pipe(gulpDartSass.sync().on("error", gulpDartSass.logError)) .pipe(gulpDartSass.sync().on("error", gulpDartSass.logError))
.pipe( .pipe(
gulpPostcss([ gulpPostcss([
postcssCriticalSplit({ postcssCriticalSplit({
blockTag: "@load-async", blockTag: "@load-async",
output: "rest", output: "rest",
}), }),
]) ])
) )
.pipe(gulpPostcss(postcssPlugins(isProd))) .pipe(gulpPostcss(postcssPlugins(isProd)))
.pipe(gulp.dest(buildFolder)) .pipe(gulp.dest(buildFolder));
.pipe(browserSync.stream()); }
}
export const main = {
// Builds the css main // Builds the css main
gulp.task("css.main.dev", () => { dev: () => mainTask({ isProd: false }),
return mainTask({ isProd: false });
});
// Builds the css main in prod (=minified) // Builds the css main in prod (=minified)
gulp.task("css.main.prod", () => { prod: () => mainTask({ isProd: true }),
return mainTask({ isProd: true }); };
});
gulp.task("css.dev", gulp.parallel("css.main.dev", "css.resources.dev")); export const dev = gulp.parallel(main.dev, resources.dev);
gulp.task("css.prod", gulp.parallel("css.main.prod", "css.resources.prod")); export const prod = gulp.parallel(main.prod, resources.prod);
}

View File

@ -1,38 +1,36 @@
import path from "path/posix"; import path from "path/posix";
import fs from "fs"; import fs from "fs/promises";
import gulp from "gulp";
import gulpRename from "gulp-rename"; import gulpRename from "gulp-rename";
import stripJsonComments from "strip-json-comments"; import stripJsonComments from "strip-json-comments";
export default function gulptasksDocs(gulp, buildFolder) { export function convertJsToTs() {
gulp.task("docs.convertJsToTs", () => { return gulp
return gulp .src(path.join("..", "src", "js", "**", "*.js"))
.src(path.join("..", "src", "js", "**", "*.js")) .pipe(
.pipe( gulpRename(path => {
gulpRename(path => { path.extname = ".ts";
path.extname = ".ts"; })
}) )
) .pipe(gulp.dest(path.join("..", "tsc_temp")));
.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 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);

View File

@ -1,68 +1,69 @@
import path from "path/posix"; 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 { getRevision, getVersion } from "./buildutils.js";
import gulpRename from "gulp-rename"; import gulpRename from "gulp-rename";
import gulpSftp from "gulp-sftp"; 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 = [ const additionalGlobs = [
path.join(additionalFolder, "*"), path.join(additionalFolder, "*"),
path.join(additionalFolder, "*.*"), path.join(additionalFolder, "*.*"),
path.join(additionalFolder, ".*"), path.join(additionalFolder, ".*"),
]; ];
const credentials = { const credentials = {
alpha: { alpha: {
host: process.env.SHAPEZ_CLI_SERVER_HOST, host: process.env.SHAPEZ_CLI_SERVER_HOST,
user: process.env.SHAPEZ_CLI_ALPHA_FTP_USER, user: process.env.SHAPEZ_CLI_ALPHA_FTP_USER,
pass: process.env.SHAPEZ_CLI_ALPHA_FTP_PW, pass: process.env.SHAPEZ_CLI_ALPHA_FTP_PW,
}, },
staging: { staging: {
host: process.env.SHAPEZ_CLI_SERVER_HOST, host: process.env.SHAPEZ_CLI_SERVER_HOST,
user: process.env.SHAPEZ_CLI_STAGING_FTP_USER, user: process.env.SHAPEZ_CLI_STAGING_FTP_USER,
pass: process.env.SHAPEZ_CLI_STAGING_FTP_PW, pass: process.env.SHAPEZ_CLI_STAGING_FTP_PW,
}, },
prod: { prod: {
host: process.env.SHAPEZ_CLI_SERVER_HOST, host: process.env.SHAPEZ_CLI_SERVER_HOST,
user: process.env.SHAPEZ_CLI_LIVE_FTP_USER, user: process.env.SHAPEZ_CLI_LIVE_FTP_USER,
pass: process.env.SHAPEZ_CLI_LIVE_FTP_PW, pass: process.env.SHAPEZ_CLI_LIVE_FTP_PW,
}, },
}; };
// Write the "commit.txt" file // Write the "commit.txt" file
gulp.task("ftp.writeVersion", cb => { export async function writeVersion() {
fs.writeFileSync( await fs.writeFile(
path.join(buildFolder, "version.json"), path.join(buildFolder, "version.json"),
JSON.stringify( JSON.stringify(
{ {
commit: getRevision(), commit: getRevision(),
appVersion: getVersion(), appVersion: getVersion(),
buildTime: new Date().getTime(), buildTime: new Date().getTime(),
}, },
null, null,
4 4
) )
); );
cb(); }
});
const gameSrcGlobs = [ const gameSrcGlobs = [
path.join(buildFolder, "**/*.*"), path.join(buildFolder, "**/*.*"),
path.join(buildFolder, "**/.*"), path.join(buildFolder, "**/.*"),
path.join(buildFolder, "**/*"), path.join(buildFolder, "**/*"),
path.join(buildFolder, "!**/index.html"), 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]; const deployCredentials = credentials[deployEnv];
gulp.task(`ftp.upload.${deployEnv}.game`, () => { function game() {
return gulp return gulp
.src(gameSrcGlobs, { base: buildFolder }) .src(gameSrcGlobs, { base: buildFolder })
.pipe( .pipe(
@ -71,30 +72,28 @@ export default function gulptasksFTP(gulp, buildFolder) {
}) })
) )
.pipe(gulpSftp(deployCredentials)); .pipe(gulpSftp(deployCredentials));
}); }
gulp.task(`ftp.upload.${deployEnv}.indexHtml`, () => { function indexHtml() {
return gulp return gulp
.src([path.join(buildFolder, "index.html"), path.join(buildFolder, "version.json")], { .src([path.join(buildFolder, "index.html"), path.join(buildFolder, "version.json")], {
base: buildFolder, base: buildFolder,
}) })
.pipe(gulpSftp(deployCredentials)); .pipe(gulpSftp(deployCredentials));
}); }
gulp.task(`ftp.upload.${deployEnv}.additionalFiles`, () => { function additionalFiles() {
return gulp return gulp.src(additionalGlobs, { base: additionalFolder }).pipe(gulpSftp(deployCredentials));
.src(additionalFiles, { base: additionalFolder }) // }
.pipe(gulpSftp(deployCredentials));
});
gulp.task( return [
`ftp.upload.${deployEnv}`, deployEnv,
gulp.series( {
"ftp.writeVersion", game,
`ftp.upload.${deployEnv}.game`, indexHtml,
`ftp.upload.${deployEnv}.indexHtml`, additionalFiles,
`ftp.upload.${deployEnv}.additionalFiles` all: gulp.series(writeVersion, game, indexHtml, additionalFiles),
) },
); ];
} })
} );

View File

@ -1,303 +1,23 @@
import gulp from "gulp"; import gulp from "gulp";
import BrowserSync from "browser-sync"; import * as tasks from "./tasks.js";
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,
})
);
});
/** /**
* * @typedef {import("gulp").TaskFunction} TaskFunction
* @param {object} param0 * @typedef {TaskFunction | { [k: string]: Tasks }} Tasks
* @param {keyof typeof BUILD_VARIANTS} param0.version
*/ */
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")); * @param {Tasks} tasks
* @param {string=} prefix
// Watch .html files, those trigger a html rebuild */
gulp.watch("../src/**/*.html", gulp.series("html.dev")); function register(tasks, prefix) {
gulp.watch("./preloader/*.*", gulp.series("html.dev")); if (tasks instanceof Function) {
gulp.task(prefix, tasks);
// Watch translations return;
gulp.watch("../translations/**/*.yaml", gulp.series("translations.convertToJson")); }
for (const [k, v] of Object.entries(tasks)) {
gulp.watch( register(v, prefix == null ? k : `${prefix}.${k}`);
["../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}`
)
);
}
} }
// serve
gulp.task(
"serve." + variant,
gulp.series("build.prepare.dev", "html.dev", () => serveHTML({ version: variant }))
);
} }
// Deploying! register(tasks);
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"));

View File

@ -2,6 +2,8 @@ import { getRevision } from "./buildutils.js";
import fs from "fs"; import fs from "fs";
import path from "path/posix"; import path from "path/posix";
import crypto from "crypto"; import crypto from "crypto";
import gulp from "gulp";
import { buildFolder } from "./config.js";
import gulpDom from "gulp-dom"; import gulpDom from "gulp-dom";
import gulpHtmlmin from "gulp-htmlmin"; import gulpHtmlmin from "gulp-htmlmin";
@ -20,32 +22,31 @@ function computeIntegrityHash(fullPath, algorithm = "sha256") {
* html.dev * html.dev
* html.prod * html.prod
*/ */
export default function gulptasksHTML(gulp, buildFolder) { const commitHash = getRevision();
const commitHash = getRevision(); async function buildHtml({ integrity = true }) {
async function buildHtml({ integrity = true }) { return gulp
return gulp .src("../src/html/index.html")
.src("../src/html/index.html") .pipe(
.pipe( gulpDom(
gulpDom( /** @this {Document} **/ function () {
/** @this {Document} **/ function () { const document = this;
const document = this;
// Append css // Append css
const css = document.createElement("link"); const css = document.createElement("link");
css.rel = "stylesheet"; css.rel = "stylesheet";
css.type = "text/css"; css.type = "text/css";
css.media = "none"; css.media = "none";
css.setAttribute("onload", "this.media='all'"); css.setAttribute("onload", "this.media='all'");
css.href = "main.css"; css.href = "main.css";
if (integrity) { if (integrity) {
css.setAttribute( css.setAttribute(
"integrity", "integrity",
computeIntegrityHash(path.join(buildFolder, "main.css")) computeIntegrityHash(path.join(buildFolder, "main.css"))
); );
} }
document.head.appendChild(css); document.head.appendChild(css);
let fontCss = ` let fontCss = `
@font-face { @font-face {
font-family: "GameFont"; font-family: "GameFont";
font-style: normal; font-style: normal;
@ -54,59 +55,54 @@ export default function gulptasksHTML(gulp, buildFolder) {
src: url('res/fonts/GameFont.woff2') format("woff2"); src: url('res/fonts/GameFont.woff2') format("woff2");
} }
`; `;
let loadingCss = let loadingCss =
fontCss + fs.readFileSync(path.join("preloader", "preloader.css")).toString(); fontCss + fs.readFileSync(path.join("preloader", "preloader.css")).toString();
const style = document.createElement("style"); const style = document.createElement("style");
style.setAttribute("type", "text/css"); style.setAttribute("type", "text/css");
style.textContent = loadingCss; style.textContent = loadingCss;
document.head.appendChild(style); document.head.appendChild(style);
let bodyContent = fs let bodyContent = fs.readFileSync(path.join("preloader", "preloader.html")).toString();
.readFileSync(path.join("preloader", "preloader.html"))
.toString();
const bundleScript = document.createElement("script"); const bundleScript = document.createElement("script");
bundleScript.type = "text/javascript"; bundleScript.type = "text/javascript";
bundleScript.src = "bundle.js"; bundleScript.src = "bundle.js";
if (integrity) { if (integrity) {
bundleScript.setAttribute( bundleScript.setAttribute(
"integrity", "integrity",
computeIntegrityHash(path.join(buildFolder, "bundle.js")) computeIntegrityHash(path.join(buildFolder, "bundle.js"))
); );
}
document.head.appendChild(bundleScript);
document.body.innerHTML = bodyContent;
} }
) document.head.appendChild(bundleScript);
)
.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));
}
gulp.task("html.dev", () => { document.body.innerHTML = bodyContent;
return buildHtml({ }
integrity: false, )
}); )
}); .pipe(
gulp.task("html.prod", () => { gulpHtmlmin({
return buildHtml({ caseSensitive: true,
integrity: 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,
});

View File

@ -1,12 +1,19 @@
import fs from "fs"; import fs from "fs/promises";
import path from "path/posix"; 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"; import childProcess from "child_process";
const execute = command => import { promisify } from "util";
execSync(command, { const exec = promisify(childProcess.exec);
const execute = command => {
const promise = exec(command, {
encoding: "utf-8", encoding: "utf-8",
}); });
promise.child.stderr.pipe(process.stderr);
return promise;
};
import gulpImagemin from "gulp-imagemin"; import gulpImagemin from "gulp-imagemin";
import imageminJpegtran from "imagemin-jpegtran"; import imageminJpegtran from "imagemin-jpegtran";
@ -15,182 +22,161 @@ import imageminPngquant from "imagemin-pngquant";
import gulpIf from "gulp-if"; import gulpIf from "gulp-if";
import gulpCached from "gulp-cached"; import gulpCached from "gulp-cached";
import gulpClean from "gulp-clean"; import gulpClean from "gulp-clean";
import { nonImageResourcesGlobs, imageResourcesGlobs } from "./config.js";
// 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",
];
// Link to download LibGDX runnable-texturepacker.jar // Link to download LibGDX runnable-texturepacker.jar
const runnableTPSource = const runnableTPSource =
"https://libgdx-nightlies.s3.eu-central-1.amazonaws.com/libgdx-runnables/runnable-texturepacker.jar"; "https://libgdx-nightlies.s3.eu-central-1.amazonaws.com/libgdx-runnables/runnable-texturepacker.jar";
export default function gulptasksImageResources(gulp, buildFolder) { // Lossless options
// Lossless options const minifyImagesOptsLossless = () => [
const minifyImagesOptsLossless = () => [ imageminJpegtran({
imageminJpegtran({ progressive: true,
progressive: true, }),
}), gulpImagemin.svgo({}),
gulpImagemin.svgo({}), gulpImagemin.optipng({
gulpImagemin.optipng({ optimizationLevel: 3,
optimizationLevel: 3, }),
}), imageminGifsicle({
imageminGifsicle({ optimizationLevel: 3,
optimizationLevel: 3, colors: 128,
colors: 128, }),
}), ];
];
// Lossy options // Lossy options
const minifyImagesOpts = () => [ const minifyImagesOpts = () => [
gulpImagemin.mozjpeg({ gulpImagemin.mozjpeg({
quality: 80, quality: 80,
maxMemory: 1024 * 1024 * 8, maxMemory: 1024 * 1024 * 8,
}), }),
gulpImagemin.svgo({}), gulpImagemin.svgo({}),
imageminPngquant({ imageminPngquant({
speed: 1, speed: 1,
strip: true, strip: true,
quality: [0.65, 0.9], quality: [0.65, 0.9],
dithering: false, dithering: false,
verbose: false, verbose: false,
}), }),
gulpImagemin.optipng({ gulpImagemin.optipng({
optimizationLevel: 3, optimizationLevel: 3,
}), }),
imageminGifsicle({ imageminGifsicle({
optimizationLevel: 3, optimizationLevel: 3,
colors: 128, colors: 128,
}), }),
]; ];
// Where the resources folder are // Where the resources folder are
const resourcesDestFolder = path.join(buildFolder, "res"); const resourcesDestFolder = path.join(buildFolder, "res");
/** /**
* Determines if an atlas must use lossless compression * Determines if an atlas must use lossless compression
* @param {string} fname * @param {string} fname
*/ */
function fileMustBeLossless(fname) { function fileMustBeLossless(fname) {
return fname.indexOf("lossless") >= 0; return fname.indexOf("lossless") >= 0;
} }
/////////////// ATLAS ///////////////////// /////////////// ATLAS /////////////////////
gulp.task("imgres.buildAtlas", cb => { export async function buildAtlas() {
const config = JSON.stringify("../res_raw/atlas.json"); const config = JSON.stringify("../res_raw/atlas.json");
const source = JSON.stringify("../res_raw"); const source = JSON.stringify("../res_raw");
const dest = JSON.stringify("../res_built/atlas"); const dest = JSON.stringify("../res_built/atlas");
try { try {
// First check whether Java is installed // First check whether Java is installed
execute("java -version"); await execute("java -version");
// Now check and try downloading runnable-texturepacker.jar (22MB) // Now check and try downloading runnable-texturepacker.jar (22MB)
if (!fs.existsSync("./runnable-texturepacker.jar")) { try {
const escapedLink = JSON.stringify(runnableTPSource); await fs.access("./runnable-texturepacker.jar");
} catch {
try { const escapedLink = JSON.stringify(runnableTPSource);
execute(`curl -o runnable-texturepacker.jar ${escapedLink}`);
} catch { try {
throw new Error("Failed to download runnable-texturepacker.jar!"); 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?"); await execute(`java -jar runnable-texturepacker.jar ${source} ${dest} atlas0 ${config}`);
} } catch {
cb(); console.warn("Building atlas failed. Java not found / unsupported version?");
}); }
}
// Converts .atlas LibGDX files to JSON
gulp.task("imgres.atlasToJson", cb => { // Converts .atlas LibGDX files to JSON
atlasToJson("../res_built/atlas"); export async function atlasToJson() {
cb(); atlas2Json("../res_built/atlas");
}); }
// Copies the atlas to the final destination // Copies the atlas to the final destination
gulp.task("imgres.atlas", () => { export function atlas() {
return gulp.src(["../res_built/atlas/*.png"]).pipe(gulp.dest(resourcesDestFolder)); return gulp.src(["../res_built/atlas/*.png"]).pipe(gulp.dest(resourcesDestFolder));
}); }
// Copies the atlas to the final destination after optimizing it (lossy compression) // Copies the atlas to the final destination after optimizing it (lossy compression)
gulp.task("imgres.atlasOptimized", () => { export function atlasOptimized() {
return gulp return gulp
.src(["../res_built/atlas/*.png"]) .src(["../res_built/atlas/*.png"])
.pipe( .pipe(
gulpIf( gulpIf(
fname => fileMustBeLossless(fname.history[0]), fname => fileMustBeLossless(fname.history[0]),
gulpImagemin(minifyImagesOptsLossless()), gulpImagemin(minifyImagesOptsLossless()),
gulpImagemin(minifyImagesOpts()) gulpImagemin(minifyImagesOpts())
) )
) )
.pipe(gulp.dest(resourcesDestFolder)); .pipe(gulp.dest(resourcesDestFolder));
}); }
//////////////////// RESOURCES ////////////////////// //////////////////// RESOURCES //////////////////////
// Copies all resources which are no ui resources // Copies all resources which are no ui resources
gulp.task("imgres.copyNonImageResources", () => { export function copyNonImageResources() {
return gulp.src(nonImageResourcesGlobs).pipe(gulp.dest(resourcesDestFolder)); return gulp.src(nonImageResourcesGlobs).pipe(gulp.dest(resourcesDestFolder));
}); }
// Copies all ui resources // Copies all ui resources
gulp.task("imgres.copyImageResources", () => { export function copyImageResources() {
return gulp return gulp
.src(imageResourcesGlobs) .src(imageResourcesGlobs)
.pipe(gulpCached("imgres.copyImageResources"))
.pipe(gulpCached("imgres.copyImageResources")) .pipe(gulp.dest(path.join(resourcesDestFolder)));
.pipe(gulp.dest(path.join(resourcesDestFolder))); }
});
// Copies all ui resources and optimizes them
// Copies all ui resources and optimizes them export function copyImageResourcesOptimized() {
gulp.task("imgres.copyImageResourcesOptimized", () => { return gulp
return gulp .src(imageResourcesGlobs)
.src(imageResourcesGlobs) .pipe(
.pipe( gulpIf(
gulpIf( fname => fileMustBeLossless(fname.history[0]),
fname => fileMustBeLossless(fname.history[0]), gulpImagemin(minifyImagesOptsLossless()),
gulpImagemin(minifyImagesOptsLossless()), gulpImagemin(minifyImagesOpts())
gulpImagemin(minifyImagesOpts()) )
) )
) .pipe(gulp.dest(path.join(resourcesDestFolder)));
.pipe(gulp.dest(path.join(resourcesDestFolder))); }
});
// Copies all resources and optimizes them
// Copies all resources and optimizes them export const allOptimized = gulp.parallel(
gulp.task( gulp.series(buildAtlas, atlasToJson, atlasOptimized),
"imgres.allOptimized", copyNonImageResources,
gulp.parallel( copyImageResourcesOptimized
"imgres.buildAtlas", );
"imgres.atlasToJson",
"imgres.atlasOptimized", // Cleans up unused images which are instead inline into the css
"imgres.copyNonImageResources", export function cleanupUnusedCssInlineImages() {
"imgres.copyImageResourcesOptimized" return gulp
) .src(
); [
path.join(buildFolder, "res", "ui", "**", "*.png"),
// Cleans up unused images which are instead inline into the css path.join(buildFolder, "res", "ui", "**", "*.jpg"),
gulp.task("imgres.cleanupUnusedCssInlineImages", () => { path.join(buildFolder, "res", "ui", "**", "*.svg"),
return gulp path.join(buildFolder, "res", "ui", "**", "*.gif"),
.src( ],
[ { read: false }
path.join(buildFolder, "res", "ui", "**", "*.png"), )
path.join(buildFolder, "res", "ui", "**", "*.jpg"), .pipe(gulpIf(fname => fname.history[0].indexOf("noinline") < 0, gulpClean({ force: true })));
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 })));
});
} }

View File

@ -1,4 +1,7 @@
import gulp from "gulp";
import webpack from "webpack";
import { BUILD_VARIANTS } from "./build_variants.js"; import { BUILD_VARIANTS } from "./build_variants.js";
import { buildFolder } from "./config.js";
import webpackConfig from "./webpack.config.js"; import webpackConfig from "./webpack.config.js";
import webpackProductionConfig from "./webpack.production.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) { export default Object.fromEntries(
const data = BUILD_VARIANTS[variant]; 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", () => { const dev = {
gulp.src("../src/js/main.js") build,
.pipe(webpackStream(webpackConfig)) };
.pipe(gulp.dest(buildFolder))
.pipe(browserSync.stream());
});
let prod;
if (!data.standalone) { if (!data.standalone) {
// WEB // WEB
gulp.task("js." + variant + ".dev", () => { function transpiled() {
return gulp return gulp
.src("../src/js/main.js") .src("../src/js/main.js")
.pipe(webpackStream(webpackConfig)) .pipe(webpackStream(webpackProductionConfig, webpack))
.pipe(gulp.dest(buildFolder));
});
gulp.task("js." + variant + ".prod.transpiled", () => {
return gulp
.src("../src/js/main.js")
.pipe(webpackStream(webpackProductionConfig))
.pipe(gulpRename("bundle-transpiled.js")) .pipe(gulpRename("bundle-transpiled.js"))
.pipe(gulp.dest(buildFolder)); .pipe(gulp.dest(buildFolder));
}); }
gulp.task("js." + variant + ".prod.es6", () => { function es6() {
return gulp return gulp
.src("../src/js/main.js") .src("../src/js/main.js")
.pipe(webpackStream(webpackProductionConfig)) .pipe(webpackStream(webpackProductionConfig, webpack))
.pipe(gulp.dest(buildFolder)); .pipe(gulp.dest(buildFolder));
}); }
gulp.task(
"js." + variant + ".prod",
// transpiled currently not used prod = {
// gulp.parallel("js." + variant + ".prod.transpiled", "js." + variant + ".prod.es6") transpiled,
gulp.parallel("js." + variant + ".prod.es6") es6,
); build:
// transpiled currently not used
// gulp.parallel("js." + variant + ".prod.transpiled", "js." + variant + ".prod.es6")
es6,
};
} else { } else {
// STANDALONE // STANDALONE
gulp.task("js." + variant + ".dev", () => { function build() {
return gulp return gulp
.src("../src/js/main.js") .src("../src/js/main.js")
.pipe(webpackStream(webpackConfig)) .pipe(webpackStream(webpackProductionConfig, webpack))
.pipe(gulp.dest(buildFolder)); .pipe(gulp.dest(buildFolder));
}); }
gulp.task("js." + variant + ".prod", () => {
return gulp prod = { build };
.src("../src/js/main.js")
.pipe(webpackStream(webpackProductionConfig))
.pipe(gulp.dest(buildFolder));
});
} }
}
} return [variant, { dev, prod }];
})
);

View File

@ -1,14 +1,10 @@
import fs from "fs"; import fs from "fs/promises";
const configTemplatePath = "../src/js/core/config.local.template.js"; const configTemplatePath = "../src/js/core/config.local.template.js";
const configPath = "../src/js/core/config.local.js"; const configPath = "../src/js/core/config.local.js";
export default function gulptasksLocalConfig(gulp) { export async function findOrCreate() {
gulp.task("localConfig.findOrCreate", cb => { try {
if (!fs.existsSync(configPath)) { await fs.copyFile(configTemplatePath, configPath, fs.constants.COPYFILE_EXCL);
fs.copyFileSync(configTemplatePath, configPath); } catch {}
}
cb();
});
} }

View File

@ -1,135 +1,132 @@
import path from "path/posix"; 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 gulpClean from "gulp-clean";
import gulpCache from "gulp-cache"; import gulpCache from "gulp-cache";
import gulpPlumber from "gulp-plumber"; import gulpPlumber from "gulp-plumber";
import gulpFluentFfmpeg from "gulp-fluent-ffmpeg"; import gulpFluentFfmpeg from "gulp-fluent-ffmpeg";
export default function gulptasksSounds(gulp, buildFolder) { // Gather some basic infos
// Gather some basic infos const soundsDir = path.join("..", "res_raw", "sounds");
const soundsDir = path.join("..", "res_raw", "sounds"); const builtSoundsDir = path.join("..", "res_built", "sounds");
const builtSoundsDir = path.join("..", "res_built", "sounds");
gulp.task("sounds.clear", () => { export function clear() {
return gulp.src(builtSoundsDir, { read: false, allowEmpty: true }).pipe(gulpClean({ force: true })); 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({ const fileCache = new gulpCache.Cache({
cacheDirName: "shapezio-precompiled-sounds", cacheDirName: "shapezio-precompiled-sounds",
}); });
function getFileCacheValue(file) { function getFileCacheValue(file) {
const { _isVinyl, base, cwd, contents, history, stat, path } = file; const { _isVinyl, base, cwd, contents, history, stat, path } = file;
const encodedContents = Buffer.from(contents).toString("base64"); const encodedContents = Buffer.from(contents).toString("base64");
return { _isVinyl, base, cwd, contents: encodedContents, history, stat, path }; return { _isVinyl, base, cwd, contents: encodedContents, history, stat, path };
} }
// Encodes the game music // Encodes the game music
gulp.task("sounds.music", () => { export function music() {
return gulp return gulp
.src([path.join(soundsDir, "music", "**", "*.wav"), path.join(soundsDir, "music", "**", "*.mp3")]) .src([path.join(soundsDir, "music", "**", "*.wav"), path.join(soundsDir, "music", "**", "*.mp3")])
.pipe(gulpPlumber()) .pipe(gulpPlumber())
.pipe( .pipe(
gulpCache( 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(
gulpFluentFfmpeg("mp3", function (cmd) { gulpFluentFfmpeg("mp3", function (cmd) {
return cmd return cmd
.audioBitrate(128) .audioBitrate(48)
.audioChannels(1) .audioChannels(1)
.audioFrequency(22050) .audioFrequency(22050)
.audioCodec("libmp3lame") .audioCodec("libmp3lame")
.audioFilters(filters); .audioFilters(["volume=0.15"]);
}) }),
{
name: "music",
fileCache,
value: getFileCacheValue,
}
) )
.pipe(gulp.dest(path.join(builtSoundsDir))); )
}); .pipe(gulp.dest(path.join(builtSoundsDir, "music")));
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"));
} }
// 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);

View File

@ -2,8 +2,11 @@ import packager from "electron-packager";
import pj from "../electron/package.json" assert { type: "json" }; import pj from "../electron/package.json" assert { type: "json" };
import path from "path/posix"; import path from "path/posix";
import { getVersion } from "./buildutils.js"; import { getVersion } from "./buildutils.js";
import fs from "fs"; import fs from "fs/promises";
import { execSync } from "child_process"; 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 { BUILD_VARIANTS } from "./build_variants.js";
import gulpClean from "gulp-clean"; import gulpClean from "gulp-clean";
@ -11,121 +14,132 @@ import gulpClean from "gulp-clean";
const platforms = /** @type {const} */ (["win32", "linux", "darwin"]); const platforms = /** @type {const} */ (["win32", "linux", "darwin"]);
const architectures = /** @type {const} */ (["x64", "arm64"]); const architectures = /** @type {const} */ (["x64", "arm64"]);
export default function gulptasksStandalone(gulp) { export default Object.fromEntries(
for (const variant in BUILD_VARIANTS) { Object.entries(BUILD_VARIANTS)
const variantData = BUILD_VARIANTS[variant]; .filter(([variant, variantData]) => variantData.standalone)
if (!variantData.standalone) { .map(([variant, variantData]) => {
continue; const tempDestDir = path.join("..", "build_output", variant);
} const electronBaseDir = path.join("..", "electron");
const tempDestDir = path.join("..", "build_output", variant); const tempDestBuildDir = path.join(tempDestDir, "built");
const taskPrefix = "standalone." + variant;
const electronBaseDir = path.join("..", "electron");
const tempDestBuildDir = path.join(tempDestDir, "built");
gulp.task(taskPrefix + ".prepare.cleanup", () => { function cleanup() {
return gulp.src(tempDestDir, { read: false, allowEmpty: true }).pipe(gulpClean({ force: true })); return gulp
}); .src(tempDestDir, { read: false, allowEmpty: true })
.pipe(gulpClean({ force: true }));
}
gulp.task(taskPrefix + ".prepare.copyPrefab", () => { function copyPrefab() {
const requiredFiles = [ const requiredFiles = [
path.join(electronBaseDir, "node_modules", "**", "*.*"), path.join(electronBaseDir, "node_modules", "**", "*.*"),
path.join(electronBaseDir, "node_modules", "**", ".*"), path.join(electronBaseDir, "node_modules", "**", ".*"),
path.join(electronBaseDir, "favicon*"), path.join(electronBaseDir, "favicon*"),
]; ];
return gulp.src(requiredFiles, { base: electronBaseDir }).pipe(gulp.dest(tempDestBuildDir)); return gulp.src(requiredFiles, { base: electronBaseDir }).pipe(gulp.dest(tempDestBuildDir));
}); }
gulp.task(taskPrefix + ".prepare.writePackageJson", cb => { async function writePackageJson() {
const packageJsonString = JSON.stringify( const packageJsonString = JSON.stringify(
{ {
scripts: { scripts: {
start: pj.scripts.start, start: pj.scripts.start,
},
devDependencies: pj.devDependencies,
dependencies: pj.dependencies,
optionalDependencies: pj.optionalDependencies,
}, },
devDependencies: pj.devDependencies, null,
dependencies: pj.dependencies, 4
optionalDependencies: pj.optionalDependencies, );
},
null,
4
);
fs.writeFileSync(path.join(tempDestBuildDir, "package.json"), packageJsonString); await fs.writeFile(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")));
} }
cb(); function minifyCode() {
} return gulp.src(path.join(electronBaseDir, "*.js")).pipe(gulp.dest(tempDestBuildDir));
}
for (const platform of platforms) { function copyGamefiles() {
for (const arch of architectures) { return gulp.src("../build/**/*.*", { base: "../build" }).pipe(gulp.dest(tempDestBuildDir));
gulp.task(taskPrefix + `.package.${platform}-${arch}`, cb => }
packageStandalone(platform, arch, cb)
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 const pack = {
gulp.task(taskPrefix + ".package.all", cb => ...Object.fromEntries(
packageStandalone([...platforms], [...architectures], cb) 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,
},
];
})
);

289
gulp/tasks.js Normal file
View File

@ -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<string, {
* code: TaskFunction,
* resourcesAndCode: TaskFunction,
* all: TaskFunction,
* full: TaskFunction,
* }> & { prepare: typeof prepare }}
*/
({
prepare,
});
/**
* @type {Record<string, Record<string, TaskFunction>>}
*/
const pack = {};
export { pack as package };
/** @type {Record<string, TaskFunction>} */
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"]);

View File

@ -1,21 +1,20 @@
import path from "path/posix"; import path from "path/posix";
import fs from "fs"; import fs from "fs/promises";
import gulpYaml from "gulp-yaml";
import YAML from "yaml"; import YAML from "yaml";
import gulp from "gulp";
import gulpPlumber from "gulp-plumber"; import gulpPlumber from "gulp-plumber";
import gulpYaml from "gulp-yaml";
const translationsSourceDir = path.join("..", "translations"); const translationsSourceDir = path.join("..", "translations");
const translationsJsonDir = path.join("..", "src", "js", "built-temp"); const translationsJsonDir = path.join("..", "src", "js", "built-temp");
export default function gulptasksTranslations(gulp) { export function convertToJson() {
gulp.task("translations.convertToJson", () => { return gulp
return gulp .src(path.join(translationsSourceDir, "*.yaml"))
.src(path.join(translationsSourceDir, "*.yaml")) .pipe(gulpPlumber())
.pipe(gulpPlumber()) .pipe(gulpYaml({ space: 2, safe: true }))
.pipe(gulpYaml({ space: 2, safe: true })) .pipe(gulp.dest(translationsJsonDir));
.pipe(gulp.dest(translationsJsonDir));
});
gulp.task("translations.fullBuild", gulp.series("translations.convertToJson"));
} }
export const fullBuild = convertToJson;

View File

@ -95,7 +95,6 @@ export default {
extensions: [".ts", ".js", ".tsx", ".jsx"], extensions: [".ts", ".js", ".tsx", ".jsx"],
}, },
devtool: "cheap-source-map", devtool: "cheap-source-map",
watch: true,
cache: false, cache: false,
plugins: [ plugins: [
new webpack.DefinePlugin(globalDefs), new webpack.DefinePlugin(globalDefs),