Modding the Final Earth 2
This guide will give you a quick introduction on how to mod the Steam version of The Final Earth 2. Modding is a work in process feature, so note that this is not necessarily final. Modding tools and this guide will be expanded over time.
The possibilities for mods are endless. However, complex mods currently require some extensive knowledge of the game code. Simpler mods, like adding a new building, are much easier to make. Some coding knowledge is required, though, except for very simple mods such as texture packs.
To talk to other modders, check out their creations and share yours, you're welcome to join the Discord and check out the #modding channel!
I hope you have fun modding the game and I'm excited to see what you'll create!
Table of Contents
- Table of Contents
- Requirements
- Checking out the game files
- Creating a mod: directory and files
- Load Order
- Coding your mod
- Delta timing
- Testing your mod
- Helper functions
ModTools.makeBuilding
ModTools.makeBuildingUpgrade
- Adding City Upgrades, World Resources or City Policies
ModTools.addBuildBasedUnlock
ModTools.addStatBasedUnlock
ModTools.addMaterial
ModTools.produce
ModTools.consume
ModTools.onCityUpdate
ModTools.onModsLoaded
ModTools.onLoadStart
ModTools.addSaveData
ModTools.addSaveDataEarly
- Other code documentation
- The ResizingBytesQueue
- Citizen movement
Requirements
To be able to mod The Final Earth 2, you will need to have the Steam version of The Final Earth 2 installed.
You will also need a text/code editor and a sprite editor with transparency support (e.g. not just Paint). I personally use Visual Studio Code and Paint.NET, which are both free and also very good.
Checking out the game files
While making mods, you may sometimes want to see how something is implemented in-game. To do this, right-click the game in your Steam libraries, choose Properties, then Local Files, then Browse. The game files are in the game
directory.
Sprites are packed into a single sprite sheet. In case you need the original sprites, you can download them here. Note that some of these sprites are unused.
Creating a mod: directory and files
To start modding, open Explorer and go to %localappdata%\the-final-earth-2\User Data\Default\mods
. Just add a folder here to make a new mod.
All files in your modding directory and its subdirectories will be loaded by the game. In particular:
buildinginfo.json
should contain buildings your mod adds.buildingUpgradesInfo.json
should contain building upgrades.cityUpgradesInfo.json
is for city upgrades.policiesInfo.json
can contain new city policies.buildableWorldResourcesInfo.json
should contain world resources, such as new forests, that can be placed by the player.buildingCategoriesInfo.json
can contain any new building category you want to add.decorationsInfo.json
should contain decorations.stories.json
should describe scenarios you want to add to the game. The scenarios themselves are JSON files.
These JSON files should contain an array with info objects about the elements you want to add. For an example of what this should look like, open the respective game file. You can also overwrite existing elements of the game by using their className
.
Note that in most cases, such as for buildings and upgrades, you will need to add code in addition to the JSON object. Otherwise, your mod won't work and the game may crash.
Here's an example of a possible buildinginfo.json
:
[
{
"className": "CorporationOfTheOwl",
"name": "Corporation of the Owl HQ",
"description": "They see everything.",
"food": 50000,
"wood": 50000,
"stone": 0,
"machineParts": 0,
"refinedMetal": 0,
"computerChips": 1250,
"knowledge": 75000,
"category": "Unique Buildings",
"unlockedByDefault": false,
"specialInfo": [
"unique"
],
"residents": 12,
"quality": 100,
"jobs": 12,
"showUnlockHint": "Build The Machine to unlock!"
},
{
"className": "FunctionalHouse",
"name": "Functional House",
"description": "Don't complain, this house is exactly what you need.",
"food": 0,
"wood": 0,
"stone": 10,
"machineParts": 0,
"knowledge": 10000,
"computerChips": 1,
"category": "Houses",
"unlockedByDefault": false,
"specialInfo": [],
"residents": 10,
"quality": 35
}
]
Furthermore:
- JS files will be executed automatically. See Coding your mod below for more info about this.
- PNG image files will be loaded as a texture. Any existing texture with the same name in-game will be overwritten, which you can use to make texture packs.
- Audio files (ogg, mp3, wav) will be loaded and can be played by code.
- You can also load textures from sprite sheets generated by tools like ShoeBox (free) or TexturePacker (paid, faster). You will need to use PixiJS compatible export settings. This will generate a JSON file and a PNG.
Load Order
Currently, the order in which mods are loaded is not defined. Please keep this into account.
Coding your mod
While the game is originally written in Haxe, mods use JavaScript and can hook into the compiled JavaScript of the game. You can also use all features of PixiJS version 6.2.1, the graphics library used by the game.
To add code to your mod, just create a JavaScript file and start coding. The code will be automatically executed after the game has been loaded.
You can overwrite or expand functions in the game code. In most cases, you will want to call the parent function, plus do something extra before or after that. Here's an example of how that might look like:
(function(existingFunction) {
gui_CityGUI.prototype.addGeneralStatistics = function () {
existingFunction.call(this);
//Insert new code here
};
}(gui_CityGUI.prototype.addGeneralStatistics));
With some actual code inserted to add a button, it could look like this:
(function(existingFunction) {
gui_CityGUI.prototype.addGeneralStatistics = function () {
existingFunction.call(this);
// Constants for later use
const _gthis = this;
const generalStatistics =
this.cityInfo.children[this.cityInfo.children.length - 1];
// A stat to show on the button
function getWorkBuildingNumber() {
return _gthis.city.workBuildings.length;
}
// Create a new button
// (onclick, onhover, text, icon, parent, default width, show active)
this.workBuildingsButton = this.createInfoButton(
function() {
// Hide/show a window on clicking the button
if(_gthis.windowRelatedTo == "workBuildings") {
_gthis.closeWindow();
} else {
_gthis.createWindow("workBuildings");
_gthis.windowAddInfoText(null,
() => `There are ${getWorkBuildingNumber()} work buildings` +
"in the city.");
_gthis.windowAddBottomButtons();
}
},
function() {
_gthis.tooltip.setText("Work Buildings",
"The number of work buildings in the city.");
},
function() {
return getWorkBuildingNumber() + "";
},
"spr_work", generalStatistics, 20, function() {
return _gthis.windowRelatedTo == "workBuildings";
});
this.workBuildingsButton.keyboardButton = Keyboard.getLetterCode("B");
// Add it to the UI!
generalStatistics.insertChild(this.workBuildingsButton, 1);
};
}(gui_CityGUI.prototype.addGeneralStatistics));
There are also some helper functions. These can be used to easily add buildings, materials and more. For a full list, see Helper functions below.
Here's an example of how to add a new building in code:
ModTools.makeBuilding("IronHouse", (superClass) => { return {
walkAround: function(citizen, stepsInBuilding) {
if (random_Random.getInt(3) == 1) {
citizen.changeFloor();
return;
}
//Slowly move in the house
citizen.moveAndWaitRandom(3, 17, 60, 90, null, false, true);
},
get_possibleUpgrades: function() {
return [];
}
};}, "spr_ironhouse");
Delta timing
The game uses delta timing. This means all timing is based on the length of time the last frame took. timeMod
is passed to many functions so you can deal with this, i.e. by multiplying the material amounts produced per step with it.
Testing your mod
Simply run the game to test your mod. There are a few useful keyboard shortcuts you can use for testing:
Ctrl+F5 to reload the game. This loads changes you have made to mods, so you can test them more quickly.Ctrl+F12 to open developer tools.
Helper functions
Helper functions are part of the ModTools
object. Currently, the following helper functions are available. A ? denotes that a function argument is optional.
ModTools.makeBuilding(className, fields, spriteName, ?saveFunc, ?loadFunc, ?superClass)
Define a new building.
className
(string) - The name of the building class. Should be equal to a className inbuildinginfo.json
.fields
(object or function) - Fields and field overwrites to add this building. This will define its behaviour. In case you need the parent class for calling super functions, pass a function that takes the parent class as argument and returns the fields. Some common fields:walkAround(citizen, stepsInBuilding)
- defines citizen behaviour in houses. Called whenever a citizen is in their house and has nothing to do (i.e. no path). In most cases, you'd assign a path here. See the Citizen movement section for common functions to use here.work(citizen, timeMod, shouldStopWorking)
- defines citizen behaviour at workplaces. Be sure to callcitizen.stopWork()
ifshouldStopWorking
is true and the citizen is done with their job. LikewalkAround
, this is only called when the citizen has no path, so not necessarily every update.update(timeMod)
- should contain anything that has to happen on every update.get_possibleUpgrades()
- should return an array with all building upgrades of this building.spriteName
(string) - The sprite texture of this building. A building sprite should be 64x20 pixels; three frames of 20x20 pixels each. The first frame should contain the building foreground without a door. The second one should contain the building foreground with a door. The third should contain the background. There should be two pixels between frames.saveFunc
(optional, function) - If you need to save something custom for the building, you can do it by passing a save function. It should take aResizingBytesQueue
(see the section below) as argument, and you can simply usethis
to get access to the building properties.loadFunc
(optional, function) - You should load anything you saved here. It again gets passed a ResizingBytesQueue.superClass
(optional, class) - The super class of this building. This is set correctly automatically in most cases.
ModTools.buildingAddEntertainmentProperties(building, getEntertainmentCapacity,
beEntertained, entertainmentType, minimumNormalTimeToSpend, maximumNormalTimeToSpend,
minimumEntertainmentGroupSatisfy, maximumEntertainmentGroupSatisfy, isOpenFunc)
After defining a building, if it is an entertainment building, you can add entertainment properties to it. Note that some uncommon functionality, such as get_isOpenForExistingVisitors
, is not covered by this function and will have to be set manually if you need it.
building
(class) - The building class to add properties to. This is returned fromModTools.makeBuilding
, or you can usebuilding_className
.getEntertainmentCapacity
(function) - A function that should return the capacity of the building. This controls how much the building improves the relevant happiness. This is a function as it can depend on properties like the number of workers.beEntertained
(function) - What should happen when a citizen is being entertained here. For example, this could be dancing for a nightclub. Should be a function that takes a citizen and the timeMod. Similar towalkAround
, this is only called if the citizen does not have a path.entertainmentType
- The entertainment type of this building. This can be one of the following:entertainmentTypes.Club
entertainmentTypes.Bar
entertainmentTypes.Art
entertainmentTypes.Nature
entertainmentTypes.Games
entertainmentTypes.Education
minimumNormalTimeToSpend
(number) - How long a citizen normally spends here at least.maximumNormalTimeToSpend
(number) - How long a citizen normally spends here at most.minimumEntertainmentGroupSatisfy
(number) - How many days the citizen won't visit entertainment of this group, at least.maximumEntertainmentGroupSatisfy
(number) - For how many days at most the citizen has enough of visiting entertainment of this group.isOpenFunc
(function) - Whether this building is open or not. Should be a function returning a boolean.
ModTools.makeBuildingUpgrade(className, fields, ?onCreate, ?spriteName, ?displayLayer)
Define a new building upgrade. You should also add it to one or more buildings by adding it to a get_possibleUpgrades
.
className
(string) - The name of the upgrade class. Should be equal to a className inbuildingUpgradesInfo.json
fields
(object or function) - Fields and field overwrites to add this upgrade. This will define its behaviour. In case you need the parent class for calling super functions, pass a function that takes the parent class as argument and returns the fields. You can use thebuilding
variable to get the building the upgrade has been applied to. A common field:get_bonusAttractiveness()
- How much quality this adds to a house.onCreate
(optional, function) - What to do when this building upgrade is created. This is called after upgrading, and after loading a save file. You can use thebuilding
variable to get the building the upgrade has been applied to.spriteName
(optional, string) - The name of the texture of this upgrade.displayLayer
(optional) - The layer on which to display this upgrade. Can be one of the following:upgradeDisplayLayer.Foreground
- This upgrade should be displayed in front of the building.upgradeDisplayLayer.Middle
- This upgrade should be displayed behind the windows of the building, but above any citizens inside.upgradeDisplayLayer.Background
- This upgrade should be displayed in the background, behind any citizens in the building.
Adding City Upgrades, World Resources or City Policies
Currently, no helper functions are available for adding city upgrades, world resources or city policies. You can construct the classes manually for now.
ModTools.addBuildBasedUnlock(elementClass, unlockFunc, ?fullUnlockFunc)
Set the unlock condition of a city element (building, upgrade or policy), based on the current number of buildings per type. For buildings, you can set an unlock hint in buildinginfo.json
. If this is set, the unlockFunc
defines when this unlock hint is shown, and the building is only actually unlocked when fullUnlockFunc
returns true. Otherwise, you only need to define the unlockFunc
. Both of these functions are passed a haxe dictionary as an argument. Use its h
property to get a JavaScript object containing the number of buildings per type.
elementClass
(class) - The city element class to set unlock conditions for.unlockFunc
(function) - See abovefullUnlockFunc
(optional, function) - See above
Example code:
ModTools.addBuildBasedUnlock(buildingUpgrades_FancyTablets, function(blds) {
return blds.h["buildings.School"] >= 10;
});
ModTools.addStatBasedUnlock(elementClass, unlockFunc, ?fullUnlockFunc)
Set the unlock condition of a city element (building, upgrade or policy), based on the stats of the city (e.g. happiness). For buildings, you can set an unlock hint in buildinginfo.json
. If this is set, the unlockFunc
defines when this unlock hint is shown, and the building is only actually unlocked when fullUnlockFunc
returns true. Otherwise, you only need to define the unlockFunc
. Both of these functions are passed the city as an argument.
elementClass
(class) - The city element class to set unlock conditions for.unlockFunc
(function) - See above.fullUnlockFunc
(optional, function) - See above.
ModTools.addMaterial(varName, displayName, description, ?tooltipExt)
Add a custom material to the game. It will then be shown in the UI and can be produced, consumed and used in costs.
varName
(string) - The name of this material in code. Should not contain spaces or spacial characters. There should be a 10x10 sized sprite called spr_resource_varName, where varName should be equal to this argument.displayName
(string) - The name of this material in the game.description
(string) - The description of this material. This will be put in the tooltip you see when you hover above the material in the UI.tooltipExt
(optional, function) - A function returning a string which will be added to the tooltip.
ModTools.produce(city, varName, amount, ?ind)
Produce a given amount of a material. Using this function ensures that this is added to the daily production.
city
- The city.varName
(string) - What material to produce. This can be a custom material or one of the following:"food"
"wood"
"stone"
"machineParts"
"refinedMetal"
"computerChips"
"knowledge"
amount
(number) - How much to produce of this material.ind
(optional, number) - You can pass the index of the material for optimization reasons. Otherwise, it will be looked up which is slightly slower. You can look up the index yourself withMaterialsHelper.findMaterialIndex
. Never hardcode it though - just look it up once and store it in a variable.
ModTools.consume(city, varName, amount, ?ind)
Consume a given amount of a material. See ModTools.produce
above for a description of the parameters.
ModTools.onCityUpdate(func)
Adds a function that will be called on every city update.
func
(function) - This function will be called on each update. It can have two arguments: the city and the timeMod for delta timing.
ModTools.onModsLoaded(func)
Adds a function that will be called after all mods have been loaded.
func
(function) - This function will be called after all mods have been loaded.
ModTools.onLoadStart(func)
Adds a function that will be called before loading a save file.
func
(function) - This function will be called before starting to load a save file. The city is passed as an argument.
ModTools.addSaveData(modIdentifier, saveFunc, loadFunc, ?version)
Add something to the game save. This can be used for anything you want to save that's relevant for the whole city, instead of just for a single building. The specified functions trigger after saving/loading everything else. Also see the section below on how to use the ResizingBytesQueue
.
modIdentifier
(string) - An identifier of your mod, for example the mod name. The game will use this to ensure your mod gets the data it saved back.saveFunc
(function) - What to save. This function gets passed the city and a ResizingBytesQueue in which you can save anything you need.loadFunc
(function) - How to load what you saved. This function gets the city, the ResizingBytesQueue with save data, and the version (see below).version
(optional, integer number, default 0) - The version of your mod save file. You can use this to ensure your mod keeps working smoothly on version upgrades, as it is passed toloadFunc
. For example, if you add an additional number that's only saved in version 3 or later, only read it whenversion >= 3
.
ModTools.addSaveDataEarly(modIdentifier, saveFunc, loadFunc, ?version)
The same as ModTools.addSaveData
, except that the functions specified here trigger before loading anything else.
Other code documentation
The code of The Final Earth 2 is quite complex, and I'm definitively not able to document all of it here. Instead, this section contains documentation of some parts you may use commonly.
The ResizingBytesQueue
The ResizingBytesQueue is a automatically resizing array of bytes, which is used to store save data. It is passed to all saving related functions. While saving, use the add... functions to add data to it. While loading, use the read... functions to read the data back. There are functions for various data types. The following are common:
addFloat(value)
- Add a 64 bit floating point number to the queue.readFloat()
- Read a 64 bit floating point number from the queue.addInt(value)
- Add a 32 bit integer number to the queue.readInt()
- Read a 32 bit integer number from the queue.addByte(value)
- Add a single byte (0-255) to the queue.readByte()
- Read a single byte (0-255) from the queue.addBool(value)
- Add a boolean (true or false) to the queue.readBool()
- Read a boolean (true or false) from the queue.addString(value)
- Add a string to the queue.readString()
- Read a string from the queue.
Citizen movement
For most buildings, you'll want to model movement of citizens inside. There are some helper functions on citizen objects to assign paths within a building. These are commonly used:
wait(time, ?then)
- Wait for a number of steps.waitRandom(timeMin, timeMax, then)
- Wait for a random number of steps between two values.move(x, ?then, ?slowMove)
- Move horizontally to the given position.moveRandom(xMin, xMax, ?then, ?slowMove)
- Move horizontally to a X position between two values.moveAndWait(x, time, ?then, ?modifyWithHappiness, ?slowMove)
- Move, then wait. You can also usemodifyWithHappiness
to increase the wait time if the happiness is high and decrease it if it's low.moveAndWaitRandom(xMin, xMax, timeMin, timeMax, ?then, ?modifyWithHappiness, ?slowMove)
- Move to a random position, then wait a random time, both within given bounds.changeFloor(?then)
- Most buildings have two floors. You can use this to move towards the other floor.changeFloorAndWait(waitTime, ?then)
- Move towards the other floor, then wait.changeFloorAndWaitRandom(minTime, maxTime, ?then)
- Move towards the other floor, then wait randomly.changeFloorAndMoveRandom(xMin, xMax, ?then)
- Move towards the other floor, then move horizontally.isAtBottomFloor()
- Returns whether the citizen is on the bottom floor of a building.
You may also need the following properties:
relativeX
- The horizontal position of the citizen relative to the building.relativeY
- The vertical position of the citizen relative to the building. Note that 0 is at the bottom of the building here.