From d13c190411abf92a5747ca6ea43b6ae9bc1cf953 Mon Sep 17 00:00:00 2001 From: CleverNucleus Date: Sun, 18 Dec 2022 17:03:55 +0000 Subject: [PATCH] Added chunk-based xp limiting +Implemented chunk-based xp limiting: mob farms have been a real issue for PlayerEx. However, limiting xp from mob farms whilst allowing xp from dungeons was complicated to implement. This is a new feature that aims to solve this problem. It works by attaching a factor to each chunk, which sits at 1.0. This factor is the chance for xp to spawn in the chunk (so 1.0 means guaranteed). Every time xp is created in the chunk, the chunk's factor decreases, therefore decreasing the chance for more xp to spawn. Meanwhile, the chunk has a restorative force, acting to bring the factor back to 1.0 over time. All of these values are configurable and this whole system can be disabled. More documentation to come. --- build.gradle | 2 + gradle.properties | 2 +- .../clevernucleus/playerex/api/ExAPI.java | 2 + .../clevernucleus/playerex/api/ExConfig.java | 15 +++++ .../playerex/api/ExperienceData.java | 7 +++ .../playerex/config/ConfigImpl.java | 28 +++++++++ .../impl/ExperienceDataContainer.java | 14 +++++ .../playerex/impl/ExperienceDataManager.java | 63 +++++++++++++++++++ .../mixin/ExperienceOrbEntityMixin.java | 30 +++++++++ .../playerex/mixin/ServerWorldMixin.java | 15 +++++ .../resources/assets/playerex/lang/en_us.json | 9 +++ src/main/resources/fabric.mod.json | 6 +- src/main/resources/playerex.mixins.json | 2 + 13 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/github/clevernucleus/playerex/api/ExperienceData.java create mode 100644 src/main/java/com/github/clevernucleus/playerex/impl/ExperienceDataContainer.java create mode 100644 src/main/java/com/github/clevernucleus/playerex/impl/ExperienceDataManager.java create mode 100644 src/main/java/com/github/clevernucleus/playerex/mixin/ExperienceOrbEntityMixin.java create mode 100644 src/main/java/com/github/clevernucleus/playerex/mixin/ServerWorldMixin.java diff --git a/build.gradle b/build.gradle index 689abf5b..a203f523 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,8 @@ dependencies { include "dev.onyxstudios.cardinal-components-api:cardinal-components-base:${project.cardinal_components_version}" modImplementation "dev.onyxstudios.cardinal-components-api:cardinal-components-entity:${project.cardinal_components_version}" include "dev.onyxstudios.cardinal-components-api:cardinal-components-entity:${project.cardinal_components_version}" + modImplementation "dev.onyxstudios.cardinal-components-api:cardinal-components-chunk:${project.cardinal_components_version}" + include "dev.onyxstudios.cardinal-components-api:cardinal-components-chunk:${project.cardinal_components_version}" modApi("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { exclude(group: "net.fabricmc.fabric-api") diff --git a/gradle.properties b/gradle.properties index 61081adb..0ae176b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ minecraft_version=1.19.2 yarn_mappings=1.19.2+build.28 loader_version=0.14.10 -mod_version = 3.3.8 +mod_version = 3.3.9 maven_group = com.github.clevernucleus archives_base_name = playerex diff --git a/src/main/java/com/github/clevernucleus/playerex/api/ExAPI.java b/src/main/java/com/github/clevernucleus/playerex/api/ExAPI.java index b8a33918..51e64323 100644 --- a/src/main/java/com/github/clevernucleus/playerex/api/ExAPI.java +++ b/src/main/java/com/github/clevernucleus/playerex/api/ExAPI.java @@ -36,6 +36,8 @@ public final class ExAPI { public static final CacheableValue LEVEL_VALUE = OfflinePlayerCache.register(new com.github.clevernucleus.playerex.impl.LevelValue()); /** The Cardinal Components Key for PlayerEx player data. */ public static final ComponentKey PLAYER_DATA = ComponentRegistry.getOrCreate(new Identifier(MODID, "player_data"), PlayerData.class); + /** The Cardinal Components Key for PlayerEx experience data. */ + public static final ComponentKey EXPERIENCE_DATA = ComponentRegistry.getOrCreate(new Identifier(MODID, "experience_data"), ExperienceData.class); public static final EntityAttributeSupplier LEVEL = define("level"); public static final EntityAttributeSupplier CONSTITUTION = define("constitution"); diff --git a/src/main/java/com/github/clevernucleus/playerex/api/ExConfig.java b/src/main/java/com/github/clevernucleus/playerex/api/ExConfig.java index 6583384f..6772ea73 100644 --- a/src/main/java/com/github/clevernucleus/playerex/api/ExConfig.java +++ b/src/main/java/com/github/clevernucleus/playerex/api/ExConfig.java @@ -36,6 +36,21 @@ public interface ExConfig { */ int requiredXp(final PlayerEntity player); + /** + * @return The number of ticks taken for a chunk to restore the experience negation factor. + */ + int restorativeForceTicks(); + + /** + * @return The restorative force multiplier. + */ + float restorativeForceMultiplier(); + + /** + * @return The multiplier for experience negation. + */ + float expNegationFactor(); + /** * Client option. * @return 0 - 1.5. Volume multiplier for level up event. diff --git a/src/main/java/com/github/clevernucleus/playerex/api/ExperienceData.java b/src/main/java/com/github/clevernucleus/playerex/api/ExperienceData.java new file mode 100644 index 00000000..f2949c2d --- /dev/null +++ b/src/main/java/com/github/clevernucleus/playerex/api/ExperienceData.java @@ -0,0 +1,7 @@ +package com.github.clevernucleus.playerex.api; + +import dev.onyxstudios.cca.api.v3.component.tick.ServerTickingComponent; + +public interface ExperienceData extends ServerTickingComponent { + boolean updateExperienceNegationFactor(final int amount); +} diff --git a/src/main/java/com/github/clevernucleus/playerex/config/ConfigImpl.java b/src/main/java/com/github/clevernucleus/playerex/config/ConfigImpl.java index b9b68eaf..0ee5f6af 100644 --- a/src/main/java/com/github/clevernucleus/playerex/config/ConfigImpl.java +++ b/src/main/java/com/github/clevernucleus/playerex/config/ConfigImpl.java @@ -33,6 +33,19 @@ public static enum Tooltip { DEFAULT, VANILLA, PLAYEREX; } @ConfigEntry.Gui.Tooltip(count = 2) protected String levelFormula = "stairs(x,0.2,2.4,17,10,25)"; + @ConfigEntry.Category(value = "server") + @ConfigEntry.Gui.Tooltip(count = 2) + private int restorativeForceTicks = 600; + + @ConfigEntry.Category(value = "server") + @ConfigEntry.Gui.Tooltip(count = 2) + private int restorativeForceMultiplier = 110; + + @ConfigEntry.Category(value = "server") + @ConfigEntry.BoundedDiscrete(min = 1, max = 100) + @ConfigEntry.Gui.Tooltip(count = 2) + private int expNegationFactor = 95; + @ConfigEntry.Category(value = "client") @ConfigEntry.BoundedDiscrete(min = 0, max = 150) @ConfigEntry.Gui.Tooltip @@ -95,6 +108,21 @@ public int requiredXp(final PlayerEntity player) { return DataAttributesAPI.ifPresent(player, ExAPI.LEVEL, 1, ConfigServer.INSTANCE::level); } + @Override + public int restorativeForceTicks() { + return this.restorativeForceTicks; + } + + @Override + public float restorativeForceMultiplier() { + return (float)this.restorativeForceMultiplier * 0.01F; + } + + @Override + public float expNegationFactor() { + return (float)this.expNegationFactor * 0.01F; + } + @Override public float levelUpVolume() { return this.levelUpVolume * 0.01F; diff --git a/src/main/java/com/github/clevernucleus/playerex/impl/ExperienceDataContainer.java b/src/main/java/com/github/clevernucleus/playerex/impl/ExperienceDataContainer.java new file mode 100644 index 00000000..f1a27d34 --- /dev/null +++ b/src/main/java/com/github/clevernucleus/playerex/impl/ExperienceDataContainer.java @@ -0,0 +1,14 @@ +package com.github.clevernucleus.playerex.impl; + +import com.github.clevernucleus.playerex.api.ExAPI; + +import dev.onyxstudios.cca.api.v3.chunk.ChunkComponentFactoryRegistry; +import dev.onyxstudios.cca.api.v3.chunk.ChunkComponentInitializer; + +public final class ExperienceDataContainer implements ChunkComponentInitializer { + + @Override + public void registerChunkComponentFactories(ChunkComponentFactoryRegistry registry) { + registry.register(ExAPI.EXPERIENCE_DATA, ExperienceDataManager.class, ExperienceDataManager::new); + } +} diff --git a/src/main/java/com/github/clevernucleus/playerex/impl/ExperienceDataManager.java b/src/main/java/com/github/clevernucleus/playerex/impl/ExperienceDataManager.java new file mode 100644 index 00000000..92431c09 --- /dev/null +++ b/src/main/java/com/github/clevernucleus/playerex/impl/ExperienceDataManager.java @@ -0,0 +1,63 @@ +package com.github.clevernucleus.playerex.impl; + +import java.util.Random; + +import com.github.clevernucleus.playerex.api.ExAPI; +import com.github.clevernucleus.playerex.api.ExConfig; +import com.github.clevernucleus.playerex.api.ExperienceData; + +import net.minecraft.nbt.NbtCompound; +import net.minecraft.world.chunk.Chunk; + +public final class ExperienceDataManager implements ExperienceData { + private static final String KEY_EXP_NEGATION_CHANCE = "ExpNegationFactor"; + private static final Random RANDOM = new Random(); + private final Chunk chunk; + private float expNegationFactor; + private int ticks; + private final int restorativeForceTicks; + private final float restorativeForce; + private final float expNegationMultiplier; + + public ExperienceDataManager(final Chunk chunk) { + this.chunk = chunk; + this.expNegationFactor = 1.0F; + this.ticks = 0; + + ExConfig config = ExAPI.getConfig(); + this.restorativeForceTicks = config.restorativeForceTicks(); + this.restorativeForce = config.restorativeForceMultiplier(); + this.expNegationMultiplier = config.expNegationFactor(); + } + + @Override + public boolean updateExperienceNegationFactor(final int amount) { + if(RANDOM.nextFloat() > this.expNegationFactor) return true; + float dynamicMultiplier = this.expNegationMultiplier + ((1.0F - this.expNegationMultiplier) * (1.0F - (0.1F * (float)amount))); + this.expNegationFactor = Math.max(this.expNegationFactor * dynamicMultiplier, 0.0F); + this.chunk.setNeedsSaving(true); + return false; + } + + @Override + public void serverTick() { + if(this.expNegationFactor == 1.0F) return; + if(this.ticks < this.restorativeForceTicks) { + this.ticks++; + } else { + this.ticks = 0; + this.expNegationFactor = Math.min(this.expNegationFactor * this.restorativeForce, 1.0F); + this.chunk.setNeedsSaving(true); + } + } + + @Override + public void readFromNbt(NbtCompound tag) { + this.expNegationFactor = tag.getFloat(KEY_EXP_NEGATION_CHANCE); + } + + @Override + public void writeToNbt(NbtCompound tag) { + tag.putFloat(KEY_EXP_NEGATION_CHANCE, this.expNegationFactor); + } +} diff --git a/src/main/java/com/github/clevernucleus/playerex/mixin/ExperienceOrbEntityMixin.java b/src/main/java/com/github/clevernucleus/playerex/mixin/ExperienceOrbEntityMixin.java new file mode 100644 index 00000000..fd33d81d --- /dev/null +++ b/src/main/java/com/github/clevernucleus/playerex/mixin/ExperienceOrbEntityMixin.java @@ -0,0 +1,30 @@ +package com.github.clevernucleus.playerex.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.github.clevernucleus.playerex.api.ExAPI; + +import net.minecraft.entity.Entity.RemovalReason; +import net.minecraft.entity.ExperienceOrbEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; + +@Mixin(ExperienceOrbEntity.class) +abstract class ExperienceOrbEntityMixin { + + @Inject(method = "", at = @At("TAIL")) + public void playerex_init(World world, double x, double y, double z, int amount, CallbackInfo ci) { + BlockPos pos = new BlockPos(x, y, z); + Chunk chunk = world.getChunk(pos); + + ExAPI.EXPERIENCE_DATA.maybeGet(chunk).ifPresent(data -> { + if(data.updateExperienceNegationFactor(amount)) { + ((ExperienceOrbEntity)(Object)this).remove(RemovalReason.DISCARDED); + } + }); + } +} diff --git a/src/main/java/com/github/clevernucleus/playerex/mixin/ServerWorldMixin.java b/src/main/java/com/github/clevernucleus/playerex/mixin/ServerWorldMixin.java new file mode 100644 index 00000000..fb9cf191 --- /dev/null +++ b/src/main/java/com/github/clevernucleus/playerex/mixin/ServerWorldMixin.java @@ -0,0 +1,15 @@ +package com.github.clevernucleus.playerex.mixin; + +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import net.minecraft.server.world.ServerWorld; + +@Mixin(ServerWorld.class) +abstract class ServerWorldMixin { + + @Redirect(method = "addEntity", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;)V")) + private void playerex_addEntity(Logger logger, String arg0, Object arg1) {} +} diff --git a/src/main/resources/assets/playerex/lang/en_us.json b/src/main/resources/assets/playerex/lang/en_us.json index c9704f42..b9908c0d 100644 --- a/src/main/resources/assets/playerex/lang/en_us.json +++ b/src/main/resources/assets/playerex/lang/en_us.json @@ -17,6 +17,15 @@ "text.autoconfig.playerex.option.levelFormula": "Level Up Formula", "text.autoconfig.playerex.option.levelFormula.@Tooltip[0]": "The number of experience points required to level up. The 'x' \nvariable refers to the player's current level; 'x' is required.", "text.autoconfig.playerex.option.levelFormula.@Tooltip[1]": "(Requires restart)", + "text.autoconfig.playerex.option.restorativeForceTicks": "Restorative Force Ticks", + "text.autoconfig.playerex.option.restorativeForceTicks.@Tooltip[0]": "The number of ticks between every restorative event. \nNote that 20 ticks is 1 second.", + "text.autoconfig.playerex.option.restorativeForceTicks.@Tooltip[1]": "(Requires restart)", + "text.autoconfig.playerex.option.restorativeForceMultiplier": "Restorative Force", + "text.autoconfig.playerex.option.restorativeForceMultiplier.@Tooltip[0]": "The restorative force multiplier.", + "text.autoconfig.playerex.option.restorativeForceMultiplier.@Tooltip[1]": "(Requires restart)", + "text.autoconfig.playerex.option.expNegationFactor": "XP Negation Factor", + "text.autoconfig.playerex.option.expNegationFactor.@Tooltip[0]": "The chance for xp orbs to drop in a given chunk. \nSet to 100 for vanilla behaviour.", + "text.autoconfig.playerex.option.expNegationFactor.@Tooltip[1]": "(Requires restart)", "text.autoconfig.playerex.option.levelUpVolume": "Level Up Volume", "text.autoconfig.playerex.option.levelUpVolume.@Tooltip": "Volume multiplier as a percentage.", "text.autoconfig.playerex.option.skillUpVolume": "Skill Up Volume", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 7df61ed4..4ee86717 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -22,7 +22,8 @@ "com.github.clevernucleus.playerex.client.PlayerExClient" ], "cardinal-components": [ - "com.github.clevernucleus.playerex.impl.PlayerDataContainer" + "com.github.clevernucleus.playerex.impl.PlayerDataContainer", + "com.github.clevernucleus.playerex.impl.ExperienceDataContainer" ], "modmenu": [ "com.github.clevernucleus.playerex.config.ModMenuCompat" @@ -30,7 +31,8 @@ }, "custom": { "cardinal-components": [ - "playerex:player_data" + "playerex:player_data", + "playerex:experience_data" ] }, "mixins": [ diff --git a/src/main/resources/playerex.mixins.json b/src/main/resources/playerex.mixins.json index fc91612c..05a78c6d 100644 --- a/src/main/resources/playerex.mixins.json +++ b/src/main/resources/playerex.mixins.json @@ -7,6 +7,8 @@ "LivingEntityMixin", "PlayerEntityMixin", "ServerPlayerEntityMixin", + "ExperienceOrbEntityMixin", + "ServerWorldMixin", "PlayerInventoryMixin", "PersistentProjectileEntityMixin" ],