1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-11 09:11:50 +00:00

Squashed commit of the following:

commit 176343785eea110e529f26027bf864ae04068384
Author: EmeraldBlock <yygengjunior@gmail.com>
Date:   Fri Dec 8 23:03:51 2023 -0600

    update readme

commit 8c1c3a0c47f5125126cb00d32a48f4f9344a3fb3
Author: EmeraldBlock <yygengjunior@gmail.com>
Date:   Fri Dec 8 23:00:05 2023 -0600

    fix bugs

commit ea881e68c693a447e0698a3a6e7cfb1f25ccb6cc
Author: EmeraldBlock <yygengjunior@gmail.com>
Date:   Fri Dec 8 22:46:46 2023 -0600

    expose all tasks with old api

commit fa6d7a3920ff573eadb61425cc077f0e00406164
Author: EmeraldBlock <yygengjunior@gmail.com>
Date:   Fri Dec 8 21:51:20 2023 -0600

    switch to exported gulp tasks

commit 348b19a0171e65400bcd434cf7b7432f3488a411
Author: EmeraldBlock <yygengjunior@gmail.com>
Date:   Mon Nov 20 22:55:38 2023 -0600

    parallelize dev build

commit 56de73e2d18d20e5ea7202afc021573a746e5012
Author: EmeraldBlock <yygengjunior@gmail.com>
Date:   Mon Nov 20 20:44:10 2023 -0600

    use promises in gulpfile

commit 6ab54372482f26acb4769428eefbdc48240a12a1
Author: EmeraldBlock <yygengjunior@gmail.com>
Date:   Mon Nov 20 20:33:36 2023 -0600

    make java -version print again

commit b0e4cf57bdc404bb3b0e45b7b233d5f7648c800e
Author: EmeraldBlock <yygengjunior@gmail.com>
Date:   Mon Nov 20 20:14:13 2023 -0600

    use promises for gulp tasks
This commit is contained in:
EmeraldBlock 2024-04-24 16:30:01 -05:00
parent b5a7f7736a
commit 39b7e6cb59
14 changed files with 1079 additions and 1024 deletions

View File

@ -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 <https://github.com/tobspr-games/shapez.io/issues/1473>)
- [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

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 gulp from "gulp";
import { getRevision } from "./buildutils.js";
import { buildFolder, browserSync } from "./config.js";
import gulpPostcss from "gulp-postcss";
import postcssAssets from "postcss-assets";
@ -13,112 +15,106 @@ 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))
.pipe(browserSync.stream());
}
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))
.pipe(browserSync.stream());
}
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
import gulp from "gulp";
import { BUILD_VARIANTS } from "./build_variants.js";
import { buildFolder, browserSync } from "./config.js";
import webpackConfig from "./webpack.config.js";
import webpackProductionConfig from "./webpack.production.config.js";
@ -15,64 +17,69 @@ 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];
gulp.task("js." + variant + ".dev.watch", () => {
gulp.src("../src/js/main.js")
export default Object.fromEntries(
Object.entries(BUILD_VARIANTS).map(([variant, data]) => {
function watch() {
return gulp
.src("../src/js/main.js")
.pipe(webpackStream(webpackConfig))
.pipe(gulp.dest(buildFolder))
.pipe(browserSync.stream());
});
}
function build() {
return gulp
.src("../src/js/main.js")
.pipe(webpackStream(webpackConfig))
.pipe(gulp.dest(buildFolder));
}
const dev = {
watch,
build,
};
let prod;
if (!data.standalone) {
// WEB
gulp.task("js." + variant + ".dev", () => {
return gulp
.src("../src/js/main.js")
.pipe(webpackStream(webpackConfig))
.pipe(gulp.dest(buildFolder));
});
gulp.task("js." + variant + ".prod.transpiled", () => {
function transpiled() {
return gulp
.src("../src/js/main.js")
.pipe(webpackStream(webpackProductionConfig))
.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(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", () => {
return gulp
.src("../src/js/main.js")
.pipe(webpackStream(webpackConfig))
.pipe(gulp.dest(buildFolder));
});
gulp.task("js." + variant + ".prod", () => {
function build() {
return gulp
.src("../src/js/main.js")
.pipe(webpackStream(webpackProductionConfig))
.pipe(gulp.dest(buildFolder));
});
}
prod = { build };
}
}
}
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 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 {}
}

View File

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

View File

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

304
gulp/tasks.js Normal file
View File

@ -0,0 +1,304 @@
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.series(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
*/
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"], css.dev);
// Watch .html files, those trigger a html rebuild
gulp.watch("../src/**/*.html", html.dev);
gulp.watch("./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
const extensions = ["html", "js", "png", "gif", "jpg", "svg", "mp3", "ico", "woff2", "json"];
gulp.watch(extensions.map(ext => path.join(buildFolder, "**", "*." + ext))).on("change", p =>
gulp
.src(pathNative.resolve(p).replaceAll(pathNative.sep, path.sep))
.pipe(browserSync.reload({ stream: true }))
);
gulp.watch("../src/js/built-temp/*.json").on("change", p =>
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
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: gulp.series(
utils.cleanup,
utils.copyAdditionalBuildFiles,
localConfig.findOrCreate,
gulp.parallel(
gulp.series(imgres.buildAtlas, imgres.atlasToJson, imgres.atlas),
sounds.dev,
gulp.series(imgres.copyImageResources, css.dev),
imgres.copyNonImageResources,
translations.fullBuild
)
),
};
/**
* @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, html.dev, () => 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 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;