Mod Support - 1.5.0 Update (#1361)

* initial modloader draft

* modloader features

* Refactor mods to use signals

* Add support for modifying and registering new transltions

* Minor adjustments

* Support for string building ids for mods

* Initial support for adding new buildings

* Refactor how mods are loaded to resolve circular dependencies and prepare for future mod loading

* Lazy Load mods to make sure all dependencies are loaded

* Expose all exported members automatically to mods

* Fix duplicate exports

* Allow loading mods from standalone

* update changelog

* Fix mods folder incorrect path

* Fix modloading in standalone

* Fix sprites not getting replaced, update demo mod

* Load dev mod via raw loader

* Improve mod developing so mods are directly ready to be deployed, load mods from local file server

* Proper mods ui

* Allow mods to register game systems and draw stuff

* Change mods path

* Fix sprites not loading

* Minor adjustments, closes #1333

* Add support for loading atlases via mods

* Add support for loading mods from external sources in DEV

* Add confirmation when loading mods

* Fix circular dependency

* Minor Keybindings refactor, add support for keybindings to mods, add support for dialogs to mods

* Add some mod signals

* refactor game loading states

* Make shapez exports global

* Start to make mods safer

* Refactor file system electron event handling

* Properly isolate electron renderer process

* Update to latest electron

* Show errors when loading mods

* Update confirm dialgo

* Minor restructure, start to add mod examples

* Allow adding custom themesw

* Add more examples and allow defining custom item processor operations

* Add interface to register new buildings

* Fixed typescript type errors (#1335)

* Refactor building registry, make it easier for mods to add new buildings

* Allow overriding existing methods

* Add more examples and more features

* More mod examples

* Make mod loading simpler

* Add example how to add custom drawings

* Remove unused code

* Minor modloader adjustments

* Support for rotation variants in mods (was broken previously)

* Allow mods to replace builtin sub shapes

* Add helper methods to extend classes

* Fix menu bar on mac os

* Remember window state

* Add support for paste signals

* Add example how to add custom components and systems

* Support for mod settings

* Add example for adding a new item type

* Update class extensions

* Minor adjustments

* Fix typo

* Add notification blocks mod example

* Add small tutorial

* Update readme

* Add better instructions

* Update JSDoc for Replacing Methods (#1336)

* upgraded types for overriding methods

* updated comments

Co-authored-by: Edward Badel <you@example.com>

* Direction lock now indicates when there is a building inbetween

* Fix mod examples

* Fix linter error

* Game state register (#1341)

* Added a gamestate register helper

Added a gamestate register helper

* Update mod_interface.js

* export build options

* Fix runBeforeMethod and runAfterMethod

* Minor game system code cleanup

* Belt path drawing optimization

* Fix belt path optimization

* Belt drawing improvements, again

* Do not render belts in statics disabled view

* Allow external URL to load more than one mod (#1337)

* Allow external URL to load more than one mod

Instead of loading the text returned from the remote server, load a JSON object with a `mods` field, containing strings of all the mods. This lets us work on more than one mod at a time or without separate repos. This will break tooling such as `create-shapezio-mod` though.

* Update modloader.js

* Prettier fixes

* Added link to create-shapezio-mod npm page (#1339)

Added link to create-shapezio-mod npm page: https://www.npmjs.com/package/create-shapezio-mod

* allow command line switch to load more than one mod (#1342)

* Fixed class handle type (#1345)

* Fixed class handle type

* Fixed import game state

* Minor adjustments

* Refactor item acceptor to allow only single direction slots

* Allow specifying minimumGameVersion

* Add sandbox example

* Replaced concatenated strings with template literals (#1347)

* Mod improvements

* Make wired pins component optional on the storage

* Fix mod examples

* Bind `this` for method overriding JSDoc (#1352)

* fix entity debugger reaching HTML elements (#1353)

* Store mods in savegame and show warning when it differs

* Closes #1357

* Fix All Shapez Exports Being Const (#1358)

* Allowed setting of variables inside webpack modules

* remove console log

* Fix stringification of things inside of eval

Co-authored-by: Edward Badel <you@example.com>

* Fix building placer intersection warning

* Add example for storing data in the savegame

* Fix double painter bug (#1349)

* Add example on how to extend builtin buildings

* update readme

* Disable steam achievements when playing with mods

* Update translations

Co-authored-by: Thomas (DJ1TJOO) <44841260+DJ1TJOO@users.noreply.github.com>
Co-authored-by: Bagel03 <70449196+Bagel03@users.noreply.github.com>
Co-authored-by: Edward Badel <you@example.com>
Co-authored-by: Emerald Block <69981203+EmeraldBlock@users.noreply.github.com>
Co-authored-by: saile515 <63782477+saile515@users.noreply.github.com>
Co-authored-by: Sense101 <67970865+Sense101@users.noreply.github.com>
pull/1363/head
tobspr 2 years ago committed by GitHub
parent a7a2aad2b6
commit c41aaa1fc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1 @@
mods/*.js

@ -1,27 +1,40 @@
/* eslint-disable quotes,no-undef */
const { app, BrowserWindow, Menu, MenuItem, ipcMain, shell } = require("electron");
const { app, BrowserWindow, Menu, MenuItem, ipcMain, shell, dialog, session } = require("electron");
const path = require("path");
const url = require("url");
const fs = require("fs");
const steam = require("./steam");
const asyncLock = require("async-lock");
const windowStateKeeper = require("electron-window-state");
const isDev = process.argv.indexOf("--dev") >= 0;
const isLocal = process.argv.indexOf("--local") >= 0;
// Disable hardware key handling, i.e. being able to pause/resume the game music
// with hardware keys
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling");
const isDev = app.commandLine.hasSwitch("dev");
const isLocal = app.commandLine.hasSwitch("local");
const safeMode = app.commandLine.hasSwitch("safe-mode");
const externalMod = app.commandLine.getSwitchValue("load-mod");
const roamingFolder =
process.env.APPDATA ||
(process.platform == "darwin"
? process.env.HOME + "/Library/Preferences"
: process.env.HOME + "/.local/share");
let storePath = path.join(roamingFolder, "shapez.io", "saves");
let modsPath = path.join(roamingFolder, "shapez.io", "mods");
if (!fs.existsSync(storePath)) {
// No try-catch by design
fs.mkdirSync(storePath, { recursive: true });
}
if (!fs.existsSync(modsPath)) {
fs.mkdirSync(modsPath, { recursive: true });
}
/** @type {BrowserWindow} */
let win = null;
let menu = null;
@ -32,26 +45,44 @@ function createWindow() {
faviconExtension = ".ico";
}
const mainWindowState = windowStateKeeper({
defaultWidth: 1000,
defaultHeight: 800,
});
win = new BrowserWindow({
width: 1280,
height: 800,
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
show: false,
backgroundColor: "#222428",
useContentSize: true,
useContentSize: false,
minWidth: 800,
minHeight: 600,
title: "shapez.io Standalone",
transparent: false,
icon: path.join(__dirname, "favicon" + faviconExtension),
// fullscreen: true,
autoHideMenuBar: true,
autoHideMenuBar: !isDev,
webPreferences: {
nodeIntegration: true,
webSecurity: false,
nodeIntegration: false,
nodeIntegrationInWorker: false,
nodeIntegrationInSubFrames: false,
contextIsolation: true,
enableRemoteModule: false,
disableBlinkFeatures: "Auxclick",
webSecurity: true,
sandbox: true,
preload: path.join(__dirname, "preload.js"),
experimentalFeatures: false,
},
allowRunningInsecureContent: false,
});
mainWindowState.manage(win);
if (isLocal) {
win.loadURL("http://localhost:3005");
} else {
@ -66,9 +97,67 @@ function createWindow() {
win.webContents.session.clearCache();
win.webContents.session.clearStorageData();
////// SECURITY
// Disable permission requests
win.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
callback(false);
});
session.fromPartition("default").setPermissionRequestHandler((webContents, permission, callback) => {
callback(false);
});
app.on("web-contents-created", (event, contents) => {
// Disable vewbiew
contents.on("will-attach-webview", (event, webPreferences, params) => {
event.preventDefault();
});
// Disable navigation
contents.on("will-navigate", (event, navigationUrl) => {
event.preventDefault();
});
});
win.webContents.on("will-redirect", (contentsEvent, navigationUrl) => {
// Log and prevent the app from redirecting to a new page
console.error(
`The application tried to redirect to the following address: '${navigationUrl}'. This attempt was blocked.`
);
contentsEvent.preventDefault();
});
// Filter loading any module via remote;
// you shouldn't be using remote at all, though
// https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module
app.on("remote-require", (event, webContents, moduleName) => {
event.preventDefault();
});
// built-ins are modules such as "app"
app.on("remote-get-builtin", (event, webContents, moduleName) => {
event.preventDefault();
});
app.on("remote-get-global", (event, webContents, globalName) => {
event.preventDefault();
});
app.on("remote-get-current-window", (event, webContents) => {
event.preventDefault();
});
app.on("remote-get-current-web-contents", (event, webContents) => {
event.preventDefault();
});
//// END SECURITY
win.webContents.on("new-window", (event, pth) => {
event.preventDefault();
shell.openExternal(pth);
if (pth.startsWith("https://")) {
shell.openExternal(pth);
}
});
win.on("closed", () => {
@ -79,15 +168,17 @@ function createWindow() {
if (isDev) {
menu = new Menu();
win.webContents.toggleDevTools();
const mainItem = new MenuItem({
label: "Toggle Dev Tools",
click: () => win.toggleDevTools(),
click: () => win.webContents.toggleDevTools(),
accelerator: "F12",
});
menu.append(mainItem);
const reloadItem = new MenuItem({
label: "Restart",
label: "Reload",
click: () => win.reload(),
accelerator: "F5",
});
@ -100,7 +191,15 @@ function createWindow() {
});
menu.append(fullscreenItem);
Menu.setApplicationMenu(menu);
const mainMenu = new Menu();
mainMenu.append(
new MenuItem({
label: "shapez.io",
submenu: menu,
})
);
Menu.setApplicationMenu(mainMenu);
} else {
Menu.setApplicationMenu(null);
}
@ -114,7 +213,7 @@ function createWindow() {
if (!app.requestSingleInstanceLock()) {
app.exit(0);
} else {
app.on("second-instance", (event, commandLine, workingDirectory) => {
app.on("second-instance", () => {
// Someone tried to run a second instance, we should focus
if (win) {
if (win.isMinimized()) {
@ -136,7 +235,7 @@ ipcMain.on("set-fullscreen", (event, flag) => {
win.setFullScreen(flag);
});
ipcMain.on("exit-app", (event, flag) => {
ipcMain.on("exit-app", () => {
win.close();
app.quit();
});
@ -167,14 +266,14 @@ async function writeFileSafe(filename, contents) {
if (!fs.existsSync(filename)) {
// this one is easy
console.log(prefix, "Writing file instantly because it does not exist:", niceFileName(filename));
await fs.promises.writeFile(filename, contents, { encoding: "utf8" });
await fs.promises.writeFile(filename, contents, "utf8");
return;
}
// first, write a temporary file (.tmp-XXX)
const tempName = filename + ".tmp-" + transactionId;
console.log(prefix, "Writing temporary file", niceFileName(tempName));
await fs.promises.writeFile(tempName, contents, { encoding: "utf8" });
await fs.promises.writeFile(tempName, contents, "utf8");
// now, rename the original file to (.backup-XXX)
const oldTemporaryName = filename + ".backup-" + transactionId;
@ -216,68 +315,74 @@ async function writeFileSafe(filename, contents) {
});
}
async function performFsJob(job) {
const fname = path.join(storePath, job.filename);
ipcMain.handle("fs-job", async (event, job) => {
const filenameSafe = job.filename.replace(/[^a-z\.\-_0-9]/i, "_");
const fname = path.join(storePath, filenameSafe);
switch (job.type) {
case "read": {
if (!fs.existsSync(fname)) {
return {
// Special FILE_NOT_FOUND error code
error: "file_not_found",
};
}
try {
const data = await fs.promises.readFile(fname, { encoding: "utf8" });
return {
success: true,
data,
};
} catch (ex) {
return {
error: ex,
};
// Special FILE_NOT_FOUND error code
return { error: "file_not_found" };
}
return await fs.promises.readFile(fname, "utf8");
}
case "write": {
try {
await writeFileSafe(fname, job.contents);
return {
success: true,
data: job.contents,
};
} catch (ex) {
return {
error: ex,
};
}
await writeFileSafe(fname, job.contents);
return job.contents;
}
case "delete": {
try {
await fs.promises.unlink(fname);
} catch (ex) {
return {
error: ex,
};
}
return {
success: true,
data: null,
};
await fs.promises.unlink(fname);
return;
}
default:
throw new Error("Unkown fs job: " + job.type);
throw new Error("Unknown fs job: " + job.type);
}
});
ipcMain.handle("open-mods-folder", async () => {
shell.openPath(modsPath);
});
console.log("Loading mods ...");
function loadMods() {
if (safeMode) {
console.log("Safe Mode enabled for mods, skipping mod search");
}
console.log("Loading mods from", modsPath);
let modFiles = safeMode
? []
: fs
.readdirSync(modsPath)
.filter(filename => filename.endsWith(".js"))
.map(filename => path.join(modsPath, filename));
if (externalMod) {
console.log("Adding external mod source:", externalMod);
const externalModPaths = externalMod.split(",");
modFiles = modFiles.concat(externalModPaths);
}
return modFiles.map(filename => fs.readFileSync(filename, "utf8"));
}
ipcMain.on("fs-job", async (event, arg) => {
const result = await performFsJob(arg);
event.reply("fs-response", { id: arg.id, result });
let mods = [];
try {
mods = loadMods();
console.log("Loaded", mods.length, "mods");
} catch (ex) {
console.error("Failed ot load mods");
dialog.showErrorBox("Failed to load mods:", ex);
}
ipcMain.handle("get-mods", async () => {
return mods;
});
steam.init(isDev);
steam.listen();
if (mods) {
steam.listen();
}

@ -0,0 +1,6 @@
Here you can place mods. Every mod should be a single file ending with ".js".
--- WARNING ---
Mods can potentially access to your filesystem.
Please only install mods from trusted sources and developers.
--- WARNING ---

@ -9,13 +9,13 @@
"startDevGpu": "electron --enable-gpu-rasterization --enable-accelerated-2d-canvas --num-raster-threads=8 --enable-zero-copy . --dev --local",
"start": "electron --disable-direct-composition --in-process-gpu ."
},
"devDependencies": {
"electron": "10.4.3"
},
"devDependencies": {},
"optionalDependencies": {
"shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v82"
"shapez.io-private-artifacts": "github:tobspr/shapez.io-private-artifacts#abi-v99"
},
"dependencies": {
"async-lock": "^1.2.8"
"async-lock": "^1.2.8",
"electron": "16.0.7",
"electron-window-state": "^5.0.3"
}
}

@ -0,0 +1,7 @@
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("ipcRenderer", {
invoke: ipcRenderer.invoke.bind(ipcRenderer),
on: ipcRenderer.on.bind(ipcRenderer),
send: ipcRenderer.send.bind(ipcRenderer),
});

@ -13,7 +13,6 @@ try {
// greenworks is not installed
console.warn("Failed to load steam api:", err);
}
function init(isDev) {
if (!greenworks) {
return;

@ -2,10 +2,10 @@
# yarn lockfile v1
"@electron/get@^1.0.1":
version "1.12.4"
resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.12.4.tgz#a5971113fc1bf8fa12a8789dc20152a7359f06ab"
integrity sha512-6nr9DbJPUR9Xujw6zD3y+rS95TyItEVM0NVjt1EehY2vUWfIgPiIPVHxCvaTS0xr2B+DRxovYVKbuOWqC35kjg==
"@electron/get@^1.13.0":
version "1.13.1"
resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.13.1.tgz#42a0aa62fd1189638bd966e23effaebb16108368"
integrity sha512-U5vkXDZ9DwXtkPqlB45tfYnnYBN8PePp1z/XDCupnSpdrxT8/ThCv9WCwPLf9oqiSGZTkH6dx2jDUPuoXpjkcA==
dependencies:
debug "^4.1.1"
env-paths "^2.2.0"
@ -15,7 +15,7 @@
semver "^6.2.0"
sumchecker "^3.0.1"
optionalDependencies:
global-agent "^2.0.2"
global-agent "^3.0.0"
global-tunnel-ng "^2.7.1"
"@sindresorhus/is@^0.14.0":
@ -30,10 +30,10 @@
dependencies:
defer-to-connect "^1.0.1"
"@types/node@^12.0.12":
version "12.20.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.5.tgz#4ca82a766f05c359fd6c77505007e5a272f4bb9b"
integrity sha512-5Oy7tYZnu3a4pnJ//d4yVvOImExl4Vtwf0D40iKUlU+XlUsyV9iyFWyCFlwy489b72FMAik/EFwRkNLjjOdSPg==
"@types/node@^14.6.2":
version "14.18.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.5.tgz#0dd636fe7b2c6055cbed0d4ca3b7fb540f130a96"
integrity sha512-LMy+vDDcQR48EZdEx5wRX1q/sEl6NdGuHXPnfeL8ixkwCOSZ2qnIyIZmcCbdX0MeRqHhAcHmX+haCbrS8Run+A==
async-lock@^1.2.8:
version "1.2.8"
@ -93,11 +93,6 @@ config-chain@^1.1.11:
ini "^1.3.4"
proto-list "~1.2.1"
core-js@^3.6.5:
version "3.9.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae"
integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg==
core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -146,13 +141,21 @@ duplexer3@^0.1.4:
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
electron@10.4.3:
version "10.4.3"
resolved "https://registry.yarnpkg.com/electron/-/electron-10.4.3.tgz#8d1c0f5e562d1b78dcec8074c0d59e58137fd508"
integrity sha512-qL8XZBII9KQHr1+YmVMj1AqyTR2I8/lxozvKEWoKKSkF8Hl6GzzxrLXRfISP7aDAvsJEyyhc6b2/42ME8hG5JA==
electron-window-state@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/electron-window-state/-/electron-window-state-5.0.3.tgz#4f36d09e3f953d87aff103bf010f460056050aa8"
integrity sha512-1mNTwCfkolXl3kMf50yW3vE2lZj0y92P/HYWFBrb+v2S/pCka5mdwN3cagKm458A7NjndSwijynXgcLWRodsVg==
dependencies:
"@electron/get" "^1.0.1"
"@types/node" "^12.0.12"
jsonfile "^4.0.0"
mkdirp "^0.5.1"
electron@16.0.7:
version "16.0.7"
resolved "https://registry.yarnpkg.com/electron/-/electron-16.0.7.tgz#87eaccd05ab61563d3c17dfbad2949bba7ead162"
integrity sha512-/IMwpBf2svhA1X/7Q58RV+Nn0fvUJsHniG4TizaO7q4iKFYSQ6hBvsLz+cylcZ8hRMKmVy5G1XaMNJID2ah23w==
dependencies:
"@electron/get" "^1.13.0"
"@types/node" "^14.6.2"
extract-zip "^1.0.3"
encodeurl@^1.0.2:
@ -222,13 +225,12 @@ get-stream@^5.1.0:
dependencies:
pump "^3.0.0"
global-agent@^2.0.2:
version "2.1.12"
resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-2.1.12.tgz#e4ae3812b731a9e81cbf825f9377ef450a8e4195"
integrity sha512-caAljRMS/qcDo69X9BfkgrihGUgGx44Fb4QQToNQjsiWh+YlQ66uqYVAdA8Olqit+5Ng0nkz09je3ZzANMZcjg==
global-agent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-3.0.0.tgz#ae7cd31bd3583b93c5a16437a1afe27cc33a1ab6"
integrity sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==
dependencies:
boolean "^3.0.1"
core-js "^3.6.5"
es6-error "^4.1.1"
matcher "^3.0.0"
roarr "^2.15.3"
@ -357,7 +359,7 @@ minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
mkdirp@^0.5.4:
mkdirp@^0.5.1, mkdirp@^0.5.4:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@ -503,9 +505,9 @@ serialize-error@^7.0.1:
dependencies:
type-fest "^0.13.1"
"shapez.io-private-artifacts@github:tobspr/shapez.io-private-artifacts#abi-v82":
"shapez.io-private-artifacts@github:tobspr/shapez.io-private-artifacts#abi-v99":
version "0.1.0"
resolved "git+ssh://git@github.com/tobspr/shapez.io-private-artifacts.git#8aa3bfd3b569eb5695fc8a585a3f2ee3ed2db290"
resolved "git+ssh://git@github.com/tobspr/shapez.io-private-artifacts.git#b638501e81bba324923fc1b9d9aadc925da9b2c6"
sprintf-js@^1.1.2:
version "1.1.2"

@ -49,9 +49,12 @@ function createWindow() {
// fullscreen: true,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
webSecurity: false,
contextIsolation: false,
nodeIntegration: false,
webSecurity: true,
sandbox: true,
contextIsolation: true,
preload: path.join(__dirname, "preload.js"),
},
allowRunningInsecureContent: false,
});
@ -165,20 +168,20 @@ async function writeFileSafe(filename, contents) {
console.warn(prefix, "Concurrent write process on", filename);
}
await fileLock.acquire(filename, async () => {
fileLock.acquire(filename, async () => {
console.log(prefix, "Starting write on", niceFileName(filename), "in transaction", transactionId);
if (!fs.existsSync(filename)) {
// this one is easy
console.log(prefix, "Writing file instantly because it does not exist:", niceFileName(filename));
fs.writeFileSync(filename, contents, { encoding: "utf8" });
await fs.promises.writeFile(filename, contents, "utf8");
return;
}
// first, write a temporary file (.tmp-XXX)
const tempName = filename + ".tmp-" + transactionId;
console.log(prefix, "Writing temporary file", niceFileName(tempName));
fs.writeFileSync(tempName, contents, { encoding: "utf8" });
await fs.promises.writeFile(tempName, contents, "utf8");
// now, rename the original file to (.backup-XXX)
const oldTemporaryName = filename + ".backup-" + transactionId;
@ -189,7 +192,7 @@ async function writeFileSafe(filename, contents) {
"to",
niceFileName(oldTemporaryName)
);
fs.renameSync(filename, oldTemporaryName);
await fs.promises.rename(filename, oldTemporaryName);
// now, rename the temporary file (.tmp-XXX) to the target
console.log(
@ -199,7 +202,7 @@ async function writeFileSafe(filename, contents) {
"to the original",
niceFileName(filename)
);
fs.renameSync(tempName, filename);
await fs.promises.rename(tempName, filename);
// we are done now, try to create a backup, but don't fail if the backup fails
try {
@ -208,82 +211,43 @@ async function writeFileSafe(filename, contents) {
if (fs.existsSync(backupFileName)) {
console.log(prefix, "Deleting old backup file", niceFileName(backupFileName));
// delete the old backup
fs.unlinkSync(backupFileName);
await fs.promises.unlink(backupFileName);
}
// rename the old file to the new backup file
console.log(prefix, "Moving", niceFileName(oldTemporaryName), "to the backup file location");
fs.renameSync(oldTemporaryName, backupFileName);
await fs.promises.rename(oldTemporaryName, backupFileName);
} catch (ex) {
console.error(prefix, "Failed to switch backup files:", ex);
}
});
}
async function performFsJob(job) {
const fname = path.join(storePath, job.filename);
ipcMain.handle("fs-job", async (event, job) => {
const filenameSafe = job.filename.replace(/[^a-z\.\-_0-9]/i, "");
const fname = path.join(storePath, filenameSafe);
switch (job.type) {
case "read": {
if (!fs.existsSync(fname)) {
return {
// Special FILE_NOT_FOUND error code
error: "file_not_found",
};
}
try {
const data = fs.readFileSync(fname, { encoding: "utf8" });
return {
success: true,
data,
};
} catch (ex) {
console.error(ex);
return {
error: ex,
};
// Special FILE_NOT_FOUND error code
return { error: "file_not_found" };
}
return await fs.promises.readFile(fname, "utf8");
}
case "write": {
try {
writeFileSafe(fname, job.contents);
return {
success: true,
data: job.contents,
};
} catch (ex) {
console.error(ex);
return {
error: ex,
};
}
await writeFileSafe(fname, job.contents);
return job.contents;
}
case "delete": {
try {
fs.unlinkSync(fname);
} catch (ex) {
console.error(ex);
return {
error: ex,
};
}
return {
success: true,
data: null,
};
await fs.promises.unlink(fname);
return;
}
default:
throw new Error("Unkown fs job: " + job.type);
throw new Error("Unknown fs job: " + job.type);
}
}
ipcMain.on("fs-job", async (event, arg) => {
const result = await performFsJob(arg);
event.sender.send("fs-response", { id: arg.id, result });
});
wegame.init(isDev);
wegame.listen();

@ -146,7 +146,7 @@ gulp.task("main.webserver", () => {
*/
function serve({ version = "web" }) {
browserSync.init({
server: buildFolder,
server: [buildFolder, path.join(baseDir, "mod_examples")],
port: 3005,
ghostMode: {
clicks: false,

@ -0,0 +1,3 @@
module.exports = function (source, map) {
return source + `\nexport let $s=(n,v)=>eval(n+"=v")`;
};

@ -46,6 +46,7 @@
"postcss": ">=5.0.0",
"promise-polyfill": "^8.1.0",
"query-string": "^6.8.1",
"raw-loader": "^4.0.2",
"rusha": "^0.8.13",
"serialize-error": "^3.0.0",
"stream-browserify": "^3.0.0",

@ -143,7 +143,7 @@ function gulptasksStandalone($, gulp) {
packager({
dir: tempDestBuildDir,
appCopyright: "Tobias Springer",
appCopyright: "tobspr Games",
appVersion: getVersion(),
buildVersion: "1.0.0",
arch,

@ -71,6 +71,7 @@ module.exports = ({ watch = false, standalone = false, chineseVersion = false, w
type: "javascript/auto",
},
{ test: /\.(png|jpe?g|svg)$/, loader: "ignore-loader" },
{ test: /\.nobuild/, loader: "ignore-loader" },
{
test: /\.md$/,
use: [
@ -92,6 +93,9 @@ module.exports = ({ watch = false, standalone = false, chineseVersion = false, w
end: "typehints:end",
},
},
{
loader: path.resolve(__dirname, "mod.js"),
},
],
},
{

@ -145,7 +145,7 @@ module.exports = ({
braces: false,
ecma: es6 ? 6 : 5,
preamble:
"/* shapez.io Codebase - Copyright 2020 Tobias Springer - " +
"/* shapez.io Codebase - Copyright 2022 tobspr Games - " +
getVersion() +
" @ " +
getRevision() +
@ -177,6 +177,7 @@ module.exports = ({
type: "javascript/auto",
},
{ test: /\.(png|jpe?g|svg)$/, loader: "ignore-loader" },
{ test: /\.nobuild/, loader: "ignore-loader" },
{
test: /\.js$/,
enforce: "pre",

@ -989,6 +989,11 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/json-schema@^7.0.8":
version "7.0.9"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz"
@ -1237,6 +1242,11 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1:
resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz"
integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
ajv-keywords@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
ajv@^4.7.0:
version "4.11.8"
resolved "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz"
@ -1255,6 +1265,16 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.5.5, ajv@^6.9.1:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^6.12.5:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
alphanum-sort@^1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz"
@ -7407,6 +7427,15 @@ loader-utils@^1.0.0, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4
emojis-list "^3.0.0"
json5 "^1.0.1"
loader-utils@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
json5 "^2.1.2"
localtunnel@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.0.tgz"
@ -10227,6 +10256,14 @@ raw-body@^2.3.2:
iconv-lite "0.4.24"
unpipe "1.0.0"
raw-loader@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6"
integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==
dependencies:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
rcedit@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/rcedit/-/rcedit-2.0.0.tgz"
@ -10869,6 +10906,15 @@ schema-utils@^2.6.5:
ajv "^6.12.0"
ajv-keywords "^3.4.1"
schema-utils@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==
dependencies:
"@types/json-schema" "^7.0.8"
ajv "^6.12.5"
ajv-keywords "^3.5.2"
seek-bzip@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz"

@ -0,0 +1,59 @@
# shapez.io Modding
## General Instructions
Currently there are two options to develop mods for shapez.io:
1. Writing single file mods, which doesn't require any additional tools and can be loaded directly in the game
2. Using the [create-shapezio-mod](https://www.npmjs.com/package/create-shapezio-mod) package. This package is still in development but allows you to pack multiple files and images into a single mod file, so you don't have to base64 encode your images etc.
Since the `create-shapezio-mod` package is still in development, the current recommended way is to write single file mods, which I'll explain now.
## Mod Developer Discord
A great place to get help with mod development is the official [shapez.io modloader discord](https://discord.gg/xq5v8uyMue).
## Setting up your development environment
The simplest way of developing mods is by just creating a `mymod.js` file and putting it in the `mods/` folder of the standalone (You can find the `mods/` folder by clicking "Open Mods Folder" in the shapez.io Standalone, be sure to select the 1.5.0-modloader branch on Steam).
You can then add `--dev` to the launch options on Steam. This adds an application menu where you can click "Restart" to reload your mod, and will also show the developer console where you can see any potential errors.
## Getting started
To get into shapez.io modding, I highly recommend checking out all of the examples in this folder. Here's a list of examples and what features of the modloader they show:
| Example | Description | Demonstrates |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| [base.js](base.js) | The most basic mod | Base structure of a mod |
| [class_extensions.js](class_extensions.js) | Shows how to extend multiple methods of one class at once, useful for overriding a lot of methods | Overriding and extending builtin methods |
| [custom_css.js](custom_css.js) | Modifies the Main Menu State look | Modifying the UI styles with CSS |
| [replace_builtin_sprites.js](replace_builtin_sprites.js) | Replaces all color sprites with icons | Replacing builtin sprites |
| [translations.js](translations.js) | Shows how to replace and add new translations in multiple languages | Adding and replacing translations |
| [add_building_basic.js](add_building_basic.js) | Shows how to add a new building | Registering a new building |
| [add_building_flipper.js](add_building_flipper.js) | Adds a "flipper" building which mirrors shapes from top to bottom | Registering a new building, Adding a custom shape and item processing operation (flip) |
| [custom_drawing.js](custom_drawing.js) | Displays a a small indicator on every item processing building whether it is currently working | Adding a new GameSystem and drawing overlays |
| [custom_keybinding.js](custom_keybinding.js) | Adds a new customizable ingame keybinding (Shift+F) | Adding a new keybinding |
| [custom_sub_shapes.js](custom_sub_shapes.js) | Adds a new type of sub-shape (Line) | Adding a new sub shape and drawing it, making it spawn on the map, modifying the builtin levels |
| [modify_theme.js](modify_theme.js) | Modifies the default game themes | Modifying the builtin themes |
| [custom_theme.js](custom_theme.js) | Adds a new UI and map theme | Adding a new game theme |
| [mod_settings.js](mod_settings.js) | Shows a dialog counting how often the mod has been launched | Reading and storing mod settings |
| [storing_data_in_savegame.js](storing_data_in_savegame.js) | Shows how to store custom (structured) data in the savegame | Storing custom data in savegame |
| [modify_existing_building.js](modify_existing_building.js) | Makes the rotator building always unlocked and adds a new statistic to the building panel | Modifying a builtin building, replacing builtin methods |
| [modify_ui.js](modify_ui.js) | Shows how to add custom IU elements to builtin game states (the Main Menu in this case) | Extending builtin UI states, Adding CSS |
| [pasting.js](pasting.js) | Shows a dialog when pasting text in the game | Listening to paste events |
| [sandbox.js](sandbox.js) | Makes blueprints free and always unlocked | Overriding builtin methods |
### Advanced Examples
| Example | Description | Demonstrates |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [notification_blocks.js](notification_blocks.js) | Adds a notification block building, which shows a user defined notification when receiving a truthy signal | Adding a new Component, Adding a new GameSystem, Working with wire networks, Adding a new building, Adding a new HUD part, Using Input Dialogs, Adding Translations |
| [usage_statistics.js](usage_statistics.js) | Displays a percentage on every building showing its utilization | Adding a new component, Adding a new GameSystem, Drawing within a GameSystem, Modifying builtin buildings, Adding custom game logic |
| [new_item_type.js](new_item_type.js) | Adds a new type of items to the map (fluids) | Adding a new item type, modifying map generation |
| [buildings_have_cost.js](buildings_have_cost.js) | Adds a new currency, and belts cost 1 of that currency | Extending and replacing builtin methods, Adding CSS and custom sprites |
| [mirrored_cutter.js](mirrored_cutter.js) | Adds a mirorred variant of the cutter | Adding a new variant to existing buildings |
### Creating new sprites
If you want to add new buildings and create sprites for them, you can download the original Photoshop PSD files here: https://static.shapez.io/building-psds.zip

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,20 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Base",
version: "1",
id: "base",
description: "The most basic mod",
minimumGameVersion: ">=1.5.0",
// You can specify this parameter if savegames will still work
// after your mod has been uninstalled
doesNotAffectSavegame: true,
};
class Mod extends shapez.Mod {
init() {
// Start the modding here
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,32 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Class Extensions",
version: "1",
id: "class-extensions",
description: "Shows how to extend builtin classes",
minimumGameVersion: ">=1.5.0",
};
const BeltExtension = ({ $super, $old }) => ({
getShowWiresLayerPreview() {
// Access the old method
return !$old.getShowWiresLayerPreview();
},
getIsReplaceable() {
// Instead of super, use $super
return $super.getIsReplaceable.call(this);
},
getIsRemoveable() {
return false;
},
});
class Mod extends shapez.Mod {
init() {
this.modInterface.extendClass(shapez.MetaBeltBuilding, BeltExtension);
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,63 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: custom drawing",
version: "1",
id: "base",
description: "Displays an indicator on every item processing building when its working",
minimumGameVersion: ">=1.5.0",
// You can specify this parameter if savegames will still work
// after your mod has been uninstalled
doesNotAffectSavegame: true,
};
class ItemProcessorStatusGameSystem extends shapez.GameSystem {
drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
const processorComp = entity.components.ItemProcessor;
if (!processorComp) {
continue;
}
const staticComp = entity.components.StaticMapEntity;
const context = parameters.context;
const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace();
// Culling for better performance
if (parameters.visibleRect.containsCircle(center.x, center.y, 40)) {
// Circle
context.fillStyle = processorComp.ongoingCharges.length === 0 ? "#aaa" : "#53cf47";
context.strokeStyle = "#000";
context.lineWidth = 1;
context.beginCircle(center.x + 5, center.y + 5, 4);
context.fill();
context.stroke();
}
}
}
}
class Mod extends shapez.Mod {
init() {
// Register our game system
this.modInterface.registerGameSystem({
id: "item_processor_status",
systemClass: ItemProcessorStatusGameSystem,
// Specify at which point the update method will be called,
// in this case directly before the belt system. You can use
// before: "end" to make it the last system
before: "belt",
// Specify where our drawChunk method should be called, check out
// map_chunk_view
drawHooks: ["staticAfter"],
});
}
}

@ -0,0 +1,32 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Custom Keybindings",
version: "1",
id: "base",
description: "Shows how to add a new keybinding",
minimumGameVersion: ">=1.5.0",
// You can specify this parameter if savegames will still work
// after your mod has been uninstalled
doesNotAffectSavegame: true,
};
class Mod extends shapez.Mod {
init() {
// Register keybinding
this.modInterface.registerIngameKeybinding({
id: "demo_mod_binding",
keyCode: shapez.keyToKeyCode("F"),
translation: "Do something (always with SHIFT)",
modifiers: {
shift: true,
},
handler: root => {
this.dialogs.showInfo("Mod Message", "It worked!");
return shapez.STOP_PROPAGATION;
},
});
}
}

@ -0,0 +1,46 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Custom Sub Shapes",
version: "1",
id: "custom-sub-shapes",
description: "Shows how to add custom sub shapes",
minimumGameVersion: ">=1.5.0",
};
class Mod extends shapez.Mod {
init() {
// Add a new type of sub shape ("Line", short code "L")
this.modInterface.registerSubShapeType({
id: "line",
shortCode: "L",
// Make it spawn on the map
weightComputation: distanceToOriginInChunks =>
Math.round(20 + Math.max(Math.min(distanceToOriginInChunks, 30), 0)),
// This defines how to draw it
draw: ({ context, quadrantSize, layerScale }) => {
const quadrantHalfSize = quadrantSize / 2;
context.beginPath();
context.moveTo(-quadrantHalfSize, quadrantHalfSize);
context.arc(
-quadrantHalfSize,
quadrantHalfSize,
quadrantSize * layerScale,
-Math.PI * 0.25,
0
);
context.closePath();
context.fill();
context.stroke();
},
});
// Modify the goal of the first level to add our goal
this.signals.modifyLevelDefinitions.add(definitions => {
definitions[0].shape = "LuLuLuLu";
});
}
}

@ -0,0 +1,99 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Custom Game Theme",
version: "1",
id: "custom-theme",
description: "Shows how to add a custom game theme",
minimumGameVersion: ">=1.5.0",
// You can specify this parameter if savegames will still work
// after your mod has been uninstalled
doesNotAffectSavegame: true,
};
class Mod extends shapez.Mod {
init() {
this.modInterface.registerGameTheme({
id: "my-theme",
name: "My fancy theme",
theme: RESOURCES["my-theme.json"],
});
}
}
const RESOURCES = {
"my-theme.json": {
map: {
background: "#abc",
grid: "#ccc",
gridLineWidth: 1,
selectionOverlay: "rgba(74, 163, 223, 0.7)",
selectionOutline: "rgba(74, 163, 223, 0.5)",
selectionBackground: "rgba(74, 163, 223, 0.2)",
chunkBorders: "rgba(0, 30, 50, 0.03)",
directionLock: {
regular: {
color: "rgb(74, 237, 134)",
background: "rgba(74, 237, 134, 0.2)",
},
wires: {
color: "rgb(74, 237, 134)",
background: "rgba(74, 237, 134, 0.2)",
},
error: {
color: "rgb(255, 137, 137)",
background: "rgba(255, 137, 137, 0.2)",
},
},
colorBlindPickerTile: "rgba(50, 50, 50, 0.4)",
resources: {
shape: "#eaebec",
red: "#ffbfc1",
green: "#cbffc4",
blue: "#bfdaff",
},
chunkOverview: {
empty: "#a6afbb",
filled: "#c5ccd6",
beltColor: "#777",
},
wires: {
overlayColor: "rgba(97, 161, 152, 0.75)",
previewColor: "rgb(97, 161, 152, 0.4)",
highlightColor: "rgba(72, 137, 255, 1)",
},
connectedMiners: {
overlay: "rgba(40, 50, 60, 0.5)",
textColor: "#fff",
textColorCapped: "#ef5072",
background: "rgba(40, 50, 60, 0.8)",
},
zone: {
borderSolid: "rgba(23, 192, 255, 1)",
outerColor: "rgba(240, 240, 255, 0.5)",
},
},
items: {
outline: "#55575a",
outlineWidth: 0.75,
circleBackground: "rgba(40, 50, 65, 0.1)",
},
shapeTooltip: {
background: "#dee1ea",
outline: "#54565e",
},
},
};

File diff suppressed because one or more lines are too long

@ -0,0 +1,32 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Mod Settings",
version: "1",
id: "mod-settings",
description: "Shows how to add settings to your mod",
minimumGameVersion: ">=1.5.0",
settings: {
timesLaunched: 0,
},
};
class Mod extends shapez.Mod {
init() {
// Increment the setting every time we launch the mod
this.settings.timesLaunched++;
this.saveSettings();
// Show a dialog in the main menu with the settings
this.signals.stateEntered.add(state => {
if (state instanceof shapez.MainMenuState) {
this.dialogs.showInfo(
"Welcome back",
`You have launched this mod ${this.settings.timesLaunched} times`
);
}
});
}
}

@ -0,0 +1,27 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Modify existing building",
version: "1",
id: "modify-existing-building",
description: "Shows how to modify an existing building",
minimumGameVersion: ">=1.5.0",
};
class Mod extends shapez.Mod {
init() {
// Make Rotator always unlocked
this.modInterface.replaceMethod(shapez.MetaRotaterBuilding, "getIsUnlocked", function () {
return true;
});
// Add some custom stats to the info panel when selecting the building
this.modInterface.replaceMethod(shapez.MetaRotaterBuilding, "getAdditionalStatistics", function (
root,
variant
) {
return [["Awesomeness", 5]];
});
}
}

@ -0,0 +1,24 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Modify Builtin Themes",
version: "1",
id: "modify-theme",
description: "Shows how to modify builtin themes",
minimumGameVersion: ">=1.5.0",
// You can specify this parameter if savegames will still work
// after your mod has been uninstalled
doesNotAffectSavegame: true,
};
class Mod extends shapez.Mod {
init() {
shapez.THEMES.light.map.background = "#eee";
shapez.THEMES.light.items.outline = "#000";
shapez.THEMES.dark.map.background = "#245";
shapez.THEMES.dark.items.outline = "#fff";
}
}

@ -0,0 +1,46 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Modify UI",
version: "1",
id: "modify-ui",
description: "Shows how to modify a builtin game state, in this case the main menu",
minimumGameVersion: ">=1.5.0",
// You can specify this parameter if savegames will still work
// after your mod has been uninstalled
doesNotAffectSavegame: true,
};
class Mod extends shapez.Mod {
init() {
// Add fancy sign to main menu
this.signals.stateEntered.add(state => {
if (state.key === "MainMenuState") {
const element = document.createElement("div");
element.id = "demo_mod_hello_world_element";
document.body.appendChild(element);
const button = document.createElement("button");
button.classList.add("styledButton");
button.innerText = "Hello!";
button.addEventListener("click", () => {
this.dialogs.showInfo("Mod Message", "Button clicked!");
});
element.appendChild(button);
}
});
this.modInterface.registerCss(`
#demo_mod_hello_world_element {
position: absolute;
top: calc(10px * var(--ui-scale));
left: calc(10px * var(--ui-scale));
color: red;
z-index: 0;
}
`);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,23 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Pasting",
version: "1",
id: "pasting",
description: "Shows how to properly receive paste events ingame",
minimumGameVersion: ">=1.5.0",
};
class Mod extends shapez.Mod {
init() {
this.signals.gameInitialized.add(root => {
root.gameState.inputReciever.paste.add(event => {
event.preventDefault();
const data = event.clipboardData.getData("text");
this.dialogs.showInfo("Pasted", `You pasted: '${data}'`);
});
});
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,21 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Sandbox",
version: "1",
id: "sandbox",
description: "Blueprints are always unlocked and cost no money, also all buildings are unlocked",
minimumGameVersion: ">=1.5.0",
};
class Mod extends shapez.Mod {
init() {
this.modInterface.replaceMethod(shapez.Blueprint, "getCost", function () {
return 0;
});
this.modInterface.replaceMethod(shapez.HubGoals, "isRewardUnlocked", function () {
return true;
});
}
}

@ -0,0 +1,78 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Storing Data in Savegame",
version: "1",
id: "storing-savegame-data",
description: "Shows how to add custom data to a savegame",
minimumGameVersion: ">=1.5.0",
};
class Mod extends shapez.Mod {
init() {
////////////////////////////////////////////////////////////////////
// Option 1: For simple data
this.signals.gameSerialized.add((root, data) => {
data.modExtraData["storing-savegame-data"] = Math.random();
});
this.signals.gameDeserialized.add((root, data) => {
alert("The value stored in the savegame was: " + data.modExtraData["storing-savegame-data"]);
});
////////////////////////////////////////////////////////////////////
// Option 2: If you need a structured way of storing data
class SomeSerializableObject extends shapez.BasicSerializableObject {
static getId() {
return "SomeSerializableObject";
}
static getSchema() {
return {
someInt: shapez.types.int,
someString: shapez.types.string,
someVector: shapez.types.vector,
// this value is allowed to be null
nullableInt: shapez.types.nullable(shapez.types.int),
// There is a lot more .. be sure to checkout src/js/savegame/serialization.js
// You can have maps, classes, arrays etc..
// And if you need something specific you can always ask in the modding discord.
};
}
constructor() {
super();
this.someInt = 42;
this.someString = "Hello World";
this.someVector = new shapez.Vector(1, 2);
this.nullableInt = null;
}
}
// Store our object in the global game root
this.signals.gameInitialized.add(root => {
root.myObject = new SomeSerializableObject();
});
// Save it within the savegame
this.signals.gameSerialized.add((root, data) => {
data.modExtraData["storing-savegame-data-2"] = root.myObject.serialize();
});
// Restore it when the savegame is loaded
this.signals.gameDeserialized.add((root, data) => {
const errorText = root.myObject.deserialize(data.modExtraData["storing-savegame-data-2"]);
if (errorText) {
alert("Mod failed to deserialize from savegame: " + errorText);
}
alert("The other value stored in the savegame (option 2) was " + root.myObject.someInt);
});
////////////////////////////////////////////////////////////////////
}
}

@ -0,0 +1,66 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Translations",
version: "1",
id: "translations",
description: "Shows how to add and modify translations",
minimumGameVersion: ">=1.5.0",
// You can specify this parameter if savegames will still work
// after your mod has been uninstalled
doesNotAffectSavegame: true,
};
class Mod extends shapez.Mod {
init() {
// Replace an existing translation in the english language
this.modInterface.registerTranslations("en", {
ingame: {
interactiveTutorial: {
title: "Hello",
hints: {
"1_1_extractor": "World!",
},
},
},
});
// Replace an existing translation in german
this.modInterface.registerTranslations("de", {
ingame: {
interactiveTutorial: {
title: "Hallo",
hints: {
"1_1_extractor": "Welt!",
},
},
},
});
// Add an entirely new translation which is localized in german and english
this.modInterface.registerTranslations("en", {
mods: {
mymod: {
test: "Test Translation",
},
},
});
this.modInterface.registerTranslations("de", {
mods: {
mymod: {
test: "Test Übersetzung",
},
},
});
// Show a dialog in the main menu
this.signals.stateEntered.add(state => {
if (state instanceof shapez.MainMenuState) {
// Will show differently based on the selected language
this.dialogs.showInfo("My translation", shapez.T.mods.mymod.test);
}
});
}
}

@ -0,0 +1,148 @@
// @ts-nocheck
const METADATA = {
website: "https://tobspr.io",
author: "tobspr",
name: "Mod Example: Usage Statistics",
version: "1",
id: "usage-statistics",
description:
"Shows how to add a new component to the game, how to save additional data and how to add custom logic and drawings",
minimumGameVersion: ">=1.5.0",
};
/**
* Quick info on how this mod works:
*
* It tracks how many ticks a building was idle within X seconds to compute
* the usage percentage.
*
* Every tick the logic checks if the building is idle, if so, it increases aggregatedIdleTime.
* Once X seconds are over, the aggregatedIdleTime is copied to computedUsage which
* is displayed on screen via the UsageStatisticsSystem
*/
const MEASURE_INTERVAL_SECONDS = 5;
class UsageStatisticsComponent extends shapez.Component {
static getId() {
return "UsageStatistics";
}
static getSchema() {
// Here you define which properties should be saved to the savegame
// and get automatically restored
return {
lastTimestamp: shapez.types.float,
computedUsage: shapez.types.float,
aggregatedIdleTime: shapez.types.float,
};
}
constructor() {
super();
this.lastTimestamp = 0;
this.computedUsage = 0;
this.aggregatedIdleTime = 0;
}
}
class UsageStatisticsSystem extends shapez.GameSystemWithFilter {
constructor(root) {
// By specifying the list of components, `this.allEntities` will only
// contain entities which have *all* of the specified components
super(root, [UsageStatisticsComponent, shapez.ItemProcessorComponent]);
}
update() {
const now = this.root.time.now();
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const processorComp = entity.components.ItemProcessor;
const usageComp = entity.components.UsageStatistics;
if (now - usageComp.lastTimestamp > MEASURE_INTERVAL_SECONDS) {
usageComp.computedUsage = shapez.clamp(
1 - usageComp.aggregatedIdleTime / MEASURE_INTERVAL_SECONDS
);
usageComp.aggregatedIdleTime = 0;
usageComp.lastTimestamp = now;
}
if (processorComp.ongoingCharges.length === 0) {
usageComp.aggregatedIdleTime += this.root.dynamicTickrate.deltaSeconds;
}
}
}
drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
const usageComp = entity.components.UsageStatistics;
if (!usageComp) {
continue;
}
const staticComp = entity.components.StaticMapEntity;
const context = parameters.context;
const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace();
// Culling for better performance
if (parameters.visibleRect.containsCircle(center.x, center.y, 40)) {
// Background badge
context.fillStyle = "rgba(250, 250, 250, 0.8)";
context.beginRoundedRect(center.x - 10, center.y + 3, 20, 8, 2);
context.fill();
// Text
const usage = usageComp.computedUsage * 100.0;
if (usage > 99.99) {
context.fillStyle = "green";
} else if (usage > 70) {
context.fillStyle = "orange";
} else {
context.fillStyle = "red";
}
context.textAlign = "center";
context.font = "7px GameFont";
context.fillText(Math.round(usage) + "%", center.x, center.y + 10);
}
}
}
}
class Mod extends shapez.Mod {
init() {
// Register the component
this.modInterface.registerComponent(UsageStatisticsComponent);
// Add our new component to all item processor buildings so we can see how many items it processed.
// You can also inspect the entity with F8
const buildings = [
shapez.MetaBalancerBuilding,
shapez.MetaCutterBuilding,
shapez.MetaRotaterBuilding,
shapez.MetaStackerBuilding,
shapez.MetaMixerBuilding,
shapez.MetaPainterBuilding,
];
buildings.forEach(metaClass => {
this.modInterface.runAfterMethod(metaClass, "setupEntityComponents", function (entity) {
entity.addComponent(new UsageStatisticsComponent());
});
});
// Register our game system so we can update and draw stuff
this.modInterface.registerGameSystem({
id: "demo_mod",
systemClass: UsageStatisticsSystem,
before: "belt",
drawHooks: ["staticAfter"],
});
}
}

@ -3,7 +3,7 @@
"version": "1.0.0",
"main": "index.js",
"repository": "https://github.com/tobspr/shapez.io",
"author": "Tobias Springer <tobias.springer1@gmail.com>",
"author": "tobspr Games <hello@tobspr.io>",
"license": "MIT",
"private": true,
"scripts": {
@ -19,7 +19,8 @@
"publishStandalone": "yarn publishOnItch && yarn publishOnSteam",
"publishWeb": "cd gulp && yarn main.deploy.prod",
"publish": "yarn publishStandalone && yarn publishWeb",
"syncTranslations": "node sync-translations.js"
"syncTranslations": "node sync-translations.js",
"buildTypes": "tsc src/js/application.js --declaration --allowJs --emitDeclarationOnly --skipLibCheck --out types.js"
},
"dependencies": {
"@babel/core": "^7.5.4",
@ -55,6 +56,7 @@
"promise-polyfill": "^8.1.0",
"query-string": "^6.8.1",
"rusha": "^0.8.13",
"semver": "^7.3.5",
"serialize-error": "^3.0.0",
"strictdom": "^1.0.1",
"string-replace-webpack-plugin": "^0.1.3",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

@ -169,6 +169,10 @@
margin: 1px 0;
}
h3 {
@include S(margin-top, 10px);
}
input {
background: #eee;
color: #333438;
@ -214,6 +218,33 @@
}
}
}
.dialogModsMod {
background: rgba(0, 0, 0, 0.05);
@include S(padding, 5px);
@include S(margin, 10px, 0);
@include S(border-radius, $globalBorderRadius);
display: grid;
grid-template-columns: 1fr D(100px);
@include DarkThemeOverride {
background: rgba(0, 0, 0, 0.2);
}
button {
grid-column: 2 / 3;
grid-row: 1 / 3;
align-self: start;
}
.version {
@include SuperSmallText;
opacity: 0.5;
}
.name {
}
}
}
> .buttons {

@ -31,6 +31,7 @@
@import "states/mobile_warning";
@import "states/changelog";
@import "states/puzzle_menu";
@import "states/mods";
@import "ingame_hud/buildings_toolbar";
@import "ingame_hud/building_placer";

@ -61,7 +61,8 @@ $buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2,
background-image: uiResource("res/ui/building_tutorials/virtual_processor-cutter.png") !important;
}
$icons: notification_saved, notification_success, notification_upgrade;
$icons: notification_saved, notification_success, notification_upgrade, notification_info,
notification_warning, notification_error;
@each $icon in $icons {
[data-icon="icons/#{$icon}.png"] {
/* @load-async */

@ -185,21 +185,20 @@
.updateLabel {
position: absolute;
transform: translateX(50%) rotate(-5deg);
color: #ff590b;
@include Heading;
color: #fff;
@include PlainText;
font-weight: bold;
@include S(right, 40px);
@include S(bottom, 20px);
background: $modsColor;
@include S(border-radius, $globalBorderRadius);
@include S(padding, 0, 5px, 1px, 5px);
@include InlineAnimation(1.3s ease-in-out infinite) {
50% {
transform: translateX(50%) rotate(-7deg) scale(1.1);
}
}
@include DarkThemeOverride {
color: $colorBlueBright;
}
}
}
@ -290,6 +289,99 @@
}
}
.modsOverview {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: #fff;
grid-row: 1 / 2;
grid-column: 2 / 3;
position: relative;
text-align: left;
align-items: flex-start;
@include S(width, 250px);
@include S(padding, 15px);
@include S(padding-bottom, 10px);
@include S(border-radius, $globalBorderRadius);
.header {
display: flex;
width: 100%;
align-items: center;
@include S(margin-bottom, 10px);
.editMods {
margin-left: auto;
@include S(width, 20px);
@include S(height, 20px);
padding: 0;
opacity: 0.5;
background: transparent center center/ 80% no-repeat;
& {
/* @load-async */
background-image: uiResource("icons/edit_key.png") !important;
}
@include DarkThemeInvert;
}
}
h3 {
@include Heading;
color: $modsColor;
margin: 0;
}
.dlcHint {
@include SuperSmallText;
@include S(margin-top, 10px);
width: 100%;
display: grid;
grid-template-columns: 1fr auto;
grid-gap: 20px;
align-items: center;
}
.mod {
background: #eee;
width: 100%;
@include S(border-radius, $globalBorderRadius);
@include S(padding, 5px);
box-sizing: border-box;
@include PlainText;
@include S(margin-bottom, 5px);
display: flex;
flex-direction: column;
.author,
.version {
@include SuperSmallText;
align-self: end;
opacity: 0.4;
}
.name {
overflow: hidden;
}
}
.modsList {
box-sizing: border-box;
@include S(height, 100px);
@include S(padding, 5px);
border: D(1px) solid #eee;
overflow-y: scroll;
width: 100%;
display: flex;
flex-direction: column;
pointer-events: all;
:last-child {
margin-bottom: auto;
}
}
}
.mainContainer {
display: flex;
align-items: center;
@ -308,6 +400,7 @@
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.modeButtons {
@ -352,27 +445,33 @@
}
}
.importButton {
.outer {
@include S(margin-top, 15px);
@include IncreasedClickArea(0px);
}
.newGameButton {
.importButton {
@include IncreasedClickArea(0px);
@include S(margin-top, 15px);
@include S(margin-left, 15px);
}
.playModeButton {
.newGameButton {
@include IncreasedClickArea(0px);
@include S(margin-top, 15px);
@include S(margin-left, 15px);
@include S(margin-left, 10px);
}
.editModeButton {
.modsButton {
@include IncreasedClickArea(0px);
@include S(margin-top, 15px);
@include S(margin-left, 15px);
@include S(margin-left, 10px);
// @include S(width, 20px);
// & {
// /* @load-async */
// background-image: uiResource("res/ui/icons/mods_white.png") !important;
// }
background-position: center center;
background-size: D(15px);
background-color: $modsColor !important;
background-repeat: no-repeat;
}
.savegames {
@ -736,6 +835,23 @@
}
}
.modsOverview {
background: $darkModeControlsBackground;
.modsList {
border-color: darken($darkModeControlsBackground, 5);
.mod {
background: darken($darkModeControlsBackground, 5);
color: white;
}
}
.dlcHint {
color: $accentColorBright;
}
}
.footer {
> a,
.sidelinks > a {

@ -0,0 +1,141 @@
#state_ModsState {
.mainContent {
display: flex;
flex-direction: column;
}
> .headerBar {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
> h1 {
justify-self: start;
}
.openModsFolder {
background-color: $modsColor;
}
}
.noModSupport {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
.steamLink {
@include S(height, 50px);
@include S(width, 220px);
background: #171a23 center center / contain no-repeat;
overflow: hidden;
display: block;
text-indent: -999em;
cursor: pointer;
@include S(margin-top, 30px);
pointer-events: all;
transition: all 0.12s ease-in;
transition-property: opacity, transform;
@include S(border-radius, $globalBorderRadius);
&:hover {
opacity: 0.9;
}
}
}
.modsStats {
@include PlainText;
color: $accentColorDark;
&.noMods {
@include S(width, 400px);
align-self: center;
justify-self: center;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
@include Text;
@include S(margin-top, 100px);
color: lighten($accentColorDark, 15);
button {
@include S(margin-top, 10px);
@include S(padding, 10px, 20px);
}
&::before {
@include S(margin-bottom, 15px);
content: "";
@include S(width, 50px);
@include S(height, 50px);
background-position: center center;
background-size: contain;
opacity: 0.2;
}
&::before {
/* @load-async */
background-image: uiResource("res/ui/icons/mods.png") !important;
}
}
}
.modsList {
@include S(margin-top, 10px);
overflow-y: scroll;
pointer-events: all;
@include S(padding-right, 5px);
flex-grow: 1;
.mod {
@include S(border-radius, $globalBorderRadius);
background: $accentColorBright;
@include S(margin-bottom, 4px);
@include S(padding, 7px, 10px);
@include S(grid-gap, 15px);
display: grid;
grid-template-columns: 1fr D(100px) D(80px) D(50px);
@include DarkThemeOverride {
background: darken($darkModeControlsBackground, 5);
}
.checkbox {
align-self: center;
justify-self: center;
}
.mainInfo {
display: flex;
flex-direction: column;
.description {
@include SuperSmallText;
@include S(margin-top, 5px);
color: $accentColorDark;
}
.website {
text-transform: uppercase;
align-self: start;
@include SuperSmallText;
@include S(margin-top, 5px);
}
}
.version,
.author {
display: flex;
flex-direction: column;
align-self: center;
strong {
text-transform: uppercase;
color: $accentColorDark;
@include SuperSmallText;
}
}
}
}
}

@ -15,20 +15,21 @@
}
.sidebar {
display: grid;
display: flex;
@include S(min-width, 210px);
@include S(max-width, 320px);
@include S(grid-gap, 3px);
grid-template-rows: auto auto auto auto auto 1fr;
flex-direction: column;
@include StyleBelowWidth($layoutBreak) {
grid-template-rows: 1fr 1fr;
grid-template-columns: auto auto;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
@include S(grid-gap, 5px);
max-width: unset !important;
}
button {
text-align: left;
@include S(margin-bottom, 3px);
&::after {
content: unset;
}
@ -37,15 +38,26 @@
@include StyleBelowWidth($layoutBreak) {
text-align: center;
height: D(30px) !important;
padding: D(5px) !important;
}
}
.other {
@include S(margin-top, 10px);
align-self: end;
margin-top: auto;
@include StyleBelowWidth($layoutBreak) {
margin-top: 0;
display: grid;
grid-template-columns: 1fr 1fr;
@include S(grid-gap, 5px);
max-width: unset !important;
grid-column: 1 / 3;
button {
margin: 0 !important;
}
}
}
@ -69,8 +81,28 @@
}
}
button.privacy {
@include S(margin-top, 4px);
button.manageMods {
background-color: lighten($modsColor, 38);
color: $modsColor;
display: flex;
@include S(padding-right, 5px);
.newBadge {
color: #fff;
@include S(border-radius, $globalBorderRadius);
background: $modsColor;
margin-left: auto;
@include S(padding, 0, 3px, 0, 3px);
@include InlineAnimation(1.3s ease-in-out infinite) {
50% {
transform: rotate(0deg) scale(1.1);
}
}
}
&.active {
background-color: $colorGreenBright;
}
}
.versionbar {

@ -38,6 +38,7 @@ $colorRedBright: #ef5072;
$colorOrangeBright: #ef9d50;
$themeColor: #393747;
$ingameHudBg: rgba(#333438, 0.9);
$modsColor: rgb(214, 60, 228);
$text3dColor: #f4ffff;

@ -15,8 +15,8 @@
<meta name="theme-color" content="#393747" />
<!-- seo -->
<meta name="copyright" content="2020 Tobias Springer IT Solutions and .io Games" />
<meta name="author" content="Tobias Springer, tobias.springer1@gmail.com" />
<meta name="copyright" content="2022 tobspr Games" />
<meta name="author" content="tobspr Games - tobspr.io" />
<meta
name="description"
content="shapez.io is an open-source factory building game about combining and producing different types of shapes."

@ -35,6 +35,9 @@ import { PuzzleMenuState } from "./states/puzzle_menu";
import { ClientAPI } from "./platform/api";
import { LoginState } from "./states/login";
import { WegameSplashState } from "./states/wegame_splash";
import { MODS } from "./mods/modloader";
import { MOD_SIGNALS } from "./mods/mod_signals";
import { ModsState } from "./states/mods";
/**
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
@ -62,10 +65,24 @@ if (typeof document.hidden !== "undefined") {
}
export class Application {
constructor() {
/**
* Boots the application
*/
async boot() {
console.log("Booting ...");
assert(!GLOBAL_APP, "Tried to construct application twice");
logger.log("Creating application, platform =", getPlatformName());
setGlobalApp(this);
MODS.app = this;
// MODS
try {
await MODS.initMods();
} catch (ex) {
alert("Failed to load mods (launch with --dev for more info): \n\n" + ex);
}
this.unloaded = false;
@ -128,6 +145,31 @@ export class Application {
// Store the mouse position, or null if not available
/** @type {Vector|null} */
this.mousePosition = null;
this.registerStates();
this.registerEventListeners();
Loader.linkAppAfterBoot(this);
if (G_WEGAME_VERSION) {
this.stateMgr.moveToState("WegameSplashState");
}
// Check for mobile
else if (IS_MOBILE) {
this.stateMgr.moveToState("MobileWarningState");
} else {
this.stateMgr.moveToState("PreloadState");
}
// Starting rendering
this.ticker.frameEmitted.add(this.onFrameEmitted, this);
this.ticker.bgFrameEmitted.add(this.onBackgroundFrame, this);
this.ticker.start();
window.focus();
MOD_SIGNALS.appBooted.dispatch();
}
/**
@ -167,6 +209,7 @@ export class Application {
ChangelogState,
PuzzleMenuState,
LoginState,
ModsState,
];
for (let i = 0; i < states.length; ++i) {
@ -322,35 +365,6 @@ export class Application {
}
}
/**
* Boots the application
*/
boot() {
console.log("Booting ...");
this.registerStates();
this.registerEventListeners();
Loader.linkAppAfterBoot(this);
if (G_WEGAME_VERSION) {
this.stateMgr.moveToState("WegameSplashState");
}
// Check for mobile
else if (IS_MOBILE) {
this.stateMgr.moveToState("MobileWarningState");
} else {
this.stateMgr.moveToState("PreloadState");
}
// Starting rendering
this.ticker.frameEmitted.add(this.onFrameEmitted, this);
this.ticker.bgFrameEmitted.add(this.onBackgroundFrame, this);
this.ticker.start();
window.focus();
}
/**
* Deinitializes the application
*/

@ -1,4 +1,12 @@
export const CHANGELOG = [
{
version: "1.5.0",
date: "unreleased",
entries: [
"This version adds an official modloader! You can now load mods by placing it in the mods/ folder of the game.",
"When holding shift while placing a belt, the indicator now becomes red when crossing buildings",
],
},
{
version: "1.4.4",
date: "29.08.2021",

@ -9,6 +9,7 @@ import { SOUNDS, MUSIC } from "../platform/sound";
import { AtlasDefinition, atlasFiles } from "./atlas_definitions";
import { initBuildingCodesAfterResourcesLoaded } from "../game/meta_building_registry";
import { cachebust } from "./cachebust";
import { MODS } from "../mods/modloader";
const logger = createLogger("background_loader");

@ -28,6 +28,8 @@ export const THIRDPARTY_URLS = {
25: "https://www.youtube.com/watch?v=7OCV1g40Iew&",
26: "https://www.youtube.com/watch?v=gfm6dS1dCoY",
},
modBrowser: "https://shapez.mod.io/?preview=f55f6304ca4873d9a25f3b575571b948",
};
// export const A_B_TESTING_LINK_TYPE = Math.random() > 0.95 ? "steam_1_pr" : "steam_2_npr";
@ -42,7 +44,7 @@ export const globalConfig = {
// Which dpi the assets have
assetsDpi: 192 / 32,
assetsSharpness: 1.5,
shapesSharpness: 1.4,
shapesSharpness: 1.3,
// Achievements
achievementSliceDuration: 10, // Seconds
@ -61,6 +63,8 @@ export const globalConfig = {
mapChunkOverviewMinZoom: 0.9,
mapChunkWorldSize: null, // COMPUTED
maxBeltShapeBundleSize: 20,
// Belt speeds
// NOTICE: Update webpack.production.config too!
beltSpeedItemsPerSecond: 2,

@ -116,5 +116,11 @@ export default {
// Disables slow asserts, useful for debugging performance
// disableSlowAsserts: true,
// -----------------------------------------------------------------------------------
// Allows to load a mod from an external source for developing it
// externalModUrl: "http://localhost:3005/combined.js",
// -----------------------------------------------------------------------------------
// Visualizes the shape grouping on belts
// showShapeGrouping: true
// -----------------------------------------------------------------------------------
/* dev:end */
};

@ -15,3 +15,20 @@ export function setGlobalApp(app) {
assert(!GLOBAL_APP, "Create application twice!");
GLOBAL_APP = app;
}
export const BUILD_OPTIONS = {
HAVE_ASSERT: G_HAVE_ASSERT,
APP_ENVIRONMENT: G_APP_ENVIRONMENT,
TRACKING_ENDPOINT: G_TRACKING_ENDPOINT,
CHINA_VERSION: G_CHINA_VERSION,
WEGAME_VERSION: G_WEGAME_VERSION,
IS_DEV: G_IS_DEV,
IS_RELEASE: G_IS_RELEASE,
IS_MOBILE_APP: G_IS_MOBILE_APP,
IS_BROWSER: G_IS_BROWSER,
IS_STANDALONE: G_IS_STANDALONE,
BUILD_TIME: G_BUILD_TIME,
BUILD_COMMIT_HASH: G_BUILD_COMMIT_HASH,
BUILD_VERSION: G_BUILD_VERSION,
ALL_UI_IMAGES: G_ALL_UI_IMAGES,
};

@ -149,6 +149,8 @@ export class InputDistributor {
window.addEventListener("mouseup", this.handleKeyMouseUp.bind(this));
window.addEventListener("blur", this.handleBlur.bind(this));
document.addEventListener("paste", this.handlePaste.bind(this));
}
forwardToReceiver(eventId, payload = null) {
@ -186,6 +188,13 @@ export class InputDistributor {
this.keysDown.clear();
}
/**
*
*/
handlePaste(ev) {
this.forwardToReceiver("paste", ev);
}
/**
* @param {KeyboardEvent | MouseEvent} event
*/
@ -211,6 +220,7 @@ export class InputDistributor {
keyCode: keyCode,
shift: event.shiftKey,
alt: event.altKey,
ctrl: event.ctrlKey,
initial: isInitial,
event,
}) === STOP_PROPAGATION

@ -12,12 +12,15 @@ export class InputReceiver {
// Dispatched on destroy
this.destroyed = new Signal();
this.paste = new Signal();
}
cleanup() {
this.backButton.removeAll();
this.keydown.removeAll();
this.keyup.removeAll();
this.paste.removeAll();
this.destroyed.dispatch();
}

@ -169,6 +169,9 @@ class LoaderImpl {
sprite = new AtlasSprite(spriteName);
this.sprites.set(spriteName, sprite);
}
if (sprite.frozen) {
continue;
}
const link = new SpriteAtlasLink({
packedX: frame.x,
@ -181,6 +184,7 @@ class LoaderImpl {
w: sourceSize.w,
h: sourceSize.h,
});
sprite.linksByResolution[scale] = link;
}
}

@ -90,8 +90,9 @@ export class Dialog {
* @param {number} param0.keyCode
* @param {boolean} param0.shift
* @param {boolean} param0.alt
* @param {boolean} param0.ctrl
*/
handleKeydown({ keyCode, shift, alt }) {
handleKeydown({ keyCode, shift, alt, ctrl }) {
if (keyCode === kbEnter && this.enterHandler) {
this.internalButtonHandler(this.enterHandler);
return STOP_PROPAGATION;

@ -1,7 +1,6 @@
import { BaseItem } from "../game/base_item";
import { ClickDetector } from "./click_detector";
import { Signal } from "./signal";
import { getIPCRenderer } from "./utils";
/*
* ***************************************************
@ -113,13 +112,11 @@ export class FormElementInput extends FormElement {
if (G_WEGAME_VERSION) {
const value = String(this.element.value);
getIPCRenderer()
.invoke("profanity-check", value)
.then(newValue => {
if (value !== newValue && this.element) {
this.element.value = newValue;
}
});
ipcRenderer.invoke("profanity-check", value).then(newValue => {
if (value !== newValue && this.element) {
this.element.value = newValue;
}
});
}
}

@ -71,6 +71,8 @@ export class AtlasSprite extends BaseSprite {
/** @type {Object.<string, SpriteAtlasLink>} */
this.linksByResolution = {};
this.spriteName = spriteName;
this.frozen = false;
}
getRawTexture() {

@ -6,6 +6,7 @@ import { GameState } from "./game_state";
import { createLogger } from "./logging";
import { APPLICATION_ERROR_OCCURED } from "./error_handler";
import { waitNextFrame, removeAllChildren } from "./utils";
import { MOD_SIGNALS } from "../mods/mod_signals";
const logger = createLogger("state_manager");
@ -109,6 +110,8 @@ export class StateManager {
key
);
MOD_SIGNALS.stateEntered.dispatch(this.currentState);
waitNextFrame().then(() => {
document.body.classList.add("arrived");
});

@ -42,21 +42,6 @@ export function getPlatformName() {
return "unknown";
}
/**
* Returns the IPC renderer, or null if not within the standalone
* @returns {object|null}
*/
let ipcRenderer = null;
export function getIPCRenderer() {
if (!G_IS_STANDALONE) {
return null;
}
if (!ipcRenderer) {
ipcRenderer = eval("require")("electron").ipcRenderer;
}
return ipcRenderer;
}
/**
* Makes a new 2D array with undefined contents
* @param {number} w
@ -395,7 +380,7 @@ export function clamp(v, minimum = 0, maximum = 1) {
* @param {Array<string>=} classes
* @param {string=} innerHTML
*/
function makeDivElement(id = null, classes = [], innerHTML = "") {
export function makeDivElement(id = null, classes = [], innerHTML = "") {
const div = document.createElement("div");
if (id) {
div.id = id;

@ -2,9 +2,6 @@ import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters";
import { BasicSerializableObject } from "../savegame/serialization";
/** @type {ItemType[]} **/
export const itemTypes = ["shape", "color", "boolean"];
/**
* Class for items on belts etc. Not an entity for performance reasons
*/

@ -1,7 +1,9 @@
import { globalConfig } from "../core/config";
import { smoothenDpi } from "../core/dpi_manager";
import { DrawParameters } from "../core/draw_parameters";
import { createLogger } from "../core/logging";
import { Rectangle } from "../core/rectangle";
import { ORIGINAL_SPRITE_SCALE } from "../core/sprites";
import { clamp, epsilonCompare, round4Digits } from "../core/utils";
import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector";
import { BasicSerializableObject, types } from "../savegame/serialization";
@ -1430,6 +1432,12 @@ export class BeltPath extends BasicSerializableObject {
let trackPos = 0.0;
/**
* @type {Array<[Vector, BaseItem]>}
*/
let drawStack = [];
let drawStackProp = "";
// Iterate whole track and check items
for (let i = 0; i < this.entityPath.length; ++i) {
const entity = this.entityPath[i];
@ -1449,25 +1457,185 @@ export class BeltPath extends BasicSerializableObject {
const worldPos = staticComp.localTileToWorld(localPos).toWorldSpaceCenterOfTile();
const distanceAndItem = this.items[currentItemIndex];
const item = distanceAndItem[1 /* item */];
const nextItemDistance = distanceAndItem[0 /* nextDistance */];
distanceAndItem[1 /* item */].drawItemCenteredClipped(
worldPos.x,
worldPos.y,
parameters,
globalConfig.defaultItemDiameter
);
if (
!parameters.visibleRect.containsCircle(
worldPos.x,
worldPos.y,
globalConfig.defaultItemDiameter
)
) {
// this one isn't visible, do not append it
// Start a new stack
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
} else {
if (drawStack.length > 1) {
// Check if we can append to the stack, since its already a stack of two same items
const referenceItem = drawStack[0];
if (Math.abs(referenceItem[0][drawStackProp] - worldPos[drawStackProp]) < 0.001) {
// Will continue stack
} else {
// Start a new stack, since item doesn't follow in row
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
}
} else if (drawStack.length === 1) {
const firstItem = drawStack[0];
// Check if we can make it a stack
if (firstItem[1 /* item */].equals(item)) {
// Same item, check if it is either horizontal or vertical
const startPos = firstItem[0 /* pos */];
if (Math.abs(startPos.x - worldPos.x) < 0.001) {
drawStackProp = "x";
} else if (Math.abs(startPos.y - worldPos.y) < 0.001) {
drawStackProp = "y";
} else {
// Start a new stack
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
}
} else {
// Start a new stack, since item doesn't equal
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
}
} else {
// First item of stack, do nothing
}
drawStack.push([worldPos, item]);
}
// Check for the next item
currentItemPos += distanceAndItem[0 /* nextDistance */];
currentItemPos += nextItemDistance;
++currentItemIndex;
if (
nextItemDistance > globalConfig.itemSpacingOnBelts + 0.001 ||
drawStack.length > globalConfig.maxBeltShapeBundleSize
) {
// If next item is not directly following, abort drawing
this.drawDrawStack(drawStack, parameters, drawStackProp);
drawStack = [];
drawStackProp = "";
}
if (currentItemIndex >= this.items.length) {
// We rendered all items
this.drawDrawStack(drawStack, parameters, drawStackProp);
return;
}
}
trackPos += beltLength;
}
this.drawDrawStack(drawStack, parameters, drawStackProp);
}
/**
*
* @param {HTMLCanvasElement} canvas
* @param {CanvasRenderingContext2D} context
* @param {number} w
* @param {number} h
* @param {number} dpi
* @param {object} param0
* @param {string} param0.direction
* @param {Array<[Vector, BaseItem]>} param0.stack
* @param {GameRoot} param0.root
* @param {number} param0.zoomLevel
*/
drawShapesInARow(canvas, context, w, h, dpi, { direction, stack, root, zoomLevel }) {
context.scale(dpi, dpi);
if (G_IS_DEV && globalConfig.debug.showShapeGrouping) {
context.fillStyle = "rgba(0, 0, 255, 0.5)";
context.fillRect(0, 0, w, h);
}
const parameters = new DrawParameters({
context,
desiredAtlasScale: ORIGINAL_SPRITE_SCALE,
root,
visibleRect: new Rectangle(-1000, -1000, 2000, 2000),
zoomLevel,
});
const itemSize = globalConfig.itemSpacingOnBelts * globalConfig.tileSize;
const item = stack[0];
const pos = new Vector(itemSize / 2, itemSize / 2);
for (let i = 0; i < stack.length; i++) {
item[1].drawItemCenteredClipped(pos.x, pos.y, parameters, globalConfig.defaultItemDiameter);
pos[direction] += globalConfig.itemSpacingOnBelts * globalConfig.tileSize;
}
}
/**
* @param {Array<[Vector, BaseItem]>} stack
* @param {DrawParameters} parameters
*/
drawDrawStack(stack, parameters, directionProp) {
if (stack.length === 0) {
return;
}
const firstItem = stack[0];
const firstItemPos = firstItem[0];
if (stack.length === 1) {
firstItem[1].drawItemCenteredClipped(
firstItemPos.x,
firstItemPos.y,
parameters,
globalConfig.defaultItemDiameter
);
return;
}
const itemSize = globalConfig.itemSpacingOnBelts * globalConfig.tileSize;
const inverseDirection = directionProp === "x" ? "y" : "x";
const dimensions = new Vector(itemSize, itemSize);
dimensions[inverseDirection] *= stack.length;
const directionVector = firstItemPos.copy().sub(stack[1][0]);
const dpi = smoothenDpi(globalConfig.shapesSharpness * parameters.zoomLevel);
const sprite = this.root.buffers.getForKey({
key: "beltpaths",
subKey: "stack-" + directionProp + "-" + dpi + "-" + stack.length + firstItem[1].serialize(),
dpi,
w: dimensions.x,
h: dimensions.y,
redrawMethod: this.drawShapesInARow.bind(this),
additionalParams: {
direction: inverseDirection,
stack,
root: this.root,
zoomLevel: parameters.zoomLevel,
},
});
const anchor = directionVector[inverseDirection] < 0 ? firstItem : stack[stack.length - 1];
parameters.context.drawImage(
sprite,
anchor[0].x - itemSize / 2,
anchor[0].y - itemSize / 2,
dimensions.x,
dimensions.y
);
}
}

@ -82,7 +82,7 @@ export class Blueprint {
const rect = staticComp.getTileSpaceBounds();
rect.moveBy(tile.x, tile.y);
if (!parameters.root.logic.checkCanPlaceEntity(entity, tile)) {
if (!parameters.root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
parameters.context.globalAlpha = 0.3;
} else {
parameters.context.globalAlpha = 1;
@ -131,7 +131,7 @@ export class Blueprint {
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
if (root.logic.checkCanPlaceEntity(entity, tile)) {
if (root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
anyPlaceable = true;
}
}
@ -160,7 +160,7 @@ export class Blueprint {
let count = 0;
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
if (!root.logic.checkCanPlaceEntity(entity, { offset: tile })) {
continue;
}

@ -4,6 +4,8 @@ import { AtlasSprite } from "../core/sprites";
import { Vector } from "../core/vector";
/* typehints:end */
import { gMetaBuildingRegistry } from "../core/global_registries";
/**
* @typedef {{
* metaClass: typeof MetaBuilding,
@ -19,7 +21,7 @@ import { Vector } from "../core/vector";
/**
* Stores a lookup table for all building variants (for better performance)
* @type {Object<number, BuildingVariantIdentifier>}
* @type {Object<number|string, BuildingVariantIdentifier>}
*/
export const gBuildingVariants = {
// Set later
@ -27,13 +29,13 @@ export const gBuildingVariants = {
/**
* Mapping from 'metaBuildingId/variant/rotationVariant' to building code
* @type {Map<string, number>}
* @type {Map<string, number|string>}
*/
const variantsCache = new Map();
/**
* Registers a new variant
* @param {number} code
* @param {number|string} code
* @param {typeof MetaBuilding} meta
* @param {string} variant
* @param {number} rotationVariant
@ -47,6 +49,7 @@ export function registerBuildingVariant(
assert(!gBuildingVariants[code], "Duplicate id: " + code);
gBuildingVariants[code] = {
metaClass: meta,
metaInstance: gMetaBuildingRegistry.findByClass(meta),
variant,
rotationVariant,
// @ts-ignore
@ -54,9 +57,20 @@ export function registerBuildingVariant(
};
}
/**
* Hashes the combination of buildng, variant and rotation variant
* @param {string} buildingId
* @param {string} variant
* @param {number} rotationVariant
* @returns
*/
function generateBuildingHash(buildingId, variant, rotationVariant) {
return buildingId + "/" + variant + "/" + rotationVariant;
}
/**
*
* @param {number} code
* @param {string|number} code
* @returns {BuildingVariantIdentifier}
*/
export function getBuildingDataFromCode(code) {
@ -70,8 +84,8 @@ export function getBuildingDataFromCode(code) {
export function buildBuildingCodeCache() {
for (const code in gBuildingVariants) {
const data = gBuildingVariants[code];
const hash = data.metaInstance.getId() + "/" + data.variant + "/" + data.rotationVariant;
variantsCache.set(hash, +code);
const hash = generateBuildingHash(data.metaInstance.getId(), data.variant, data.rotationVariant);
variantsCache.set(hash, isNaN(+code) ? code : +code);
}
}
@ -80,10 +94,10 @@ export function buildBuildingCodeCache() {
* @param {MetaBuilding} metaBuilding
* @param {string} variant
* @param {number} rotationVariant
* @returns {number}
* @returns {number|string}
*/
export function getCodeFromBuildingData(metaBuilding, variant, rotationVariant) {
const hash = metaBuilding.getId() + "/" + variant + "/" + rotationVariant;
const hash = generateBuildingHash(metaBuilding.getId(), variant, rotationVariant);
const result = variantsCache.get(hash);
if (G_IS_DEV) {
if (!result) {

@ -3,7 +3,7 @@ import { enumDirection, Vector } from "../../core/vector";
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@ -14,6 +14,15 @@ export class MetaAnalyzerBuilding extends MetaBuilding {
super("analyzer");
}
static getAllVariantCombinations() {
return [
{
internalId: 43,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#3a52bc";
}

@ -31,6 +31,31 @@ export class MetaBalancerBuilding extends MetaBuilding {
super("balancer");
}
static getAllVariantCombinations() {
return [
{
internalId: 4,
variant: defaultBuildingVariant,
},
{
internalId: 5,
variant: enumBalancerVariants.merger,
},
{
internalId: 6,
variant: enumBalancerVariants.mergerInverse,
},
{
internalId: 47,
variant: enumBalancerVariants.splitter,
},
{
internalId: 48,
variant: enumBalancerVariants.splitterInverse,
},
];
}
getDimensions(variant) {
switch (variant) {
case defaultBuildingVariant:
@ -154,11 +179,11 @@ export class MetaBalancerBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
{
pos: new Vector(1, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
]);
@ -179,15 +204,14 @@ export class MetaBalancerBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
{
pos: new Vector(0, 0),
directions: [
direction:
variant === enumBalancerVariants.mergerInverse
? enumDirection.left
: enumDirection.right,
],
},
]);
@ -206,7 +230,7 @@ export class MetaBalancerBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
]);

@ -5,7 +5,7 @@ import { SOUNDS } from "../../platform/sound";
import { T } from "../../translations";
import { BeltComponent } from "../components/belt";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { THEME } from "../theme";
@ -22,6 +22,26 @@ export class MetaBeltBuilding extends MetaBuilding {
super("belt");
}
static getAllVariantCombinations() {
return [
{
internalId: 1,
variant: defaultBuildingVariant,
rotationVariant: 0,
},
{
internalId: 2,
variant: defaultBuildingVariant,
rotationVariant: 1,
},
{
internalId: 3,
variant: defaultBuildingVariant,
rotationVariant: 2,
},
];
}
getSilhouetteColor() {
return THEME.map.chunkOverview.beltColor;
}

@ -2,13 +2,22 @@
import { Entity } from "../entity";
/* typehints:end */
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
export class MetaBlockBuilding extends MetaBuilding {
constructor() {
super("block");
}
static getAllVariantCombinations() {
return [
{
internalId: 64,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#333";
}

@ -2,7 +2,7 @@ import { enumDirection, Vector } from "../../core/vector";
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@ -11,6 +11,15 @@ export class MetaComparatorBuilding extends MetaBuilding {
super("comparator");
}
static getAllVariantCombinations() {
return [
{
internalId: 46,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#823cab";
}

@ -6,13 +6,22 @@ import { enumDirection, Vector } from "../../core/vector";
import { ConstantSignalComponent } from "../components/constant_signal";
import { ItemEjectorComponent } from "../components/item_ejector";
import { ItemProducerComponent } from "../components/item_producer";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
export class MetaConstantProducerBuilding extends MetaBuilding {
constructor() {
super("constant_producer");
}
static getAllVariantCombinations() {
return [
{
internalId: 62,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#bfd630";
}

@ -1,7 +1,7 @@
import { enumDirection, Vector } from "../../core/vector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { ConstantSignalComponent } from "../components/constant_signal";
import { generateMatrixRotations } from "../../core/utils";
@ -14,6 +14,15 @@ export class MetaConstantSignalBuilding extends MetaBuilding {
super("constant_signal");
}
static getAllVariantCombinations() {
return [
{
internalId: 31,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#2b84fd";
}

@ -17,6 +17,19 @@ export class MetaCutterBuilding extends MetaBuilding {
super("cutter");
}
static getAllVariantCombinations() {
return [
{
internalId: 9,
variant: defaultBuildingVariant,
},
{
internalId: 10,
variant: enumCutterVariants.quad,
},
];
}
getSilhouetteColor() {
return "#7dcda2";
}
@ -83,7 +96,7 @@ export class MetaCutterBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "shape",
},
],

@ -1,7 +1,7 @@
import { enumDirection, Vector } from "../../core/vector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { DisplayComponent } from "../components/display";
import { enumHubGoalRewards } from "../tutorial_goals";
@ -11,6 +11,15 @@ export class MetaDisplayBuilding extends MetaBuilding {
super("display");
}
static getAllVariantCombinations() {
return [
{
internalId: 40,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#aaaaaa";
}

@ -6,7 +6,7 @@ import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@ -15,6 +15,15 @@ export class MetaFilterBuilding extends MetaBuilding {
super("filter");
}
static getAllVariantCombinations() {
return [
{
internalId: 37,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#c45c2e";
}
@ -69,7 +78,7 @@ export class MetaFilterBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
],
})

@ -6,13 +6,22 @@ import { enumDirection, Vector } from "../../core/vector";
import { GoalAcceptorComponent } from "../components/goal_acceptor";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
export class MetaGoalAcceptorBuilding extends MetaBuilding {
constructor() {
super("goal_acceptor");
}
static getAllVariantCombinations() {
return [
{
internalId: 63,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#ce418a";
}
@ -36,7 +45,7 @@ export class MetaGoalAcceptorBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "shape",
},
],

@ -3,7 +3,7 @@ import { HubComponent } from "../components/hub";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { WiredPinsComponent, enumPinSlotType } from "../components/wired_pins";
export class MetaHubBuilding extends MetaBuilding {
@ -11,6 +11,15 @@ export class MetaHubBuilding extends MetaBuilding {
super("hub");
}
static getAllVariantCombinations() {
return [
{
internalId: 26,
variant: defaultBuildingVariant,
},
];
}
getDimensions() {
return new Vector(4, 4);
}
@ -61,80 +70,22 @@ export class MetaHubBuilding extends MetaBuilding {
})
);
/**
* @type {Array<import("../components/item_acceptor").ItemAcceptorSlotConfig>}
*/
const slots = [];
for (let i = 0; i < 4; ++i) {
slots.push(
{ pos: new Vector(i, 0), direction: enumDirection.top, filter: "shape" },
{ pos: new Vector(i, 3), direction: enumDirection.bottom, filter: "shape" },
{ pos: new Vector(0, i), direction: enumDirection.left, filter: "shape" },
{ pos: new Vector(3, i), direction: enumDirection.right, filter: "shape" }
);
}
entity.addComponent(
new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.top, enumDirection.left],
filter: "shape",
},
{
pos: new Vector(1, 0),
directions: [enumDirection.top],
filter: "shape",
},
{
pos: new Vector(2, 0),
directions: [enumDirection.top],
filter: "shape",
},
{
pos: new Vector(3, 0),
directions: [enumDirection.top, enumDirection.right],
filter: "shape",
},
{
pos: new Vector(0, 3),
directions: [enumDirection.bottom, enumDirection.left],
filter: "shape",
},
{
pos: new Vector(1, 3),
directions: [enumDirection.bottom],
filter: "shape",
},
{
pos: new Vector(2, 3),
directions: [enumDirection.bottom],
filter: "shape",
},
{
pos: new Vector(3, 3),
directions: [enumDirection.bottom, enumDirection.right],
filter: "shape",
},
{
pos: new Vector(0, 1),
directions: [enumDirection.left],
filter: "shape",
},
{
pos: new Vector(0, 2),
directions: [enumDirection.left],
filter: "shape",
},
{
pos: new Vector(0, 3),
directions: [enumDirection.left],
filter: "shape",
},
{
pos: new Vector(3, 1),
directions: [enumDirection.right],
filter: "shape",
},
{
pos: new Vector(3, 2),
directions: [enumDirection.right],
filter: "shape",
},
{
pos: new Vector(3, 3),
directions: [enumDirection.right],
filter: "shape",
},
],
slots,
})
);
}

@ -3,13 +3,22 @@ import { ItemEjectorComponent } from "../components/item_ejector";
import { ItemProducerComponent } from "../components/item_producer";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
export class MetaItemProducerBuilding extends MetaBuilding {
constructor() {
super("item_producer");
}
static getAllVariantCombinations() {
return [
{
internalId: 61,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#b37dcd";
}

@ -1,7 +1,7 @@
import { enumDirection, Vector } from "../../core/vector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { LeverComponent } from "../components/lever";
import { enumHubGoalRewards } from "../tutorial_goals";
@ -11,6 +11,15 @@ export class MetaLeverBuilding extends MetaBuilding {
super("lever");
}
static getAllVariantCombinations() {
return [
{
internalId: 33,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
// @todo: Render differently based on if its activated or not
return "#1a678b";

@ -15,7 +15,7 @@ export const enumLogicGateVariants = {
};
/** @enum {string} */
export const enumVariantToGate = {
const enumVariantToGate = {
[defaultBuildingVariant]: enumLogicGateType.and,
[enumLogicGateVariants.not]: enumLogicGateType.not,
[enumLogicGateVariants.xor]: enumLogicGateType.xor,
@ -41,6 +41,27 @@ export class MetaLogicGateBuilding extends MetaBuilding {
super("logic_gate");
}
static getAllVariantCombinations() {
return [
{
internalId: 32,
variant: defaultBuildingVariant,
},
{
internalId: 34,
variant: enumLogicGateVariants.not,
},
{
internalId: 35,
variant: enumLogicGateVariants.xor,
},
{
internalId: 36,
variant: enumLogicGateVariants.or,
},
];
}
getSilhouetteColor(variant) {
return colors[variant];
}

@ -21,6 +21,19 @@ export class MetaMinerBuilding extends MetaBuilding {
super("miner");
}
static getAllVariantCombinations() {
return [
{
internalId: 7,
variant: defaultBuildingVariant,
},
{
internalId: 8,
variant: enumMinerVariants.chainable,
},
];
}
getSilhouetteColor() {
return "#b37dcd";
}

@ -5,7 +5,7 @@ import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@ -14,6 +14,15 @@ export class MetaMixerBuilding extends MetaBuilding {
super("mixer");
}
static getAllVariantCombinations() {
return [
{
internalId: 15,
variant: defaultBuildingVariant,
},
];
}
getDimensions() {
return new Vector(2, 1);
}
@ -64,12 +73,12 @@ export class MetaMixerBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "color",
},
{
pos: new Vector(1, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "color",
},
],

@ -22,6 +22,27 @@ export class MetaPainterBuilding extends MetaBuilding {
super("painter");
}
static getAllVariantCombinations() {
return [
{
internalId: 16,
variant: defaultBuildingVariant,
},
{
internalId: 17,
variant: enumPainterVariants.mirrored,
},
{
internalId: 18,
variant: enumPainterVariants.double,
},
{
internalId: 19,
variant: enumPainterVariants.quad,
},
];
}
getDimensions(variant) {
switch (variant) {
case defaultBuildingVariant:
@ -107,12 +128,12 @@ export class MetaPainterBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.left],
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(1, 0),
directions: [enumDirection.top],
direction: enumDirection.top,
filter: "color",
},
],
@ -139,14 +160,13 @@ export class MetaPainterBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.left],
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(1, 0),
directions: [
direction:
variant === defaultBuildingVariant ? enumDirection.top : enumDirection.bottom,
],
filter: "color",
},
]);
@ -172,17 +192,17 @@ export class MetaPainterBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.left],
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(0, 1),
directions: [enumDirection.left],
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(1, 0),
directions: [enumDirection.top],
direction: enumDirection.top,
filter: "color",
},
]);
@ -230,27 +250,27 @@ export class MetaPainterBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.left],
direction: enumDirection.left,
filter: "shape",
},
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "color",
},
{
pos: new Vector(1, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "color",
},
{
pos: new Vector(2, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "color",
},
{
pos: new Vector(3, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "color",
},
]);

@ -4,7 +4,7 @@ import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { BeltUnderlaysComponent } from "../components/belt_underlays";
import { BeltReaderComponent } from "../components/belt_reader";
@ -18,6 +18,15 @@ export class MetaReaderBuilding extends MetaBuilding {
super("reader");
}
static getAllVariantCombinations() {
return [
{
internalId: 49,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#25fff2";
}
@ -75,7 +84,7 @@ export class MetaReaderBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
],
})

@ -23,6 +23,23 @@ export class MetaRotaterBuilding extends MetaBuilding {
super("rotater");
}
static getAllVariantCombinations() {
return [
{
internalId: 11,
variant: defaultBuildingVariant,
},
{
internalId: 12,
variant: enumRotaterVariants.ccw,
},
{
internalId: 13,
variant: enumRotaterVariants.rotate180,
},
];
}
getSilhouetteColor() {
return "#7dc6cd";
}
@ -111,7 +128,7 @@ export class MetaRotaterBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "shape",
},
],

@ -5,7 +5,7 @@ import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@ -14,6 +14,15 @@ export class MetaStackerBuilding extends MetaBuilding {
super("stacker");
}
static getAllVariantCombinations() {
return [
{
internalId: 14,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#9fcd7d";
}
@ -64,12 +73,12 @@ export class MetaStackerBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "shape",
},
{
pos: new Vector(1, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
filter: "shape",
},
],

@ -6,7 +6,7 @@ import { ItemEjectorComponent } from "../components/item_ejector";
import { StorageComponent } from "../components/storage";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@ -17,6 +17,15 @@ export class MetaStorageBuilding extends MetaBuilding {
super("storage");
}
static getAllVariantCombinations() {
return [
{
internalId: 21,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#bbdf6d";
}
@ -65,11 +74,11 @@ export class MetaStorageBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 1),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
{
pos: new Vector(1, 1),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
],
})

@ -22,6 +22,19 @@ export class MetaTransistorBuilding extends MetaBuilding {
super("transistor");
}
static getAllVariantCombinations() {
return [
{
internalId: 38,
variant: defaultBuildingVariant,
},
{
internalId: 60,
variant: enumTransistorVariants.mirrored,
},
];
}
getSilhouetteColor() {
return "#bc3a61";
}

@ -4,7 +4,7 @@ import { ACHIEVEMENTS } from "../../platform/achievement_provider";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@ -15,6 +15,15 @@ export class MetaTrashBuilding extends MetaBuilding {
super("trash");
}
static getAllVariantCombinations() {
return [
{
internalId: 20,
variant: defaultBuildingVariant,
},
];
}
getIsRotateable() {
return false;
}
@ -67,12 +76,19 @@ export class MetaTrashBuilding extends MetaBuilding {
slots: [
{
pos: new Vector(0, 0),
directions: [
enumDirection.top,
enumDirection.right,
enumDirection.bottom,
enumDirection.left,
],
direction: enumDirection.top,
},
{
pos: new Vector(0, 0),
direction: enumDirection.right,
},
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
},
{
pos: new Vector(0, 0),
direction: enumDirection.left,
},
],
})

@ -40,6 +40,31 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding {
super("underground_belt");
}
static getAllVariantCombinations() {
return [
{
internalId: 22,
variant: defaultBuildingVariant,
rotationVariant: 0,
},
{
internalId: 23,
variant: defaultBuildingVariant,
rotationVariant: 1,
},
{
internalId: 24,
variant: enumUndergroundBeltVariants.tier2,
rotationVariant: 0,
},
{
internalId: 25,
variant: enumUndergroundBeltVariants.tier2,
rotationVariant: 1,
},
];
}
getSilhouetteColor(variant, rotationVariant) {
return colorsByRotationVariant[rotationVariant];
}
@ -252,7 +277,7 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding {
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
direction: enumDirection.bottom,
},
]);
return;

@ -19,7 +19,7 @@ export const enumVirtualProcessorVariants = {
};
/** @enum {string} */
export const enumVariantToGate = {
const enumVariantToGate = {
[defaultBuildingVariant]: enumLogicGateType.cutter,
[enumVirtualProcessorVariants.rotater]: enumLogicGateType.rotater,
[enumVirtualProcessorVariants.unstacker]: enumLogicGateType.unstacker,
@ -40,6 +40,31 @@ export class MetaVirtualProcessorBuilding extends MetaBuilding {
super("virtual_processor");
}
static getAllVariantCombinations() {
return [
{
internalId: 42,
variant: defaultBuildingVariant,
},
{
internalId: 44,
variant: enumVirtualProcessorVariants.rotater,
},
{
internalId: 45,
variant: enumVirtualProcessorVariants.unstacker,
},
{
internalId: 50,
variant: enumVirtualProcessorVariants.stacker,
},
{
internalId: 51,
variant: enumVirtualProcessorVariants.painter,
},
];
}
getSilhouetteColor(variant) {
return colors[variant];
}

@ -37,6 +37,51 @@ export class MetaWireBuilding extends MetaBuilding {
super("wire");
}
static getAllVariantCombinations() {
return [
{
internalId: 27,
variant: defaultBuildingVariant,
rotationVariant: 0,
},
{
internalId: 28,
variant: defaultBuildingVariant,
rotationVariant: 1,
},
{
internalId: 29,
variant: defaultBuildingVariant,
rotationVariant: 2,
},
{
internalId: 30,
variant: defaultBuildingVariant,
rotationVariant: 3,
},
{
internalId: 52,
variant: enumWireVariant.second,
rotationVariant: 0,
},
{
internalId: 53,
variant: enumWireVariant.second,
rotationVariant: 1,
},
{
internalId: 54,
variant: enumWireVariant.second,
rotationVariant: 2,
},
{
internalId: 55,
variant: enumWireVariant.second,
rotationVariant: 3,
},
];
}
getHasDirectionLockAvailable() {
return true;
}

@ -2,7 +2,7 @@ import { generateMatrixRotations } from "../../core/utils";
import { Vector } from "../../core/vector";
import { WireTunnelComponent } from "../components/wire_tunnel";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
@ -13,6 +13,15 @@ export class MetaWireTunnelBuilding extends MetaBuilding {
super("wire_tunnel");
}
static getAllVariantCombinations() {
return [
{
internalId: 39,
variant: defaultBuildingVariant,
},
];
}
getSilhouetteColor() {
return "#777a86";
}

@ -22,31 +22,34 @@ import { ItemProducerComponent } from "./components/item_producer";
import { GoalAcceptorComponent } from "./components/goal_acceptor";
export function initComponentRegistry() {
gComponentRegistry.register(StaticMapEntityComponent);
gComponentRegistry.register(BeltComponent);
gComponentRegistry.register(ItemEjectorComponent);
gComponentRegistry.register(ItemAcceptorComponent);
gComponentRegistry.register(MinerComponent);
gComponentRegistry.register(ItemProcessorComponent);
gComponentRegistry.register(UndergroundBeltComponent);
gComponentRegistry.register(HubComponent);
gComponentRegistry.register(StorageComponent);
gComponentRegistry.register(WiredPinsComponent);
gComponentRegistry.register(BeltUnderlaysComponent);
gComponentRegistry.register(WireComponent);
gComponentRegistry.register(ConstantSignalComponent);
gComponentRegistry.register(LogicGateComponent);
gComponentRegistry.register(LeverComponent);
gComponentRegistry.register(WireTunnelComponent);
gComponentRegistry.register(DisplayComponent);
gComponentRegistry.register(BeltReaderComponent);
gComponentRegistry.register(FilterComponent);
gComponentRegistry.register(ItemProducerComponent);
gComponentRegistry.register(GoalAcceptorComponent);
const components = [
StaticMapEntityComponent,
BeltComponent,
ItemEjectorComponent,
ItemAcceptorComponent,
MinerComponent,
ItemProcessorComponent,
UndergroundBeltComponent,
HubComponent,
StorageComponent,
WiredPinsComponent,
BeltUnderlaysComponent,
WireComponent,
ConstantSignalComponent,
LogicGateComponent,
LeverComponent,
WireTunnelComponent,
DisplayComponent,
BeltReaderComponent,
FilterComponent,
ItemProducerComponent,
GoalAcceptorComponent,
];
components.forEach(component => gComponentRegistry.register(component));
// IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS
// Sanity check - If this is thrown, you (=me, lol) forgot to add a new component here
// Sanity check - If this is thrown, you forgot to add a new component here
assert(
// @ts-ignore

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save