diff --git a/mod_examples/README.md b/mod_examples/README.md index 25b1d75e..e38d5834 100644 --- a/mod_examples/README.md +++ b/mod_examples/README.md @@ -41,6 +41,7 @@ To get into shapez.io modding, I highly recommend checking out all of the exampl | [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 diff --git a/mod_examples/add_building_basic.js b/mod_examples/add_building_basic.js index 97e0a358..6b92e769 100644 --- a/mod_examples/add_building_basic.js +++ b/mod_examples/add_building_basic.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "add-building-basic", description: "Shows how to add a new basic building", + minimumGameVersion: ">=1.5.0", }; class MetaDemoModBuilding extends shapez.ModMetaBuilding { diff --git a/mod_examples/add_building_flipper.js b/mod_examples/add_building_flipper.js index c04354bf..03442499 100644 --- a/mod_examples/add_building_flipper.js +++ b/mod_examples/add_building_flipper.js @@ -7,6 +7,7 @@ const METADATA = { id: "add-building-extended", description: "Shows how to add a new building with logic, in this case it flips/mirrors shapez from top to down", + minimumGameVersion: ">=1.5.0", }; // Declare a new type of item processor diff --git a/mod_examples/base.js b/mod_examples/base.js index 6dc8df8f..09e12f61 100644 --- a/mod_examples/base.js +++ b/mod_examples/base.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "base", description: "The most basic mod", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/buildings_have_cost.js b/mod_examples/buildings_have_cost.js index 79061d35..3dae84ae 100644 --- a/mod_examples/buildings_have_cost.js +++ b/mod_examples/buildings_have_cost.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "patch-methods", description: "Shows how to patch existing methods to change the game by making the belts cost shapes", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/class_extensions.js b/mod_examples/class_extensions.js index c5b5b95e..8647fd45 100644 --- a/mod_examples/class_extensions.js +++ b/mod_examples/class_extensions.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "class-extensions", description: "Shows how to extend builtin classes", + minimumGameVersion: ">=1.5.0", }; const BeltExtension = ({ $super, $old }) => ({ diff --git a/mod_examples/custom_css.js b/mod_examples/custom_css.js index 74d19057..dce316c5 100644 --- a/mod_examples/custom_css.js +++ b/mod_examples/custom_css.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "custom-css", description: "Shows how to add custom css", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/custom_drawing.js b/mod_examples/custom_drawing.js index 6ba49454..e1c25b30 100644 --- a/mod_examples/custom_drawing.js +++ b/mod_examples/custom_drawing.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "base", description: "Displays an indicator on every item processing building when its working", + minimumGameVersion: ">=1.5.0", }; class ItemProcessorStatusGameSystem extends shapez.GameSystem { diff --git a/mod_examples/custom_keybinding.js b/mod_examples/custom_keybinding.js index 650065f0..0a6b11fc 100644 --- a/mod_examples/custom_keybinding.js +++ b/mod_examples/custom_keybinding.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "base", description: "Shows how to add a new keybinding", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/custom_sub_shapes.js b/mod_examples/custom_sub_shapes.js index afb901c0..3aea03cf 100644 --- a/mod_examples/custom_sub_shapes.js +++ b/mod_examples/custom_sub_shapes.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "custom-sub-shapes", description: "Shows how to add custom sub shapes", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/custom_theme.js b/mod_examples/custom_theme.js index a596799c..cc4c9de8 100644 --- a/mod_examples/custom_theme.js +++ b/mod_examples/custom_theme.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "custom-theme", description: "Shows how to add a custom game theme", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/mod_settings.js b/mod_examples/mod_settings.js index d89f8243..b87c138b 100644 --- a/mod_examples/mod_settings.js +++ b/mod_examples/mod_settings.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "mod-settings", description: "Shows how to add settings to your mod", + minimumGameVersion: ">=1.5.0", settings: { timesLaunched: 0, diff --git a/mod_examples/modify_existing_building.js b/mod_examples/modify_existing_building.js index 1264b2c3..b09f5a20 100644 --- a/mod_examples/modify_existing_building.js +++ b/mod_examples/modify_existing_building.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "modify-existing-building", description: "Shows how to modify an existing building", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/modify_theme.js b/mod_examples/modify_theme.js index dfdfb6e9..4bc9be34 100644 --- a/mod_examples/modify_theme.js +++ b/mod_examples/modify_theme.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "modify-theme", description: "Shows how to modify builtin themes", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/modify_ui.js b/mod_examples/modify_ui.js index 749e191e..0b2d1341 100644 --- a/mod_examples/modify_ui.js +++ b/mod_examples/modify_ui.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "modify-ui", description: "Shows how to modify a builtin game state, in this case the main menu", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/new_item_type.js b/mod_examples/new_item_type.js index 3c4b5f98..3cd52cef 100644 --- a/mod_examples/new_item_type.js +++ b/mod_examples/new_item_type.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "new-item-type", description: "Shows how to add a new item type (fluid)", + minimumGameVersion: ">=1.5.0", }; // Define which fluid types there are diff --git a/mod_examples/notification_blocks.js b/mod_examples/notification_blocks.js index a8116849..23f95943 100644 --- a/mod_examples/notification_blocks.js +++ b/mod_examples/notification_blocks.js @@ -7,6 +7,8 @@ const METADATA = { id: "notification-blocks", description: "Adds a new building to the wires layer, 'Notification Blocks' which show a custom notification when they get a truthy signal.", + + minimumGameVersion: ">=1.5.0", }; //////////////////////////////////////////////////////////////////////// diff --git a/mod_examples/pasting.js b/mod_examples/pasting.js index 5d0eaa00..698edeff 100644 --- a/mod_examples/pasting.js +++ b/mod_examples/pasting.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "pasting", description: "Shows how to properly receive paste events ingame", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/replace_builtin_sprites.js b/mod_examples/replace_builtin_sprites.js index edc9d718..885846e7 100644 --- a/mod_examples/replace_builtin_sprites.js +++ b/mod_examples/replace_builtin_sprites.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "replace-builtin-sprites", description: "Shows how to replace builtin sprites", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/sandbox.js b/mod_examples/sandbox.js new file mode 100644 index 00000000..f405ab59 --- /dev/null +++ b/mod_examples/sandbox.js @@ -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; + }); + } +} diff --git a/mod_examples/translations.js b/mod_examples/translations.js index 8a3e07da..2f3a4015 100644 --- a/mod_examples/translations.js +++ b/mod_examples/translations.js @@ -6,6 +6,7 @@ const METADATA = { version: "1", id: "translations", description: "Shows how to add and modify translations", + minimumGameVersion: ">=1.5.0", }; class Mod extends shapez.Mod { diff --git a/mod_examples/usage_statistics.js b/mod_examples/usage_statistics.js index 36bb81f5..64da102b 100644 --- a/mod_examples/usage_statistics.js +++ b/mod_examples/usage_statistics.js @@ -7,6 +7,8 @@ const METADATA = { 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", }; /** diff --git a/package.json b/package.json index 4ee134b7..ef752aac 100644 --- a/package.json +++ b/package.json @@ -56,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", diff --git a/src/css/states/main_menu.scss b/src/css/states/main_menu.scss index 2458350e..9027d8a8 100644 --- a/src/css/states/main_menu.scss +++ b/src/css/states/main_menu.scss @@ -199,10 +199,6 @@ transform: translateX(50%) rotate(-7deg) scale(1.1); } } - - @include DarkThemeOverride { - color: $colorBlueBright; - } } } @@ -326,6 +322,7 @@ /* @load-async */ background-image: uiResource("icons/edit_key.png") !important; } + @include DarkThemeInvert; } } @@ -354,13 +351,14 @@ box-sizing: border-box; @include PlainText; @include S(margin-bottom, 5px); - display: grid; - grid-template-columns: 1fr auto auto; - @include S(grid-gap, 5px); + display: flex; + flex-direction: column; .author, .version { @include SuperSmallText; + align-self: end; + opacity: 0.4; } .name { overflow: hidden; @@ -457,19 +455,19 @@ .newGameButton { @include IncreasedClickArea(0px); - @include S(margin-left, 15px); + @include S(margin-left, 10px); } .modsButton { @include IncreasedClickArea(0px); - @include S(margin-left, 15px); + @include S(margin-left, 10px); - @include S(width, 20px); + // @include S(width, 20px); - & { - /* @load-async */ - background-image: uiResource("res/ui/icons/mods_white.png") !important; - } + // & { + // /* @load-async */ + // background-image: uiResource("res/ui/icons/mods_white.png") !important; + // } background-position: center center; background-size: D(15px); background-color: $modsColor !important; @@ -837,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 { diff --git a/src/css/states/mods.scss b/src/css/states/mods.scss index 54d8422b..acec41fb 100644 --- a/src/css/states/mods.scss +++ b/src/css/states/mods.scss @@ -62,6 +62,11 @@ @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: ""; @@ -94,6 +99,10 @@ 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; diff --git a/src/js/core/config.js b/src/js/core/config.js index 4df005e3..f98b009e 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -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"; diff --git a/src/js/game/hud/parts/entity_debugger.js b/src/js/game/hud/parts/entity_debugger.js index 640ad4d6..debd456d 100644 --- a/src/js/game/hud/parts/entity_debugger.js +++ b/src/js/game/hud/parts/entity_debugger.js @@ -94,11 +94,12 @@ export class HUDEntityDebugger extends BaseHUDPart {
`; for (const property in val) { - const isRoot = val[property] == this.root; - const isRecursive = recursion.includes(val[property]); - - let hiddenValue = isRoot ? "" : null; - if (isRecursive) { + let hiddenValue = null; + if (val[property] == this.root) { + hiddenValue = ""; + } else if (val[property] instanceof Node) { + hiddenValue = `<${val[property].constructor.name}>`; + } else if (recursion.includes(val[property])) { // Avoid recursion by not "expanding" object more than once hiddenValue = ""; } diff --git a/src/js/game/systems/storage.js b/src/js/game/systems/storage.js index cd204448..20204a89 100644 --- a/src/js/game/systems/storage.js +++ b/src/js/game/systems/storage.js @@ -50,8 +50,13 @@ export class StorageSystem extends GameSystemWithFilter { let targetAlpha = storageComp.storedCount > 0 ? 1 : 0; storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05); - pinsComp.slots[0].value = storageComp.storedItem; - pinsComp.slots[1].value = storageComp.getIsFull() ? BOOL_TRUE_SINGLETON : BOOL_FALSE_SINGLETON; + // a wired pins component is not guaranteed, but if its there, set the value + if (pinsComp) { + pinsComp.slots[0].value = storageComp.storedItem; + pinsComp.slots[1].value = storageComp.getIsFull() + ? BOOL_TRUE_SINGLETON + : BOOL_FALSE_SINGLETON; + } } } diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index e3b9e59b..195c005b 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -31,6 +31,12 @@ import { BaseHUDPart } from "../game/hud/base_hud_part"; * @typedef {{new(...args: any[]): any, prototype: any}} constructable */ +/** + * @template {(...args: any) => any} F The function + * @template {object} T The value of this + * @typedef {(this: T, ...args: Parameters) => ReturnType} bindThis + */ + /** * @template {(...args: any[]) => any} F * @template P @@ -400,7 +406,7 @@ export class ModInterface { * @template {extendsPrams} O the method that will override the old one * @param {C} classHandle * @param {M} methodName - * @param {beforePrams} override + * @param {bindThis, InstanceType>} override */ replaceMethod(classHandle, methodName, override) { const oldMethod = classHandle.prototype[methodName]; @@ -418,7 +424,7 @@ export class ModInterface { * @template {extendsPrams} O the method that will run before the old one * @param {C} classHandle * @param {M} methodName - * @param {O} executeBefore + * @param {bindThis>} executeBefore */ runBeforeMethod(classHandle, methodName, executeBefore) { const oldHandle = classHandle.prototype[methodName]; @@ -437,7 +443,7 @@ export class ModInterface { * @template {extendsPrams} O the method that will run before the old one * @param {C} classHandle * @param {M} methodName - * @param {O} executeAfter + * @param {bindThis>} executeAfter */ runAfterMethod(classHandle, methodName, executeAfter) { const oldHandle = classHandle.prototype[methodName]; diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 96743038..48911abd 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -10,6 +10,9 @@ import { Mod } from "./mod"; import { ModInterface } from "./mod_interface"; import { MOD_SIGNALS } from "./mod_signals"; +import semverValidRange from "semver/ranges/valid"; +import semverSatisifies from "semver/functions/satisfies"; + const LOG = createLogger("mods"); /** @@ -20,6 +23,7 @@ const LOG = createLogger("mods"); * website: string; * description: string; * id: string; + * minimumGameVersion?: string; * settings: [] * }} ModMetadata */ @@ -158,6 +162,27 @@ export class ModLoader { const { modClass, meta } = this.modLoadQueue[i]; const modDataFile = "modsettings_" + meta.id + "__" + meta.version + ".json"; + if (meta.minimumGameVersion) { + console.warn(meta.minimumGameVersion, G_BUILD_VERSION); + const minimumGameVersion = meta.minimumGameVersion; + if (!semverValidRange(minimumGameVersion)) { + alert("Mod " + meta.id + " has invalid minimumGameVersion: " + minimumGameVersion); + continue; + } + if (!semverSatisifies(G_BUILD_VERSION, minimumGameVersion)) { + alert( + "Mod '" + + meta.id + + "' is incompatible with this version of the game: \n\n" + + "Mod requires version " + + minimumGameVersion + + " but this game has version " + + G_BUILD_VERSION + ); + continue; + } + } + let settings = meta.settings; if (meta.settings) { diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index b23a15f3..4d62f2ce 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -161,7 +161,10 @@ export class MainMenuState extends GameState { ${MODS.mods .map(mod => { return ` -
${mod.metadata.name} @ v${mod.metadata.version}
+
+
${mod.metadata.name}
+
by ${mod.metadata.author}
+
`; }) .join("")} @@ -413,7 +416,10 @@ export class MainMenuState extends GameState { } // Mods - this.trackClicks(makeButton(outerDiv, ["modsButton", "styledButton"], " "), this.onModsClicked); + this.trackClicks( + makeButton(outerDiv, ["modsButton", "styledButton"], T.mods.title), + this.onModsClicked + ); buttonContainer.appendChild(outerDiv); } diff --git a/src/js/states/mods.js b/src/js/states/mods.js index c3cbce52..42add8bd 100644 --- a/src/js/states/mods.js +++ b/src/js/states/mods.js @@ -18,6 +18,11 @@ export class ModsState extends TextualGameState {

${this.getStateHeaderTitle()}

+ ${ + (G_IS_STANDALONE || G_IS_DEV) && MODS.mods.length > 0 + ? `` + : "" + } ${ G_IS_STANDALONE || G_IS_DEV ? `` @@ -53,8 +58,9 @@ export class ModsState extends TextualGameState { return `
- ${T.mods.modsInfo} + +
`; @@ -100,6 +106,10 @@ export class ModsState extends TextualGameState { if (openModsFolder) { this.trackClicks(openModsFolder, this.openModsFolder); } + const browseMods = this.htmlElement.querySelector(".browseMods"); + if (browseMods) { + this.trackClicks(browseMods, this.openBrowseMods); + } const checkboxes = this.htmlElement.querySelectorAll(".checkbox"); Array.from(checkboxes).forEach(checkbox => { @@ -119,6 +129,11 @@ export class ModsState extends TextualGameState { ipcRenderer.invoke("open-mods-folder"); } + openBrowseMods() { + this.app.analytics.trackUiClick("mods_sbrowse_link"); + this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.modBrowser); + } + onSteamLinkClicked() { this.app.analytics.trackUiClick("mods_steam_link"); this.app.platformWrapper.openExternalLink( diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 2f76c00f..7070c318 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -1098,6 +1098,7 @@ mods: version: Version openFolder: Open Mods Folder folderOnlyStandalone: Opening the mod folder is only possible when running the standalone. + browseMods: Browse Mods modsInfo: >- To install and manage mods, copy them to the mods folder within the game directory. You can also use the 'Open Mods Folder' button on the top right. diff --git a/yarn.lock b/yarn.lock index cdcdd19e..27552f93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5245,6 +5245,13 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + lz-string@^1.4.4: version "1.4.4" resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz" @@ -7496,6 +7503,13 @@ semver@^7.2.1, semver@^7.3.2: resolved "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== +semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + send@0.17.1: version "0.17.1" resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz" @@ -8800,6 +8814,11 @@ yallist@^3.0.2: resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yaml-js@^0.1.3: version "0.1.5" resolved "https://registry.npmjs.org/yaml-js/-/yaml-js-0.1.5.tgz"