Registering Custom Quests
Register Custom Quests with Rib, which can then be used across all of your sidemods
The QuestRegistry holds Quest information inside of Rib for use by our sidemods, such as Research Tasks. To register your own custom quests, first implement Rib from our repository.
repositories {
maven { url = uri("https://vault.roanoke.dev/releases") }
}
dependencies {
modImplementation("dev.roanoke:rib:<version>")
}To get started, create a class that extends the Quest abstract class
class CatchPokemonQuest(name: String = "Catch Pokemon Quest",
id: String = UUID.randomUUID().toString(),
type: String = "CatchPokemonQuest",
provider: QuestProvider,
group: QuestGroup
) :
Quest(name, id, type, provider, group) { }The Quest class has some required parameters. The most important is "id", which is the key element when loading Quests from config. "id" and "name" are defaults that can be over-ridden by the Quest config as well. You can ignore QuestProvider and QuestGroup, just pass them into the Quest constructor.
class CatchPokemonQuest(name: String = "Default Catch Pokemon Quest Title",
id: String = UUID.randomUUID().toString(),
type: String = "CatchPokemonQuest",
provider: QuestProvider,
group: QuestGroup,
var pokeMatch: PokeMatch = PokeMatch(),
var taskMessage: String = "Catch a Pokemon!",
var amount: Int = 1,
var progress: Int = 0
) :
Quest(name, id, type, provider, group) { }Add relevant fields needed for your specific Quest. Decide whether these are stateful (i.e., are updated and need to be saved - such as progress) or definitions (defining the overall Quest, don't need to be saved per player, like a PokeMatch or Task Message).
All Quests must have their own QuestFactory companion which is passed into the QuestRegistry later to facilitate loading from configs.
companion object : QuestFactory {
override fun fromJson(json: JsonObject, state: JsonObject, provider: QuestProvider, group: QuestGroup): Quest {
val pokeMatch = PokeMatch.fromJson(
json.get("pokeMatch")?.asJsonObject ?: JsonObject()
)
val taskMessage = json.get("taskMessage")?.asString ?: "Catch a Pokemon!"
val amount = json.get("amount")?.asInt ?: 1
val progress = state.get("progress")?.asInt ?: 0
return CatchPokemonQuest(provider = provider,
group = group, pokeMatch = pokeMatch,
taskMessage = taskMessage, amount = amount,
progress = progress).apply {
loadDefaultValues(json, state)
}
}
}The fromJson method recieves a GSON JsonObject for both the definition (json) and state (state). The QuestProvider & QuestGroup for this specific instance are also passed. Sorry the definition field is called json, I'm meaning to change that. Legacy!
Make sure to note the sneaky ".apply" which handles loading default values across quests like "name", "id", and rewards.
CatchPokemonQuest(...).apply { loadDefaultValues(json, state) }Should my field be state or definition?
When loading (and later saving) your custom fields, be careful to consider what should be stored as stateful or defined when loading. Depending on where this Quest is being used, it is stored and loaded differently. For instance, in permanent assignments, ResearchTasks only stores the Quest state per player (in this instance, it'd just store the ID of the Quest and the "progress" field). However, in daily tasks, where each player may get a unique Quest, the entire Quest (definitions & state) are stored.
Implementing required saving/loading methods
override fun getQuestState(): JsonObject {
return JsonObject().apply {
addProperty("progress", progress)
}
}
override fun applyQuestState(state: JsonObject) {
progress = state.get("progress")?.asInt ?: progress
}
override fun saveSpecifics(): MutableMap<String, JsonElement> {
val specifics: MutableMap<String, JsonElement> = mutableMapOf()
specifics["progress"] = JsonPrimitive(progress)
specifics["amount"] = JsonPrimitive(amount)
specifics["taskMessage"] = JsonPrimitive(taskMessage)
specifics["pokeMatch"] = pokeMatch.toJson()
return specifics
}getQuestState() is used to... get the state of the Quest when saving a players copy. Add any stateful properties your Quest contains here. Default values (in Quest's case, just "rewardsClaimed") are included already.
applyQuestState() does the opposite, which can be useful depending on how the Quest is loaded. Grab whatever stateful properties you have any set them here.
saveSpecifics() requests all of your custom properties, both definitions (i.e. taskMessage) and stateful (progress). This is mainly used for daily tasks.
Other Required Methods
override fun completed(): Boolean {
return progress >= amount
}
override fun taskMessage(): Text {
return Text.literal(taskMessage)
}
override fun progressMessage(): Text {
return Text.literal("(${progress}/${amount})")
}Pretty self explanatory. Return a yes/no as to whether the Quest is completed, return a Task Message and a Progress Message. These methods are used in a lot of places, especially when displaying content to players.
Where to implement the actual Quest logic (event driven)
If your Quest is event driven, we'd recommend defining your code in the init block of your Quest.
Here we listen to the POKEMON_CAPTURED event. First, we need to check whether this Quest is active, "isActive" is a built in Quest method that checks whether the Quest is completed, and whether the QuestProvider wants this to be progressable at the moment.
Then, we check if this event is even relevant to our QuestGroup. The QuestGroup class includes a few utility methods, it is effectively a list of player UUIDs. Generally, a PlayerQuestGroup is provided.
You can message players (messages, action bars, titles) using their QuestGroup here, but for most of our mods, notifications are handled by the Quest Provider, so it's unnecessary.
In this example Quest, the meat of it is whether the Pokemon captured matches the Quest's PokeMatch. If this event has increased their progress, we increment our progress field and call notifyProgress() which will trigger an event on the QuestProvider, which generally results in saving state & notifying the player(s).
init {
CobblemonEvents.POKEMON_CAPTURED.subscribe {
if (!isActive()) {
return@subscribe
}
if (!group.includesPlayer(it.player)) {
return@subscribe
}
if (!pokeMatch.matches(it.pokemon)) {
return@subscribe
}
progress += 1
this.notifyProgress()
}
}Override the Inventory GUI Element for a Quest.
override fun getButton(player: ServerPlayerEntity): GuiElementBuilder {
return GuiElementBuilder.from(
ItemBuilder(pokeMatch.getPokemonItem())
.setCustomName(Rib.Rib.parseText(name))
.addLore(getButtonLore()
).build()
).setCallback { _, _, _ ->
getButtonCallback().invoke(player)
}
}The Quest abstract class implements a default getButton() method, but if you want to make it nicer by displaying a custom item or adding your own callbacks, you can do so. We use Patbox's sgui behind the scenes. You could also just override the getButtonCallback() method or the getButtonLore() methods to change that functionality.
See the Quest class code for more detail, or DM me.
Registering the Quest with Rib
Once your Quest is finished, you can register it when your mod is initialised as so:
override fun onInitialize() {
RegisterQuestCallback.EVENT.register {
QuestRegistry.registerQuestType("CatchPokemonQuest", CatchPokemonQuest.Companion)
}
}QuestRegistry.registerQuestType(type: String, QuestFactory) takes the type and the QuestFactory you defined. Once registered, the Quest can be loaded from configs by sidemods.
RegisterQuestCallback is invoked after the server has started and default quests are loaded, but before other sidemods using Quests are initiated.
Last updated