1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2024-10-27 20:34:29 +00:00
tobspr_shapez.io/src/js/application.js
tobspr c41aaa1fc5
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>
2022-02-01 16:35:49 +01:00

448 lines
15 KiB
JavaScript

import { AnimationFrame } from "./core/animation_frame";
import { BackgroundResourcesLoader } from "./core/background_resources_loader";
import { IS_MOBILE } from "./core/config";
import { GameState } from "./core/game_state";
import { GLOBAL_APP, setGlobalApp } from "./core/globals";
import { InputDistributor } from "./core/input_distributor";
import { Loader } from "./core/loader";
import { createLogger, logSection } from "./core/logging";
import { StateManager } from "./core/state_manager";
import { TrackedState } from "./core/tracked_state";
import { getPlatformName, waitNextFrame } from "./core/utils";
import { Vector } from "./core/vector";
import { AdProviderInterface } from "./platform/ad_provider";
import { NoAdProvider } from "./platform/ad_providers/no_ad_provider";
import { NoAchievementProvider } from "./platform/browser/no_achievement_provider";
import { AnalyticsInterface } from "./platform/analytics";
import { GoogleAnalyticsImpl } from "./platform/browser/google_analytics";
import { SoundImplBrowser } from "./platform/browser/sound";
import { PlatformWrapperImplBrowser } from "./platform/browser/wrapper";
import { PlatformWrapperImplElectron } from "./platform/electron/wrapper";
import { PlatformWrapperInterface } from "./platform/wrapper";
import { ApplicationSettings } from "./profile/application_settings";
import { SavegameManager } from "./savegame/savegame_manager";
import { AboutState } from "./states/about";
import { ChangelogState } from "./states/changelog";
import { InGameState } from "./states/ingame";
import { KeybindingsState } from "./states/keybindings";
import { MainMenuState } from "./states/main_menu";
import { MobileWarningState } from "./states/mobile_warning";
import { PreloadState } from "./states/preload";
import { SettingsState } from "./states/settings";
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
import { RestrictionManager } from "./core/restriction_manager";
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
* @typedef {import("./platform/game_analytics").GameAnalyticsInterface} GameAnalyticsInterface
* @typedef {import("./platform/sound").SoundInterface} SoundInterface
* @typedef {import("./platform/storage").StorageInterface} StorageInterface
*/
const logger = createLogger("application");
// Set the name of the hidden property and the change event for visibility
let pageHiddenPropName, pageVisibilityEventName;
if (typeof document.hidden !== "undefined") {
// Opera 12.10 and Firefox 18 and later support
pageHiddenPropName = "hidden";
pageVisibilityEventName = "visibilitychange";
// @ts-ignore
} else if (typeof document.msHidden !== "undefined") {
pageHiddenPropName = "msHidden";
pageVisibilityEventName = "msvisibilitychange";
// @ts-ignore
} else if (typeof document.webkitHidden !== "undefined") {
pageHiddenPropName = "webkitHidden";
pageVisibilityEventName = "webkitvisibilitychange";
}
export class Application {
/**
* 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;
// Global stuff
this.settings = new ApplicationSettings(this);
this.ticker = new AnimationFrame();
this.stateMgr = new StateManager(this);
this.savegameMgr = new SavegameManager(this);
this.inputMgr = new InputDistributor(this);
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
this.clientApi = new ClientAPI(this);
// Restrictions (Like demo etc)
this.restrictionMgr = new RestrictionManager(this);
// Platform dependent stuff
/** @type {StorageInterface} */
this.storage = null;
/** @type {SoundInterface} */
this.sound = null;
/** @type {PlatformWrapperInterface} */
this.platformWrapper = null;
/** @type {AchievementProviderInterface} */
this.achievementProvider = null;
/** @type {AdProviderInterface} */
this.adProvider = null;
/** @type {AnalyticsInterface} */
this.analytics = null;
/** @type {GameAnalyticsInterface} */
this.gameAnalytics = null;
this.initPlatformDependentInstances();
// Track if the window is focused (only relevant for browser)
this.focused = true;
// Track if the window is visible
this.pageVisible = true;
// Track if the app is paused (cordova)
this.applicationPaused = false;
/** @type {TypedTrackedState<boolean>} */
this.trackedIsRenderable = new TrackedState(this.onAppRenderableStateChanged, this);
// Dimensions
this.screenWidth = 0;
this.screenHeight = 0;
// Store the timestamp where we last checked for a screen resize, since orientationchange is unreliable with cordova
this.lastResizeCheck = null;
// 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();
}
/**
* Initializes all platform instances
*/
initPlatformDependentInstances() {
logger.log("Creating platform dependent instances (standalone=", G_IS_STANDALONE, ")");
if (G_IS_STANDALONE) {
this.platformWrapper = new PlatformWrapperImplElectron(this);
} else {
this.platformWrapper = new PlatformWrapperImplBrowser(this);
}
// Start with empty ad provider
this.adProvider = new NoAdProvider(this);
this.sound = new SoundImplBrowser(this);
this.analytics = new GoogleAnalyticsImpl(this);
this.gameAnalytics = new ShapezGameAnalytics(this);
this.achievementProvider = new NoAchievementProvider(this);
}
/**
* Registers all game states
*/
registerStates() {
/** @type {Array<typeof GameState>} */
const states = [
WegameSplashState,
PreloadState,
MobileWarningState,
MainMenuState,
InGameState,
SettingsState,
KeybindingsState,
AboutState,
ChangelogState,
PuzzleMenuState,
LoginState,
ModsState,
];
for (let i = 0; i < states.length; ++i) {
this.stateMgr.register(states[i]);
}
}
/**
* Registers all event listeners
*/
registerEventListeners() {
window.addEventListener("focus", this.onFocus.bind(this));
window.addEventListener("blur", this.onBlur.bind(this));
window.addEventListener("resize", () => this.checkResize(), true);
window.addEventListener("orientationchange", () => this.checkResize(), true);
if (!G_IS_MOBILE_APP && !IS_MOBILE) {
window.addEventListener("mousemove", this.handleMousemove.bind(this));
window.addEventListener("mouseout", this.handleMousemove.bind(this));
window.addEventListener("mouseover", this.handleMousemove.bind(this));
window.addEventListener("mouseleave", this.handleMousemove.bind(this));
}
// Unload events
window.addEventListener("beforeunload", this.onBeforeUnload.bind(this), true);
window.addEventListener("unload", this.onUnload.bind(this), true);
document.addEventListener(pageVisibilityEventName, this.handleVisibilityChange.bind(this), false);
// Track touches so we can update the focus appropriately
document.addEventListener("touchstart", this.updateFocusAfterUserInteraction.bind(this), true);
document.addEventListener("touchend", this.updateFocusAfterUserInteraction.bind(this), true);
}
/**
* Checks the focus after a touch
* @param {TouchEvent} event
*/
updateFocusAfterUserInteraction(event) {
const target = /** @type {HTMLElement} */ (event.target);
if (!target || !target.tagName) {
// Safety check
logger.warn("Invalid touchstart/touchend event:", event);
return;
}
// When clicking an element which is not the currently focused one, defocus it
if (target !== document.activeElement) {
// @ts-ignore
if (document.activeElement.blur) {
// @ts-ignore
document.activeElement.blur();
}
}
// If we click an input field, focus it now
if (target.tagName.toLowerCase() === "input") {
// We *really* need the focus
waitNextFrame().then(() => target.focus());
}
}
/**
* Handles a page visibility change event
* @param {Event} event
*/
handleVisibilityChange(event) {
window.focus();
const pageVisible = !document[pageHiddenPropName];
if (pageVisible !== this.pageVisible) {
this.pageVisible = pageVisible;
logger.log("Visibility changed:", this.pageVisible);
this.trackedIsRenderable.set(this.isRenderable());
}
}
/**
* Handles a mouse move event
* @param {MouseEvent} event
*/
handleMousemove(event) {
this.mousePosition = new Vector(event.clientX, event.clientY);
}
/**
* Internal on focus handler
*/
onFocus() {
this.focused = true;
}
/**
* Internal blur handler
*/
onBlur() {
this.focused = false;
}
/**
* Returns if the app is currently visible
*/
isRenderable() {
return !this.applicationPaused && this.pageVisible;
}
onAppRenderableStateChanged(renderable) {
logger.log("Application renderable:", renderable);
window.focus();
const currentState = this.stateMgr.getCurrentState();
if (!renderable) {
if (currentState) {
currentState.onAppPause();
}
} else {
if (currentState) {
currentState.onAppResume();
}
this.checkResize();
}
this.sound.onPageRenderableStateChanged(renderable);
}
/**
* Internal unload handler
*/
onUnload(event) {
if (!this.unloaded) {
logSection("UNLOAD HANDLER", "#f77");
this.unloaded = true;
const currentState = this.stateMgr.getCurrentState();
if (currentState) {
currentState.onBeforeExit();
}
this.deinitialize();
}
}
/**
* Internal before-unload handler
*/
onBeforeUnload(event) {
logSection("BEFORE UNLOAD HANDLER", "#f77");
const currentState = this.stateMgr.getCurrentState();
if (!G_IS_DEV && currentState && currentState.getHasUnloadConfirmation()) {
if (!G_IS_STANDALONE) {
// Need to show a "Are you sure you want to exit"
event.preventDefault();
event.returnValue = "Are you sure you want to exit?";
}
}
}
/**
* Deinitializes the application
*/
deinitialize() {
return this.sound.deinitialize();
}
/**
* Background frame update callback
* @param {number} dt
*/
onBackgroundFrame(dt) {
if (this.isRenderable()) {
return;
}
const currentState = this.stateMgr.getCurrentState();
if (currentState) {
currentState.onBackgroundTick(dt);
}
}
/**
* Frame update callback
* @param {number} dt
*/
onFrameEmitted(dt) {
if (!this.isRenderable()) {
return;
}
const time = performance.now();
// Periodically check for resizes, this is expensive (takes 2-3ms so only do it once in a while!)
if (!this.lastResizeCheck || time - this.lastResizeCheck > 1000) {
this.checkResize();
this.lastResizeCheck = time;
}
const currentState = this.stateMgr.getCurrentState();
if (currentState) {
currentState.onRender(dt);
}
}
/**
* Checks if the app resized. Only does this once in a while
* @param {boolean} forceUpdate Forced update of the dimensions
*/
checkResize(forceUpdate = false) {
const w = window.innerWidth;
const h = window.innerHeight;
if (this.screenWidth !== w || this.screenHeight !== h || forceUpdate) {
this.screenWidth = w;
this.screenHeight = h;
const currentState = this.stateMgr.getCurrentState();
if (currentState) {
currentState.onResized(this.screenWidth, this.screenHeight);
}
const scale = this.getEffectiveUiScale();
waitNextFrame().then(() => document.documentElement.style.setProperty("--ui-scale", `${scale}`));
window.focus();
}
}
/**
* Returns the effective ui sclae
*/
getEffectiveUiScale() {
return this.platformWrapper.getUiScale() * this.settings.getInterfaceScaleValue();
}
/**
* Callback after ui scale has changed
*/
updateAfterUiScaleChanged() {
this.checkResize(true);
}
}