Skip to main content

Mod Extension (API Guide)

If you want to implement your own task, action, or condition, the mod allows you to do so.

Action Condition Task Placeholders

Action

Actions are executed if the button is pressed successfully (if the quest conditions are not met, the button redirects to alt_to_go but does not activate actions other than save and close). Actions are executed sequentially and server-side.

To add it, you need to inherit the AbstractAction class.

import net.noyji.thequestforge.api.quest.action.AbstractAction;

package paht;

public class YourAction extends AbstractAction {
@Override
public boolean handler(ActionContext context) {
//Action logic that returns true if everything was successful, false if something went wrong.
}

@Override
public ResourceLocation getLocation() {
// This should be your action ID, for example, your_mod_id:name_action
// return TheQuestForge.id("accept");
}
}

The ActionContext contains:

  • Player
  • Entity (NPC)
  • Pressed dialog button (TemplateDialogButton)

Next, register

import net.noyji.thequestforge.api.quest.registry.ActionRegistry;
import net.noyji.thequestforge.api.quest.action.AbstractAction;

public class YourModAxtionRegisty {
public static final DeferredRegister<AbstractAction> ACTIONS = DeferredRegister.create(ActionRegistry.ACTION_REGISTRY_KEY, YourMod.MODID);

public static final RegistryObject<AbstractAction> YOUR_ACTION = ACTIONS.register("your_action_name", () -> new YourAction());

public static void register(IEventBus eventBus) { ACTIONS.register(eventBus); }
}


Conditions

Conditions can be used to control the appearance of a quest and ensure it is generated only under specific conditions. The condition is checked at the time the quest is generated (upon the first interaction with an NPC), on the server side.

import net.noyji.thequestforge.api.quest.requirements.AbstractRequirement;

public class YourRequirement extends AbstractRequirement{
@Override
public boolean check(RequirementContext context) {
//Condition logic that returns true if everything was successful, false if something went wrong.
}

@Override
public ResourceLocation getLocation() {

// This should be your condition ID, for example, your_mod_id:name_requirement
}
}

The RequirementContext contains:

  • Player - the player
  • Entity (NPC) - the NPC the player is talking to.
  • QuestRarity - Quest rarity
  • String value - Values ​​after your_mod_id:name_requirement: .... For example, if thequestforge:biome_is_not:minecraft:plains is specified, then value = minecraft:plains, meaning the rest of the string after the ID.
  • Map<String, String> custonData - Custom data as a string.
// To add a custom date to a quest
context.addCustomData("data_key", "string_data");

// This mode is used for quests that search for structures or biomes.
//context.addCustomData("direction", Util.getDirectionText(blockPos, blockPosTo));

//Custom data can be retrieved from the quest.
String direction = quest.getCustomData("direction");
//Where quest is a PlayerQuest or Quest object.

And registration

import net.noyji.thequestforge.api.quest.registry.RequirementRegistry;
import net.noyji.thequestforge.api.quest.requirements.AbstractRequirement;

public class YourModRequirements {
public static final DeferredRegister<AbstractRequirement> REQUIREMENTS =
DeferredRegister.create(RequirementRegistry.REQUIREMENT_REGISTRY_KEY, YourMod.MODID);

public static final RegistryObject<AbstractRequirement> YOUR_REQUIREMENT = REQUIREMENTS.register("name_requirement", () -> new YourRequirement());

public static void register(IEventBus eventBus) {
REQUIREMENTS.register(eventBus);
}
}

Task

The basic logic for increasing quest progress. What needs to be done:

  • Create a task.
  • Register it.
  • Create a renderer for this task in the book.
  • Register the renderer.
  • Subscribe to the event.

For example, let's look at an existing gathering task.

package net.noyji.thequestforge.api.quest.task;

import net.minecraft.nbt.CompoundTag;

import net.minecraft.resources.ResourceLocation;

import net.minecraft.util.RandomSource;

import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;

import net.minecraftforge.event.TickEvent;

import net.noyji.thequestforge.TheQuestForge;

import net.noyji.thequestforge.common.util.Util;

import net.noyji.thequestforge.data.group.components.JsonTask;

import net.noyji.thequestforge.data.quest.player.components.QuestRarity;

//TickEvent.PlayerTickEvent is the event we subscribed to.
public class CollectTask extends AbstractTask<TickEvent.PlayerTickEvent>{
//The AbstractTask class already has fields: int goal - the goal to be achieved, int progress = 0 - the current progress.

//Custom fields just for this task.
private ItemStack itemStack;

public CollectTask() {
}
//This method is needed for rendering and is also specific to this class.
public ItemStack getItemStack(){
return itemStack;
}

//The ID of what we need, in this case (ItemStack).
@Override
public ResourceLocation getTarget() {
return Util.getItemResourceLocation(itemStack);
}

//The task ID.
@Override
public ResourceLocation getLocation() {
return TheQuestForge.id("collect");
}

//You must return your event class here.
@Override
public Class<TickEvent.PlayerTickEvent> getEventClass() {
return TickEvent.PlayerTickEvent.class;
}

//A parser that converts a JsonTask to your task.
@Override
public void parse(JsonTask jsonTask, RandomSource randomSource, QuestRarity rarity) {
if (jsonTask == null) return;

//Here we get the number of required collectible items via the getCount(randomSource, rarity) method.
goal = jsonTask.getCount(randomSource, rarity);

//Here we get the item itself via the utility method. You can see how it works in the source code. Otherwise, jsonTask has the ResourceLocation getTarget(), CompoundTag getTag(), and List<EnchantmentContext> getEnchantments(RandomSource randomSource) methods.
itemStack = Util.parseItemStack(jsonTask, goal, randomSource);
}

//What to do if the quest is completed and turned in. In this example, we simply remove the necessary items; this is an optional method.
@Override
public void inComplete(Player player) {
Util.removeItemFromPlayer(player, itemStack, progress);
}

// Getting the target name to display when talking to an NPC, example (Gold Block)
@Override
public String getTargetName() {
return itemStack.getCount() + " " + itemStack.getDisplayName().getString().replace("[", "").replace("]", "");
}

// Task handler. In this example, progress is set to the value of the target items in the player's inventory, from the server side.
@Override
public void handle(TickEvent.PlayerTickEvent event) {
Player player = event.player;

progress = Util.countMatchingItems(player, itemStack);
}

//Optional debug method.
@Override
public void info() {
TheQuestForge.LOGGER.debug("{}: Target: {} Goal: {}", getLocation(), getTarget(), goal);
}

//And saving and loading are mandatory.
@Override
public void serializeNBT(CompoundTag nbt) {
nbt.putInt("Goal", this.goal);
nbt.putInt("Count", this.progress);

CompoundTag itemTag = new CompoundTag();
if (this.itemStack != null && !this.itemStack.isEmpty()) {
this.itemStack.save(itemTag);
}
nbt.put("ItemStack", itemTag);
}

@Override
public void deserializeNBT(CompoundTag nbt) {
this.goal = nbt.getInt("Goal");
this.progress = nbt.getInt("Count");

if (nbt.contains("ItemStack")) {
this.itemStack = ItemStack.of(nbt.getCompound("ItemStack"));
} else {
this.itemStack = ItemStack.EMPTY;
}
}
}

Registration (of your assignment)

public class YourTasks {
public static final DeferredRegister<TaskType<?>> TASK_TYPES = DeferredRegister.create(TaskHandlerRegistry.TASK_REGISTRY_KEY, YourMod.MODID);

public static final RegistryObject<TaskType<CraftTask>> YOUR_TASK = TASK_TYPES.register("name_task", () -> new TaskType<>(YourTask::new));

public static void register(IEventBus eventBus) {
TASK_TYPES.register(eventBus);
}
}

This is the event class for quest trigger

public class QuestEvents {
private static final ResourceLocation KILL_KEY = TheQuestForge.id("kill");
@SubscribeEvent
public static void onLivingDeath(LivingDeathEvent event) {
LivingEntity deathMob = event.getEntity();
if (deathMob.level().isClientSide()) return;

Entity killer = event.getSource().getEntity();

if (killer == null){
killer = deathMob.getLastAttacker();
}

if (killer instanceof Player player){
ResourceLocation mob = Util.getEntityResourceLocation(deathMob);
CapabilityUtil.getPlayerQuestData(player).progressUpdate(KILL_KEY, mob, event, player);
}
}

@SubscribeEvent
public static void onTickPlayerTick(TickEvent.PlayerTickEvent event) {
if (event.side.isClient() || event.phase == TickEvent.Phase.START) return;

Player player = event.player;

if ((player.tickCount + player.getId()) % 20 == 0){
//This is a slightly different method for finding the key, so it's a separate method.
CapabilityUtil.getPlayerQuestData(player).checkCollectTasks(player, event);
}
}
}

Class Renderer

public class CollectTaskRenderer implements ITaskRenderer<CollectTask> {
// The task name, such as the item name.
@Override
public Component getName(CollectTask task) {
if (task.getItemStack() == null || task.getItemStack().isEmpty() ) {
return Component.literal("Empty Item");
}
return task.getItemStack().getHoverName();
}

// The item icon itself. @Override
public void renderIcon(GuiGraphics guiGraphics, CollectTask task, int x, int y) {
if (task.getItemStack() != null && !task.getItemStack().isEmpty()) {
guiGraphics.renderItem(task.getItemStack(), x, y);
}
}
}

Registration of render (yours)

import net.noyji.thequestforge.api.client.registry.TaskRendererRegistry;

@Mod.EventBusSubscriber(modid = YourMod.MODID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)

public class YourSetup {
@SubscribeEvent
public static void onClientSetup(FMLClientSetupEvent event) {
event.enqueueWork(() -> {
TaskRendererRegistry.register(YourTask.class, new YourTaskRenderer());
});
}
}

Placeholders

We already know what placeholders are for; now let's learn how to create them.

public class YourPlaceholder implements IPlaceholderResolver {
@Override public String resolve(Player player, Entity entity, PlayerQuest quest, String[] args) {
// Your placeholder logic goes here.
// Arguments are supported, "your_placeholder_name-arg1-arg2-agr3- and so on," so I think this will give you a lot of options.
}
}

and registration

@Mod.EventBusSubscriber(modid = YourMod.MODID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
public class YourPlaceholderRegistry {

@SubscribeEvent
public static void onCommonSetup(FMLCommonSetupEvent event) {
event.enqueueWork(() -> {
PlaceholderRegistry.register("your_placeholder_name", new YourPlaceholder());
});
}
}

That's it!