Mohist icon indicating copy to clipboard operation
Mohist copied to clipboard

[1.20.1] TFC Nutrients resets after death

Open Proxwian opened this issue 11 months ago • 2 comments

Minecraft Version : 1.20.1

Mohist Version : 584-Server

Operating System : Ubuntu

Concerned mod / plugin : TerraFirmaCraft

Logs :

Steps to Reproduce :

  1. Install TFC mod on server
  2. Set keepNutrientsAtDeath to true in serverconfig (it's already true by default)
  3. Join server
  4. Eat some meat
  5. Kill your character
  6. See at nutrients tab at inventory

Expected behaviour: Player's nutrient level keeps after death

Actual behaviour: Player's nutrient level resets to default value

Proxwian avatar Mar 13 '24 17:03 Proxwian

After updating Mohist and TFC to the latest version problem is still exist

Proxwian avatar Apr 14 '24 12:04 Proxwian

I solved this problem on the Arclight server for TFC 1.18.2 I will provide the code of the 2 files not compiled if you need to compile from scratch for version 1.20. And the 2 files ready, try just pasting them in with replacements. If there are any errors, please write me. net\dries007\tfc\common\capabilities\food\TFCFoodData.java.

` /*

  • Licensed under the EUPL, Version 1.2.
  • You may obtain a copy of the Licence at:
  • https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 */

package net.dries007.tfc.common.capabilities.food; import java.nio.file.Files; import java.nio.file.Path; import net.minecraft.nbt.NbtIo; import net.minecraft.world.level.storage.LevelResource; import net.minecraftforge.event.entity.living.LivingDeathEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.event.entity.player.PlayerEvent; import java.util.Random; import net.minecraft.nbt.CompoundTag; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.Mth; import net.minecraft.world.Difficulty; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.storage.PlayerDataStorage; import net.minecraftforge.network.PacketDistributor; import org.jetbrains.annotations.Nullable;

import net.dries007.tfc.common.TFCDamageSources; import net.dries007.tfc.common.capabilities.player.PlayerDataCapability; import net.dries007.tfc.config.TFCConfig; import net.dries007.tfc.mixin.accessor.PlayerAccessor; import net.dries007.tfc.network.FoodDataReplacePacket; import net.dries007.tfc.network.FoodDataUpdatePacket; import net.dries007.tfc.network.PacketHandler; import net.dries007.tfc.util.advancements.TFCAdvancements; import net.dries007.tfc.util.calendar.ICalendar; import net.dries007.tfc.util.climate.Climate; import net.dries007.tfc.ForgeEventHandler; import net.minecraft.network.chat.Component;

/**

  • A note on the reason why {@link TFCFoodData} serializes to an external capability (player data):

  • We don't use the vanilla read/add save data methods.

  • Why? Because we replace the {@link net.minecraft.world.food.FoodData} instance way after it has been already deserialized in vanilla.

    • {@link net.minecraftforge.event.entity.EntityEvent.EntityConstructing} is too early as {@link Player} constructor would overwrite our food data
    • {@link net.minecraftforge.event.AttachCapabilitiesEvent} fires just after, and is where we attach the player data capability
    • {@link net.minecraftforge.event.entity.player.PlayerEvent.LoadFromFile} fires later, and has access to the save data, but is too early as the player's connection is not yet set, so we can't properly sync that change to client.
    • {@link net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedInEvent} is where we can reliably update the {@link net.minecraft.world.food.FoodData} instance on server, and then sync that change to client.
  • Saving is then just written to the capability NBT at point of writing

  • The capability is deserialized in {@link net.minecraft.world.entity.Entity#load(CompoundTag)}, but the food stats doesn't exist yet, so the saved data is cached on the capability and copied over in the food data constructor

  • Now, at this point, we can actually read directly from our capability, as it has been deserialized much earlier in Entity#load.

  • Reading is a bit different: we read from the capability data early, store the NBT, and then copy it to the food stats on the instantiation of the custom food stats. */ public class TFCFoodData extends net.minecraft.world.food.FoodData { // Vanilla constants public static final int MAX_HUNGER = 20; public static final float MAX_SATURATION = 20; public static final float MAX_EXHAUSTION = 40; public static final float EXHAUSTION_PER_HUNGER = 4;

    public static final float MAX_THIRST = 100f;

    public static final float PASSIVE_HEALING_PER_TEN_TICKS = 20 * 0.0002f; // On the display: 1 HP / 5 seconds public static final float EXHAUSTION_MULTIPLIER = 0.4f; // Multiplier for all sources of exhaustion. Vanilla sources get reduced, while passive exhaustion factors in this multiplier. public static final float PASSIVE_EXHAUSTION_PER_TICK = MAX_HUNGER * EXHAUSTION_PER_HUNGER / (2.5f * ICalendar.TICKS_IN_DAY * EXHAUSTION_MULTIPLIER); // Passive exhaustion will deplete your food bar once every 2.5 days. Food bar holds ~5 "meals", this requires two per day public static final float PASSIVE_EXHAUSTION_PER_SECOND = 20 * PASSIVE_EXHAUSTION_PER_TICK;

    public static final float MAX_TEMPERATURE_THIRST_DECAY = 0.4f;

    public static final float DEFAULT_AVERAGE_NUTRITION = 0.4f; // 1/2 of 4 bars = 0.5 x 4 / 5

    public static void replaceFoodStats(Player player) { // Only replace the server player's stats if they aren't already final net.minecraft.world.food.FoodData foodStats = player.getFoodData();

     if (!(foodStats instanceof TFCFoodData))
     {
         // Replace, and then read from the cached data on the player capability (will be present if this is initial log-in / read from disk)
         final TFCFoodData newStats = new TFCFoodData(player, foodStats);
         ((PlayerAccessor) player).accessor$setFoodData(newStats);
         player.getCapability(PlayerDataCapability.CAPABILITY).ifPresent(cap -> cap.writeTo(newStats));
    
    
     }
     // Send the update regardless so the client can perform the same logic
         if (player instanceof ServerPlayer serverPlayer)
     {
         PacketHandler.send(PacketDistributor.PLAYER.with(() -> serverPlayer), new FoodDataReplacePacket());
     }
    

    }

    public static void restoreFoodStatsAfterDeath(ServerPlayer oldPlayer, ServerPlayer newPlayer) { if (oldPlayer.getFoodData() instanceof TFCFoodData oldStats) { final TFCFoodData newStats = new TFCFoodData(oldPlayer, newPlayer.getFoodData(), oldStats.getNutrition()); ((PlayerAccessor) newPlayer).accessor$setFoodData(newStats); } Path filePath = newPlayer.getServer().getWorldPath(LevelResource.PLAYER_DATA_DIR).resolve(newPlayer.getUUID().toString() + ".dat"); if (Files.exists(filePath)) { try { CompoundTag playerData = NbtIo.readCompressed(filePath.toFile()); if (playerData.contains("TFCFoodData")) { TFCFoodData newStats = new TFCFoodData(newPlayer, newPlayer.getFoodData()); newStats.deserializeFromPlayerData(playerData.getCompound("TFCFoodData")); ((PlayerAccessor) newPlayer).accessor$setFoodData(newStats); }

    } catch (Exception e) {
        e.printStackTrace();
    }
    

    } }

    private final Player sourcePlayer; private final net.minecraft.world.food.FoodData delegate; // We keep this here to do normal vanilla tracking (rather than using super). This is also friendlier to other mods if they replace this private final NutritionData nutritionData; // Separate handler for nutrition, because it's a bit complex private long lastDrinkTick; private float thirst;

    public TFCFoodData(Player sourcePlayer, net.minecraft.world.food.FoodData delegate) { this(sourcePlayer, delegate, new NutritionData(0.5f, 0.0f)); }

    public TFCFoodData(Player sourcePlayer, net.minecraft.world.food.FoodData delegate, NutritionData oldNutritionData) { this.sourcePlayer = sourcePlayer; this.delegate = delegate; this.nutritionData = oldNutritionData; this.thirst = MAX_THIRST; }

    @Override public void eat(int foodLevelIn, float foodSaturationModifier) { // This should never be called directly - when it is we assume it's direct stat modifications (saturation potion, eating cake) // We make modifications to vanilla logic, as saturation needs to be unaffected by hunger }

    @Override public void eat(Item maybeFood, ItemStack stack, @Nullable LivingEntity entity) { stack.getCapability(FoodCapability.CAPABILITY).ifPresent(this::eat); }

    /**

    • Called from {@link Player#tick()} on server side only

    • @param player the player who's food stats this is */ @Override public void tick(Player player) { final Difficulty difficulty = player.level.getDifficulty(); if (difficulty == Difficulty.PEACEFUL && TFCConfig.SERVER.enablePeacefulDifficultyPassiveRegeneration.get()) { // Extra-Peaceful Difficulty // Health regeneration modified from PlayerEntity#aiStep() if (sourcePlayer.getHealth() < sourcePlayer.getMaxHealth() && sourcePlayer.tickCount % 20 == 0) { sourcePlayer.heal(1.0F); } if (sourcePlayer.tickCount % 10 == 0) { if (needsFood()) { setFoodLevel(getFoodLevel() + 1); } if (thirst < MAX_THIRST) { addThirst(5f); } } } else { // Passive exhaustion - call the source player instead of the local method player.causeFoodExhaustion(PASSIVE_EXHAUSTION_PER_TICK * TFCConfig.SERVER.passiveExhaustionModifier.get().floatValue());

       // Same check as the original food stats, so hunger and thirst loss are synced
       if (delegate.getExhaustionLevel() >= 4.0F)
       {
           addThirst(-getThirstModifier(player));
      
           // Vanilla will consume exhaustion and saturation in peaceful, but won't modify hunger.
           // We mimic the same checks that are about to happen in tick(), and if needed, consume hunger in advance
           if (difficulty == Difficulty.PEACEFUL && getSaturationLevel() <= 0)
           {
               setFoodLevel(Math.max(getFoodLevel() - 1, 0));
           }
       }
      
       if (difficulty == Difficulty.PEACEFUL)
       {
           // Copied from vanilla's food stats, so we consume food in peaceful mode (would normally be part of the super.onUpdate call
           if (delegate.getExhaustionLevel() > 4.0F)
           {
               setFoodLevel(Math.max(getFoodLevel() - 1, 0));
           }
       }
      

      }

      // Next, tick the original food stats delegate.tick(player);

      // Apply custom TFC regeneration if (player.tickCount % 10 == 0) { if (player.isHurt() && getFoodLevel() >= 4.0f && getThirst() > 20f) { final float foodBonus = Mth.inverseLerp(getFoodLevel(), 4, MAX_HUNGER); final float thirstBonus = Mth.inverseLerp(getThirst(), 20, MAX_THIRST); final float multiplier = 1 + foodBonus + thirstBonus; // Range: [1, 4] depending on total thirst and hunger

           player.heal(multiplier * PASSIVE_HEALING_PER_TEN_TICKS * TFCConfig.SERVER.naturalRegenerationModifier.get().floatValue());
       }
      

      }

      // Last, apply negative effects due to thirst if (player.tickCount % 100 == 0 && difficulty != Difficulty.PEACEFUL && !player.getAbilities().invulnerable) { if (thirst < 10f) { player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 160, 1, false, false)); player.addEffect(new MobEffectInstance(MobEffects.DIG_SLOWDOWN, 160, 1, false, false)); if (thirst <= 0f) { // Hurt the player, same as starvation TFCDamageSources.dehydration(player, 1f); } } else if (thirst < 20f) { player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 160, 0, false, false)); player.addEffect(new MobEffectInstance(MobEffects.DIG_SLOWDOWN, 160, 0, false, false)); } }

      // Since this is only called server side, and vanilla has a custom packet for this stuff, we need our own if (player instanceof ServerPlayer serverPlayer) { PacketHandler.send(PacketDistributor.PLAYER.with(() -> serverPlayer), new FoodDataUpdatePacket(nutritionData.getNutrients(), thirst)); } }

    @Override public void readAdditionalSaveData(CompoundTag vanillaNbt) { delegate.readAdditionalSaveData(vanillaNbt); }

    @Override public void addAdditionalSaveData(CompoundTag vanillaNbt) { delegate.addAdditionalSaveData(vanillaNbt); }

    @Override public int getFoodLevel() { return delegate.getFoodLevel(); }

    @Override public boolean needsFood() { return delegate.needsFood(); }

    @Override public void addExhaustion(float exhaustion) { // Exhaustion from all vanilla sources is reduced delegate.addExhaustion(EXHAUSTION_MULTIPLIER * exhaustion); }

    @Override public float getSaturationLevel() { return delegate.getSaturationLevel(); }

    @Override public void setFoodLevel(int food) { delegate.setFoodLevel(food); }

    @Override public void setSaturation(float saturation) { delegate.setSaturation(saturation); }

    public void eat(IFood food) { // Eating items has nutritional benefits final FoodData data = food.getData(); if (!food.isRotten()) { eat(data); } else if (this.sourcePlayer instanceof ServerPlayer) // Check for server side first { // Minor effects from eating rotten food final Random random = sourcePlayer.getRandom(); if (random.nextFloat() < 0.6) { sourcePlayer.addEffect(new MobEffectInstance(MobEffects.HUNGER, 1800, 1)); if (random.nextFloat() < 0.15) { sourcePlayer.addEffect(new MobEffectInstance(MobEffects.POISON, 1800, 0)); } } } }

    public void eat(FoodData data) { addThirst(data.water()); nutritionData.addNutrients(data);

     if (this.sourcePlayer instanceof ServerPlayer serverPlayer && nutritionData.getAverageNutrition() >= 0.999)
     {
         TFCAdvancements.FULL_NUTRITION.trigger(serverPlayer);
     }
    
     if (data.hunger() > 0)
     {
         // In order to get the exact saturation we want, apply this scaling factor here
         delegate.eat(data.hunger(), data.saturation() / (2f * data.hunger()));
     }
    

    }

    public CompoundTag serializeToPlayerData() { CompoundTag nbt = new CompoundTag();

     nbt.putFloat("thirst", thirst);
     nbt.putFloat("lastDrinkTick", lastDrinkTick);
     nbt.put("nutrients", nutritionData.writeToNbt());
    
     return nbt;
    

    }

    public void deserializeFromPlayerData(CompoundTag nbt) { thirst = nbt.getFloat("thirst"); lastDrinkTick = nbt.getLong("lastDrinkTick"); nutritionData.readFromNbt(nbt.getCompound("nutrients")); }

    /**

    • Sets data from a packet, received on client side. Does not contain the full data only the important information */ public void onClientUpdate(float[] nutrients, float thirst) { this.nutritionData.onClientUpdate(nutrients); this.thirst = thirst; }

    public float getHealthModifier() { final float averageNutrition = nutritionData.getAverageNutrition(); // In [0, 1] return averageNutrition < DEFAULT_AVERAGE_NUTRITION ? // Lerp [0, default] -> [min, default] modifier Mth.map(averageNutrition, 0.0f, DEFAULT_AVERAGE_NUTRITION, TFCConfig.SERVER.nutritionMinimumHealthModifier.get().floatValue(), TFCConfig.SERVER.nutritionDefaultHealthModifier.get().floatValue()) : // Lerp [default, 1] -> [default, max] modifier Mth.map(averageNutrition, DEFAULT_AVERAGE_NUTRITION, 1.0f, TFCConfig.SERVER.nutritionDefaultHealthModifier.get().floatValue(), TFCConfig.SERVER.nutritionMaximumHealthModifier.get().floatValue()); }

    /**

    • @return The total thirst loss per tick, on a scale of [0, 100], 100 being the entire thirst bar */ public float getThirstModifier(Player player) { return TFCConfig.SERVER.thirstModifier.get().floatValue() * (1 + getThirstContributionFromTemperature(player)); }

    /**

    • @return The thirst loss from the ambient temperature on top of regular loss */ public float getThirstContributionFromTemperature(Player player) { if (TFCConfig.SERVER.enableThirstOverheating.get()) { final float temp = Climate.getTemperature(player.level, player.blockPosition()); return Mth.clampedMap(temp, 22f, 34f, 0f, MAX_TEMPERATURE_THIRST_DECAY); } return 0; }

    public float getThirst() { return thirst; }

    public void setThirst(float thirst) { this.thirst = Mth.clamp(thirst, 0, MAX_THIRST); }

    public void addThirst(float toAdd) { setThirst(thirst + toAdd); }

    public NutritionData getNutrition() { return nutritionData; } } net\dries007\tfc\ForgeEventHandler.java/*

  • Licensed under the EUPL, Version 1.2.

  • You may obtain a copy of the Licence at:

  • https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 */

package net.dries007.tfc; import java.lang.reflect.Method; import java.nio.file.Path; import java.util.HashMap; import java.util.Random; import java.util.UUID; import java.nio.file.Files; import java.nio.file.Paths; import java.util.concurrent.Executor; import com.mojang.datafixers.util.Pair; import com.mojang.logging.LogUtils; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Holder; import net.minecraft.core.Registry; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.Tag; import net.minecraft.server.MinecraftServer; import net.minecraft.server.WorldStem; import net.minecraft.server.level.PlayerRespawnLogic; import net.minecraft.server.level.ServerChunkCache; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.players.PlayerList; import net.minecraft.tags.FluidTags; import net.minecraft.util.Mth; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LightningBolt; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.MobSpawnType; import net.minecraft.world.entity.animal.Chicken; import net.minecraft.world.entity.animal.IronGolem; import net.minecraft.world.entity.animal.SnowGolem; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.monster.Monster; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.projectile.Projectile; import net.minecraft.world.entity.vehicle.Boat; import net.minecraft.world.entity.vehicle.Minecart; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.TieredItem; import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.item.crafting.RecipeManager; import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.GameRules; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.BambooBlock; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.HorizontalDirectionalBlock; import net.minecraft.world.level.block.LecternBlock; import net.minecraft.world.level.block.SnowLayerBlock; import net.minecraft.world.level.block.TntBlock; import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.ChunkStatus; import net.minecraft.world.level.chunk.EmptyLevelChunk; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.ProtoChunk; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.storage.LevelResource; import net.minecraft.world.level.storage.ServerLevelData; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.TierSortingRegistry; import net.minecraftforge.common.ToolActions; import net.minecraftforge.event.AddReloadListenerEvent; import net.minecraftforge.event.AttachCapabilitiesEvent; import net.minecraftforge.event.OnDatapackSyncEvent; import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.event.ServerChatEvent; import net.minecraftforge.event.TagsUpdatedEvent; import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.entity.EntityJoinWorldEvent; import net.minecraftforge.event.entity.EntityMountEvent; import net.minecraftforge.event.entity.ProjectileImpactEvent; import net.minecraftforge.event.entity.item.ItemExpireEvent; import net.minecraftforge.event.entity.living.AnimalTameEvent; import net.minecraftforge.event.entity.living.LivingDeathEvent; import net.minecraftforge.event.entity.living.LivingEntityUseItemEvent; import net.minecraftforge.event.entity.living.LivingEvent; import net.minecraftforge.event.entity.living.LivingHurtEvent; import net.minecraftforge.event.entity.living.LivingSpawnEvent; import net.minecraftforge.event.entity.living.PotionEvent; import net.minecraftforge.event.entity.living.ShieldBlockEvent; import net.minecraftforge.event.entity.player.BonemealEvent; import net.minecraftforge.event.entity.player.PlayerContainerEvent; import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.event.entity.player.PlayerInteractEvent; import net.minecraftforge.event.world.BlockEvent; import net.minecraftforge.event.world.ChunkDataEvent; import net.minecraftforge.event.world.ChunkEvent; import net.minecraftforge.event.world.ChunkWatchEvent; import net.minecraftforge.event.world.ExplosionEvent; import net.minecraftforge.event.world.WorldEvent; import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.eventbus.api.EventPriority; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.network.PacketDistributor; import net.minecraftforge.registries.ForgeRegistries; import org.slf4j.Logger; import org.spongepowered.asm.mixin.struct.SourceMap.File;

import net.dries007.tfc.mixin.accessor.PlayerAccessor; import net.minecraft.network.chat.Component;

import net.dries007.tfc.client.ClientHelpers; import net.dries007.tfc.client.TFCSounds; import net.dries007.tfc.common.TFCEffects; import net.dries007.tfc.common.TFCTags; import net.dries007.tfc.common.blockentities.AbstractFirepitBlockEntity; import net.dries007.tfc.common.blockentities.BloomeryBlockEntity; import net.dries007.tfc.common.blockentities.CharcoalForgeBlockEntity; import net.dries007.tfc.common.blockentities.PitKilnBlockEntity; import net.dries007.tfc.common.blockentities.TFCBlockEntities; import net.dries007.tfc.common.blockentities.TickCounterBlockEntity; import net.dries007.tfc.common.blocks.CharcoalPileBlock; import net.dries007.tfc.common.blocks.TFCBlocks; import net.dries007.tfc.common.blocks.TFCCandleBlock; import net.dries007.tfc.common.blocks.TFCCandleCakeBlock; import net.dries007.tfc.common.blocks.devices.AnvilBlock; import net.dries007.tfc.common.blocks.devices.BarrelBlock; import net.dries007.tfc.common.blocks.devices.BlastFurnaceBlock; import net.dries007.tfc.common.blocks.devices.BloomeryBlock; import net.dries007.tfc.common.blocks.devices.BurningLogPileBlock; import net.dries007.tfc.common.blocks.devices.CharcoalForgeBlock; import net.dries007.tfc.common.blocks.devices.LampBlock; import net.dries007.tfc.common.blocks.devices.PitKilnBlock; import net.dries007.tfc.common.blocks.devices.PowderkegBlock; import net.dries007.tfc.common.blocks.devices.SluiceBlock; import net.dries007.tfc.common.blocks.rock.AqueductBlock; import net.dries007.tfc.common.blocks.rock.Rock; import net.dries007.tfc.common.blocks.rock.RockAnvilBlock; import net.dries007.tfc.common.blocks.wood.TFCLecternBlock; import net.dries007.tfc.common.capabilities.egg.EggCapability; import net.dries007.tfc.common.capabilities.egg.EggHandler; import net.dries007.tfc.common.capabilities.food.FoodCapability; import net.dries007.tfc.common.capabilities.food.FoodDefinition; import net.dries007.tfc.common.capabilities.food.FoodHandler; import net.dries007.tfc.common.capabilities.food.IFood; import net.dries007.tfc.common.capabilities.food.TFCFoodData; import net.dries007.tfc.common.capabilities.forge.Forging; import net.dries007.tfc.common.capabilities.forge.ForgingBonus; import net.dries007.tfc.common.capabilities.forge.ForgingCapability; import net.dries007.tfc.common.capabilities.heat.HeatCapability; import net.dries007.tfc.common.capabilities.heat.HeatDefinition; import net.dries007.tfc.common.capabilities.player.PlayerData; import net.dries007.tfc.common.capabilities.player.PlayerDataCapability; import net.dries007.tfc.common.capabilities.size.ItemSizeManager; import net.dries007.tfc.common.commands.TFCCommands; import net.dries007.tfc.common.container.BlockEntityContainer; import net.dries007.tfc.common.container.Container; import net.dries007.tfc.common.container.PestContainer; import net.dries007.tfc.common.entities.Fauna; import net.dries007.tfc.common.entities.HoldingMinecart; import net.dries007.tfc.common.entities.predator.Predator; import net.dries007.tfc.common.items.DynamicBowlFood; import net.dries007.tfc.common.items.TFCItems; import net.dries007.tfc.common.recipes.CollapseRecipe; import net.dries007.tfc.config.TFCConfig; import net.dries007.tfc.mixin.accessor.ChunkAccessAccessor; import net.dries007.tfc.mixin.accessor.RecipeManagerAccessor; import net.dries007.tfc.network.ChunkUnwatchPacket; import net.dries007.tfc.network.EffectExpirePacket; import net.dries007.tfc.network.PacketHandler; import net.dries007.tfc.network.PlayerDrinkPacket; import net.dries007.tfc.network.UpdateClimateModelPacket; import net.dries007.tfc.util.AxeLoggingHelper; import net.dries007.tfc.util.Drinkable; import net.dries007.tfc.util.EntityDamageResistance; import net.dries007.tfc.util.Fertilizer; import net.dries007.tfc.util.Fuel; import net.dries007.tfc.util.Helpers; import net.dries007.tfc.util.InteractionManager; import net.dries007.tfc.util.ItemDamageResistance; import net.dries007.tfc.util.LampFuel; import net.dries007.tfc.util.LegacyMaterials; import net.dries007.tfc.util.Metal; import net.dries007.tfc.util.Pannable; import net.dries007.tfc.util.PhysicalDamageType; import net.dries007.tfc.util.SelfTests; import net.dries007.tfc.util.Sluiceable; import net.dries007.tfc.util.Support; import net.dries007.tfc.util.calendar.ICalendar; import net.dries007.tfc.util.climate.Climate; import net.dries007.tfc.util.climate.ClimateModel; import net.dries007.tfc.util.climate.ClimateRange; import net.dries007.tfc.util.climate.OverworldClimateModel; import net.dries007.tfc.util.collections.IndirectHashCollection; import net.dries007.tfc.util.events.LoggingEvent; import net.dries007.tfc.util.events.SelectClimateModelEvent; import net.dries007.tfc.util.events.StartFireEvent; import net.dries007.tfc.util.tracker.WeatherHelpers; import net.dries007.tfc.util.tracker.WorldTracker; import net.dries007.tfc.util.tracker.WorldTrackerCapability; import net.dries007.tfc.world.NoopClimateSampler; import net.dries007.tfc.world.biome.BiomeSourceExtension; import net.dries007.tfc.world.biome.TFCBiomes; import net.dries007.tfc.world.chunkdata.ChunkData; import net.dries007.tfc.world.chunkdata.ChunkDataCache; import net.dries007.tfc.world.chunkdata.ChunkDataCapability; import net.dries007.tfc.world.chunkdata.ChunkGeneratorExtension; import net.dries007.tfc.world.settings.RockLayerSettings;

public final class ForgeEventHandler { private static final Logger LOGGER = LogUtils.getLogger(); private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyz"; private static final BlockHitResult FAKE_MISS = BlockHitResult.miss(Vec3.ZERO, Direction.UP, BlockPos.ZERO);

 public static void init()
 {
     final IEventBus bus = MinecraftForge.EVENT_BUS;

     bus.addListener(ForgeEventHandler::onCreateWorldSpawn);
     bus.addGenericListener(LevelChunk.class, ForgeEventHandler::attachChunkCapabilities);
     bus.addGenericListener(Level.class, ForgeEventHandler::attachWorldCapabilities);
     bus.addGenericListener(ItemStack.class, ForgeEventHandler::attachItemCapabilities);
     bus.addGenericListener(Entity.class, ForgeEventHandler::attachEntityCapabilities);
     bus.addListener(ForgeEventHandler::onChunkWatch);
     bus.addListener(ForgeEventHandler::onChunkUnwatch);
     bus.addListener(ForgeEventHandler::onChunkLoad);
     bus.addListener(ForgeEventHandler::onChunkUnload);
     bus.addListener(ForgeEventHandler::onChunkDataSave);
     bus.addListener(ForgeEventHandler::onChunkDataLoad);
     bus.addListener(ForgeEventHandler::registerCommands);
     bus.addListener(ForgeEventHandler::onBlockBroken);
     bus.addListener(ForgeEventHandler::onBlockPlace);
     bus.addListener(ForgeEventHandler::onBreakSpeed);
     bus.addListener(ForgeEventHandler::onNeighborUpdate);
     bus.addListener(ForgeEventHandler::onExplosionDetonate);
     bus.addListener(ForgeEventHandler::onWorldTick);
     bus.addListener(ForgeEventHandler::onWorldLoad);
     bus.addListener(ForgeEventHandler::onCreateNetherPortal);
     bus.addListener(ForgeEventHandler::onFluidPlaceBlock);
     bus.addListener(ForgeEventHandler::onFluidCreateSource);
     bus.addListener(ForgeEventHandler::onFireStart);
     bus.addListener(ForgeEventHandler::onProjectileImpact);
     bus.addListener(ForgeEventHandler::onPlayerTick);
     bus.addListener(ForgeEventHandler::onEffectRemove);
     bus.addListener(ForgeEventHandler::onEffectExpire);
     bus.addListener(ForgeEventHandler::onLivingJump);
     bus.addListener(ForgeEventHandler::onLivingHurt);
     bus.addListener(ForgeEventHandler::onShieldBlock);
     bus.addListener(ForgeEventHandler::onLivingSpawnCheck);
     bus.addListener(ForgeEventHandler::onEntityJoinWorld);
     bus.addListener(ForgeEventHandler::onItemExpire);
     bus.addListener(ForgeEventHandler::onPlayerLoggedIn);
     bus.addListener(ForgeEventHandler::onPlayerRespawn);
     bus.addListener(ForgeEventHandler::onPlayerDeath);
     bus.addListener(ForgeEventHandler::onPlayerDeathSaveData);
     bus.addListener(ForgeEventHandler::onPlayerChangeDimension);
     bus.addListener(ForgeEventHandler::onServerChat);
     bus.addListener(ForgeEventHandler::onPlayerRightClickBlock);
     bus.addListener(EventPriority.LOWEST, true, ForgeEventHandler::onPlayerRightClickBlockLowestPriority);
     bus.addListener(ForgeEventHandler::onPlayerRightClickItem);
     bus.addListener(ForgeEventHandler::onPlayerRightClickEmpty);
     bus.addListener(ForgeEventHandler::onItemUseFinish);
     bus.addListener(ForgeEventHandler::addReloadListeners);
     bus.addListener(ForgeEventHandler::onDataPackSync);
     bus.addListener(ForgeEventHandler::onTagsUpdated);
     bus.addListener(ForgeEventHandler::onBoneMeal);
     bus.addListener(ForgeEventHandler::onSelectClimateModel);
     bus.addListener(ForgeEventHandler::onAnimalTame);
     bus.addListener(ForgeEventHandler::onContainerOpen);
     bus.addListener(ForgeEventHandler::onCropsGrow);
     bus.addListener(ForgeEventHandler::onMount);
     bus.addListener(ForgeEventHandler::onEntityInteract);
 }

 /**
  * Duplicates logic from {@link MinecraftServer#
  * setInitialSpawn(ServerLevel, ServerLevelData, boolean, boolean)} as that version only asks the dimension for the sea level...
  */
 public static void onCreateWorldSpawn(WorldEvent.CreateSpawnPosition event)
 {
     if (event.getWorld() instanceof ServerLevel level && level.getChunkSource().getGenerator() instanceof ChunkGeneratorExtension extension)
     {
         final ChunkGenerator generator = extension.self();
         final ServerLevelData settings = event.getSettings();
         final BiomeSourceExtension source = extension.getBiomeSourceExtension();
         final Random random = new Random(level.getSeed());

         Pair<BlockPos, Holder<Biome>> posPair = generator.getBiomeSource().findBiomeHorizontal(source.settings().spawnCenterX(), 0, source.settings().spawnCenterZ(), source.settings().spawnDistance(), source.settings().spawnDistance() / 256, biome -> TFCBiomes.getExtensionOrThrow(level, biome.value()).isSpawnable(), random, false, NoopClimateSampler.INSTANCE);
         BlockPos pos;
         ChunkPos chunkPos;
         if (posPair == null)
         {
             LOGGER.warn("Unable to find spawn biome!");
             pos = new BlockPos(0, generator.getSeaLevel(), 0);
         }
         else
         {
             pos = posPair.getFirst();
         }
         chunkPos = new ChunkPos(pos);

         settings.setSpawn(chunkPos.getWorldPosition().offset(8, generator.getSpawnHeight(level), 8), 0.0F);
         boolean foundExactSpawn = false;
         int x = 0, z = 0;
         int xStep = 0;
         int zStep = -1;

         for (int tries = 0; tries < 1024; ++tries)
         {
             if (x > -16 && x <= 16 && z > -16 && z <= 16)
             {
                 final BlockPos spawnPos = PlayerRespawnLogic.getSpawnPosInChunk(level, new ChunkPos(chunkPos.x + x, chunkPos.z + z));
                 if (spawnPos != null)
                 {
                     settings.setSpawn(spawnPos, 0);
                     foundExactSpawn = true;
                     break;
                 }
             }

             if ((x == z) || (x < 0 && x == -z) || (x > 0 && x == 1 - z))
             {
                 final int swap = xStep;
                 xStep = -zStep;
                 zStep = swap;
             }

             x += xStep;
             z += zStep;
         }

         if (!foundExactSpawn)
         {
             LOGGER.warn("Unable to find a suitable spawn location!");
         }

         if (level.getServer().getWorldData().worldGenSettings().generateBonusChest())
         {
             LOGGER.warn("No bonus chest for you, you cheaty cheater!");
         }

         event.setCanceled(true);
     }
 }

 public static void attachChunkCapabilities(AttachCapabilitiesEvent<LevelChunk> event)
 {
     final LevelChunk chunk = event.getObject();
     if (!chunk.isEmpty())
     {
         final Level level = event.getObject().getLevel();
         final ChunkPos chunkPos = event.getObject().getPos();

         ChunkData data;
         if (Helpers.isClientSide(level))
         {
             // This may happen before or after the chunk is watched and synced to client
             // Default to using the cache. If later the sync packet arrives it will update the same instance in the chunk capability and cache
             // We don't want to use getOrEmpty here, as the instance has to be mutable. In addition, we can't just wait for the chunk data to arrive, we have to assign one.
             data = ChunkDataCache.CLIENT.computeIfAbsent(chunkPos, ChunkData::createClient);
         }
         else
         {
             // Chunk was created on server thread.
             // We try and promote partial data, if it's available via an identifiable chunk generator.
             // Otherwise, we fallback to empty data.
             if (level instanceof ServerLevel serverLevel && serverLevel.getChunkSource().getGenerator() instanceof ChunkGeneratorExtension ex)
             {
                 data = ex.getChunkDataProvider().promotePartialOrCreate(chunkPos);
             }
             else
             {
                 data = new ChunkData(chunkPos, RockLayerSettings.EMPTY);
             }

         }
         event.addCapability(ChunkDataCapability.KEY, data);
     }
 }

 public static void attachWorldCapabilities(AttachCapabilitiesEvent<Level> event)
 {
     event.addCapability(WorldTrackerCapability.KEY, new WorldTracker(event.getObject()));
 }

 public static void attachItemCapabilities(AttachCapabilitiesEvent<ItemStack> event)
 {
     ItemStack stack = event.getObject();
     if (!stack.isEmpty())
     {
         // Attach mandatory capabilities
         event.addCapability(ForgingCapability.KEY, new Forging(stack));

         // Optional capabilities
         HeatDefinition def = HeatCapability.get(stack);
         if (def != null)
         {
             event.addCapability(HeatCapability.KEY, def.create());
         }

         FoodDefinition food = FoodCapability.get(stack);
         if (food != null)
         {
             event.addCapability(FoodCapability.KEY, FoodDefinition.getHandler(food, stack));
         }

         if (stack.getItem() == Items.EGG)
         {
             event.addCapability(EggCapability.KEY, new EggHandler(stack));
         }
     }
 }

 public static void attachEntityCapabilities(AttachCapabilitiesEvent<Entity> event)
 {
     if (event.getObject() instanceof Player player)
     {
         event.addCapability(PlayerDataCapability.KEY, new PlayerData(player));
     }
 }

 public static void onChunkWatch(ChunkWatchEvent.Watch event)
 {
     // Send an update packet to the client when watching the chunk
     ChunkPos pos = event.getPos();
     ChunkData chunkData = ChunkData.get(event.getWorld(), pos);
     if (chunkData.getStatus() != ChunkData.Status.EMPTY)
     {
         PacketHandler.send(PacketDistributor.PLAYER.with(event::getPlayer), chunkData.getUpdatePacket());
     }
     else
     {
         // Chunk does not exist yet but it's queue'd for watch. Queue an update packet to be sent on chunk load
         ChunkDataCache.WATCH_QUEUE.enqueueUnloadedChunk(pos, event.getPlayer());
     }
 }

 public static void onChunkUnwatch(ChunkWatchEvent.UnWatch event)
 {
     // Send an update packet to the client when un-watching the chunk
     ChunkPos pos = event.getPos();
     PacketHandler.send(PacketDistributor.PLAYER.with(event::getPlayer), new ChunkUnwatchPacket(pos));
     ChunkDataCache.WATCH_QUEUE.dequeueChunk(pos, event.getPlayer());
 }

 public static void onChunkLoad(ChunkEvent.Load event)
 {
     if (!Helpers.isClientSide(event.getWorld()) && !(event.getChunk() instanceof EmptyLevelChunk))
     {
         ChunkPos pos = event.getChunk().getPos();
         ChunkData.getCapability(event.getChunk()).ifPresent(data -> {
             ChunkDataCache.SERVER.update(pos, data);
             ChunkDataCache.WATCH_QUEUE.dequeueLoadedChunk(pos, data);
         });
     }
 }

 public static void onChunkUnload(ChunkEvent.Unload event)
 {
     // Clear server side chunk data cache
     if (!Helpers.isClientSide(event.getWorld()) && !(event.getChunk() instanceof EmptyLevelChunk))
     {
         ChunkDataCache.SERVER.remove(event.getChunk().getPos());
     }
 }

 /**
  * Serialize chunk data on chunk primers, before the chunk data capability is present.
  * - This saves the effort of re-generating the same data for proto chunks
  * - And, due to the late setting of part of chunk data ({@link net.dries007.tfc.world.chunkdata.RockData#setSurfaceHeight(int[])}, avoids that being nullified when saving and reloading during the noise phase of generation
  */
 public static void onChunkDataSave(ChunkDataEvent.Save event)
 {
     if (event.getChunk().getStatus().getChunkType() == ChunkStatus.ChunkType.PROTOCHUNK && event.getChunk() instanceof ProtoChunk chunk && ((ServerChunkCache) event.getWorld().getChunkSource()).getGenerator() instanceof ChunkGeneratorExtension ex)
     {
         CompoundTag nbt = ex.getChunkDataProvider().savePartial(chunk);
         if (nbt != null)
         {
             event.getData().put("tfc_protochunk_data", nbt);
         }
     }
 }

 /**
  * @see #onChunkDataSave(ChunkDataEvent.Save)
  */
 public static void onChunkDataLoad(ChunkDataEvent.Load event)
 {
     if (event.getChunk().getStatus().getChunkType() == ChunkStatus.ChunkType.PROTOCHUNK && event.getData().contains("tfc_protochunk_data", Tag.TAG_COMPOUND) && event.getChunk() instanceof ProtoChunk chunk && ((ChunkAccessAccessor) chunk).accessor$getLevelHeightAccessor() instanceof ServerLevel level && level.getChunkSource().getGenerator() instanceof ChunkGeneratorExtension generator)
     {
         generator.getChunkDataProvider().loadPartial(chunk, event.getData().getCompound("tfc_protochunk_data"));
     }
 }

 public static void registerCommands(RegisterCommandsEvent event)
 {
     LOGGER.debug("Registering TFC Commands");
     TFCCommands.registerCommands(event.getDispatcher());
 }

 public static void onBlockBroken(BlockEvent.BreakEvent event)
 {
     // Trigger a collapse
     final LevelAccessor levelAccess = event.getWorld();
     final BlockPos pos = event.getPos();
     final BlockState state = levelAccess.getBlockState(pos);

     if (Helpers.isBlock(state, TFCTags.Blocks.CAN_TRIGGER_COLLAPSE) && levelAccess instanceof Level level)
     {
         CollapseRecipe.tryTriggerCollapse(level, pos);
         return;
     }

     // Chop down a tree
     final ItemStack stack = event.getPlayer().getMainHandItem();
     if (AxeLoggingHelper.isLoggingAxe(stack) && AxeLoggingHelper.isLoggingBlock(state) && !MinecraftForge.EVENT_BUS.post(new LoggingEvent(levelAccess, pos, state, stack)))
     {
         event.setCanceled(true); // Cancel regardless of outcome of logging
         AxeLoggingHelper.doLogging(levelAccess, pos, event.getPlayer(), stack);
     }
 }

 public static void onBlockPlace(BlockEvent.EntityPlaceEvent event)
 {
     if (event.getWorld() instanceof final ServerLevel world)
     {
         final BlockPos pos = event.getPos();
         final BlockState state = event.getState();

         if (Helpers.isBlock(state, TFCTags.Blocks.CAN_LANDSLIDE))
         {
             world.getCapability(WorldTrackerCapability.CAPABILITY).ifPresent(cap -> cap.addLandslidePos(pos));
         }

         if (Helpers.isBlock(state, TFCTags.Blocks.BREAKS_WHEN_ISOLATED))
         {
             world.getCapability(WorldTrackerCapability.CAPABILITY).ifPresent(cap -> cap.addIsolatedPos(pos));
         }
     }
 }

 public static void onBreakSpeed(PlayerEvent.BreakSpeed event)
 {
     // Apply mining speed modifiers from forging bonuses
     final ForgingBonus bonus = ForgingBonus.get(event.getPlayer().getMainHandItem());
     if (bonus != ForgingBonus.NONE)
     {
         event.setNewSpeed(event.getNewSpeed() * bonus.efficiency());
     }
 }

 public static void onNeighborUpdate(BlockEvent.NeighborNotifyEvent event)
 {
     if (event.getWorld() instanceof final ServerLevel level)
     {
         for (Direction direction : event.getNotifiedSides())
         {
             // Check each notified block for a potential gravity block
             final BlockPos pos = event.getPos().relative(direction);
             final BlockState state = level.getBlockState(pos);

             if (Helpers.isBlock(state, TFCTags.Blocks.CAN_LANDSLIDE))
             {
                 level.getCapability(WorldTrackerCapability.CAPABILITY).ifPresent(cap -> cap.addLandslidePos(pos));
             }

             if (Helpers.isBlock(state.getBlock(), TFCTags.Blocks.BREAKS_WHEN_ISOLATED))
             {
                 level.getCapability(WorldTrackerCapability.CAPABILITY).ifPresent(cap -> cap.addIsolatedPos(pos));
             }
         }
     }
 }

 public static void onExplosionDetonate(ExplosionEvent.Detonate event)
 {
     if (!event.getWorld().isClientSide)
     {
         event.getWorld().getCapability(WorldTrackerCapability.CAPABILITY).ifPresent(cap -> cap.addCollapsePositions(new BlockPos(event.getExplosion().getPosition()), event.getAffectedBlocks()));
     }
 }

 public static void onWorldTick(TickEvent.WorldTickEvent event)
 {
     if (event.phase == TickEvent.Phase.START && event.world instanceof ServerLevel level)
     {
         WeatherHelpers.preAdvancedWeatherCycle(level);
         level.getCapability(WorldTrackerCapability.CAPABILITY).ifPresent(cap -> cap.tick(level));
     }
 }

 public static void onWorldLoad(WorldEvent.Load event)
 {
     if (event.getWorld() instanceof final ServerLevel level)
     {
         final MinecraftServer server = level.getServer();

         if (TFCConfig.SERVER.enableForcedTFCGameRules.get())
         {
             final GameRules rules = level.getGameRules();

             rules.getRule(GameRules.RULE_NATURAL_REGENERATION).set(false, server);
             rules.getRule(GameRules.RULE_DOINSOMNIA).set(false, server);
             rules.getRule(GameRules.RULE_DO_PATROL_SPAWNING).set(false, server);
             rules.getRule(GameRules.RULE_DO_TRADER_SPAWNING).set(false, server);

             LOGGER.info("Updating TFC Relevant Game Rules for level {}.", level.dimension().location());
         }

         Climate.onWorldLoad(level);
         if (level.dimension() == Level.OVERWORLD)
         {
             ItemSizeManager.applyItemStackSizeOverrides();
             SelfTests.runServerSelfTests();
         }
     }
 }

 public static void onCreateNetherPortal(BlockEvent.PortalSpawnEvent event)
 {
     if (!TFCConfig.SERVER.enableNetherPortals.get())
     {
         event.setCanceled(true);
     }
 }

 public static void onFluidPlaceBlock(BlockEvent.FluidPlaceBlockEvent event)
 {
     // Currently, getOriginalState gets the fluid block that's placing the block, not the block getting placed
     BlockState state = event.getNewState();
     if (Helpers.isBlock(state, Blocks.STONE))
     {
         event.setNewState(TFCBlocks.ROCK_BLOCKS.get(net.dries007.tfc.common.blocks.rock.Rock.GABBRO).get(net.dries007.tfc.common.blocks.rock.Rock.BlockType.HARDENED).get().defaultBlockState());
     }
     else if (Helpers.isBlock(state, Blocks.COBBLESTONE))
     {
         event.setNewState(TFCBlocks.ROCK_BLOCKS.get(net.dries007.tfc.common.blocks.rock.Rock.RHYOLITE).get(net.dries007.tfc.common.blocks.rock.Rock.BlockType.HARDENED).get().defaultBlockState());
     }
     else if (Helpers.isBlock(state, Blocks.BASALT))
     {
         event.setNewState(TFCBlocks.ROCK_BLOCKS.get(net.dries007.tfc.common.blocks.rock.Rock.BASALT).get(Rock.BlockType.HARDENED).get().defaultBlockState());
     }
 }

 public static void onFluidCreateSource(BlockEvent.CreateFluidSourceEvent event)
 {
     final LevelReader level = event.getWorld();
     final BlockPos pos = event.getPos();
     final BlockState state = event.getState();

     if (state.getBlock() instanceof AqueductBlock)
     {
         event.setResult(Event.Result.DENY); // Waterlogged aqueducts do not count as the source when creating source blocks
     }

     for (Direction direction : Direction.Plane.HORIZONTAL)
     {
         final BlockPos relPos = pos.relative(direction).above();
         final BlockState relState = level.getBlockState(relPos);
         if (relState.getBlock() instanceof SluiceBlock && !relState.getValue(SluiceBlock.UPPER) && relState.getValue(SluiceBlock.FACING) == direction.getOpposite())
         {
             event.setResult(Event.Result.DENY); // This block might be being fed by a sluice - so don't allow it to create more source blocks.
         }
     }
 }

 public static void onFireStart(StartFireEvent event)
 {
     Level level = event.getLevel();
     BlockPos pos = event.getPos();
     BlockState state = event.getState();
     Block block = state.getBlock();

     if ((block == TFCBlocks.FIREPIT.get() || block == TFCBlocks.POT.get() || block == TFCBlocks.GRILL.get()) && event.isStrong())
     {
         final BlockEntity entity = level.getBlockEntity(pos);
         if (entity instanceof AbstractFirepitBlockEntity<?> firepit && firepit.light(state))
         {
             event.setCanceled(true);
         }
     }
     else if (block == TFCBlocks.TORCH.get() || block == TFCBlocks.WALL_TORCH.get())
     {
         level.getBlockEntity(pos, TFCBlockEntities.TICK_COUNTER.get()).ifPresent(TickCounterBlockEntity::resetCounter);
         event.setCanceled(true);
     }
     else if (block == TFCBlocks.DEAD_TORCH.get())
     {
         level.setBlockAndUpdate(pos, TFCBlocks.TORCH.get().defaultBlockState());
         level.getBlockEntity(pos, TFCBlockEntities.TICK_COUNTER.get()).ifPresent(TickCounterBlockEntity::resetCounter);
         event.setCanceled(true);
     }
     else if (block == TFCBlocks.DEAD_WALL_TORCH.get())
     {
         level.setBlockAndUpdate(pos, TFCBlocks.WALL_TORCH.get().withPropertiesOf(state));
         level.getBlockEntity(pos, TFCBlockEntities.TICK_COUNTER.get()).ifPresent(TickCounterBlockEntity::resetCounter);
         event.setCanceled(true);
     }
     else if (block == TFCBlocks.LOG_PILE.get() && event.isStrong())
     {
         BurningLogPileBlock.tryLightLogPile(level, pos);
         event.setCanceled(true);
     }
     else if (block == TFCBlocks.PIT_KILN.get() && state.getValue(PitKilnBlock.STAGE) == 15 && event.isStrong())
     {
         if (level.getBlockEntity(pos) instanceof PitKilnBlockEntity kiln && kiln.tryLight())
         {
             event.setCanceled(true);
             event.setFireResult(StartFireEvent.FireResult.ALWAYS);
         }
     }
     else if (block == TFCBlocks.CHARCOAL_PILE.get() && state.getValue(CharcoalPileBlock.LAYERS) >= 7 && CharcoalForgeBlock.isValid(level, pos) && event.isStrong())
     {
         CharcoalForgeBlockEntity.createFromCharcoalPile(level, pos);
         event.setCanceled(true);
     }
     else if (block == TFCBlocks.CHARCOAL_FORGE.get() && CharcoalForgeBlock.isValid(level, pos) && event.isStrong())
     {
         final BlockEntity entity = level.getBlockEntity(pos);
         if (entity instanceof CharcoalForgeBlockEntity forge && forge.light(state))
         {
             event.setCanceled(true);
         }
     }
     else if (block == TFCBlocks.CRUCIBLE.get() && CharcoalForgeBlock.isValid(level, pos.below()) && event.isStrong())
     {
         final BlockEntity entity = level.getBlockEntity(pos.below());
         if (entity instanceof CharcoalForgeBlockEntity forge && forge.light(level.getBlockState(pos.below())))
         {
             event.setCanceled(true);
         }
     }
     else if (block == TFCBlocks.BLOOMERY.get() && !state.getValue(BloomeryBlock.LIT) && event.isStrong())
     {
         final BlockEntity entity = level.getBlockEntity(pos);
         if (entity instanceof BloomeryBlockEntity bloomery && bloomery.light(state))
         {
             event.setCanceled(true);
         }
     }
     else if (block == TFCBlocks.POWDERKEG.get() && state.getValue(PowderkegBlock.SEALED) && event.isStrong())
     {
         level.getBlockEntity(pos, TFCBlockEntities.POWDERKEG.get()).ifPresent(entity -> {
             entity.setLit(true, event.getPlayer());
             event.setCanceled(true);
         });
     }
     else if (block == TFCBlocks.BLAST_FURNACE.get() && !state.getValue(BlastFurnaceBlock.LIT) && event.isStrong())
     {
         level.getBlockEntity(pos, TFCBlockEntities.BLAST_FURNACE.get()).ifPresent(blastFurnace -> {
             if (blastFurnace.light(level, pos, state))
             {
                 event.setCanceled(true);
             }
         });
     }
     else if (block instanceof LampBlock)
     {
         if (!state.getValue(LampBlock.LIT))
         {
             level.getBlockEntity(pos, TFCBlockEntities.LAMP.get()).ifPresent(lamp -> {
                 if (lamp.getFuel() != null)
                 {
                     level.setBlock(pos, state.setValue(LampBlock.LIT, true), 3);
                     lamp.resetCounter();
                 }
             });
             event.setCanceled(true);
         }
     }
     else if (block instanceof TFCCandleBlock || block instanceof TFCCandleCakeBlock)
     {
         level.setBlock(pos, state.setValue(TFCCandleBlock.LIT, true), Block.UPDATE_ALL_IMMEDIATE);
         TickCounterBlockEntity.reset(level, pos);
         event.setCanceled(true);
     }
     else if (block == Blocks.CARVED_PUMPKIN || block == TFCBlocks.JACK_O_LANTERN.get())
     {
         level.setBlockAndUpdate(pos, Helpers.copyProperty(TFCBlocks.JACK_O_LANTERN.get().defaultBlockState(), state, HorizontalDirectionalBlock.FACING));
         TickCounterBlockEntity.reset(level, pos);
         event.setCanceled(true);
     }
     else if (block instanceof TntBlock tnt)
     {
         tnt.onCaughtFire(state, level, pos, event.getTargetedFace(), event.getPlayer());
         level.setBlock(pos, Blocks.AIR.defaultBlockState(), 11);
         event.setCanceled(true);
     }
 }

 public static void onProjectileImpact(ProjectileImpactEvent event)
 {
     if (!TFCConfig.SERVER.enableFireArrowSpreading.get()) return;
     Projectile projectile = event.getProjectile();
     HitResult result = event.getRayTraceResult();
     if (result.getType() == HitResult.Type.BLOCK && projectile.isOnFire())
     {
         BlockHitResult blockResult = (BlockHitResult) result;
         BlockPos pos = blockResult.getBlockPos();
         StartFireEvent.startFire(projectile.level, pos, projectile.level.getBlockState(pos), blockResult.getDirection(), null, ItemStack.EMPTY);
     }
 }

 public static void onPlayerTick(TickEvent.PlayerTickEvent event)
 {
     // When facing up in the rain, player slowly recovers thirst.
     final Player player = event.player;
     final Level level = player.getLevel();
     final float angle = Mth.wrapDegrees(player.getXRot()); // Copied from DebugScreenOverlay, which is the value in F3
     if (angle <= -80 && !level.isClientSide() && level.isRainingAt(player.eyeBlockPosition()) && player.getFoodData() instanceof TFCFoodData foodData)
     {
         foodData.addThirst(TFCConfig.SERVER.thirstGainedFromDrinkingInTheRain.get().floatValue());
     }
     if (!level.isClientSide() && !player.getAbilities().invulnerable && TFCConfig.SERVER.enableOverburdening.get() && level.getGameTime() % 20 == 0)
     {
         final int hugeHeavyCount = Helpers.countOverburdened(player.getInventory());
         if (hugeHeavyCount >= 1)
         {
             player.addEffect(Helpers.getExhausted(false));
         }
         if (hugeHeavyCount == 2)
         {
             player.addEffect(Helpers.getOverburdened(false));
         }
     }
 }

 public static void onEffectRemove(PotionEvent.PotionRemoveEvent event)
 {
     if (event.getEntityLiving() instanceof ServerPlayer player)
     {
         PacketHandler.send(PacketDistributor.PLAYER.with(() -> player), new EffectExpirePacket(event.getPotion()));
         if (event.getPotion() == TFCEffects.PINNED.get())
         {
             player.setForcedPose(null);
         }
     }
 }

 public static void onEffectExpire(PotionEvent.PotionExpiryEvent event)
 {
     final MobEffectInstance instance = event.getPotionEffect();
     if (instance != null && event.getEntityLiving() instanceof ServerPlayer player)
     {
         PacketHandler.send(PacketDistributor.PLAYER.with(() -> player), new EffectExpirePacket(instance.getEffect()));
         if (instance.getEffect() == TFCEffects.PINNED.get())
         {
             player.setForcedPose(null);
         }
     }
 }

 public static void onLivingJump(LivingEvent.LivingJumpEvent event)
 {
     LivingEntity entity = event.getEntityLiving();
     if (entity.hasEffect(TFCEffects.PINNED.get()))
     {
         entity.setDeltaMovement(0, 0, 0);
         entity.hasImpulse = false;
     }
 }

 /**
  * Apply modifications from damage types, player health, and forging bonus, before armor and absorption and other resources are consumed.
  */
 public static void onLivingHurt(LivingHurtEvent event)
 {
     float amount = event.getAmount();

     // Forging bonus
     final Entity attackerEntity = event.getSource().getEntity();
     if (attackerEntity instanceof LivingEntity livingEntity)
     {
         amount *= ForgingBonus.get(livingEntity.getMainHandItem()).damage();

         if (event.getEntityLiving() instanceof Player player)
         {
             Helpers.maybeDisableShield(livingEntity.getMainHandItem(), player.isUsingItem() ? player.getUseItem() : ItemStack.EMPTY, player, livingEntity);
         }
     }

     // Physical damage type
     amount *= PhysicalDamageType.calculateMultiplier(event.getSource(), event.getEntity());

     // Player health modifier
     if (event.getEntityLiving() instanceof Player player && player.getFoodData() instanceof TFCFoodData foodData)
     {
         amount /= foodData.getHealthModifier();
     }

     event.setAmount(amount);
 }

 public static void onShieldBlock(ShieldBlockEvent event)
 {
     float damageModifier = 1f;
     final Item useItem = event.getEntityLiving().getUseItem().getItem();
     if (event.getDamageSource().getDirectEntity() instanceof LivingEntity livingEntity && livingEntity.getMainHandItem().getItem() instanceof TieredItem attackWeapon)
     {
         if (useItem instanceof TieredItem shieldItem && TierSortingRegistry.getTiersLowerThan(attackWeapon.getTier()).contains(shieldItem.getTier()))
         {
             damageModifier = 0.3f; // shield is worse tier than the attack weapon!
         }
     }
     if (useItem.equals(Items.SHIELD))
     {
         damageModifier = 0.25f; // wooden shield is bad
     }

     event.setBlockedDamage(event.getOriginalBlockedDamage() * damageModifier);
 }

 /**
  * This prevents vanilla mobs from spawning either at all or on the surface.
  */
 public static void onLivingSpawnCheck(LivingSpawnEvent.CheckSpawn event)
 {
     final LivingEntity entity = event.getEntityLiving();
     final LevelAccessor level = event.getWorld();
     final MobSpawnType spawn = event.getSpawnReason();
     // we only care about "natural" spawns
     if (spawn == MobSpawnType.NATURAL || spawn == MobSpawnType.CHUNK_GENERATION || spawn == MobSpawnType.REINFORCEMENT)
     {
         if (Helpers.isEntity(entity, TFCTags.Entities.VANILLA_MONSTERS))
         {
             if (TFCConfig.SERVER.enableVanillaMonsters.get())
             {
                 if (!TFCConfig.SERVER.enableVanillaMonstersOnSurface.get())
                 {
                     final BlockPos pos = entity.blockPosition();
                     if (entity.getType() != EntityType.SLIME && level.getRawBrightness(pos, 0) != 0)
                     {
                         event.setResult(Event.Result.DENY);
                     }
                     else if (level.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) <= pos.getY())
                     {
                         event.setResult(Event.Result.DENY);
                     }
                     else if (!Helpers.isBlock(level.getBlockState(pos.below()), TFCTags.Blocks.MONSTER_SPAWNS_ON))
                     {
                         event.setResult(Event.Result.DENY);
                     }
                 }
             }
             else
             {
                 event.setResult(Event.Result.DENY);
             }
         }
     }
 }

 /**
  * Applies multiple effect for entities joining the world:
  * <p>
  * - Set a very short lifespan to item entities that are cool-able. This causes ItemExpireEvent to fire at regular intervals
  * - Causes lightning bolts to strip nearby logs
  * - Prevents skeleton trap horses from spawning (see {@link ServerLevel#tickChunk(LevelChunk, int)}
  * - Prevents some categories of mobs from spawning. Some can't be done in {@link LivingSpawnEvent.CheckSpawn} because Forge does not always fire it.
  */
 public static void onEntityJoinWorld(EntityJoinWorldEvent event)
 {
     if (event.loadedFromDisk())
     {
         // This event is used for modifications to entity spawning, so we shouldn't apply any effects for entities that already exist in the world.
         return;
     }

     final Level level = event.getWorld();

     Entity entity = event.getEntity();
     if (entity instanceof ItemEntity itemEntity && !level.isClientSide && TFCConfig.SERVER.coolHotItemEntities.get())
     {
         final ItemStack item = itemEntity.getItem();
         item.getCapability(HeatCapability.CAPABILITY).ifPresent(cap -> {
             if (cap.getTemperature() > 0f)
             {
                 itemEntity.lifespan = TFCConfig.SERVER.ticksBeforeItemCool.get();
             }
         });
     }
     else if (entity instanceof LightningBolt lightning && !level.isClientSide && !event.isCanceled())
     {
         if (!TFCConfig.SERVER.enableLightning.get())
         {
             event.setCanceled(true);
             return;
         }
         if (TFCConfig.SERVER.enableLightningStrippingLogs.get() && level.random.nextFloat() < 0.2f)
         {
             final BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
             BlockPos pos = lightning.blockPosition();
             for (int x = -5; x <= 5; x++)
             {
                 for (int y = -5; y <= 5; y++)
                 {
                     for (int z = -5; z <= 5; z++)
                     {
                         if (level.random.nextInt(3) == 0 && x * x + y * y + z * z <= 25)
                         {
                             mutable.setWithOffset(pos, x, y, z);
                             BlockState state = level.getBlockState(mutable);
                             BlockState modified = state.getToolModifiedState(new UseOnContext(level, null, InteractionHand.MAIN_HAND, new ItemStack(Items.DIAMOND_AXE), new BlockHitResult(Vec3.atBottomCenterOf(mutable), Direction.DOWN, mutable, false)), ToolActions.AXE_STRIP, true);
                             if (modified != null)
                             {
                                 level.setBlockAndUpdate(mutable, modified);
                             }
                         }
                     }
                 }
             }
         }

     }
     if (entity instanceof Monster monster && !TFCConfig.SERVER.enableVanillaMobsSpawningWithVanillaEquipment.get())
     {
         if (Helpers.isItem(monster.getItemInHand(InteractionHand.MAIN_HAND), TFCTags.Items.DISABLED_MONSTER_HELD_ITEMS))
         {
             monster.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY);
         }
         if (Helpers.isItem(monster.getItemInHand(InteractionHand.OFF_HAND), TFCTags.Items.DISABLED_MONSTER_HELD_ITEMS))
         {
             monster.setItemInHand(InteractionHand.OFF_HAND, ItemStack.EMPTY);
         }
     }

     if (!TFCConfig.SERVER.enableChickenJockies.get())
     {
         // Need to prevent both the chicken and the jockey from spawning
         if ((entity instanceof Chicken chicken && chicken.isChickenJockey)
             || (entity.getVehicle() != null && entity.getVehicle() instanceof Chicken vehicleChicken && vehicleChicken.isChickenJockey))
         {
             event.setCanceled(true);
         }
     }

     if (entity.getType() == EntityType.SKELETON)
     {
         entity.setItemSlot(EquipmentSlot.MAINHAND, Helpers.getRandomElement(ForgeRegistries.ITEMS, TFCTags.Items.SKELETON_WEAPONS, entity.level.getRandom()).orElse(Items.BOW).getDefaultInstance());
     }
     else if (entity.getType() == EntityType.SKELETON_HORSE && !TFCConfig.SERVER.enableVanillaSkeletonHorseSpawning.get())
     {
         event.setCanceled(true);
     }
     else if ((entity instanceof IronGolem || entity instanceof SnowGolem) && !TFCConfig.SERVER.enableVanillaGolems.get())
     {
         event.setCanceled(true);
     }
 }

 /**
  * If the item is heated, we check for blocks below and within that would cause it to cool.
  * Since we don't want the item to actually expire, we set the expiry time to a small number that allows us to revisit the same code soon.
  * <p>
  * By cancelling the event, we guarantee that the item will not actually expire.
  */
 public static void onItemExpire(ItemExpireEvent event)
 {
     if (!TFCConfig.SERVER.coolHotItemEntities.get()) return;
     final ItemEntity entity = event.getEntityItem();
     final ServerLevel level = (ServerLevel) entity.getLevel();
     final ItemStack stack = entity.getItem();
     final BlockPos pos = entity.blockPosition();

     stack.getCapability(HeatCapability.CAPABILITY).ifPresent(heat -> {
         final int lifespan = stack.getItem().getEntityLifespan(stack, level);
         if (entity.lifespan >= lifespan)
             return; // the case where the item has been sitting out for longer than the lifespan. So it should be removed by the game.

         final float itemTemp = heat.getTemperature();
         if (itemTemp > 0f)
         {
             float coolAmount = 0;
             final BlockState state = level.getBlockState(pos);
             final FluidState fluid = level.getFluidState(pos);
             if (Helpers.isFluid(fluid, FluidTags.WATER))
             {
                 coolAmount = 50f;
                 if (level.random.nextFloat() < 0.001F && state.getBlock() == Blocks.WATER)
                 {
                     level.setBlockAndUpdate(pos, Blocks.AIR.defaultBlockState());
                 }
             }
             else if (Helpers.isBlock(state, Blocks.SNOW))
             {
                 coolAmount = 70f;
                 if (level.random.nextFloat() < 0.1F)
                 {
                     final int layers = state.getValue(SnowLayerBlock.LAYERS);
                     if (layers > 1)
                     {
                         level.destroyBlock(pos, false);
                         level.setBlockAndUpdate(pos, state.setValue(SnowLayerBlock.LAYERS, layers - 1));
                     }
                     else
                     {
                         level.setBlockAndUpdate(pos, Blocks.AIR.defaultBlockState());
                     }
                 }
             }
             else
             {
                 final BlockPos belowPos = pos.below();
                 final BlockState belowState = level.getBlockState(belowPos);
                 if (Helpers.isBlock(belowState, Blocks.SNOW_BLOCK))
                 {
                     coolAmount = 75f;
                     if (level.random.nextFloat() < 0.1F)
                     {
                         level.setBlockAndUpdate(belowPos, Blocks.SNOW.defaultBlockState().setValue(SnowLayerBlock.LAYERS, 7));
                     }
                 }
                 else if (LegacyMaterials.isMeltyIce(belowState))
                 {
                     coolAmount = 100f;
                     if (level.random.nextFloat() < 0.01F)
                     {
                         level.setBlockAndUpdate(belowPos, Helpers.isBlock(belowState, TFCBlocks.SEA_ICE.get()) ? TFCBlocks.SALT_WATER.get().defaultBlockState() : Blocks.WATER.defaultBlockState());
                     }
                 }
                 else if (LegacyMaterials.isSolidIce(belowState))
                 {
                     coolAmount = 125f;
                     if (level.random.nextFloat() < 0.005F)
                     {
                         level.setBlockAndUpdate(belowPos, Blocks.WATER.defaultBlockState());
                     }
                 }
             }

             if (coolAmount > 0f)
             {
                 heat.setTemperature(Math.max(0f, heat.getTemperature() - coolAmount));
                 Helpers.playSound(level, pos, TFCSounds.ITEM_COOL.get());
                 level.sendParticles(ParticleTypes.SMOKE, entity.getX(), entity.getY(), entity.getZ(), 1, 0D, 0D, 0D, 1f);
             }
             event.setExtraLife(heat.getTemperature() == 0f ? lifespan : TFCConfig.SERVER.ticksBeforeItemCool.get());
             //entity.setNoPickUpDelay();
         }
         else
         {
             event.setExtraLife(lifespan);
         }
         event.setCanceled(true);
     });
 }

 public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event)
 {    
     onNewPlayerInWorld((ServerPlayer) event.getPlayer());

 }

 public static void onPlayerRespawn(PlayerEvent.PlayerRespawnEvent event)
 { 
     onNewPlayerInWorld((ServerPlayer) event.getPlayer());
 }

 public static void onPlayerDeath(PlayerEvent.Clone event) {
     ServerPlayer oldPlayer = (ServerPlayer) event.getOriginal();
     ServerPlayer newPlayer = (ServerPlayer) event.getPlayer();


     if (event.isWasDeath()) {
         TFCFoodData.restoreFoodStatsAfterDeath(oldPlayer, newPlayer);
     }

 }

 public static void onPlayerDeathSaveData(LivingDeathEvent event) {
     if (event.getEntity() instanceof ServerPlayer serverPlayer) {
         savePlayerData(serverPlayer);
     }
 }
 
 public static void onPlayerChangeDimension(PlayerEvent.PlayerChangedDimensionEvent event)
 {
     onNewPlayerInWorld((ServerPlayer) event.getPlayer());    }

 /**
  * Common handling for creating new player entities. Called through logging in, changing dimension, and respawning.
  */
 private static void onNewPlayerInWorld(Player player)
 {
     if (player instanceof ServerPlayer serverPlayer)
     {
         TFCFoodData.replaceFoodStats(serverPlayer);

         serverPlayer.level.getCapability(WorldTrackerCapability.CAPABILITY).ifPresent(c -> c.syncTo(serverPlayer));
         serverPlayer.getCapability(PlayerDataCapability.CAPABILITY).ifPresent(PlayerData::sync);

         final ClimateModel model = Climate.model(serverPlayer.level);
         PacketHandler.send(PacketDistributor.PLAYER.with(() -> serverPlayer), new UpdateClimateModelPacket(model));
     }
 }

private static void savePlayerData(ServerPlayer player) {
try {
    CompoundTag playerData = player.saveWithoutId(new CompoundTag());

    // Сериализация данных игрока
    if (player.getFoodData() instanceof TFCFoodData oldStats) {
        playerData.put("TFCFoodData", oldStats.serializeToPlayerData());
    }

    Path filePath = player.getServer().getWorldPath(LevelResource.PLAYER_DATA_DIR).resolve(player.getUUID().toString() + ".dat");

    // Сохранение данных игрока в файл
    NbtIo.writeCompressed(playerData, filePath.toFile());

    System.out.println("Player data saved successfully for: " + player.getName().getString());
} catch (Exception e) {
    System.err.println("Failed to save player data for: " + player.getName().getString());
    e.printStackTrace();
}

}

 public static void onServerChat(ServerChatEvent event)
 {
     // Apply intoxication after six hours
     final long intoxicatedTicks = event.getPlayer().getCapability(PlayerDataCapability.CAPABILITY).map(p -> p.getIntoxicatedTicks(event.getPlayer().getLevel().isClientSide()) - 6 * ICalendar.TICKS_IN_HOUR).orElse(0L);
     if (intoxicatedTicks > 0)
     {
         final float intoxicationChance = Mth.clamp((float) (intoxicatedTicks - 6 * ICalendar.TICKS_IN_HOUR) / PlayerData.MAX_INTOXICATED_TICKS, 0, 0.7f);
         final Random random = event.getPlayer().getRandom();
         final String originalMessage = event.getMessage();
         final String[] words = originalMessage.split(" ");
         for (int i = 0; i < words.length; i++)
         {
             String word = words[i];
             if (word.length() == 0)
             {
                 continue;
             }

             // Swap two letters
             if (random.nextFloat() < intoxicationChance && word.length() >= 2)
             {
                 int pos = random.nextInt(word.length() - 1);
                 word = word.substring(0, pos) + word.charAt(pos + 1) + word.charAt(pos) + word.substring(pos + 2);
             }

             // Repeat / slur letters
             if (random.nextFloat() < intoxicationChance)
             {
                 int pos = random.nextInt(word.length());
                 char repeat = word.charAt(pos);
                 int amount = 1 + random.nextInt(3);
                 word = word.substring(0, pos) + new String(new char[amount]).replace('\0', repeat) + (pos + 1 < word.length() ? word.substring(pos + 1) : "");
             }

             // Add additional letters
             if (random.nextFloat() < intoxicationChance)
             {
                 int pos = random.nextInt(word.length());
                 char replacement = ALPHABET.charAt(random.nextInt(ALPHABET.length()));
                 if (Character.isUpperCase(word.charAt(random.nextInt(word.length()))))
                 {
                     replacement = Character.toUpperCase(replacement);
                 }
                 word = word.substring(0, pos) + replacement + (pos + 1 < word.length() ? word.substring(pos + 1) : "");
             }

             words[i] = word;
         }
         event.setComponent(Helpers.translatable("<" + event.getUsername() + "> " + String.join(" ", words)));
     }
 }

 public static void onPlayerRightClickBlock(PlayerInteractEvent.RightClickBlock event)
 {
     final Level level = event.getWorld();
     final BlockState state = level.getBlockState(event.getPos());
     final ItemStack stack = event.getItemStack();

     if (Helpers.isItem(stack, Items.WRITABLE_BOOK) || Helpers.isItem(stack, Items.WRITTEN_BOOK))
     {
         // Lecterns, we only do a modification for known items *and* known blocks, so there's no need to simulate any other interaction
         if (state.getBlock() instanceof TFCLecternBlock && LecternBlock.tryPlaceBook(event.getPlayer(), level, event.getPos(), state, stack))
         {
             event.setCanceled(true);
             event.setCancellationResult(InteractionResult.SUCCESS);
         }
     }

     // need position access to set smelled pos properly, so we cannot use container menus here.
     if (level.getBlockEntity(event.getPos()) instanceof BaseContainerBlockEntity container && container.canOpen(event.getPlayer()) && container instanceof PestContainer test && test.canBeInfested())
     {
         int infestation = 0;
         for (int i = 0; i < container.getContainerSize(); i++)
         {
             if (Helpers.isItem(container.getItem(i), TFCTags.Items.FOODS))
             {
                 infestation++;
                 if (infestation == 5)
                 {
                     break;
                 }
             }
         }
         Helpers.tickInfestation(level, container.getBlockPos(), infestation, event.getPlayer());
     }
 }

 public static void onPlayerRightClickBlockLowestPriority(PlayerInteractEvent.RightClickBlock event)
 {
     final Level level = event.getWorld();
     final BlockState state = level.getBlockState(event.getPos());
     final ItemStack stack = event.getItemStack();

     if (!event.isCanceled() && event.getHand() == InteractionHand.MAIN_HAND && stack.isEmpty())
     {
         // For drinking, when we have an empty hand, we want to first try and interact with a block.
         // We can't use interaction manager, as vanilla won't try and call onItemUse for empty stacks.
         // We do this on lowest priority, because we want other modifications to fire *first* - for instance, if a mod does a block interaction on this event, at normal priority
         // Thus if we get here, we're fairly certain another mod doesn't need to use this, so we can check the block `use()` method, and then if no, we can attempt drinking.
         // Possible issues:
         // - Right-click a chest underwater -> it should open the chest, not drink
         // - Try and remove the filter from a Create 'Basin', by right-clicking with an empty hand (create cancels this event)
         final InteractionResult useBlockResult = state.use(level, event.getPlayer(), event.getHand(), event.getHitVec());
         if (useBlockResult.consumesAction())
         {
             if (event.getPlayer() instanceof ServerPlayer serverPlayer)
             {
                 CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger(serverPlayer, event.getPos(), stack);
             }
             event.setCanceled(true);
             event.setCancellationResult(useBlockResult);
         }
         else
         {
             // If we haven't already interacted with a block, then we can attempt drinking.
             final InteractionResult result = Drinkable.attemptDrink(level, event.getPlayer(), true);
             if (result != InteractionResult.PASS)
             {
                 event.setCanceled(true);
                 event.setCancellationResult(result);
             }
         }
     }

     // Some blocks have interactions that respect sneaking, both with items in hand and not
     // These need to be able to interact, regardless of if an item has sneakBypassesUse set
     // So, we have to explicitly allow the Block.use() interaction for these blocks.
     //
     // This happens at lowest priority, regardless if the event was cancelled, as we don't want this `ALLOW` to be overwritten.
     // Otherwise it breaks anvil shift interactions, see: TerraFirmaCraft#2254
     if (state.getBlock() instanceof AnvilBlock || state.getBlock() instanceof RockAnvilBlock || Fertilizer.get(stack) != null || (state.getBlock() instanceof BarrelBlock && !state.getValue(BarrelBlock.RACK) && state.getValue(BarrelBlock.FACING).getAxis().isHorizontal() && stack.getItem() == TFCItems.BARREL_RACK.get()))
     {
         event.setUseBlock(Event.Result.ALLOW);
     }
 }

 public static void onPlayerRightClickItem(PlayerInteractEvent.RightClickItem event)
 {
     final UseOnContext context = new UseOnContext(event.getPlayer(), event.getHand(), FAKE_MISS);
     InteractionManager.onItemUse(event.getItemStack(), context, true).ifPresent(result -> {
         event.setCanceled(true);
         event.setCancellationResult(result);
     });
 }

 public static void onPlayerRightClickEmpty(PlayerInteractEvent.RightClickEmpty event)
 {
     if (event.getHand() == InteractionHand.MAIN_HAND && event.getItemStack().isEmpty())
     {
         // Cannot be cancelled, only fired on client.
         InteractionResult result = Drinkable.attemptDrink(event.getWorld(), event.getPlayer(), false);
         if (result == InteractionResult.SUCCESS)
         {
             PacketHandler.send(PacketDistributor.SERVER.noArg(), new PlayerDrinkPacket());
         }
     }
 }

 public static void onItemUseFinish(LivingEntityUseItemEvent.Finish event)
 {
     final IFood food = event.getItem().getCapability(FoodCapability.CAPABILITY).resolve().orElse(null);
     if (food instanceof DynamicBowlFood.DynamicBowlHandler)
     {
         event.setResultStack(DynamicBowlFood.DynamicBowlHandler.onItemUse(event.getItem(), event.getResultStack(), event.getEntityLiving()));
     }
 }

 public static void addReloadListeners(AddReloadListenerEvent event)
 {
     // Alloy recipes are loaded as part of recipes, but have a hard dependency on metals.
     // So, we hack internal resource lists in order to stick metals before recipes.
     // see ReloadableServerResourcesMixin

     // All other resource reload listeners can be inserted after recipes.
     event.addListener(Fuel.MANAGER);
     event.addListener(Drinkable.MANAGER);
     event.addListener(Support.MANAGER);
     event.addListener(Pannable.MANAGER);
     event.addListener(Sluiceable.MANAGER);
     event.addListener(LampFuel.MANAGER);
     event.addListener(Fertilizer.MANAGER);
     event.addListener(ItemSizeManager.MANAGER);
     event.addListener(ClimateRange.MANAGER);
     event.addListener(Fauna.MANAGER);
     event.addListener(HeatCapability.MANAGER);
     event.addListener(FoodCapability.MANAGER);
     event.addListener(EntityDamageResistance.MANAGER);
     event.addListener(ItemDamageResistance.MANAGER);

     // In addition, we capture the recipe manager here
     Helpers.setCachedRecipeManager(event.getServerResources().getRecipeManager());
 }

 public static void onDataPackSync(OnDatapackSyncEvent event)
 {
     // Sync managers
     final ServerPlayer player = event.getPlayer();
     final PacketDistributor.PacketTarget target = player == null ? PacketDistributor.ALL.noArg() : PacketDistributor.PLAYER.with(() -> player);

     PacketHandler.send(target, Metal.MANAGER.createSyncPacket());
     PacketHandler.send(target, Fuel.MANAGER.createSyncPacket());
     PacketHandler.send(target, Fertilizer.MANAGER.createSyncPacket());
     PacketHandler.send(target, ItemDamageResistance.MANAGER.createSyncPacket());
     PacketHandler.send(target, HeatCapability.MANAGER.createSyncPacket());
     PacketHandler.send(target, FoodCapability.MANAGER.createSyncPacket());
     PacketHandler.send(target, ItemSizeManager.MANAGER.createSyncPacket());
     PacketHandler.send(target, ClimateRange.MANAGER.createSyncPacket());
     PacketHandler.send(target, Drinkable.MANAGER.createSyncPacket());
     PacketHandler.send(target, LampFuel.MANAGER.createSyncPacket());
     PacketHandler.send(target, Pannable.MANAGER.createSyncPacket());
     PacketHandler.send(target, Sluiceable.MANAGER.createSyncPacket());
     PacketHandler.send(target, Support.MANAGER.createSyncPacket());
 }

 /**
  * This is when tags are safe to be loaded, so we can do post reload actions that involve querying ingredients.
  * It is fired on both logical server and client after resources are reloaded (or, sent from server).
  * In addition, during the first load on a server in {@link net.minecraft.server.Main}, where {@link net.minecraft.server.WorldStem#load(WorldStem.InitConfig, WorldStem.DataPackConfigSupplier, WorldStem.WorldDataSupplier, Executor, Executor)} is invoked, the server won't exist yet at all.
  * In that case, we need to rely on the fact that {@link AddReloadListenerEvent} will be fired before that point, and we can capture the server's recipe manager there.
  */
 @SuppressWarnings({"unchecked", "rawtypes"})
 public static void onTagsUpdated(TagsUpdatedEvent event)
 {
     // First, reload all caches
     final RecipeManager manager = Helpers.getUnsafeRecipeManager();
     IndirectHashCollection.reloadAllCaches(manager);

     // Then apply post reload actions which may query the cache
     Support.updateMaximumSupportRange();
     Metal.updateMetalFluidMap();
     ItemSizeManager.applyItemStackSizeOverrides();
     FoodCapability.markRecipeOutputsAsNonDecaying(manager);

     if (TFCConfig.COMMON.enableDatapackTests.get())
     {
         SelfTests.validateDatapacks(manager);
     }

     if (event.getUpdateCause() == TagsUpdatedEvent.UpdateCause.CLIENT_PACKET_RECEIVED)
     {
         ClientHelpers.updateSearchTrees();
     }

     final RecipeManagerAccessor accessor = (RecipeManagerAccessor) manager;
     for (RecipeType<?> type : Registry.RECIPE_TYPE)
     {
         LOGGER.info("Loaded {} recipes of type {}", accessor.invoke$byType((RecipeType) type).size(), Registry.RECIPE_TYPE.getKey(type));
     }
 }

 /**
  * Deny all traditional uses of bone meal directly to grow crops.
  * Fertilizer is used as a replacement.
  */
 public static void onBoneMeal(BonemealEvent event)
 {
     if (!TFCConfig.SERVER.enableVanillaBonemeal.get())
     {
         event.setResult(Event.Result.DENY);
         event.setCanceled(true);
     }
 }

 public static void onSelectClimateModel(SelectClimateModelEvent event)
 {
     final ServerLevel level = event.level();
     if (event.level().dimension() == Level.OVERWORLD && level.getChunkSource().getGenerator() instanceof ChunkGeneratorExtension)
     {
         // TFC decides to select the climate model for the overworld, if we're using a TFC enabled chunk generator
         event.setModel(new OverworldClimateModel());
     }
 }

 public static void onEntityInteract(PlayerInteractEvent.EntityInteract event)
 {
     final Player player = event.getPlayer();
     if (event.getTarget().getType() == EntityType.MINECART && event.getTarget() instanceof Minecart oldCart && player.isShiftKeyDown() && player.isSecondaryUseActive())
     {
         ItemStack held = player.getItemInHand(event.getHand());
         if (held.getItem() instanceof BlockItem bi && Helpers.isBlock(bi.getBlock(), TFCTags.Blocks.MINECART_HOLDABLE))
         {
             final ItemStack holdingItem = held.split(1);
             if (!player.level.isClientSide)
             {
                 final HoldingMinecart minecart = new HoldingMinecart(player.level, oldCart.getX(), oldCart.getY(), oldCart.getZ());
                 HoldingMinecart.copyMinecart(oldCart, minecart);
                 minecart.setHoldItem(holdingItem);
                 oldCart.discard();
                 player.level.addFreshEntity(minecart);
             }
             event.setCancellationResult(InteractionResult.SUCCESS);
         }
     }
 }

 public static void onMount(EntityMountEvent event)
 {
     if (event.getEntityBeingMounted() instanceof Boat && event.getEntityMounting() instanceof Predator)
     {
         event.setCanceled(true);
     }
 }

 public static void onAnimalTame(AnimalTameEvent event)
 {
     if (Helpers.isEntity(event.getEntity(), TFCTags.Entities.HORSES))
     {
         event.setCanceled(true); // cancel vanilla taming methods
     }
 }

 public static void onContainerOpen(PlayerContainerEvent.Open event)
 {
     if (event.getContainer() instanceof BlockEntityContainer<?> container && event.getContainer() instanceof PestContainer test && test.canBeInfested())
     {
         final Player player = event.getPlayer();
         final Level level = player.level;
         if (level.isClientSide) return;
         int amount = 0;
         if (TFCConfig.SERVER.enableInfestations.get())
         {
             for (Slot slot : container.slots)
             {
                 if (container.typeOf(slot.index) == Container.IndexType.CONTAINER && Helpers.isItem(slot.getItem(), TFCTags.Items.FOODS))
                 {
                     amount++;
                     if (amount == 5)
                     {
                         break;
                     }
                 }
             }
         }
         Helpers.tickInfestation(level, container.getBlockEntity().getBlockPos(), amount, player);
     }
 }

 public static void onCropsGrow(BlockEvent.CropGrowEvent.Pre event)
 {
     final BlockState state = event.getState();
     final LevelAccessor level = event.getWorld();
     if (state.getBlock() instanceof BambooBlock)
     {
         if (level instanceof ServerLevel server && server.random.nextFloat() > TFCConfig.SERVER.plantLongGrowthChance.get())
         {
             event.setResult(Event.Result.DENY);
         }
     }
 }

} `

fix.zip

Timemix avatar Jul 11 '24 11:07 Timemix