-
Notifications
You must be signed in to change notification settings - Fork 1
Script Mods
Wayward is written in TypeScript, a strongly-typed superset of JavaScript. This means that mods will use TypeScript as well, working against the base-game code.
This guide will use Windows for commands. For command examples with macOS and Linux, see the Mod Setup Examples.
In a console, simply run a command to set up your mod. For more information on this command, see this guide.
cd "C:/Program Files (x86)/Steam/steamapps/common/Wayward/mods"
..\wayward.cmd +mod create ExampleMod +script
Example output:
Creating mod...
tsconfig.json: created
src/Mod.ts: created
README.md: created
.gitignore: created
mod.json: merged new properties
Initialized mod
Checking/updating types version...
Now on types version 2.9.0-beta
This command automatically sets up your new mod to work with the latest version of Wayward.
In the mods
folder, you should now see a ExampleMod
folder which contains the following folders and files:
node_modules
src
.gitignore
mod.json
README.md
tsconfig.json
Contains data that tells TypeScript about the stuff that's in the base game, stuff that you can import and mess with. Never edit the files in this folder. To update them to the latest version, run +mod update
.
This is the TypeScript class file for your mod. To the game, it is the equivalent of your mod. With it, you can hook into base-game functionality and register new parts of the game.
To make a mod, we recommend using git. Git is version-control software, it basically means that you can save snapshots of your mod's code at any time, to be able to roll back to that version later. Many, many developers, organisations, etc use git, and one of the best places to publish the source code for your mod is right here, GitHub.
This file is a file for git — basically, it hides the node_modules
and out
folders from git, so that only your source code is snapshotted, or "committed".
Contains metadata for your mod. For example, its name, description, version, what's in the mod, etc.
This "readme" is a markdown file for your mod. Usually you use a file like this to describe what your mod is and does. If you're publishing your mod's source code to GitHub, the file will be on the front page of your repository, public to all.
Used to compile your TypeScript code. For more on tsconfig.json
, see the official TypeScript documentation.
Changing your mod to use a different Wayward version is easy, all it takes is a single command after switching to the version you want to use.
- If you want to update your mod to the latest version, simply update Wayward on Steam, then run the above command.
- If you want to make a mod for an older version of Wayward, switch to an older branch (at least 2.8.0), then run the above command.
- If you want to make a mod that works with the development version of Wayward, switch to the Development Branch, then run the above command.
In a console, run commands such as:
cd "C:/Program Files (x86)/Steam/steamapps/common/Wayward/mods"
..\wayward.cmd +mod update HelloWorld
Example output:
Checking/updating types version...
tsconfig.json: updated target to es2019
Now on types version 2.9.0-beta
Open Mod.ts
. You should start with something like this:
import Mod from "mod/Mod";
export default class MyWaywardMod extends Mod {
}
Before we go any further, let's explain what's happening here.
In the first line, we "import" the Mod
class. Basically, this means that we're telling TypeScript "Hey, we need to use this Mod
thing inside Wayward's mod/Mod
file.
What exactly is Mod
? Mod
is what's called an abstract class — that means it can't be used on its own, but it provides functionality that other subclasses can use. In this case, Wayward provides the abstract class Mod
which contains default mod functionality that mod developers can build their own mods with.
In the next line, we're declaring our own class, MyWaywardMod
, and we're saying that it extends the Mod
class. In other words, MyWaywardMod
is a subclass of Mod
.
All mod entry files (the file
property in the mod.json
) must export default
a subclass of Mod
. The code that we start with actually has already done that, so at this point, our mod would actually be fully functional! ...Although it would do absolutely nothing.
If you're using VSCode, you should be able to compile this basic mod with Ctrl + Shift + B. After it is compiled, you will see an out
folder created in your mod directory, with a file Mod.js
inside it. This is the code that Wayward can run, which means that now, if you load Wayward and open the "Mods" menu, you will now see your mod under "Local Mods".
NOTE: The following part of the guide was copied directly from the previous guide and may be out of date. It is currently under-construction and will be finished soon. Thanks!
How about we start adding some functionality?
If you open the "Options" menu in Wayward, in the Developer section, you'll see a checkbox for enabling "Developer Mode". This mode should be enabled (checked). Once it has been enabled, you should now have some more options available, including the button "Toggle Developer Tools", which will open the Chromium Developer Tools. (Alternatively, there is also a default binding of F10 for toggling the developer tools. It still requires "Developer Mode" to be enabled.)
With the Developer Tools open, you now have access to the "DevTools Console", which is where the game logs debug information. This is one common way to figure out why your code isn't working — you can log the values of variables or expressions to the console to figure out why something didn't do what you expected, or to see if the game is running the code that you expect it to.
Of course, our mod is going to be so simple that we shouldn't need to debug anything. Regardless, let's log some information anyway.
import Mod from "mod/Mod";
import Log from "utilities/Log";
let log: Log;
export default class HelloWorld extends Mod {
public onInitialize() {
log = this.getLog();
}
public onLoad() {
log.info("Hello World!");
}
public onUnload() {
log.info("Goodbye World!");
}
}
You'll notice we've imported something new, the Log
class. We're only going to use that class to specify the type of a variable.
First off, we create a variable in the top scope of our file (so that it's easy to access) and name it log
. We give the variable our imported Log
type. We'll store the actual mod log in it later.
At this point, you may be wondering what "hooks" actually are. In Wayward, there are specific places in the base game's code where we allow mods to manipulate the functionality. Each of these places is called a "hook" (we're going to use some of these in a short while). There is another kind of "hook", which you can consider "mod loading cycle" hooks. Basically, as the state of your mod changes, your mod can do any required initialization or de-initialization.
Here's a list of all of these "mod loading cycle" hooks:
-
onInitialize
:- Executed when your mod initializes or becomes enabled. This happens in two places:
- As Wayward is loaded, if your mod is enabled.
- When your mod is enabled via the Mods menu.
- Usually used for complex mod registrations, or setting up custom functionality. You can also use it to load and interact with global save data for your mod.
- Executed when your mod initializes or becomes enabled. This happens in two places:
-
onUninitialize
:- Executed when your mod is disabled via the Mods menu.
- This hook should be used to remove any custom functionality that was added by your mod. Making use of this hook will allow users to enable or disable the custom functionality you provide at will.
-
onLoad
:- Executed when you load a Wayward save (this includes starting a new game).
-
onUnload
:- Executed when a Wayward save is unloaded (the user closes the game or quits to the main menu).
-
onSave
:- Executed before
onUnload
. - Used to save data for your mod.
- Executed before
Armed with this additional knowledge, you'll see that we used three of these "mod loading cycle" hooks for our mod:
- When our mod initializes (
onInitialize
), we store its log (this.getLog()
) in ourlog
variable. - When a save is loaded (
onLoad
), we log"Hello World!"
- When a save is unloaded (
onUnload
), we log"Goodbye World!"
If you test the mod at this point with the console open, you'll see that when you load a save, "Hello World!" is logged, and when you exit a save, "Goodbye World!" is logged. The logged messages will also be added to the log file which is saved on disk.
So, our mod actually does something now, but if a player installed it, it wouldn't actually seem like it did to them. Why don't we send a "Hello World!" message to the player?
To log messages, we're going to need to need a bunch more imports. We need HookMethod
, Message
, MessageType
, and Register
. You can try to guess the path of an import, but it's fastest to just start typing it and allow VSCode/TypeScript to suggest the import for you (press Tab
when the correct import suggestion is highlighted). You can also use the TSLint import fixer.
import { Message, MessageType } from "language/IMessages";
import { HookMethod } from "mod/IHookHost";
import Mod from "mod/Mod";
import Register from "mod/ModRegistry";
export default class HelloWorld extends Mod {
@Register.message("HelloWorld")
public readonly messageHelloWorld: Message;
@HookMethod
public onGameScreenVisible(): void {
localPlayer.messages.send(this.messageHelloWorld);
}
}
Note: The old logging code has been removed for clarity.
You'll notice two new surprising things here, decorators! We use @Register.message(<name>)
to register a "message" in the game, and @HookMethod
to register our onGameScreenVisible
hook.
When you're adding new stuff to the game, most of the time you'll be using the @Register
decorators. These usually go on fields, but can sometimes also decorate methods (such as actions, commands, and options sections). The field is "injected" with the ID of your added thing, in this case it injects the ID of the message which we can then use anywhere a message is required.
- In old versions of Wayward, registration was done with simple
.add
methods, such as.addMessage(<name>, <message>)
. This was changed because it was impossible to know whether the method should be called inonInitialize
oronLoad
, and also for simplicity & consistency: Now every registration requires a field which will be injected with the value.
In the last section, we touched on the hooks from the "mod loading cycle". These hooks did not require the @HookMethod
decorator — this is because they are always called for every mod. All other hooks, on the other hand, are only called if they're decorated with @HookMethod
.
- This is to improve the efficiency. Say there are a hundred mods enabled — whenever a hook is called, if not for requiring the decorator, the game would have to call that hook for every one of those hundred mods, pointlessly!
-
@HookMethod
also provides another benefit: You can provide a "priority" for your hook, to allow it to be called before or after the same hook in another mod (we won't need that in ours).
To wrap up what we've covered:
- We register a message by the name
"HelloWorld"
into the fieldmessageHelloWorld
. - We register a hook,
onGameScreenVisible
, and inside it, we send our registered message to the local player.
If you run the mod at this point and load a game, you'll see that it actually doesn't log Hello World
, it logs Message.ModHelloWorldHelloWorld
. What gives?
Well, what it logged was the internal "translation ID" of our message. We haven't actually provided an English translation of our message yet. Let's set this up now.
In lang/english.json
:
{
"extends": "English",
"dictionaries": {
"message": {
"modHelloWorldHelloWorld": "Hello World!"
}
}
}
This is all we need at this point. Let's navigate through the JSON file to understand what it's doing.
-
extends
— This is the language we're adding translations to. In this case, we're adding to the language of the base game, English. -
dictionaries
— This is an object indexed by the names of dictionaries, with each value being an object containing translations.-
message
— In our script, we've only registered an entry to themessage
dictionary. In this dictionary, we map the message name to the translation.-
modHelloWorldHelloWorld
— Wait,Message
...modHelloWorldHelloWorld
... this is what was logged in the console! Yes, if you see untranslated text, it's providing the exact location in the language.json file to translate it. In this case, we translate our message to"Hello World!"
-
-
Here's the plan:
- We're going to change the colour of our initial greeting to be green. Because that's prettier.
- We're going to rename the "branch" item to a "greetings stick".
- We're going to make equipping the greetings stick say hello to the hand that equipped it.
- We're going to say hello to the ground whenever we move.
import { Direction, EquipType, ItemType } from "Enums";
import { IItem } from "item/IItem";
import { itemDescriptions } from "item/Items";
import { Message } from "language/IMessages";
import { HookMethod } from "mod/IHookHost";
import Mod from "mod/Mod";
import Register from "mod/ModRegistry";
import IPlayer from "entity/player/IPlayer";
import terrainDescriptions from "tile/Terrains";
import TileHelpers from "utilities/TileHelpers";
export default class HelloWorld extends Mod {
@Register.message("HelloWorld")
public readonly messageHelloWorld: Message;
@Register.message("HelloLeftHand")
public readonly messageHelloLeftHand: Message;
@Register.message("HelloRightHand")
public readonly messageHelloRightHand: Message;
@Register.message("HelloTerrain")
public readonly messageHelloTerrain: Message;
@HookMethod
public onGameScreenVisible() {
// we send a "hello world" message to the local player, using the "good" type (green)
localPlayer.messages.type(MessageType.Good)
.send(this.messageHelloWorld);
}
@HookMethod
public onItemEquip(player: IPlayer, item: IItem, slot: EquipType) {
// if the item that is being equipped is *not*, a "greetings stick", we're not going to touch it
if (item.type !== ItemType.Branch) return;
// we changed the branch item to be called the "greetings stick", now let's add some extra functionality to it
// we send the player a message, saying hello to whichever hand the branch was equipped to
player.messages.send(slot === EquipType.LeftHand ? this.messageHelloLeftHand : this.messageHelloRightHand);
}
@HookMethod
public onMove(player: IPlayer, nextX: number, nextY: number, tile: ITile, direction: Direction): boolean | undefined {
const tileType = TileHelpers.getType(tile);
// we send a message to this player with the type "stat" (orange)
player.messages.type(MessageType.Stat)
// we first list which message we're sending, then we list all the arguments we're giving to that message.
// in this case, the only argument we're giving it is the name of the tile we're standing on
.send(this.messageHelloTerrain, terrainDescriptions[tileType].name);
return undefined;
}
}
When the "greetings stick" is equipped, using the onItemEquip
hook, we send a message depending on which hand it was equipped to.
We also say hello to the world literally, by hooking into onMove
, and sending a message with the name of the terrain the player is moving to.
To finish up the mod, let's add translations for the rest of our messages, and rename the branch to the "greetings stick".
{
"extends": "English",
"dictionaries": {
"item": {
"branch": ["greetings stick", "A tool with which to say hello."]
},
"message": {
"modHelloWorldHelloWorld": "Hello World!",
"modHelloWorldHelloLeftHand": "Hello Left Hand!",
"modHelloWorldHelloRightHand": "Hello Right Hand!",
"modHelloWorldHelloTerrain": "Hello {0}!"
}
}
}
The {0}
in the modHelloWorldHelloTerrain
translation is called an "interpolation". Basically, it means the first thing sent with the message is printed at that spot in the message. {1}
would print the second thing sent with the message. In our case, we do .send(this.messageHelloTerrain, terrainDescriptions[tileType].name);
— the message to send, and then the name of the terrain. So the message ends up being "Hello <name of terrain>!"
That's the end of the Hello World tutorial. Congratulations for making it this far! If you understand what you've done, you'll probably do fine with the rest of Wayward modding. And if you ever have questions you can always feel free to ask us in the #modding
channel of our Discord.
- To see the Hello World repository, visit the GitHub. There's some additional documentation & comments in the file.
- To learn more about the JSON files used for Wayward modding, continue into the next section.
- Or you could start your own mod! Do something crazy, like making all the creatures say "hello world". That would be intense!
Getting Started
- Introduction
- Prerequisites
+mod create
&+mod update
- mod.json
- Extracting Assets
- Resources & Examples
- Frequently Asked Questions
Mod Content
Script Documentation
- Using Translations
- Registrations
- Event Handlers
- Injection
- Adding Items
- Adding Doodads
- Adding Creatures
- Adding Magical Properties
- Actions & Multiplayer
- Adding Dialogs
- Context Menu/Action Bar Actions
- Inter-mod Registries
(apologies for all the missing guides, we'll get to them at some point)