Skip to content

Commit

Permalink
Implement modifier traits
Browse files Browse the repository at this point in the history
This is a very powerful hook that allows a modifier to add additional modifiers to the tool. Example usecases:
* Add a modifier with special usages as an effect of a custom modifier, e.g. reinforced
* Add an internal modifier to run part of a modifier with a different priority
 Create a modifier that dynamically adds other modifiers under some conditions
  • Loading branch information
KnightMiner committed Dec 16, 2023
1 parent 5b495bc commit 793e549
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,23 @@ public static ModifierEntry fromJson(JsonObject json) {

/**
* Converts this entry to JSON
* @param json JSON object to fill
* @return Json object of entry
*/
public JsonObject toJson() {
JsonObject json = new JsonObject();
public JsonObject toJson(JsonObject json) {
json.addProperty("name", getId().toString());
json.addProperty("level", level);
return json;
}

/**
* Converts this entry to JSON
* @return Json object of entry
*/
public JsonObject toJson() {
return toJson(new JsonObject());
}

/**
* Reads this modifier entry from the packet buffer
* @param buffer Buffer instance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import slimeknights.tconstruct.library.modifiers.hook.TooltipModifierHook;
import slimeknights.tconstruct.library.modifiers.hook.build.AttributesModifierHook;
import slimeknights.tconstruct.library.modifiers.hook.build.ModifierRemovalHook;
import slimeknights.tconstruct.library.modifiers.hook.build.ModifierTraitHook;
import slimeknights.tconstruct.library.modifiers.hook.build.RawDataModifierHook;
import slimeknights.tconstruct.library.modifiers.hook.build.ToolStatsModifierHook;
import slimeknights.tconstruct.library.modifiers.hook.build.ValidateModifierHook;
Expand Down Expand Up @@ -159,6 +160,8 @@ public void removeRawData(IToolStackView tool, Modifier modifier, RestrictedComp
return null;
});

/** Hook for a modifier to add other modifiers to the builder */
public static final ModifierHook<ModifierTraitHook> MODIFIER_TRAITS = register("modifier_traits", ModifierTraitHook.class, ModifierTraitHook.AllMerger::new, (context, modifier, state, firstEncounter) -> {});

/* Combat */

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package slimeknights.tconstruct.library.modifiers.hook.build;

import lombok.RequiredArgsConstructor;
import slimeknights.tconstruct.TConstruct;
import slimeknights.tconstruct.library.modifiers.Modifier;
import slimeknights.tconstruct.library.modifiers.ModifierEntry;
import slimeknights.tconstruct.library.modifiers.TinkerHooks;
import slimeknights.tconstruct.library.tools.context.ToolRebuildContext;
import slimeknights.tconstruct.library.tools.nbt.ModifierNBT;

import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;

/** Hook for a modifier to add in other modifiers */
public interface ModifierTraitHook {
/**
* Add all traits from this modifier to the builder.
* This hook may be called multiple times during the building process if multiple modifiers have the same trait, use {@code firstEncounter} to distinguish.
* Do not call this method directly, call it through {@link TraitBuilder}.
* @param context Tool building context, note that volatile data has not yet been filled and modifiers does not include traits
* @param modifier Modifier entry
* @param state State keeping track of traits, use methods on this object to add traits
* @param firstEncounter If true, this is the first time this modifier has been seen while rebuilding the stats
*/
void addTraits(ToolRebuildContext context, ModifierEntry modifier, TraitBuilder state, boolean firstEncounter);

/** Builder that handles adding traits that can themselves contain traits */
@RequiredArgsConstructor
class TraitBuilder {
/** Set of all modifiers that have been encountered during this rebuild */
private final Set<Modifier> seenModifiers = new HashSet<>();
/** Modifiers that are currently adding their traits, prevents adding traits for a modifier inside itself, which will recurse infinitely */
private final Set<Modifier> currentStack = new LinkedHashSet<>();
/** Context for tool building */
private final ToolRebuildContext context;
/** Builder instance */
private final ModifierNBT.Builder builder;

/** Adds the given modifier to the builder and adds all its traits */
public void addEntry(ModifierEntry entry) {
builder.add(entry);
addTraits(entry);
}

/** Adds all traits for the given modifier entry */
private void addTraits(ModifierEntry entry) {
Modifier modifier = entry.getModifier();
// if the modifier lacks the trait hook, then we can skip tracking it, no need to add it to any data structures
ModifierTraitHook hook = modifier.getHooks().getOrNull(TinkerHooks.MODIFIER_TRAITS);
if (hook != null) {
// if this modifier is already on the stack, ignore it to avoid infinite recursion
if (currentStack.contains(modifier)) {
TConstruct.LOG.error("Encountered {} as a child of itself, previous stack {}", modifier.getId(), currentStack);
} else {
// not on the stack? add it, then recursively add traits
currentStack.add(modifier);
hook.addTraits(context, entry, this, !hasSeenModifier(modifier));
seenModifiers.add(modifier);
currentStack.remove(modifier);
}
}
}

/** Checks if the given modifier has been seen before */
public boolean hasSeenModifier(Modifier modifier) {
return seenModifiers.contains(modifier);
}
}

/** Merger that runs all hooks */
record AllMerger(Collection<ModifierTraitHook> modules) implements ModifierTraitHook {
@Override
public void addTraits(ToolRebuildContext context, ModifierEntry modifier, TraitBuilder state, boolean firstEncounter) {
for (ModifierTraitHook module : modules) {
module.addTraits(context, modifier, state, firstEncounter);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package slimeknights.tconstruct.library.modifiers.modules;

import com.google.gson.JsonObject;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.util.GsonHelper;
import slimeknights.mantle.data.GenericLoaderRegistry.IGenericLoader;
import slimeknights.tconstruct.library.modifiers.ModifierEntry;
import slimeknights.tconstruct.library.modifiers.ModifierHook;
import slimeknights.tconstruct.library.modifiers.ModifierId;
import slimeknights.tconstruct.library.modifiers.TinkerHooks;
import slimeknights.tconstruct.library.modifiers.hook.build.ModifierTraitHook;
import slimeknights.tconstruct.library.tools.context.ToolRebuildContext;

import java.util.List;

/**
* Module for a modifier to have a nested modifier as a trait.
*/
public record ModifierTraitModule(ModifierEntry modifier, boolean fixedLevel) implements ModifierTraitHook, ModifierModule {
private static final List<ModifierHook<?>> DEFAULT_HOOKS = List.of(TinkerHooks.MODIFIER_TRAITS);

public ModifierTraitModule(ModifierId id, int level, boolean fixedLevel) {
this(new ModifierEntry(id, level), fixedLevel);
}

@Override
public void addTraits(ToolRebuildContext context, ModifierEntry self, TraitBuilder state, boolean firstEncounter) {
if (fixedLevel) {
// fixed levels do not need to add again if already added
if (firstEncounter) {
state.addEntry(this.modifier);
}
} else {
// level of the trait is based on the level of the modifier, just multiply the two
state.addEntry(this.modifier.withLevel(this.modifier.getLevel() * self.getLevel()));
}
}

@Override
public List<ModifierHook<?>> getDefaultHooks() {
return DEFAULT_HOOKS;
}

@Override
public IGenericLoader<? extends ModifierTraitModule> getLoader() {
return LOADER;
}

public static final IGenericLoader<ModifierTraitModule> LOADER = new IGenericLoader<>() {
@Override
public ModifierTraitModule deserialize(JsonObject json) {
ModifierEntry modifier = ModifierEntry.fromJson(json);
boolean fixedLevel = GsonHelper.getAsBoolean(json, "fixed_level");
return new ModifierTraitModule(modifier, fixedLevel);
}

@Override
public void serialize(ModifierTraitModule object, JsonObject json) {
object.modifier.toJson(json);
json.addProperty("fixed_level", object.fixedLevel);
}

@Override
public ModifierTraitModule fromNetwork(FriendlyByteBuf buffer) {
ModifierEntry modifier = ModifierEntry.read(buffer);
boolean fixedLevel = buffer.readBoolean();
return new ModifierTraitModule(modifier, fixedLevel);
}

@Override
public void toNetwork(ModifierTraitModule object, FriendlyByteBuf buffer) {
object.modifier.write(buffer);
buffer.writeBoolean(object.fixedLevel);
}
};
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package slimeknights.tconstruct.library.tools.context;

import lombok.Data;
import lombok.With;
import net.minecraft.world.item.Item;
import slimeknights.tconstruct.library.tools.definition.ToolDefinition;
import slimeknights.tconstruct.library.tools.nbt.IModDataView;
Expand All @@ -23,6 +24,7 @@ public class ToolRebuildContext implements IToolContext {
/** List of recipe modifiers on the tool being rebuilt */
private final ModifierNBT upgrades;
/** List of all modifiers on the tool being rebuilt, from recipes and traits */
@With
private final ModifierNBT modifiers;
/** Tool stats before modifiers add stats */
private final StatsNBT baseStats;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ public ModifierNBT build() {
// sort on priority, falls back to the order they were added
.sorted(Comparator.comparingInt(entry -> -entry.getModifier().getPriority()))
.collect(Collectors.toList());
// its rare to see no modifiers, but no sense creating a new instance for that
if (list.isEmpty()) {
return EMPTY;
}
return new ModifierNBT(ImmutableList.copyOf(list));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import slimeknights.tconstruct.library.modifiers.ModifierEntry;
import slimeknights.tconstruct.library.modifiers.ModifierId;
import slimeknights.tconstruct.library.modifiers.TinkerHooks;
import slimeknights.tconstruct.library.modifiers.hook.build.ModifierTraitHook.TraitBuilder;
import slimeknights.tconstruct.library.recipe.tinkerstation.ValidatedResult;
import slimeknights.tconstruct.library.tools.SlotType;
import slimeknights.tconstruct.library.tools.context.ToolRebuildContext;
Expand All @@ -34,6 +35,7 @@
import slimeknights.tconstruct.library.utils.RestrictedCompoundTag;

import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.Set;

Expand Down Expand Up @@ -670,25 +672,38 @@ public void rebuildStats() {
for (int i = 0; i < max; i++) {
modBuilder.add(MaterialRegistry.getInstance().getTraits(materials.get(i).getId(), parts.get(i).getStatType()));
}
ModifierNBT allMods = modBuilder.build();
setModifiers(allMods);
// intermediate modifier list before we add modifier traits
ModifierNBT beforeTraits = modBuilder.build();

// pass in the list to stats, note for no part tools this should always be empty
StatsNBT stats = definition.buildStats(materials);
ModifierStatsBuilder statBuilder = ModifierStatsBuilder.builder();
definition.getData().buildStatMultipliers(statBuilder);

// next, update modifier related properties
List<ModifierEntry> modifierList = allMods.getModifiers();
if (modifierList.isEmpty()) {
List<ModifierEntry> modifierList = Collections.emptyList();
if (beforeTraits.isEmpty()) {
// if no modifiers, clear out data that only exists with modifiers
nbt.remove(TAG_VOLATILE_MOD_DATA);
setModifiers(ModifierNBT.EMPTY);
volatileModData = IModDataView.EMPTY;
} else {
ModDataNBT volatileData = new ModDataNBT();
// temporary context while we add modifier traits
ToolRebuildContext context = new ToolRebuildContext(item, getDefinition(), getMaterials(), getUpgrades(), beforeTraits, stats, getPersistentData(), volatileData);
modBuilder = ModifierNBT.builder();
TraitBuilder traitBuilder = new TraitBuilder(context, modBuilder);
for (ModifierEntry entry : beforeTraits.getModifiers()) {
traitBuilder.addEntry(entry);
}

// set the final modifier list on the tool
ModifierNBT allMods = modBuilder.build();
setModifiers(allMods);
modifierList = allMods.getModifiers();

// context for further modifier hooks
ToolRebuildContext context = new ToolRebuildContext(item, getDefinition(), getMaterials(), getUpgrades(), allMods, stats, getPersistentData(), volatileData);
context = context.withModifiers(allMods);

// build persistent data first, its a parameter to the other two hooks
for (ModifierEntry entry : modifierList) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import slimeknights.tconstruct.library.modifiers.modules.MobEffectModule;
import slimeknights.tconstruct.library.modifiers.modules.ModifierModule;
import slimeknights.tconstruct.library.modifiers.modules.ModifierSlotModule;
import slimeknights.tconstruct.library.modifiers.modules.ModifierTraitModule;
import slimeknights.tconstruct.library.modifiers.modules.RarityModule;
import slimeknights.tconstruct.library.modifiers.modules.RepairModule;
import slimeknights.tconstruct.library.modifiers.modules.SwappableSlotModule;
Expand Down Expand Up @@ -612,6 +613,7 @@ void registerSerializers(RegistryEvent.Register<RecipeSerializer<?>> event) {
ModifierModule.LOADER.register(TConstruct.getResource("mob_effect"), MobEffectModule.LOADER);
ModifierModule.LOADER.register(TConstruct.getResource("mob_disguise"), MobDisguiseModule.LOADER);
ModifierModule.LOADER.register(TConstruct.getResource("repair"), RepairModule.LOADER);
ModifierModule.LOADER.register(TConstruct.getResource("trait"), ModifierTraitModule.LOADER);

ModifierPredicate.LOADER.register(TConstruct.getResource("and"), ModifierPredicate.AND);
ModifierPredicate.LOADER.register(TConstruct.getResource("or"), ModifierPredicate.OR);
Expand Down

0 comments on commit 793e549

Please sign in to comment.