Skip to content

Commit

Permalink
Add option for tools to have arbitrary modules registered as part of …
Browse files Browse the repository at this point in the history
…the definition

For 1.19, will move AOE logic, harvest logic, and melee logic to this system. Will be useful for addons that wish to make up their own additional JSON settings, and just encourage me to do less hardcoding of stuff in classes
  • Loading branch information
KnightMiner committed Feb 1, 2023
1 parent 27b986f commit e660a36
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.LinkedHashMultimap;
import com.google.gson.JsonSyntaxException;
import lombok.RequiredArgsConstructor;
import slimeknights.tconstruct.library.modifiers.ModifierHook;

Expand All @@ -12,6 +13,7 @@
import java.util.Map.Entry;

/** Map working with modifier hooks that automatically maps the objects to the correct generics. */
@SuppressWarnings("ClassCanBeRecord")
@RequiredArgsConstructor
public class ModifierHookMap {
/** Instance with no modifiers */
Expand Down Expand Up @@ -41,11 +43,30 @@ public <T> T getOrDefault(ModifierHook<T> hook) {
return hook.getDefaultInstance();
}

/** Gets an unchecked view of all internal modules for the sake of serialization */
public Map<ModifierHook<?>,Object> getAllModules() {
return modules;
}

@SuppressWarnings("UnusedReturnValue")
public static class Builder {
/** Builder for the final map */
private final LinkedHashMultimap<ModifierHook<?>,Object> modules = LinkedHashMultimap.create();

/**
* Adds a module to the builder, validating it at runtime. Used for JSON parsing
* @throws JsonSyntaxException if the hook type is invalid
*/
@SuppressWarnings("UnusedReturnValue")
public Builder addHookChecked(Object object, ModifierHook<?> hook) {
if (hook.isValid(object)) {
modules.put(hook, object);
} else {
throw new JsonSyntaxException("Object " + object + " is invalid for hook " + hook);
}
return this;
}

/** Adds a module to the builder */
@SuppressWarnings("UnusedReturnValue")
public <H, T extends H> Builder addHook(T object, ModifierHook<H> hook) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.common.ToolAction;
import slimeknights.tconstruct.library.modifiers.ModifierEntry;
import slimeknights.tconstruct.library.modifiers.ModifierHook;
import slimeknights.tconstruct.library.modifiers.util.ModifierHookMap;
import slimeknights.tconstruct.library.tools.SlotType;
import slimeknights.tconstruct.library.tools.definition.aoe.IAreaOfEffectIterator;
import slimeknights.tconstruct.library.tools.definition.harvest.IHarvestLogic;
import slimeknights.tconstruct.library.tools.definition.module.IToolModule;
import slimeknights.tconstruct.library.tools.definition.weapon.IWeaponAttack;
import slimeknights.tconstruct.library.tools.nbt.IToolStackView;
import slimeknights.tconstruct.library.tools.nbt.ModDataNBT;
Expand All @@ -38,7 +41,7 @@ public class ToolDefinitionData {
@VisibleForTesting
protected static final Stats EMPTY_STATS = new Stats(StatsNBT.EMPTY, MultiplierNBT.EMPTY);
/** Empty tool data definition instance */
public static final ToolDefinitionData EMPTY = new ToolDefinitionData(Collections.emptyList(), EMPTY_STATS, DefinitionModifierSlots.EMPTY, Collections.emptyList(), Collections.emptySet(), null, null);
public static final ToolDefinitionData EMPTY = new ToolDefinitionData(Collections.emptyList(), EMPTY_STATS, DefinitionModifierSlots.EMPTY, Collections.emptyList(), Collections.emptySet(), null, null, ModifierHookMap.EMPTY);

@Nullable
private final List<PartRequirement> parts;
Expand All @@ -54,6 +57,8 @@ public class ToolDefinitionData {
private final Harvest harvest;
@Nullable
private final IWeaponAttack attack;
@Nullable
private final ModifierHookMap modules;


/* Getters */
Expand Down Expand Up @@ -92,6 +97,16 @@ public int getStartingSlots(SlotType type) {
return getSlots().getSlots(type);
}

/** Gets the map of internal module hooks */
public ModifierHookMap getModules() {
return requireNonNullElse(modules, ModifierHookMap.EMPTY);
}

/** Gets the given module from the tool */
public <T> T getModule(ModifierHook<T> hook) {
return getModules().getOrDefault(hook);
}


/* Stats */

Expand Down Expand Up @@ -150,6 +165,8 @@ public void buildSlots(ModDataNBT persistentModData) {

/* Harvest */

// TODO: migrate harvest into modules

/** Gets the tools's harvest logic */
public IHarvestLogic getHarvestLogic() {
if (harvest != null && harvest.logic != null) {
Expand All @@ -169,6 +186,8 @@ public IAreaOfEffectIterator getAOE() {

/* Attack */

// TODO: migrate attack into modules

/** Gets the tool's attack logic */
public IWeaponAttack getAttack() {
return requireNonNullElse(attack, IWeaponAttack.DEFAULT);
Expand Down Expand Up @@ -204,6 +223,7 @@ public void write(FriendlyByteBuf buffer) {
IHarvestLogic.LOADER.toNetwork(getHarvestLogic(), buffer);
IAreaOfEffectIterator.LOADER.toNetwork(getAOE(), buffer);
IWeaponAttack.LOADER.toNetwork(getAttack(), buffer);
IToolModule.write(getModules(), buffer);
}

/** Reads a tool definition stat object from a packet buffer */
Expand All @@ -229,7 +249,8 @@ public static ToolDefinitionData read(FriendlyByteBuf buffer) {
IHarvestLogic harvestLogic = IHarvestLogic.LOADER.fromNetwork(buffer);
IAreaOfEffectIterator aoe = IAreaOfEffectIterator.LOADER.fromNetwork(buffer);
IWeaponAttack attack = IWeaponAttack.LOADER.fromNetwork(buffer);
return new ToolDefinitionData(parts.build(), new Stats(bonuses, multipliers), slots, traits.build(), actions.build(), new Harvest(harvestLogic, aoe), attack);
ModifierHookMap modules = IToolModule.read(buffer);
return new ToolDefinitionData(parts.build(), new Stats(bonuses, multipliers), slots, traits.build(), actions.build(), new Harvest(harvestLogic, aoe), attack, modules);
}

/** Internal stats object */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@
import net.minecraftforge.common.ToolAction;
import slimeknights.tconstruct.library.materials.stats.MaterialStatsId;
import slimeknights.tconstruct.library.modifiers.ModifierEntry;
import slimeknights.tconstruct.library.modifiers.ModifierHook;
import slimeknights.tconstruct.library.modifiers.ModifierId;
import slimeknights.tconstruct.library.modifiers.util.LazyModifier;
import slimeknights.tconstruct.library.modifiers.util.ModifierHookMap;
import slimeknights.tconstruct.library.tools.SlotType;
import slimeknights.tconstruct.library.tools.definition.ToolDefinitionData.Harvest;
import slimeknights.tconstruct.library.tools.definition.ToolDefinitionData.Stats;
import slimeknights.tconstruct.library.tools.definition.aoe.IAreaOfEffectIterator;
import slimeknights.tconstruct.library.tools.definition.harvest.IHarvestLogic;
import slimeknights.tconstruct.library.tools.definition.harvest.TagHarvestLogic;
import slimeknights.tconstruct.library.tools.definition.module.IToolModule;
import slimeknights.tconstruct.library.tools.definition.weapon.IWeaponAttack;
import slimeknights.tconstruct.library.tools.nbt.MultiplierNBT;
import slimeknights.tconstruct.library.tools.nbt.StatsNBT;
Expand All @@ -42,6 +45,8 @@ public class ToolDefinitionDataBuilder {
private final DefinitionModifierSlots.Builder slots = DefinitionModifierSlots.builder();
private final ImmutableList.Builder<ModifierEntry> traits = ImmutableList.builder();
private final ImmutableSet.Builder<ToolAction> actions = ImmutableSet.builder();
private final ModifierHookMap.Builder hookBuilder = new ModifierHookMap.Builder();

/** Tool's harvest logic */
@Nonnull @Setter
private IHarvestLogic harvestLogic = IHarvestLogic.DEFAULT;
Expand Down Expand Up @@ -194,6 +199,15 @@ public ToolDefinitionDataBuilder effective(TagKey<Block> tag) {
}


/* Modules */

/** Adds a module to the definition */
public <T extends IToolModule> ToolDefinitionDataBuilder module(ModifierHook<? super T> hook, T module) {
hookBuilder.addHook(module, hook);
return this;
}


/**
* Builds the final definition JSON to serialize
*/
Expand All @@ -210,13 +224,18 @@ public ToolDefinitionData build() {
// null properties if defaults
harvest = new Harvest(isDefaultHarvest ? null : harvestLogic, isDefaultAOE ? null : aoe);
}
ModifierHookMap hooks = hookBuilder.build();
if (hooks.getAllModules().isEmpty()) {
hooks = null;
}
return new ToolDefinitionData(parts.isEmpty() ? null : parts,
// null multipliers, traits, and actions if empty
new Stats(bonuses.build(), multipliers == MultiplierNBT.EMPTY ? null : multipliers),
slots.build(),
traits.isEmpty() ? null : traits,
actions.isEmpty() ? null : actions,
harvest,
attack == IWeaponAttack.DEFAULT ? null : attack);
attack == IWeaponAttack.DEFAULT ? null : attack,
hooks);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import net.minecraftforge.event.OnDatapackSyncEvent;
import slimeknights.tconstruct.common.network.TinkerNetwork;
import slimeknights.tconstruct.library.modifiers.ModifierEntry;
import slimeknights.tconstruct.library.modifiers.util.ModifierHookMap;
import slimeknights.tconstruct.library.tools.definition.aoe.IAreaOfEffectIterator;
import slimeknights.tconstruct.library.tools.definition.harvest.IHarvestLogic;
import slimeknights.tconstruct.library.tools.definition.module.IToolModule;
import slimeknights.tconstruct.library.tools.definition.weapon.IWeaponAttack;
import slimeknights.tconstruct.library.tools.nbt.MultiplierNBT;
import slimeknights.tconstruct.library.tools.nbt.StatsNBT;
Expand All @@ -50,6 +52,7 @@ public class ToolDefinitionLoader extends SimpleJsonResourceReloadListener {
.registerTypeHierarchyAdapter(IAreaOfEffectIterator.class, IAreaOfEffectIterator.LOADER)
.registerTypeHierarchyAdapter(IHarvestLogic.class, IHarvestLogic.LOADER)
.registerTypeHierarchyAdapter(IWeaponAttack.class, IWeaponAttack.LOADER)
.registerTypeAdapter(ModifierHookMap.class, IToolModule.Serializer.INSTANCE)
.setPrettyPrinting()
.disableHtmlEscaping()
.create();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package slimeknights.tconstruct.library.tools.definition.module;

import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.JsonSyntaxException;
import io.netty.handler.codec.DecoderException;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.GsonHelper;
import slimeknights.mantle.data.GenericLoaderRegistry;
import slimeknights.mantle.data.GenericLoaderRegistry.IHaveLoader;
import slimeknights.tconstruct.library.modifiers.ModifierHook;
import slimeknights.tconstruct.library.modifiers.ModifierHooks;
import slimeknights.tconstruct.library.modifiers.util.ModifierHookMap;

import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Map.Entry;

/**
* Base interface for modules within the tool definition data
*/
public interface IToolModule extends IHaveLoader<IToolModule> {
/** Loader instance for any modules loadable in tools */
GenericLoaderRegistry<IToolModule> LOADER = new GenericLoaderRegistry<>();

/** Reads the module map from the buffer */
static ModifierHookMap read(FriendlyByteBuf buffer) {
int size = buffer.readVarInt();
ModifierHookMap.Builder builder = new ModifierHookMap.Builder();
for (int i = 0; i < size; i++) {
ResourceLocation hookName = buffer.readResourceLocation();
ModifierHook<?> hook = ModifierHooks.getHook(hookName);
if (hook == null) {
throw new DecoderException("Unknown hook from network, this likely indicates a broken or outdated mod: " + hookName);
}
IToolModule module = LOADER.fromNetwork(buffer);
builder.addHookChecked(module, hook);
}
return builder.build();
}

/** Writes the module map to the buffer */
static void write(ModifierHookMap modules, FriendlyByteBuf buffer) {
// need to filter first else the count will be wrong and break the buffer
Collection<Entry<ModifierHook<?>, Object>> entries = modules.getAllModules().entrySet().stream().filter(entry -> entry.getValue() instanceof IToolModule).toList();
buffer.writeVarInt(entries.size());
for (Entry<ModifierHook<?>, Object> entry : entries) {
buffer.writeResourceLocation(entry.getKey().getName());
LOADER.toNetwork((IToolModule) entry.getValue(), buffer);
}
}

/** Logic to serialize and deserialize tool actions */
enum Serializer implements JsonSerializer<ModifierHookMap>, JsonDeserializer<ModifierHookMap> {
INSTANCE;

@Override
public ModifierHookMap deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject json = GsonHelper.convertToJsonObject(element, "modules");
ModifierHookMap.Builder builder = new ModifierHookMap.Builder();
for (Entry<String,JsonElement> entry : json.entrySet()) {
ResourceLocation hookName = ResourceLocation.tryParse(entry.getKey());
if (hookName == null) {
throw new JsonSyntaxException("Invalid hook name " + entry.getKey());
}
ModifierHook<?> hook = ModifierHooks.getHook(hookName);
if (hook == null) {
throw new JsonSyntaxException("Unknown hook name " + hookName);
}
builder.addHookChecked(LOADER.deserialize(entry.getValue()), hook);
}
return builder.build();
}

@Override
public JsonElement serialize(ModifierHookMap src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject json = new JsonObject();
for (Entry<ModifierHook<?>, Object> entry : src.getAllModules().entrySet()) {
if (entry.getValue() instanceof IToolModule module) {
json.add(entry.getKey().getName().toString(), LOADER.serialize(module));
}
}
return json;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
package slimeknights.tconstruct.library.tools.definition.module;

import net.minecraft.MethodsReturnNonnullByDefault;

import javax.annotation.ParametersAreNonnullByDefault;
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ void data_emptyContainsNoData() {

@Test
void data_nullContainsNoData() {
checkToolDataEmpty(new ToolDefinitionData(null, null, null, null, null, null, null));
checkToolDataEmpty(new ToolDefinitionData(null, null, null, null, null, null, null, null));
}

@Test
Expand Down Expand Up @@ -182,7 +182,7 @@ void data_buildSlots_withData() {
void actions_canPerform() {
assertThat(ToolDefinitionData.EMPTY.canPerformAction(ToolActions.SHOVEL_FLATTEN)).isFalse();
assertThat(ToolDefinitionData.EMPTY.canPerformAction(ToolActions.SWORD_DIG)).isFalse();
ToolDefinitionData newData = new ToolDefinitionData(null, null, null, null, ImmutableSet.of(ToolActions.SHOVEL_FLATTEN), null, null);
ToolDefinitionData newData = new ToolDefinitionData(null, null, null, null, ImmutableSet.of(ToolActions.SHOVEL_FLATTEN), null, null, null);
assertThat(newData.canPerformAction(ToolActions.SHOVEL_FLATTEN)).isTrue();
assertThat(newData.canPerformAction(ToolActions.SWORD_DIG)).isFalse();
}
Expand Down

0 comments on commit e660a36

Please sign in to comment.