Mod Framework Quick Reference - ahvonenj/synergism-hypersynergy GitHub Wiki

Mod Framework Quick Reference

Here's a collection of some "nice-to-knows" regarding what the mod offers for development.

Querying and observing DOM elements

Wiki references:

HSElementHooker.HookElement()

The HSElementHooker.HookElement() is an async and static method of HSElementHooker which allows for querying for elements in the DOM in an asynchronous manner. Whereas with e.g. synchronous document.querySelector() you're left in a kind of a "tough luck" situation if the element is not found at the time of calling this method, you can instead do something like const myElement = await HSElementHooker.HookElement('#element')` which ensures that the element has been found when using it in code after this call.

There's also a HSElementHooker.hookElements() method which is equivalent to document.querySelectorAll() and can be used to query multiple elements and to ensure that all of the elements have been found.

Example:

Here we use HookElement to await for the buildings tab button and then set it's color to red.

async applyPatch(): Promise<void> {
    const buildingBtn = await HSElementHooker.HookElement('#buildingstab') as HTMLButtonElement;
    buildingBtn.style.color = 'red';
}

HSElementHooker.watchElement()

The HSElementHooker.watchElement() method is another static method of HSElementHooker which allows for observing an element for changes. Under the hood it uses MutationObserver to achieve this and acts as a sort of an abstraction layer to hide much of the complexity when it comes to working with MutationObservers.

Example:

Here's a snipped from HSHepteracts module. In the view where you can expand and craft hepteracts, there is a text like "You possess x Hepteracts! You know where to get these, right?" shown. Since we can't access the variable itself which holds the amount of hepteracts the player currently owns, we instead have to parse it from the DOM.

Now, due to multiple reasons it doesn't do much for us to read this value only once, so we set up a watch for it. This watch will then observe for changes in the element (mainly it's HTML content in this case) and do something when the value changes. In this case we update the HSHepteract module's internal #ownedHepteracts value. Having this set up we can be pretty sure that whenever we need to use the player's current hepteract count for something, we can use the #ownedHepteracts value and know that it is automatically updated by the watch when it detects changes.

self.#ownedHepteractsWatch = HSElementHooker.watchElement(self.#ownedHepteractsElement, (value) => {
    try {
        const hepts = parseFloat(value);
        self.#ownedHepteracts = hepts;
    } catch (e) {
        HSLogger.error(`Failed to parse owned hepteracts`, self.context);
    }

    self.#watchUpdatePending = false;
}, 
{
    greedy: true,
    overrideThrottle: true,
    valueParser: (element) => {
        const subElement = element.querySelector('span');
        const value = subElement?.innerText;
        return value;
    }
});

The first argument that the HSElementHooker.watchElement() takes is the element to be watched. It is recommended to use HSElementHooker.HookElement method for this like:

const elementToBeWatched = await HSElementHooker.HookElement('#element');
HSElementHooker.watchElement(elementToBeWatched, (value) => { ... }, { ... });

The second argument is the callback function which will be called when changes to the element have been detected and AFTER the changes have gone through valueParser which can be supplied in the third argument which is the options object, so for example:

const elementToBeWatched = await HSElementHooker.HookElement('#element');
HSElementHooker.watchElement(elementToBeWatched, (value) => { ... }, { 
    valueParser: (element) => {
        const value = element?.innerText;
        return value;
    }
});

So here the valueParser is triggered first when changes have been detected. The valueParser gets the changed element as an argument and we can do things like getting the changed element's innerText and then supply that to the actual callback function of the HookElement method.

In this example the value argument in the callback for the HookElement method would be the element's innerText as returned by the valueParser.

Persisting data (saving and loading from localStorage)

Data persistence is needed when we want to have some data to persist through page reloads.

The HSStorage module is responsible for data persistence aka. saving and loading things from the localStorage. While the HSStorage module can be used as is like:

const storageModule = HSModuleManager.getModule('HSStorage') as HSStorage;

if(storageModule) {
    storageModule.setData(someKey, someData);
} else {
    HSLogger.warn(`saveState - Could not find storage module`, this.context);
}

It is recommended to use HSStorage in conjunction with HSPersistable interface. To do this you would simply define your module class which needs to persist some data like so:

export class MyClassThatNeedsDataPersistence extends HSModule implements HSPersistable {}

The HSPersistable interface is defined like so:

export interface HSPersistable {
    saveState(): Promise<any>;
    loadState(): Promise<void>;
}

So for a class to implement it, we need to add the saveState and loadState methods to it:

export class MyClassThatNeedsDataPersistence extends HSModule implements HSPersistable {
    async saveState(): Promise<any> {
        const storageModule = HSModuleManager.getModule('HSStorage') as HSStorage;

        if(storageModule) {
            storageModule.setData(key, data);
        } else {
            HSLogger.warn(`saveState - Could not find storage module`, this.context);
        }
    }

    async loadState(): Promise<void> {
        const storageModule = HSModuleManager.getModule('HSStorage') as HSStorage;

        if(storageModule) {
            const data = storageModule.getData(key) as string;

            if(!data) {
                HSLogger.warn(`loadState - No data found`, this.context);
                return;
            }

            try {
                const parsedData = JSON.parse(data);
            } catch(e) {
                HSLogger.warn(`loadState - Error parsing data`, this.context);
                return;
            }
        } else {
            HSLogger.warn(`loadState - Could not find storage module`, this.context);
        }
    }
}

Implementing data persistence like this (by implementing HSPersistable) makes it clear that your module intends to use localStorage for something and that it will have saveState and loadState methods to do so.

More full example from HSAmbrosia module:

This is how the HSAmbrosia module remembers the loadout icons (over page refreshes) which the player can save.

export class HSAmbrosia extends HSModule implements HSPersistable {

    #loadoutState: HSAmbrosiaLoadoutState = new Map<AMBROSIA_LOADOUT_SLOT, AMBROSIA_ICON>();

    constructor(moduleName: string, context: string, moduleColor?: string) {
        super(moduleName, context, moduleColor);
    }

    async init() {
        this.loadState();
        this.isInitialized = true;
    }
    async saveState(): Promise<any> {
        const storageModule = HSModuleManager.getModule('HSStorage') as HSStorage;

        if(storageModule) {
            const serializedState = JSON.stringify(Array.from(this.#loadoutState.entries()));
            storageModule.setData(HSGlobal.HSAmbrosia.storageKey, serializedState);
        } else {
            HSLogger.warn(`saveState - Could not find storage module`, this.context);
        }
    }

    async loadState(): Promise<void> {
        const storageModule = HSModuleManager.getModule('HSStorage') as HSStorage;

        if(storageModule) {
            const data = storageModule.getData(HSGlobal.HSAmbrosia.storageKey) as string;

            if(!data) {
                HSLogger.warn(`loadState - No data found`, this.context);
                return;
            }

            try {
                const parsedData = JSON.parse(data) as [AMBROSIA_LOADOUT_SLOT, AMBROSIA_ICON][];
                this.#loadoutState = new Map(parsedData);
            } catch(e) {
                HSLogger.warn(`loadState - Error parsing data`, this.context);
                return;
            }
        } else {
            HSLogger.warn(`loadState - Could not find storage module`, this.context);
        }
    }
}

Game state

Wiki references:

The HSGameState module is responsible for observing and holding information about the current game state. Currently (2.5.2025) that mainly means keeping track of in which view or page (and subview of that page) the player is currently on.

So for example when the player switches from the "Buildings" view to the "Corruptions" view, the HSGameState module will be aware of that transition.

The HSGameState works and can be used by subscribing to whatever changes you are interested in. When HSGameState module noticed changes, it will then notify all of it's subscribers of these changes.

Example:

The HSHepteracts module subscribes to view changes in order to start and stop certain high performance element watchers which would otherwise cause needless performance overhead when the player is not interacting with the hepteracts page at all.

const gameStateMod = HSModuleManager.getModule<HSGameState>('HSGameState');

if(gameStateMod) {
    gameStateMod.subscribeGameStateChange(GAME_STATE_CHANGE.MAIN_VIEW, (prevView, currentView) => {
        if(prevView.getId() === MAIN_VIEW.CUBES && 
            currentView.getId() !== MAIN_VIEW.CUBES && 
            gameStateMod.getCurrentCubeView().getId() === CUBE_VIEW.HEPTERACT_FORGE
        ) {
            if(self.#ownedHepteractsWatch) {
                HSLogger.debug("Hepteract forge view closed, stopping watch", this.context);
                // ...code
            }
        } 
    });

    gameStateMod.subscribeGameStateChange(GAME_STATE_CHANGE.CUBE_VIEW, async (prevView, currentView) => {
        if(currentView.getId() === CUBE_VIEW.HEPTERACT_FORGE) {
            HSLogger.debug("Hepteract forge view opened, starting watch", this.context);
            // ...code
        }
    });
}

As can be seen, we subscribe to two kinds of game state changes:

  • GAME_STATE_CHANGE.MAIN_VIEW for when the player switches the main tab (Buildings, Corruptions, Research, ...)
  • GAME_STATE_CHANGE.CUBE_VIEW for when the player switches the tab within the "WOW! Cubes" tab (Cube tributes, Tesseract Gifts, ...)

By subscribing to these two view changes we can then write logic to start the element watcher only when the player is in the "Hepteract Forge" tab and have the watch stopped otherwise as it is not needed anywhere else.

⚠️ **GitHub.com Fallback** ⚠️