Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enchantment api #291

Draft
wants to merge 13 commits into
base: 1.19.4
Choose a base branch
from
18 changes: 18 additions & 0 deletions library/item/enchantment/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
plugins {
id("qsl.module")
}

qslModule {
name = "Quilt Enchantment API"
moduleName = "enchantment"
id = "quilt_enchantment"
description = "An API for custom enchantments."
library = "item"
moduleDependencies {
core {
api("qsl_base")
testmodOnly("resource_loader")
}
}
accessWidener()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2023 QuiltMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.quiltmc.qsl.enchantment.api;

import java.util.List;

import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;

import net.minecraft.enchantment.Enchantment;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.enchantment.EnchantmentLevelEntry;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.AnvilScreenHandler;

import org.quiltmc.qsl.base.api.event.Event;
import org.quiltmc.qsl.enchantment.api.context.EnchantingContext;

public final class EnchantmentEvents {
/**
* An event that is called after random enchantments are generated (e.g. for an enchantment table), but before they are selected to be applied on an item.
* @see EnchantmentHelper#getPossibleEntries(int, ItemStack, boolean)
*/
public static final Event<ModifyPossibleEnchantments> MODIFY_POSSIBLE_ENCHANTMENTS = Event.create(ModifyPossibleEnchantments.class, callbacks -> (possibleEnchantments, context) -> {
for (var callback : callbacks) {
callback.modifyPossibleEntries(possibleEnchantments, context);
}
});

/**
* An event that is called when applying an {@link Enchantment} to an {@link Item} in an anvil.
* @see AnvilScreenHandler#updateResult()
*/
public static final Event<AnvilApplication> ANVIL_APPLICATION = Event.create(AnvilApplication.class, callbacks -> (enchantment, context) -> {
for (var callback : callbacks) {
if (!callback.canApply(enchantment, context)) {
return false;
}
}

return true;
});

@FunctionalInterface
public interface ModifyPossibleEnchantments {
@Contract(mutates = "param1")
void modifyPossibleEntries(List<EnchantmentLevelEntry> possibleEnchantments, @Nullable EnchantingContext context);
}

@FunctionalInterface
public interface AnvilApplication {
@Contract(pure = true)
boolean canApply(Enchantment enchantment, @Nullable EnchantingContext context);
}
}
TheGlitch76 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2023 QuiltMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.quiltmc.qsl.enchantment.api;

import org.jetbrains.annotations.Contract;

import net.minecraft.enchantment.Enchantment;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;

/**
* An interface for extending an {@link Item} with additional control over enchanting.
*/
public interface QuiltEnchantableItem {
/**
* Determines whether the provided enchantment can be applied to this item.
* <p>
* This takes highest priority for applying enchantments.
* @param stack the stack containing this item
* @param enchantment the enchantment to apply to this item
* @return {@code true} if the enchantment can be applied, or {@code false} otherwise
*/
@Contract(pure = true)
default boolean canEnchant(ItemStack stack, Enchantment enchantment) {
return enchantment.isAcceptableItem(stack);
}
Comment on lines +29 to +40
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if a TriState might be better? Either override true, override false, or default to enchantment choice. As of now, it's a complete override if the item implements this interface, hence the default implementation, so calling super will allow the enchantment to decide (Note that the enchantment's decision likely doesn't take the other enchantments on the item into account, as there's the separate Enchantment#canCombine(Enchantment)).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do a tristate. Maybe an enum would be clear? Not sure what best way is to convey meaning of overriding behaviors

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, the modder does have the enchantment and can just check the enchantment's canEnchant directly if they want to know what the default was

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2023 QuiltMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.quiltmc.qsl.enchantment.api;

import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;

import net.minecraft.enchantment.Enchantment;
import net.minecraft.enchantment.EnchantmentTarget;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.item.ItemGroup;
import net.minecraft.item.ItemStack;

import org.quiltmc.qsl.enchantment.api.context.EnchantingContext;

/**
* An extension of the default {@link Enchantment} class.
* <p>
* Allows for custom weighting in randomized enchanting contexts, as well as custom logic for application in the anvil.
*/
public abstract class QuiltEnchantment extends Enchantment {
public QuiltEnchantment(Rarity rarity, @Nullable EnchantmentTarget type, EquipmentSlot[] slotTypes) {
super(rarity, type, slotTypes);
}

/**
* Return an integer value that represents the weight of this enchantment given the current context.
* <p>
* If 0, then this enchantment won't be added.
* @param context the context of the enchanting
* @return the context-aware weight for the enchantment
*/
public @Range(from = 0, to = Integer.MAX_VALUE) int weightFromContext(EnchantingContext context) {
return this.getRarity().getWeight();
}

/**
* Determines if the given enchantment can be applied under the current context.
* <p>
* Note: {@link QuiltEnchantableItem#canEnchant(ItemStack, Enchantment)} takes priority.
* @see QuiltEnchantableItem#canEnchant(ItemStack, Enchantment)
* @param context the context of the enchanting
* @return {@code true} if this enchantment can be applied, or {@code false} otherwise
*/
public boolean isAcceptableContext(EnchantingContext context) {
if (!context.ignoresPower() && this.isAcceptablePower(context.getLevel(), context.getPower())) {
return false;
}

return this.isAcceptableItem(context.getStack());
}

/**
* Determines if the provided power is within the valid range for this enchantment at the provided level.
* @param level the level of this enchantment being queried
* @param power the power of the current enchanting context
* @return {@code true} if the power is within the valid range, or {@code false} otherwise
*/
public boolean isAcceptablePower(int level, int power) {
return power >= this.getMinPower(level) && power <= this.getMaxPower(level);
}

/**
* Determines if the given enchantment should be visible in the creative tab.
* @param visibility in what context the stack visibility is tested
* @return {@code true} if this enchantment should be visible, or {@code false} otherwise
*/
public boolean isVisible(ItemGroup.Visibility visibility) {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2023 QuiltMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.quiltmc.qsl.enchantment.api;

import org.jetbrains.annotations.Nullable;

import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.item.ItemStack;
import net.minecraft.util.random.RandomGenerator;

import org.quiltmc.qsl.enchantment.api.context.EnchantingContext;

/**
* Allows modded systems that enchant items to apply an enchanting context.
*/
public final class QuiltEnchantmentHelper {
private static final ThreadLocal<EnchantingContext> CONTEXT = new ThreadLocal<>();

/**
* Set the enchanting context for enchantments to use.
* <p>
* Note: Almost all the base values, bar the {@link net.minecraft.world.World world}, are provided to the context
* when using {@link EnchantmentHelper#enchant(RandomGenerator, ItemStack, int, boolean)} or
* {@link EnchantmentHelper#generateEnchantments(RandomGenerator, ItemStack, int, boolean)}.
* @param context the enchanting context
*/
public static void setContext(EnchantingContext context) {
CONTEXT.set(context);
}

/**
* Gets the current enchanting context.
* @return the enchanting context
*/
public static @Nullable EnchantingContext getContext() {
return CONTEXT.get();
}

/**
* Clears the current enchanting context.
* <p>
* This should be used to ensure that no information bleeds into other contexts.
*/
public static void clearContext() {
CONTEXT.remove();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2023 QuiltMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.quiltmc.qsl.enchantment.api.context;

import org.jetbrains.annotations.Contract;

import net.minecraft.item.ItemStack;
import net.minecraft.util.random.RandomGenerator;

/**
* A class that contains contextual information about the enchantment process.
*/
public class EnchantingContext {
protected final int level;
protected final int power;
protected final ItemStack stack;
protected final RandomGenerator random;
protected final boolean treasureAllowed;

public EnchantingContext(int level, int power, ItemStack stack, RandomGenerator random, boolean treasureAllowed) {
this.level = level;
this.power = power;
this.stack = stack;
this.random = random;
this.treasureAllowed = treasureAllowed;
}

@Contract("_->new")
public EnchantingContext withLevel(int level) {
return new EnchantingContext(level, this.power, this.stack, this.random, this.treasureAllowed);
}

@Contract("_->new")
public EnchantingContext withPower(int power) {
return new EnchantingContext(this.level, power, this.stack, this.random, this.treasureAllowed);
}

@Contract("_,_,_->new")
public EnchantingContext withCoreContext(ItemStack stack, RandomGenerator random, boolean treasureAllowed) {
return new EnchantingContext(this.level, this.power, stack, random, treasureAllowed);
}

/**
* @return an integer representing the current enchantment level being queried (ie. I, II, III, etc
*/
public int getLevel() {
return this.level;
}

/**
* @return the current power of the enchanting context
*/
public int getPower() {
return this.power;
}

/**
* @return the item stack for the enchantment to be applied to
*/
public ItemStack getStack() {
return this.stack;
}

/**
* @return the random used for enchanting
*/
public RandomGenerator getRandom() {
return this.random;
}

/**
* @return {@code true} if treasure enchantments are allowed, or {@code false} otherwise
*/
public boolean treasureAllowed() {
return this.treasureAllowed;
}

/**
* @return {@code true} if this context ignores power (i.e. Applying enchantments in an anvil), or {@code false} otherwise
*/
public boolean ignoresPower() {
return this.power <= 0;
}
}
Loading