mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-13 18:21:51 +00:00
initial modloader draft
This commit is contained in:
parent
a7a2aad2b6
commit
abf2fd3d94
@ -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");
|
||||
|
||||
@ -232,6 +233,7 @@ export class BackgroundResourcesLoader {
|
||||
this.numAssetsToLoadTotal = 0;
|
||||
this.numAssetsLoaded = 0;
|
||||
})
|
||||
.then(MODS.hook_injectSprites.bind(MODS))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
import { MODS } from "../mods/modloader";
|
||||
|
||||
/**
|
||||
* Used for the bug reporter, and the click detector which both have no handles to this.
|
||||
* It would be nicer to have no globals, but this is the only one. I promise!
|
||||
@ -14,4 +16,5 @@ export let GLOBAL_APP = null;
|
||||
export function setGlobalApp(app) {
|
||||
assert(!GLOBAL_APP, "Create application twice!");
|
||||
GLOBAL_APP = app;
|
||||
MODS.linkApp(app);
|
||||
}
|
||||
|
||||
@ -181,6 +181,10 @@ class LoaderImpl {
|
||||
w: sourceSize.w,
|
||||
h: sourceSize.h,
|
||||
});
|
||||
if (sprite.linksByResolution[scale]) {
|
||||
// Seems data is already present, might have been replaced by a mod
|
||||
continue;
|
||||
}
|
||||
sprite.linksByResolution[scale] = link;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { MODS } from "../mods/modloader";
|
||||
|
||||
export const THEMES = {
|
||||
dark: require("./themes/dark.json"),
|
||||
light: require("./themes/light.json"),
|
||||
@ -7,4 +9,5 @@ export let THEME = THEMES.light;
|
||||
|
||||
export function applyGameTheme(id) {
|
||||
THEME = THEMES[id];
|
||||
MODS.callHook("preprocessTheme", { id, theme: THEME });
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import { initItemRegistry } from "./game/item_registry";
|
||||
import { initMetaBuildingRegistry } from "./game/meta_building_registry";
|
||||
import { initGameModeRegistry } from "./game/game_mode_registry";
|
||||
import { initGameSpeedRegistry } from "./game/game_speed_registry";
|
||||
import { MODS } from "./mods/modloader";
|
||||
|
||||
const logger = createLogger("main");
|
||||
|
||||
@ -19,6 +20,8 @@ if (window.coreThreadLoadedCb) {
|
||||
window.coreThreadLoadedCb();
|
||||
}
|
||||
|
||||
MODS.hook_init();
|
||||
|
||||
// Logrocket
|
||||
// if (!G_IS_DEV && !G_IS_STANDALONE) {
|
||||
// const monthlyUsers = 300; // thousand
|
||||
|
||||
31
src/js/mods/demo_mod.js
Normal file
31
src/js/mods/demo_mod.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { Mod } from "./mod";
|
||||
|
||||
export class DemoMod extends Mod {
|
||||
constructor() {
|
||||
super({
|
||||
authorContact: "tobias@tobspr.io",
|
||||
authorName: "tobspr",
|
||||
name: "Demo Mod",
|
||||
version: "1",
|
||||
id: "demo-mod",
|
||||
});
|
||||
}
|
||||
|
||||
hook_init() {
|
||||
this.interface.registerCss(`
|
||||
* {
|
||||
color: red !important;
|
||||
}
|
||||
`);
|
||||
|
||||
this.interface.registerSprite(
|
||||
"sprites/colors/red.png",
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAB2AAAAdgB+lymcgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAATkSURBVHic7ZpfiFRVHMc/v5m99+6qWYhCYKFGVJpPFYgK4UNQb1GwODO5M2OLUmASItGD6OJDYX8IIqPMdWdmdWZkE6GnnozqJWkxENMyg/75tP31z+7MnZ3760GrZf7fmXuvaPfzMvA7v3PO9/e7555z5pwLISEhISEhISH/U6SXyjo4aFaM+atUndUISwEUSmjkW5XouYH86I9eiJxJDC8Tra4EHhBxrGtW+UWQr43K1bMyMWF323ZXCSglko8JJFF5EljYwvUMytFqVcfmTeQuuuljejC5NBqVzQgx4MEWrpdAjjtIbqAwdsJNH+AyAXYi9TDKGwobXPYzI8J7hil7JZP5s5WjptN3VGzdrcpzwICbTgQ9gbLTLOa+6rxOh9iJ1FZV3gEMN6Jqevspok7SKIx/2qi4Ek+udZDDwD1d9wGziO6y8rl9nUlqg4LY8dQosLkHUXOpCmwxC9mxuUY7ln5WRQ8AUS86UWXUKma3CGgrv0i7hirx1Ot4FzxAVGG0lEhu+8dQiqdfUNGDeBQ8gAjDdiL1Wlu/VoXleHITyLhXompwVHkKQITjdPAwukOHrELucLPSpgnQeHxxGfOcwGJ/hAFw5frvAr86UJiysFdJofBro/KmWS+rudfn4OFa4L4FDyCwpCLmSIvyenRw6+12X/kiMN8vYQFzxZy17pKJA3/VFjQcAWWjNMStEzzAgnK0vKlRQcMEiMoT/uoJHhEeb2SvS4Beey3W+q4oeNbryEhdvHWGUiy9DFgUiKRgWVQ6//Pdtca6BESVJcHoCZ4os3WrWl0CnGj1Vnz6ADg47RNANdJy73xzI9VaS/2kEOW3YMQET6Qa/b3OVmsw+qPnAScQRcHiGH32hVpjXQLk0KHLwDeBSAqWs3LkyKVaY+ONEPKx/3oCRmgYU+M/Q1ot+CrmBiCO5hvZGybALI5PIpzyV1KAKF82OydsfgihvOKboKARaRpL0wSY9684fouMgknz/uUfNStseSRmx4YeUYmcxLfjKt9xJCLrzSOZL5o5tAzMLI5PqvK+97qCQZR3WwUPHTxZq192AKc9UxUcZ43q9EvtnNomQDKZEugQMOOJrCAQpnGcjTIx0VZzR++2VcidBoa4ObbICjJsHR0/04lzx5ObVcgeQ9nTva6AEN1l5TPFjt3dtm/H028qusNtvSAQYb+Zz25r7/kfrpc3o5DZqegHbuv5j2SN+1Zsd1vLdQIE1JqdeR4Ya+scHGPm7NVhGRlxPUd1/YWIgtiJ9B5Ub+i8IMLbRj77Yrtb4Kb1exVQiqW2i/AWwe8WFXjZKmTb3gC3oucEANiJVFKVg/Ty8YQ7KqIybBYzPd9ce5IAgMozqfWOw4fAnV612QiFKRXdOJDPfeJFe54lAK5/2NQXOQa6xst2/0U45dD3tFdfn4HH7+28idxF02IDvqwQesj8Y+E6L4MHj0fAXK7PC/vp/f7/sgg7zXz2gBe6avEtAQAzsfTyiGie7i9bJ1Uk0Z/PfOelrrn4unQNFDM/mLPTG1DZB9TdyrSgCrxq3mat8zN48HkEzMWODa1RiYwBK9u4fh9R2WwUM58HoSuwzYtZHD9pWvIQIrsVpmrLFaYQ2W1asjqo4CHAETAXHRwcKBv9j+JE7gUg4lywKqXPOjnACAkJCQkJCQkJ8Yi/AfA6e2lfA0oPAAAAAElFTkSuQmCC"
|
||||
);
|
||||
}
|
||||
|
||||
hook_preprocessTheme({ id, theme }) {
|
||||
theme.map.background = "#eee";
|
||||
theme.items.outline = "#000";
|
||||
}
|
||||
}
|
||||
50
src/js/mods/mod.js
Normal file
50
src/js/mods/mod.js
Normal file
@ -0,0 +1,50 @@
|
||||
/* typehints:start */
|
||||
import { ModInterface } from "./mod_interface";
|
||||
/* typehints:end */
|
||||
|
||||
export class Mod {
|
||||
/**
|
||||
*
|
||||
* @param {object} metadata
|
||||
* @param {string} metadata.name
|
||||
* @param {string} metadata.version
|
||||
* @param {string} metadata.authorName
|
||||
* @param {string} metadata.authorContact
|
||||
* @param {string} metadata.id
|
||||
*/
|
||||
constructor(metadata) {
|
||||
this.metadata = metadata;
|
||||
|
||||
/**
|
||||
* @type {ModInterface}
|
||||
*/
|
||||
this.interface = undefined;
|
||||
}
|
||||
|
||||
hook_init() {}
|
||||
|
||||
executeGuarded(taskName, task) {
|
||||
try {
|
||||
return task();
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
alert(
|
||||
"Mod " +
|
||||
this.metadata.name +
|
||||
" (version " +
|
||||
this.metadata.version +
|
||||
")" +
|
||||
" failed to execute '" +
|
||||
taskName +
|
||||
"':\n\n" +
|
||||
ex +
|
||||
"\n\nPlease forward this to the mod author:\n\n" +
|
||||
this.metadata.authorName +
|
||||
" (" +
|
||||
this.metadata.authorContact +
|
||||
")"
|
||||
);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/js/mods/mod_interface.js
Normal file
77
src/js/mods/mod_interface.js
Normal file
@ -0,0 +1,77 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
import { ModLoader } from "./modloader";
|
||||
/* typehints:end */
|
||||
|
||||
import { createLogger } from "../core/logging";
|
||||
import { AtlasSprite, SpriteAtlasLink } from "../core/sprites";
|
||||
import { Mod } from "./mod";
|
||||
|
||||
const LOG = createLogger("mod-interface");
|
||||
|
||||
export class ModInterface {
|
||||
/**
|
||||
*
|
||||
* @param {ModLoader} modLoader
|
||||
* @param {Mod} mod
|
||||
*/
|
||||
constructor(modLoader, mod) {
|
||||
/**
|
||||
* @param {Application} app
|
||||
*/
|
||||
this.app = undefined;
|
||||
|
||||
this.modLoader = modLoader;
|
||||
this.mod = mod;
|
||||
}
|
||||
|
||||
registerCss(cssString) {
|
||||
const element = document.createElement("style");
|
||||
element.textContent = cssString;
|
||||
element.setAttribute("data-mod-id", this.mod.metadata.id);
|
||||
element.setAttribute("data-mod-name", this.mod.metadata.name);
|
||||
document.head.appendChild(element);
|
||||
}
|
||||
|
||||
registerSprite(spriteId, base64string) {
|
||||
assert(base64string.startsWith("data:image"));
|
||||
const img = new Image();
|
||||
img.src = base64string;
|
||||
|
||||
const sprite = new AtlasSprite(spriteId);
|
||||
|
||||
const link = new SpriteAtlasLink({
|
||||
w: img.width,
|
||||
h: img.height,
|
||||
atlas: img,
|
||||
packOffsetX: 0,
|
||||
packOffsetY: 0,
|
||||
packedW: img.width,
|
||||
packedH: img.height,
|
||||
packedX: 0,
|
||||
packedY: 0,
|
||||
});
|
||||
|
||||
sprite.linksByResolution["0.25"] = link;
|
||||
sprite.linksByResolution["0.5"] = link;
|
||||
sprite.linksByResolution["0.75"] = link;
|
||||
|
||||
// @ts-ignore
|
||||
sprite.modSource = this.mod;
|
||||
|
||||
const oldSprite = this.modLoader.lazySprites.get(spriteId);
|
||||
if (oldSprite) {
|
||||
LOG.error(
|
||||
"Sprite '" +
|
||||
spriteId +
|
||||
"' is provided twice, once by mod '" +
|
||||
// @ts-ignore
|
||||
oldSprite.modSource.metadata.name +
|
||||
"' and once by mod '" +
|
||||
this.mod.metadata.name +
|
||||
"'. This could cause artifacts."
|
||||
);
|
||||
}
|
||||
this.modLoader.lazySprites.set(spriteId, sprite);
|
||||
}
|
||||
}
|
||||
77
src/js/mods/modloader.js
Normal file
77
src/js/mods/modloader.js
Normal file
@ -0,0 +1,77 @@
|
||||
import { Loader } from "../core/loader";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { AtlasSprite } from "../core/sprites";
|
||||
import { DemoMod } from "./demo_mod";
|
||||
import { Mod } from "./mod";
|
||||
import { ModInterface } from "./mod_interface";
|
||||
|
||||
const LOG = createLogger("mods");
|
||||
|
||||
export class ModLoader {
|
||||
constructor() {
|
||||
LOG.log("modloader created");
|
||||
|
||||
/** @type {Mod[]} */
|
||||
this.mods = [];
|
||||
|
||||
/** @type {Map<string, AtlasSprite>} */
|
||||
this.lazySprites = new Map();
|
||||
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
linkApp(app) {
|
||||
this.app = app;
|
||||
this.mods.forEach(mod => (mod.interface.app = app));
|
||||
}
|
||||
|
||||
hook_init() {
|
||||
LOG.log("hook:init");
|
||||
this.initialized = true;
|
||||
this.mods.forEach(mod => {
|
||||
LOG.log("Loading mod", mod.metadata.name);
|
||||
mod.interface = new ModInterface(this, mod);
|
||||
mod.executeGuarded("hook_init", mod.hook_init.bind(mod));
|
||||
});
|
||||
}
|
||||
|
||||
hook_injectSprites() {
|
||||
LOG.log("hook:injectSprites");
|
||||
this.lazySprites.forEach((sprite, key) => {
|
||||
Loader.sprites.set(key, sprite);
|
||||
console.log("override", key);
|
||||
});
|
||||
}
|
||||
|
||||
callHook(id, structuredArgs) {
|
||||
LOG.log("hook:" + id);
|
||||
this.mods.forEach(mod => {
|
||||
const handler = mod["hook_" + id];
|
||||
if (handler) {
|
||||
mod.executeGuarded("hook:" + id, handler.bind(mod, structuredArgs));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerSprite() {}
|
||||
|
||||
registerGameState() {}
|
||||
|
||||
registerBuilding() {}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Mod} mod
|
||||
*/
|
||||
registerMod(mod) {
|
||||
LOG.log("Registering mod", mod.metadata.name);
|
||||
if (this.initialized) {
|
||||
throw new Error("Mods are already initialized, can not add mod afterwards.");
|
||||
}
|
||||
this.mods.push(mod);
|
||||
}
|
||||
}
|
||||
|
||||
export const MODS = new ModLoader();
|
||||
|
||||
MODS.registerMod(new DemoMod());
|
||||
Loading…
Reference in New Issue
Block a user