diff --git a/coreSubProjects b/coreSubProjects index e68d0d5c4..301cce3d1 160000 --- a/coreSubProjects +++ b/coreSubProjects @@ -1 +1 @@ -Subproject commit e68d0d5c45498bedbe6c1adba8d27f3b552d12e6 +Subproject commit 301cce3d119e39c3902faf367cb55c71b7180dc0 diff --git a/fabric/src/main/java/com/seibel/distanthorizons/fabric/FabricClientProxy.java b/fabric/src/main/java/com/seibel/distanthorizons/fabric/FabricClientProxy.java index 792992a09..e3f3c35ef 100644 --- a/fabric/src/main/java/com/seibel/distanthorizons/fabric/FabricClientProxy.java +++ b/fabric/src/main/java/com/seibel/distanthorizons/fabric/FabricClientProxy.java @@ -117,8 +117,11 @@ public class FabricClientProxy implements AbstractModInitializer.IEventProxy // ClientChunkLoadEvent ClientChunkEvents.CHUNK_LOAD.register((level, chunk) -> { - IClientLevelWrapper wrappedLevel = ClientLevelWrapper.getWrapper(level); - SharedApi.INSTANCE.chunkLoadEvent(new ChunkWrapper(chunk, level, wrappedLevel), wrappedLevel); + if (MC.clientConnectedToDedicatedServer()) + { + IClientLevelWrapper wrappedLevel = ClientLevelWrapper.getWrapper(level); + SharedApi.INSTANCE.chunkLoadEvent(new ChunkWrapper(chunk, level, wrappedLevel), wrappedLevel); + } }); // (kinda) block break event @@ -200,14 +203,6 @@ public class FabricClientProxy implements AbstractModInitializer.IEventProxy }); - // Client Chunk Save - ClientChunkEvents.CHUNK_UNLOAD.register((level, chunk) -> - { - IClientLevelWrapper wrappedLevel = ClientLevelWrapper.getWrapper(level); - SharedApi.INSTANCE.chunkUnloadEvent(new ChunkWrapper(chunk, level, wrappedLevel), wrappedLevel); - }); - - //==============// // render event // diff --git a/fabric/src/main/java/com/seibel/distanthorizons/fabric/mixins/server/MixinChunkMap.java b/fabric/src/main/java/com/seibel/distanthorizons/fabric/mixins/server/MixinChunkMap.java index ffef0238c..add9abccc 100644 --- a/fabric/src/main/java/com/seibel/distanthorizons/fabric/mixins/server/MixinChunkMap.java +++ b/fabric/src/main/java/com/seibel/distanthorizons/fabric/mixins/server/MixinChunkMap.java @@ -3,6 +3,7 @@ package com.seibel.distanthorizons.fabric.mixins.server; import com.seibel.distanthorizons.common.wrappers.chunk.ChunkWrapper; import com.seibel.distanthorizons.common.wrappers.world.ServerLevelWrapper; import com.seibel.distanthorizons.core.api.internal.ServerApi; +import com.seibel.distanthorizons.core.api.internal.SharedApi; import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.chunk.ChunkAccess; @@ -83,10 +84,13 @@ public class MixinChunkMap - ServerApi.INSTANCE.serverChunkSaveEvent( - new ChunkWrapper(chunk, this.level, ServerLevelWrapper.getWrapper(this.level)), - ServerLevelWrapper.getWrapper(this.level) - ); + if (!SharedApi.isChunkAtBlockPosAlreadyUpdating(chunk.getPos().getWorldPosition().getX(), chunk.getPos().getWorldPosition().getZ()) ) + { + ServerApi.INSTANCE.serverChunkSaveEvent( + new ChunkWrapper(chunk, this.level, ServerLevelWrapper.getWrapper(this.level)), + ServerLevelWrapper.getWrapper(this.level) + ); + } } } diff --git a/forge/src/main/java/com/seibel/distanthorizons/forge/ForgeClientProxy.java b/forge/src/main/java/com/seibel/distanthorizons/forge/ForgeClientProxy.java index 6784ad8e6..252d1fbdf 100644 --- a/forge/src/main/java/com/seibel/distanthorizons/forge/ForgeClientProxy.java +++ b/forge/src/main/java/com/seibel/distanthorizons/forge/ForgeClientProxy.java @@ -176,53 +176,59 @@ public class ForgeClientProxy implements AbstractModInitializer.IEventProxy @SubscribeEvent public void rightClickBlockEvent(PlayerInteractEvent.RightClickBlock event) { - if (SharedApi.isChunkAtBlockPosAlreadyUpdating(event.getPos().getX(), event.getPos().getZ())) + if (MC.clientConnectedToDedicatedServer()) { - return; - } - - //LOGGER.trace("interact or block place event at blockPos: " + event.getPos()); - - #if MC_VER < MC_1_19_2 - LevelAccessor level = event.getWorld(); - #else - LevelAccessor level = event.getLevel(); - #endif - - ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor(); - if (executor != null) - { - executor.execute(() -> + if (SharedApi.isChunkAtBlockPosAlreadyUpdating(event.getPos().getX(), event.getPos().getZ())) { - ChunkAccess chunk = level.getChunk(event.getPos()); - this.onBlockChangeEvent(level, chunk); - }); + return; + } + + //LOGGER.trace("interact or block place event at blockPos: " + event.getPos()); + + #if MC_VER < MC_1_19_2 + LevelAccessor level = event.getWorld(); + #else + LevelAccessor level = event.getLevel(); + #endif + + ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor(); + if (executor != null) + { + executor.execute(() -> + { + ChunkAccess chunk = level.getChunk(event.getPos()); + this.onBlockChangeEvent(level, chunk); + }); + } } } @SubscribeEvent public void leftClickBlockEvent(PlayerInteractEvent.LeftClickBlock event) { - if (SharedApi.isChunkAtBlockPosAlreadyUpdating(event.getPos().getX(), event.getPos().getZ())) + if (MC.clientConnectedToDedicatedServer()) { - return; - } - - //LOGGER.trace("break or block attack at blockPos: " + event.getPos()); - - #if MC_VER < MC_1_19_2 - LevelAccessor level = event.getWorld(); - #else - LevelAccessor level = event.getLevel(); - #endif - - ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor(); - if (executor != null) - { - executor.execute(() -> + if (SharedApi.isChunkAtBlockPosAlreadyUpdating(event.getPos().getX(), event.getPos().getZ())) { - ChunkAccess chunk = level.getChunk(event.getPos()); - this.onBlockChangeEvent(level, chunk); - }); + return; + } + + //LOGGER.trace("break or block attack at blockPos: " + event.getPos()); + + #if MC_VER < MC_1_19_2 + LevelAccessor level = event.getWorld(); + #else + LevelAccessor level = event.getLevel(); + #endif + + ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor(); + if (executor != null) + { + executor.execute(() -> + { + ChunkAccess chunk = level.getChunk(event.getPos()); + this.onBlockChangeEvent(level, chunk); + }); + } } } private void onBlockChangeEvent(LevelAccessor level, ChunkAccess chunk) @@ -230,21 +236,16 @@ public class ForgeClientProxy implements AbstractModInitializer.IEventProxy ILevelWrapper wrappedLevel = ProxyUtil.getLevelWrapper(level); SharedApi.INSTANCE.chunkBlockChangedEvent(new ChunkWrapper(chunk, level, wrappedLevel), wrappedLevel); } - - + @SubscribeEvent public void clientChunkLoadEvent(ChunkEvent.Load event) { - ILevelWrapper wrappedLevel = ProxyUtil.getLevelWrapper(GetEventLevel(event)); - IChunkWrapper chunk = new ChunkWrapper(event.getChunk(), GetEventLevel(event), wrappedLevel); - SharedApi.INSTANCE.chunkLoadEvent(chunk, wrappedLevel); - } - @SubscribeEvent - public void clientChunkUnloadEvent(ChunkEvent.Unload event) - { - ILevelWrapper wrappedLevel = ProxyUtil.getLevelWrapper(GetEventLevel(event)); - IChunkWrapper chunk = new ChunkWrapper(event.getChunk(), GetEventLevel(event), wrappedLevel); - SharedApi.INSTANCE.chunkUnloadEvent(chunk, wrappedLevel); + if (MC.clientConnectedToDedicatedServer()) + { + ILevelWrapper wrappedLevel = ProxyUtil.getLevelWrapper(GetEventLevel(event)); + IChunkWrapper chunk = new ChunkWrapper(event.getChunk(), GetEventLevel(event), wrappedLevel); + SharedApi.INSTANCE.chunkLoadEvent(chunk, wrappedLevel); + } } diff --git a/forge/src/main/java/com/seibel/distanthorizons/forge/ForgeServerProxy.java b/forge/src/main/java/com/seibel/distanthorizons/forge/ForgeServerProxy.java index 0c9521e0c..1be3b415b 100644 --- a/forge/src/main/java/com/seibel/distanthorizons/forge/ForgeServerProxy.java +++ b/forge/src/main/java/com/seibel/distanthorizons/forge/ForgeServerProxy.java @@ -137,14 +137,6 @@ public class ForgeServerProxy implements AbstractModInitializer.IEventProxy IChunkWrapper chunk = new ChunkWrapper(event.getChunk(), GetEventLevel(event), levelWrapper); this.serverApi.serverChunkLoadEvent(chunk, levelWrapper); } - @SubscribeEvent - public void serverChunkSaveEvent(ChunkEvent.Unload event) - { - ILevelWrapper levelWrapper = ProxyUtil.getLevelWrapper(GetEventLevel(event)); - - IChunkWrapper chunk = new ChunkWrapper(event.getChunk(), GetEventLevel(event), levelWrapper); - this.serverApi.serverChunkSaveEvent(chunk, levelWrapper); - } diff --git a/forge/src/main/java/com/seibel/distanthorizons/forge/mixins/server/MixinChunkMap.java b/forge/src/main/java/com/seibel/distanthorizons/forge/mixins/server/MixinChunkMap.java new file mode 100644 index 000000000..92e0a4a0c --- /dev/null +++ b/forge/src/main/java/com/seibel/distanthorizons/forge/mixins/server/MixinChunkMap.java @@ -0,0 +1,96 @@ +package com.seibel.distanthorizons.forge.mixins.server; + +import com.seibel.distanthorizons.common.wrappers.chunk.ChunkWrapper; +import com.seibel.distanthorizons.common.wrappers.world.ServerLevelWrapper; +import com.seibel.distanthorizons.core.api.internal.ServerApi; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ChunkMap.class) +public class MixinChunkMap +{ + + @Unique + private static final String CHUNK_SERIALIZER_WRITE + = "Lnet/minecraft/world/level/chunk/storage/ChunkSerializer;write(" + + "Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/chunk/ChunkAccess;)" + + "Lnet/minecraft/nbt/CompoundTag;"; + + @Shadow + @Final + ServerLevel level; + + // firing at INVOKE causes issues with C2ME and is probably unnecessary since we + // don't need the chunk(s) before MC has finished saving them + @Inject(method = "save", at = @At(value = "RETURN", target = CHUNK_SERIALIZER_WRITE)) + private void onChunkSave(ChunkAccess chunk, CallbackInfoReturnable ci) + { + // true means a chunk was saved to disk + if (ci.getReturnValue()) + { + // TODO is this validation necessary since we are checking above if + // the callback return value should state if the chunk was actually saved or not? + // Do we trust it to always be correct? + + //=====================================// + // corrupt/incomplete chunk validation // + //=====================================// + + // MC has a tendency to try saving incomplete or corrupted chunks (which show up as empty or black chunks) + // this logic should prevent that from happening + #if MC_VER == MC_1_16_5 || MC_VER == MC_1_17_1 + if (chunk.isUnsaved() || chunk.getUpgradeData() != null || !chunk.isLightCorrect()) + { + return; + } + #else + if (chunk.isUnsaved() || chunk.isUpgrading() || !chunk.isLightCorrect()) + { + return; + } + #endif + + + //==================// + // biome validation // + //==================// + + // some chunks may be missing their biomes, which cause issues when attempting to save them + #if MC_VER == MC_1_16_5 || MC_VER == MC_1_17_1 + if (chunk.getBiomes() == null) + { + return; + } + #else + try + { + // this will throw an exception if the biomes aren't set up + chunk.getNoiseBiome(0,0,0); + } + catch (Exception e) + { + return; + } + #endif + + + + if (!SharedApi.isChunkAtBlockPosAlreadyUpdating(chunk.getPos().getWorldPosition().getX(), chunk.getPos().getWorldPosition().getZ()) ) + { + ServerApi.INSTANCE.serverChunkSaveEvent( + new ChunkWrapper(chunk, this.level, ServerLevelWrapper.getWrapper(this.level)), + ServerLevelWrapper.getWrapper(this.level) + ); + } + } + } + +} \ No newline at end of file diff --git a/forge/src/main/resources/DistantHorizons.forge.mixins.json b/forge/src/main/resources/DistantHorizons.forge.mixins.json index c7b69cbaf..439d030d5 100644 --- a/forge/src/main/resources/DistantHorizons.forge.mixins.json +++ b/forge/src/main/resources/DistantHorizons.forge.mixins.json @@ -5,7 +5,8 @@ "mixins": [ "server.MixinUtilBackgroundThread", "server.MixinChunkGenerator", - "server.MixinTFChunkGenerator" + "server.MixinTFChunkGenerator", + "server.MixinChunkMap" ], "client": [ "client.MixinClientPacketListener", diff --git a/neoforge/src/main/java/com/seibel/distanthorizons/neoforge/NeoforgeClientProxy.java b/neoforge/src/main/java/com/seibel/distanthorizons/neoforge/NeoforgeClientProxy.java index e5dc92b42..88cec2806 100644 --- a/neoforge/src/main/java/com/seibel/distanthorizons/neoforge/NeoforgeClientProxy.java +++ b/neoforge/src/main/java/com/seibel/distanthorizons/neoforge/NeoforgeClientProxy.java @@ -63,6 +63,8 @@ import org.lwjgl.opengl.GL32; import net.neoforged.neoforge.event.TickEvent; #else import net.neoforged.neoforge.client.event.ClientTickEvent; + +import java.util.concurrent.ThreadPoolExecutor; #endif @@ -164,49 +166,55 @@ public class NeoforgeClientProxy implements AbstractModInitializer.IEventProxy @SubscribeEvent public void rightClickBlockEvent(PlayerInteractEvent.RightClickBlock event) { - if (SharedApi.isChunkAtBlockPosAlreadyUpdating(event.getPos().getX(), event.getPos().getZ())) + if (MC.clientConnectedToDedicatedServer()) { - return; - } - - // executor to prevent locking up the render/event thread - // if the getChunk() takes longer than expected - // (which can be caused by certain mods) - var executor = ThreadPoolUtil.getFileHandlerExecutor(); - if (executor != null) - { - executor.execute(() -> + if (SharedApi.isChunkAtBlockPosAlreadyUpdating(event.getPos().getX(), event.getPos().getZ())) { - //LOGGER.trace("interact or block place event at blockPos: " + event.getPos()); - - LevelAccessor level = event.getLevel(); - ChunkAccess chunk = level.getChunk(event.getPos()); - this.onBlockChangeEvent(level, chunk); - }); + return; + } + + // executor to prevent locking up the render/event thread + // if the getChunk() takes longer than expected + // (which can be caused by certain mods) + ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor(); + if (executor != null) + { + executor.execute(() -> + { + //LOGGER.trace("interact or block place event at blockPos: " + event.getPos()); + + LevelAccessor level = event.getLevel(); + ChunkAccess chunk = level.getChunk(event.getPos()); + this.onBlockChangeEvent(level, chunk); + }); + } } } @SubscribeEvent public void leftClickBlockEvent(PlayerInteractEvent.LeftClickBlock event) { - if (SharedApi.isChunkAtBlockPosAlreadyUpdating(event.getPos().getX(), event.getPos().getZ())) + if (MC.clientConnectedToDedicatedServer()) { - return; - } - - // executor to prevent locking up the render/event thread - // if the getChunk() takes longer than expected - // (which can be caused by certain mods) - var executor = ThreadPoolUtil.getFileHandlerExecutor(); - if (executor != null) - { - executor.execute(() -> + if (SharedApi.isChunkAtBlockPosAlreadyUpdating(event.getPos().getX(), event.getPos().getZ())) { - //LOGGER.trace("break or block attack at blockPos: " + event.getPos()); - - LevelAccessor level = event.getLevel(); - ChunkAccess chunk = level.getChunk(event.getPos()); - this.onBlockChangeEvent(level, chunk); - }); + return; + } + + // executor to prevent locking up the render/event thread + // if the getChunk() takes longer than expected + // (which can be caused by certain mods) + ThreadPoolExecutor executor = ThreadPoolUtil.getFileHandlerExecutor(); + if (executor != null) + { + executor.execute(() -> + { + //LOGGER.trace("break or block attack at blockPos: " + event.getPos()); + + LevelAccessor level = event.getLevel(); + ChunkAccess chunk = level.getChunk(event.getPos()); + this.onBlockChangeEvent(level, chunk); + }); + } } } private void onBlockChangeEvent(LevelAccessor level, ChunkAccess chunk) @@ -216,22 +224,6 @@ public class NeoforgeClientProxy implements AbstractModInitializer.IEventProxy } - @SubscribeEvent - public void clientChunkLoadEvent(ChunkEvent.Load event) - { - ILevelWrapper wrappedLevel = ProxyUtil.getLevelWrapper(GetEventLevel(event)); - IChunkWrapper chunk = new ChunkWrapper(event.getChunk(), GetEventLevel(event), wrappedLevel); - SharedApi.INSTANCE.chunkLoadEvent(chunk, wrappedLevel); - } - @SubscribeEvent - public void clientChunkUnloadEvent(ChunkEvent.Unload event) - { - ILevelWrapper wrappedLevel = ProxyUtil.getLevelWrapper(GetEventLevel(event)); - IChunkWrapper chunk = new ChunkWrapper(event.getChunk(), GetEventLevel(event), wrappedLevel); - SharedApi.INSTANCE.chunkUnloadEvent(chunk, wrappedLevel); - } - - //==============// // key bindings // diff --git a/neoforge/src/main/java/com/seibel/distanthorizons/neoforge/NeoforgeServerProxy.java b/neoforge/src/main/java/com/seibel/distanthorizons/neoforge/NeoforgeServerProxy.java index f7e7e366b..8b02e1779 100644 --- a/neoforge/src/main/java/com/seibel/distanthorizons/neoforge/NeoforgeServerProxy.java +++ b/neoforge/src/main/java/com/seibel/distanthorizons/neoforge/NeoforgeServerProxy.java @@ -122,14 +122,6 @@ public class NeoforgeServerProxy implements AbstractModInitializer.IEventProxy IChunkWrapper chunk = new ChunkWrapper(event.getChunk(), GetEventLevel(event), levelWrapper); this.serverApi.serverChunkLoadEvent(chunk, levelWrapper); } - @SubscribeEvent - public void serverChunkSaveEvent(ChunkEvent.Unload event) - { - ILevelWrapper levelWrapper = ProxyUtil.getLevelWrapper(GetEventLevel(event)); - - IChunkWrapper chunk = new ChunkWrapper(event.getChunk(), GetEventLevel(event), levelWrapper); - this.serverApi.serverChunkSaveEvent(chunk, levelWrapper); - } diff --git a/neoforge/src/main/java/com/seibel/distanthorizons/neoforge/mixins/server/MixinChunkMap.java b/neoforge/src/main/java/com/seibel/distanthorizons/neoforge/mixins/server/MixinChunkMap.java new file mode 100644 index 000000000..c3d22e4b9 --- /dev/null +++ b/neoforge/src/main/java/com/seibel/distanthorizons/neoforge/mixins/server/MixinChunkMap.java @@ -0,0 +1,97 @@ +package com.seibel.distanthorizons.neoforge.mixins.server; + +import com.seibel.distanthorizons.common.wrappers.chunk.ChunkWrapper; +import com.seibel.distanthorizons.common.wrappers.world.ServerLevelWrapper; +import com.seibel.distanthorizons.core.api.internal.ServerApi; +import com.seibel.distanthorizons.core.api.internal.SharedApi; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ChunkMap.class) +public class MixinChunkMap +{ + + @Unique + private static final String CHUNK_SERIALIZER_WRITE + = "Lnet/minecraft/world/level/chunk/storage/ChunkSerializer;write(" + + "Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/chunk/ChunkAccess;)" + + "Lnet/minecraft/nbt/CompoundTag;"; + + @Shadow + @Final + ServerLevel level; + + // firing at INVOKE causes issues with C2ME and is probably unnecessary since we + // don't need the chunk(s) before MC has finished saving them + @Inject(method = "save", at = @At(value = "RETURN", target = CHUNK_SERIALIZER_WRITE)) + private void onChunkSave(ChunkAccess chunk, CallbackInfoReturnable ci) + { + // true means a chunk was saved to disk + if (ci.getReturnValue()) + { + // TODO is this validation necessary since we are checking above if + // the callback return value should state if the chunk was actually saved or not? + // Do we trust it to always be correct? + + //=====================================// + // corrupt/incomplete chunk validation // + //=====================================// + + // MC has a tendency to try saving incomplete or corrupted chunks (which show up as empty or black chunks) + // this logic should prevent that from happening + #if MC_VER == MC_1_16_5 || MC_VER == MC_1_17_1 + if (chunk.isUnsaved() || chunk.getUpgradeData() != null || !chunk.isLightCorrect()) + { + return; + } + #else + if (chunk.isUnsaved() || chunk.isUpgrading() || !chunk.isLightCorrect()) + { + return; + } + #endif + + + //==================// + // biome validation // + //==================// + + // some chunks may be missing their biomes, which cause issues when attempting to save them + #if MC_VER == MC_1_16_5 || MC_VER == MC_1_17_1 + if (chunk.getBiomes() == null) + { + return; + } + #else + try + { + // this will throw an exception if the biomes aren't set up + chunk.getNoiseBiome(0,0,0); + } + catch (Exception e) + { + return; + } + #endif + + + + if (!SharedApi.isChunkAtBlockPosAlreadyUpdating(chunk.getPos().getWorldPosition().getX(), chunk.getPos().getWorldPosition().getZ()) ) + { + ServerApi.INSTANCE.serverChunkSaveEvent( + new ChunkWrapper(chunk, this.level, ServerLevelWrapper.getWrapper(this.level)), + ServerLevelWrapper.getWrapper(this.level) + ); + } + } + } + +} \ No newline at end of file diff --git a/neoforge/src/main/resources/DistantHorizons.neoforge.mixins.json b/neoforge/src/main/resources/DistantHorizons.neoforge.mixins.json index 5efe0dba6..eb99ea742 100644 --- a/neoforge/src/main/resources/DistantHorizons.neoforge.mixins.json +++ b/neoforge/src/main/resources/DistantHorizons.neoforge.mixins.json @@ -5,7 +5,8 @@ "mixins": [ "server.MixinUtilBackgroundThread", "server.MixinChunkGenerator", - "server.MixinTFChunkGenerator" + "server.MixinTFChunkGenerator", + "server.MixinChunkMap" ], "client": [ "client.MixinClientPacketListener",