Backport Async Locator

This commit is contained in:
embeddedt 2023-02-17 21:39:59 -05:00
parent 4e3b839f36
commit 8f971a2c84
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
22 changed files with 1110 additions and 1 deletions

View File

@ -9,12 +9,14 @@ import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.event.server.FMLServerStartedEvent;
import net.minecraftforge.fml.event.server.FMLServerStartingEvent;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.network.FMLNetworkConstants;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.embeddedt.modernfix.core.config.ModernFixConfig;
import org.embeddedt.modernfix.structure.AsyncLocator;
import java.lang.management.ManagementFactory;

View File

@ -42,6 +42,7 @@ public class ModernFixEarlyConfig {
this.addMixinRule("perf.faster_baking", true);
this.addMixinRule("perf.cache_model_materials", true);
this.addMixinRule("perf.datapack_reload_exceptions", true);
this.addMixinRule("perf.async_locator", true);
/* Keep this off if JEI isn't installed to prevent breaking vanilla gameplay */
this.addMixinRule("perf.blast_search_trees", FMLLoader.getLoadingModList().getModFileById("jei") != null);
this.addMixinRule("safety", true);

View File

@ -0,0 +1,12 @@
package org.embeddedt.modernfix.mixin.perf.async_locator;
import net.minecraft.commands.CommandSource;
import net.minecraft.commands.CommandSourceStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(CommandSourceStack.class)
public interface CommandSourceStackAccess {
@Accessor
CommandSource getSource();
}

View File

@ -0,0 +1,109 @@
package org.embeddedt.modernfix.mixin.perf.async_locator;
import com.google.common.collect.ImmutableSet;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.animal.Dolphin;
import net.minecraft.world.level.levelgen.feature.StructureFeature;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.structure.AsyncLocator;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
@Mixin(targets = "net.minecraft.world.entity.animal.Dolphin$DolphinSwimToTreasureGoal")
public class DolphinSwimToTreasureGoalMixin {
@Final
@Shadow
private Dolphin dolphin;
@Shadow
private boolean stuck;
private AsyncLocator.LocateTask<BlockPos> locateTask = null;
/*
Intercept DolphinSwimToTreasureGoal#start call right before it calls ServerLevel#findNearestMapFeature to pass
the logic over to an async task.
*/
@Inject(
method = "start",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/server/level/ServerLevel;findNearestMapFeature(Lnet/minecraft/world/level/levelgen/feature/StructureFeature;Lnet/minecraft/core/BlockPos;IZ)Lnet/minecraft/core/BlockPos;"
),
cancellable = true,
locals = LocalCapture.CAPTURE_FAILSOFT
)
public void findTreasureAsync(CallbackInfo ci, ServerLevel level, BlockPos blockpos) {
ModernFix.LOGGER.debug("Intercepted DolphinSwimToTreasureGoal#start call");
handleFindTreasureAsync(level, blockpos);
ci.cancel();
}
/*
Intercept DolphinSwimToTreasureGoal#canContinueToUse to return true if an async locating task is ongoing so that
the goal isn't ended early due to no treasure pos being set yet.
*/
@Inject(
method = "canContinueToUse",
at = @At(value = "HEAD"),
cancellable = true
)
public void continueToUseIfLocatingTreasure(CallbackInfoReturnable<Boolean> cir) {
if (locateTask != null) {
ModernFix.LOGGER.debug("Locating task ongoing - returning true for continueToUse()");
cir.setReturnValue(true);
}
}
@Inject(
method = "stop",
at = @At(value = "HEAD")
)
public void stopLocatingTreasure(CallbackInfo ci) {
if (locateTask != null) {
ModernFix.LOGGER.debug("Locating task ongoing - cancelling during stop()");
locateTask.cancel();
locateTask = null;
}
}
/*
Intercept DolphinSwimToTreasureGoal#tick to return early if an async locating task is ongoing so that the
dolphin doesn't try to go towards an old treasure position.
*/
@Inject(
method = "tick",
at = @At(value = "HEAD"),
cancellable = true
)
public void skipTickingIfLocatingTreasure(CallbackInfo ci) {
if (locateTask != null) {
ModernFix.LOGGER.debug("Locating task ongoing - skipping tick()");
ci.cancel();
}
}
private void handleFindTreasureAsync(ServerLevel level, BlockPos blockPos) {
locateTask = AsyncLocator.locateLevel(level, ImmutableSet.of(StructureFeature.OCEAN_RUIN, StructureFeature.SHIPWRECK), blockPos, 50, false)
.thenOnServerThread(pos -> handleLocationFound(level, pos));
}
private void handleLocationFound(ServerLevel level, BlockPos pos) {
locateTask = null;
if (pos != null) {
ModernFix.LOGGER.debug("Location found - updating dolphin treasure pos");
dolphin.setTreasurePos(pos);
level.broadcastEntityEvent(dolphin, (byte) 38);
} else {
ModernFix.LOGGER.debug("No location found - marking dolphin as stuck");
stuck = true;
}
}
}

View File

@ -0,0 +1,104 @@
package org.embeddedt.modernfix.mixin.perf.async_locator;
import net.minecraft.advancements.critereon.UsedEnderEyeTrigger;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.stats.Stat;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.EyeOfEnder;
import net.minecraft.world.item.EnderEyeItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.feature.StructureFeature;
import net.minecraft.world.phys.HitResult;
import org.embeddedt.modernfix.structure.logic.EnderEyeItemLogic;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
@Mixin(EnderEyeItem.class)
public class EnderEyeItemMixin {
/*
Intercept EnderEyeItem#use call and return BlockPos.ZERO instead. It won't be used in the EyeOfEnder entity
created later either, as we need to set the actual location ourselves.
*/
@Redirect(
method = "use",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/level/chunk/ChunkGenerator;findNearestMapFeature(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/levelgen/feature/StructureFeature;Lnet/minecraft/core/BlockPos;IZ)Lnet/minecraft/core/BlockPos;"
)
)
public BlockPos levelFindNearestMapFeature(
ChunkGenerator generator,
ServerLevel level,
StructureFeature<?> structureFeature,
BlockPos pPos,
int pRadius,
boolean pSkipExistingChunks
) {
return BlockPos.ZERO;
}
// Start the async locate task here so we have the eye of ender entity for context
@Inject(
method = "use",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/projectile/EyeOfEnder;setItem(Lnet/minecraft/world/item/ItemStack;)V"
),
locals = LocalCapture.CAPTURE_FAILEXCEPTION
)
public void startAsyncLocateTask(
Level pLevel,
Player pPlayer,
InteractionHand pHand,
CallbackInfoReturnable<InteractionResultHolder<ItemStack>> cir,
ItemStack itemstack,
HitResult hitresult,
BlockPos blockpos,
EyeOfEnder eyeofender
) {
EnderEyeItemLogic.locateAsync((ServerLevel)pLevel, pPlayer, eyeofender, (EnderEyeItem) (Object) this);
}
@Redirect(
method = "use",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/projectile/EyeOfEnder;signalTo(Lnet/minecraft/core/BlockPos;)V"
)
)
public void eyeOfEnderSignalTo(EyeOfEnder eyeOfEnder, BlockPos blockpos) {
// Do nothing - we'll do this later if a location is found
}
@Redirect(
method = "use",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/advancements/critereon/UsedEnderEyeTrigger;trigger(Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/core/BlockPos;)V"
)
)
public void triggerUsedEnderEyeCriteria(UsedEnderEyeTrigger trigger, ServerPlayer player, BlockPos pos) {
// Do nothing - we'll do this later if a location is found
}
@Redirect(
method = "use",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/player/Player;awardStat(Lnet/minecraft/stats/Stat;)V"
)
)
public void playerAwardStat(Player instance, Stat<?> pStat) {
// Do nothing - we'll do this later if a location is found
}
}

View File

@ -0,0 +1,63 @@
package org.embeddedt.modernfix.mixin.perf.async_locator;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.levelgen.feature.StructureFeature;
import net.minecraft.world.level.saveddata.maps.MapDecoration;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.functions.ExplorationMapFunction;
import net.minecraft.world.phys.Vec3;
import org.embeddedt.modernfix.structure.logic.ExplorationMapFunctionLogic;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
@Mixin(ExplorationMapFunction.class)
public class ExplorationMapFunctionMixin {
@Shadow
@Final
StructureFeature<?> destination;
@Shadow
@Final
MapDecoration.Type mapDecoration;
@Shadow
@Final
byte zoom;
@Shadow
@Final
int searchRadius;
@Shadow
@Final
boolean skipKnownStructures;
@Inject(
method = "run",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/server/level/ServerLevel;findNearestMapFeature(Lnet/minecraft/world/level/levelgen/feature/StructureFeature;Lnet/minecraft/core/BlockPos;IZ)Lnet/minecraft/core/BlockPos;"
),
locals = LocalCapture.CAPTURE_FAILSOFT,
cancellable = true
)
public void updateMapAsync(
ItemStack pStack,
LootContext pContext,
CallbackInfoReturnable<ItemStack> cir,
Vec3 vec3,
ServerLevel serverlevel
) {
ItemStack mapStack = ExplorationMapFunctionLogic.updateMapAsync(
serverlevel, new BlockPos(vec3), zoom, searchRadius, skipKnownStructures, mapDecoration, destination
);
cir.setReturnValue(mapStack);
}
}

View File

@ -0,0 +1,11 @@
package org.embeddedt.modernfix.mixin.perf.async_locator;
import net.minecraft.world.entity.projectile.EyeOfEnder;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(EyeOfEnder.class)
public interface EyeOfEnderAccess {
@Accessor
void setLife(int life);
}

View File

@ -0,0 +1,37 @@
package org.embeddedt.modernfix.mixin.perf.async_locator;
import net.minecraft.world.entity.projectile.EyeOfEnder;
import org.embeddedt.modernfix.structure.logic.EyeOfEnderData;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(EyeOfEnder.class)
public class EyeOfEnderMixin implements EyeOfEnderData {
private boolean locateTaskOngoing = false;
@Override
public void setLocateTaskOngoing(boolean locateTaskOngoing) {
this.locateTaskOngoing = locateTaskOngoing;
}
/*
Intercept EyeOfEnder#tick call and return after the super call if there's an ongoing locate task. This is to
prevent the entity from moving or dying until we have a location result.
*/
@Inject(
method = "tick",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/entity/Entity;tick()V",
shift = At.Shift.AFTER
),
cancellable = true
)
public void skipTick(CallbackInfo ci) {
if (locateTaskOngoing) {
ci.cancel();
}
}
}

View File

@ -0,0 +1,15 @@
package org.embeddedt.modernfix.mixin.perf.async_locator;
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import net.minecraft.server.commands.LocateCommand;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(LocateCommand.class)
public interface LocateCommandAccess {
@Accessor("ERROR_FAILED")
static SimpleCommandExceptionType getErrorFailed() {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,34 @@
package org.embeddedt.modernfix.mixin.perf.async_locator;
import net.minecraft.commands.CommandSource;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.commands.LocateCommand;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.levelgen.feature.StructureFeature;
import org.embeddedt.modernfix.structure.logic.LocateCommandLogic;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
@Mixin(LocateCommand.class)
public class LocateCommandMixin {
@Inject(
method = "locate",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/server/level/ServerLevel;findNearestMapFeature(Lnet/minecraft/world/level/levelgen/feature/StructureFeature;Lnet/minecraft/core/BlockPos;IZ)Lnet/minecraft/core/BlockPos;"
),
cancellable = true,
locals = LocalCapture.CAPTURE_FAILSOFT
)
private static void findLocationAsync(CommandSourceStack sourceStack, StructureFeature<?> feature, CallbackInfoReturnable<Integer> cir) {
CommandSource source = ((CommandSourceStackAccess) sourceStack).getSource();
if (source instanceof ServerPlayer || source instanceof MinecraftServer) {
LocateCommandLogic.locateAsync(sourceStack, feature);
cir.setReturnValue(0);
}
}
}

View File

@ -0,0 +1,17 @@
package org.embeddedt.modernfix.mixin.perf.async_locator;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.MapItem;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(MapItem.class)
public interface MapItemAccess {
@Invoker
static MapItemSavedData callCreateAndStoreSavedData(ItemStack pStack, Level pLevel, int pX, int pZ, int pScale, boolean pTrackingPosition, boolean pUnlimitedTracking, ResourceKey<Level> pDimension) {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,13 @@
package org.embeddedt.modernfix.mixin.perf.async_locator;
import net.minecraft.world.item.trading.MerchantOffer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(MerchantOffer.class)
public interface MerchantOfferAccess {
@Mutable
@Accessor
void setMaxUses(int maxUses);
}

View File

@ -0,0 +1,62 @@
package org.embeddedt.modernfix.mixin.perf.async_locator;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.trading.MerchantOffer;
import net.minecraft.world.level.levelgen.feature.StructureFeature;
import net.minecraft.world.level.saveddata.maps.MapDecoration;
import org.embeddedt.modernfix.structure.logic.MerchantLogic;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Locale;
import java.util.Random;
@Mixin(targets = "net.minecraft.world.entity.npc.VillagerTrades$TreasureMapForEmeralds")
public class TreasureMapForEmeraldsMixin {
@Shadow
@Final
private int emeraldCost;
@Shadow
@Final
private MapDecoration.Type destinationType;
@Shadow
@Final
private int maxUses;
@Shadow
@Final
private int villagerXp;
@Shadow
@Final
private StructureFeature<?> destination;
/*
Intercept TreasureMapForEmeralds#getOffer call right before it calls ServerLevel#findNearestMapFeature to pass
the logic over to an async task. Instead of returning the complete map or null, we'll have to always return an
incomplete filled map and later update it with the details when we have them.
*/
@Inject(
method = "getOffer",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/server/level/ServerLevel;findNearestMapFeature(Lnet/minecraft/world/level/levelgen/feature/StructureFeature;Lnet/minecraft/core/BlockPos;IZ)Lnet/minecraft/core/BlockPos;"
),
cancellable = true
)
public void updateMapAsync(Entity pTrader, Random pRand, CallbackInfoReturnable<MerchantOffer> callbackInfo) {
String displayName = "filled_map." + this.destination.getFeatureName().toLowerCase(Locale.ROOT);
MerchantOffer offer = MerchantLogic.updateMapAsync(
pTrader, emeraldCost, displayName, destinationType, maxUses, villagerXp, destination
);
if (offer != null) {
callbackInfo.setReturnValue(offer);
}
}
}

View File

@ -0,0 +1,215 @@
package org.embeddedt.modernfix.structure;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.feature.StructureFeature;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.thread.SidedThreadGroups;
import net.minecraftforge.fml.event.server.FMLServerAboutToStartEvent;
import net.minecraftforge.fml.event.server.FMLServerStoppingEvent;
import org.embeddedt.modernfix.ModernFix;
import org.jetbrains.annotations.NotNull;
import java.sql.Struct;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Mod.EventBusSubscriber(modid = ModernFix.MODID)
public class AsyncLocator {
private static ExecutorService LOCATING_EXECUTOR_SERVICE = null;
private static final AtomicInteger poolNum = new AtomicInteger(1);
private AsyncLocator() {}
private static void setupExecutorService() {
shutdownExecutorService();
int threads = 1; // very unlikely we need more than one
ModernFix.LOGGER.info("Starting locating executor service with thread pool size of {}", threads);
LOCATING_EXECUTOR_SERVICE = Executors.newFixedThreadPool(
threads,
new ThreadFactory() {
private final AtomicInteger threadNum = new AtomicInteger(1);
private final String namePrefix = "asynclocator-" + poolNum.getAndIncrement() + "-thread-";
@Override
public Thread newThread(@NotNull Runnable r) {
return new Thread(SidedThreadGroups.SERVER, r, namePrefix + threadNum.getAndIncrement());
}
}
);
}
private static void shutdownExecutorService() {
if (LOCATING_EXECUTOR_SERVICE != null) {
ModernFix.LOGGER.info("Shutting down locating executor service");
LOCATING_EXECUTOR_SERVICE.shutdown();
}
}
@SubscribeEvent
public static void handleServerAboutToStartEvent(FMLServerAboutToStartEvent ignoredEvent) {
setupExecutorService();
}
@SubscribeEvent
public static void handleServerStoppingEvent(FMLServerStoppingEvent ignoredEvent) {
shutdownExecutorService();
}
/**
* Queues a task to locate a feature using {@link ServerLevel#findNearestMapFeature(TagKey, BlockPos, int, boolean)}
* and returns a {@link LocateTask} with the futures for it.
*/
public static LocateTask<BlockPos> locateLevel(
ServerLevel level,
Collection<StructureFeature<?>> structure,
BlockPos pos,
int searchRadius,
boolean skipKnownStructures
) {
ModernFix.LOGGER.debug(
"Creating locate task for {} in {} around {} within {} chunks",
structure, level, pos, searchRadius
);
CompletableFuture<BlockPos> completableFuture = new CompletableFuture<>();
Future<?> future = LOCATING_EXECUTOR_SERVICE.submit(
() -> doLocateLevel(completableFuture, level, structure, pos, searchRadius, skipKnownStructures)
);
return new LocateTask<>(level.getServer(), completableFuture, future);
}
/**
* Queues a task to locate a feature using
* {@link ChunkGenerator#findNearestMapFeature(ServerLevel, HolderSet, BlockPos, int, boolean)} and returns a
* {@link LocateTask} with the futures for it.
*/
public static LocateTask<Pair<BlockPos, StructureFeature<?>>> locateChunkGen(
ServerLevel level,
Collection<StructureFeature<?>> structureSet,
BlockPos pos,
int searchRadius,
boolean skipKnownStructures
) {
ModernFix.LOGGER.debug(
"Creating locate task for {} in {} around {} within {} chunks",
structureSet, level, pos, searchRadius
);
CompletableFuture<Pair<BlockPos, StructureFeature<?>>> completableFuture = new CompletableFuture<>();
Future<?> future = LOCATING_EXECUTOR_SERVICE.submit(
() -> doLocateChunkGenerator(completableFuture, level, structureSet, pos, searchRadius, skipKnownStructures)
);
return new LocateTask<>(level.getServer(), completableFuture, future);
}
private static String structureSetToString(Collection<StructureFeature<?>> collection) {
return "[" + collection.stream().map(StructureFeature::getRegistryName).map(ResourceLocation::toString).collect(Collectors.joining(", ")) + "]";
}
private static void doLocateLevel(
CompletableFuture<BlockPos> completableFuture,
ServerLevel level,
Collection<StructureFeature<?>> structureTag,
BlockPos pos,
int searchRadius,
boolean skipExistingChunks
) {
String structures = structureSetToString(structureTag);
ModernFix.LOGGER.debug(
"Trying to locate {} in {} around {} within {} chunks",
structures, level, pos, searchRadius
);
Optional<BlockPos> thePosition = structureTag.stream()
.map(tag -> level.findNearestMapFeature(tag, pos, searchRadius, skipExistingChunks))
.filter(Objects::nonNull)
.findFirst();
if (!thePosition.isPresent())
ModernFix.LOGGER.debug("No {} found", structures);
else
ModernFix.LOGGER.debug("Found {} at {}", structures, thePosition.get());
completableFuture.complete(thePosition.orElse(null));
}
@SuppressWarnings({"rawtypes", "unchecked" })
private static void doLocateChunkGenerator(
CompletableFuture<Pair<BlockPos, StructureFeature<?>>> completableFuture,
ServerLevel level,
Collection<StructureFeature<?>> structureSet,
BlockPos pos,
int searchRadius,
boolean skipExistingChunks
) {
String structures = structureSetToString(structureSet);
ModernFix.LOGGER.debug(
"Trying to locate {} in {} around {} within {} chunks",
structures, level, pos, searchRadius
);
Optional<Pair<BlockPos, StructureFeature>> foundStructure = structureSet.stream()
.map(feature -> Pair.of(level.getChunkSource().getGenerator()
.findNearestMapFeature(level, feature, pos, searchRadius, skipExistingChunks), (StructureFeature)feature))
.filter(pair -> pair.getFirst() != null)
.findFirst();
if (!foundStructure.isPresent())
ModernFix.LOGGER.debug("No {} found", structures);
else
ModernFix.LOGGER.debug("Found {} at {}", structures, foundStructure.get().getFirst());
completableFuture.complete((Pair<BlockPos, StructureFeature<?>>)(Object)foundStructure.orElse(null));
}
/**
* Holder of the futures for an async locate task as well as providing some helper functions.
* The completableFuture will be completed once the call to
* {@link ServerLevel#findNearestMapFeature(TagKey, BlockPos, int, boolean)} has completed, and will hold the
* result of it.
* The taskFuture is the future for the {@link Runnable} itself in the executor service.
*/
public static class LocateTask<T> {
private final MinecraftServer server;
private final CompletableFuture<T> completableFuture;
private final Future<?> taskFuture;
public LocateTask(MinecraftServer server, CompletableFuture<T> completableFuture, Future<?> taskFuture) {
this.server = server;
this.completableFuture = completableFuture;
this.taskFuture = taskFuture;
}
/**
* Helper function that calls {@link CompletableFuture#thenAccept(Consumer)} with the given action.
* Bear in mind that the action will be executed from the task's thread. If you intend to change any game data,
* it's strongly advised you use {@link #thenOnServerThread(Consumer)} instead so that it's queued and executed
* on the main server thread instead.
*/
public LocateTask<T> then(Consumer<T> action) {
completableFuture.thenAccept(action);
return this;
}
/**
* Helper function that calls {@link CompletableFuture#thenAccept(Consumer)} with the given action on the server
* thread.
*/
public LocateTask<T> thenOnServerThread(Consumer<T> action) {
completableFuture.thenAccept(pos -> server.submit(() -> action.accept(pos)));
return this;
}
/**
* Helper function that cancels both completableFuture and taskFuture.
*/
public void cancel() {
taskFuture.cancel(true);
completableFuture.cancel(false);
}
}
}

View File

@ -0,0 +1,92 @@
package org.embeddedt.modernfix.structure.logic;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.TranslatableComponent;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ChestMenu;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.MapItem;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.saveddata.maps.MapDecoration;
import net.minecraft.world.level.saveddata.maps.MapItemSavedData;
import org.embeddedt.modernfix.mixin.perf.async_locator.MapItemAccess;
public class CommonLogic {
private CommonLogic() {}
/**
* Creates an empty "Filled Map", with a hover tooltip name stating that it's locating a feature.
*
* @return The ItemStack
*/
public static ItemStack createEmptyMap() {
ItemStack stack = new ItemStack(Items.FILLED_MAP);
stack.setHoverName(new TranslatableComponent("asynclocator.map.locating"));
return stack;
}
/**
* Updates the map stack with all the given data.
*
* @param mapStack The map ItemStack to update
* @param level The ServerLevel
* @param pos The feature position
* @param scale The map scale
* @param destinationType The map feature type
*/
public static void updateMap(
ItemStack mapStack,
ServerLevel level,
BlockPos pos,
int scale,
MapDecoration.Type destinationType
) {
updateMap(mapStack, level, pos, scale, destinationType, null);
}
/**
* Updates the map stack with all the given data.
*
* @param mapStack The map ItemStack to update
* @param level The ServerLevel
* @param pos The feature position
* @param scale The map scale
* @param destinationType The map feature type
* @param displayName The hover tooltip display name of the ItemStack
*/
public static void updateMap(
ItemStack mapStack,
ServerLevel level,
BlockPos pos,
int scale,
MapDecoration.Type destinationType,
String displayName
) {
MapItemAccess.callCreateAndStoreSavedData(
mapStack, level, pos.getX(), pos.getZ(), scale, true, true, level.dimension()
);
MapItem.renderBiomePreviewMap(level, mapStack);
MapItemSavedData.addTargetDecoration(mapStack, pos, "+", destinationType);
if (displayName != null)
mapStack.setHoverName(new TranslatableComponent(displayName));
}
/**
* Broadcasts slot changes to all players that have the chest container open.
* Won't do anything if the BlockEntity isn't an instance of {@link ChestBlockEntity}.
*/
public static void broadcastChestChanges(ServerLevel level, BlockEntity be) {
if (!(be instanceof ChestBlockEntity))
return;
level.players().forEach(player -> {
AbstractContainerMenu container = player.containerMenu;
if (container instanceof ChestMenu && ((ChestMenu)container).getContainer() == be) {
container.broadcastChanges();
}
});
}
}

View File

@ -0,0 +1,38 @@
package org.embeddedt.modernfix.structure.logic;
import com.google.common.collect.ImmutableSet;
import net.minecraft.advancements.CriteriaTriggers;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.stats.Stats;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.EyeOfEnder;
import net.minecraft.world.item.EnderEyeItem;
import net.minecraft.world.level.levelgen.feature.StructureFeature;
import org.embeddedt.modernfix.mixin.perf.async_locator.EyeOfEnderAccess;
import org.embeddedt.modernfix.structure.AsyncLocator;
public class EnderEyeItemLogic {
private EnderEyeItemLogic() {}
public static void locateAsync(ServerLevel level, Player player, EyeOfEnder eyeOfEnder, EnderEyeItem enderEyeItem) {
AsyncLocator.locateChunkGen(
level,
ImmutableSet.of(StructureFeature.STRONGHOLD),
player.blockPosition(),
100,
false
).thenOnServerThread(pos -> {
((EyeOfEnderData) eyeOfEnder).setLocateTaskOngoing(false);
if (pos != null) {
eyeOfEnder.signalTo(pos.getFirst());
CriteriaTriggers.USED_ENDER_EYE.trigger((ServerPlayer) player, pos.getFirst());
player.awardStat(Stats.ITEM_USED.get(enderEyeItem));
} else {
// Set the entity's life to long enough that it dies
((EyeOfEnderAccess) eyeOfEnder).setLife(Integer.MAX_VALUE - 100);
}
});
((EyeOfEnderData) eyeOfEnder).setLocateTaskOngoing(true);
}
}

View File

@ -0,0 +1,111 @@
package org.embeddedt.modernfix.structure.logic;
import com.google.common.collect.ImmutableSet;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.levelgen.feature.StructureFeature;
import net.minecraft.world.level.saveddata.maps.MapDecoration;
import net.minecraftforge.fml.loading.Java9BackportUtils;
import net.minecraftforge.items.CapabilityItemHandler;
import net.minecraftforge.items.IItemHandler;
import net.minecraftforge.items.IItemHandlerModifiable;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.structure.AsyncLocator;
import java.util.function.BiConsumer;
// TODO: Need to test this
public class ExplorationMapFunctionLogic {
private static final int MAX_STACK_SIZE = 64;
private ExplorationMapFunctionLogic() {}
public static void invalidateMap(ItemStack mapStack, ServerLevel level, BlockPos pos) {
handleUpdateMapInChest(mapStack, level, pos, (handler, slot) -> {
if (handler instanceof IItemHandlerModifiable) {
((IItemHandlerModifiable)handler).setStackInSlot(slot, new ItemStack(Items.MAP));
} else {
handler.extractItem(slot, MAX_STACK_SIZE, false);
handler.insertItem(slot, new ItemStack(Items.MAP), false);
}
});
}
public static void updateMap(
ItemStack mapStack,
ServerLevel level,
BlockPos pos,
int scale,
MapDecoration.Type destinationType,
BlockPos invPos
) {
CommonLogic.updateMap(mapStack, level, pos, scale, destinationType);
// Shouldn't need to set the stack in its slot again, as we're modifying the same instance
handleUpdateMapInChest(mapStack, level, invPos, (handler, slot) -> {});
}
public static void handleUpdateMapInChest(
ItemStack mapStack,
ServerLevel level,
BlockPos invPos,
BiConsumer<IItemHandler, Integer> handleSlotFound
) {
BlockEntity be = level.getBlockEntity(invPos);
if (be != null) {
Java9BackportUtils.ifPresentOrElse(be.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY).resolve(),
itemHandler -> {
for (int i = 0; i < itemHandler.getSlots(); i++) {
ItemStack slotStack = itemHandler.getStackInSlot(i);
if (slotStack == mapStack) {
handleSlotFound.accept(itemHandler, i);
CommonLogic.broadcastChestChanges(level, be);
return;
}
}
},
() -> ModernFix.LOGGER.warn(
"Couldn't find item handler capability on chest {} at {}",
be.getClass().getSimpleName(), invPos
)
);
} else {
ModernFix.LOGGER.warn(
"Couldn't find block entity on chest {} at {}",
level.getBlockState(invPos), invPos
);
}
}
public static void handleLocationFound(
ItemStack mapStack,
ServerLevel level,
BlockPos pos,
int scale,
MapDecoration.Type destinationType,
BlockPos invPos
) {
if (pos == null) {
invalidateMap(mapStack, level, invPos);
} else {
updateMap(mapStack, level, pos, scale, destinationType, invPos);
}
}
public static ItemStack updateMapAsync(
ServerLevel level,
BlockPos blockPos,
int scale,
int searchRadius,
boolean skipKnownStructures,
MapDecoration.Type destinationType,
StructureFeature<?> destination
) {
ItemStack mapStack = CommonLogic.createEmptyMap();
AsyncLocator.locateLevel(level, ImmutableSet.of(destination), blockPos, searchRadius, skipKnownStructures)
.thenOnServerThread(pos -> handleLocationFound(mapStack, level, pos, scale, destinationType, blockPos));
return mapStack;
}
}

View File

@ -0,0 +1,5 @@
package org.embeddedt.modernfix.structure.logic;
public interface EyeOfEnderData {
void setLocateTaskOngoing(boolean locateTaskOngoing);
}

View File

@ -0,0 +1,30 @@
package org.embeddedt.modernfix.structure.logic;
import com.google.common.collect.ImmutableSet;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.TextComponent;
import net.minecraft.server.commands.LocateCommand;
import net.minecraft.world.level.levelgen.feature.StructureFeature;
import org.embeddedt.modernfix.mixin.perf.async_locator.LocateCommandAccess;
import org.embeddedt.modernfix.structure.AsyncLocator;
public class LocateCommandLogic {
private LocateCommandLogic() {}
public static void locateAsync(CommandSourceStack sourceStack, StructureFeature<?> feature) {
BlockPos originPos = new BlockPos(sourceStack.getPosition());
AsyncLocator.locateLevel(sourceStack.getLevel(), ImmutableSet.of(feature), originPos, 100, false)
.thenOnServerThread(pair -> {
if (pair != null) {
LocateCommand.showLocateResult(sourceStack, feature.getFeatureName(), originPos, pair, "commands.locate.success");
} else {
sourceStack.sendFailure(
new TextComponent(
LocateCommandAccess.getErrorFailed().create().getMessage()
)
);
}
});
}
}

View File

@ -0,0 +1,125 @@
package org.embeddedt.modernfix.structure.logic;
import com.google.common.collect.ImmutableSet;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.TranslatableComponent;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.npc.AbstractVillager;
import net.minecraft.world.entity.npc.Villager;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.trading.MerchantOffer;
import net.minecraft.world.level.levelgen.feature.StructureFeature;
import net.minecraft.world.level.saveddata.maps.MapDecoration;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.mixin.perf.async_locator.MerchantOfferAccess;
import org.embeddedt.modernfix.structure.AsyncLocator;
import java.util.Optional;
public class MerchantLogic {
private static final boolean REMOVE_OFFER = false;
private MerchantLogic() {}
public static void invalidateMap(AbstractVillager merchant, ItemStack mapStack) {
mapStack.setHoverName(new TranslatableComponent("asynclocator.map.none"));
Optional<MerchantOffer> offerOptional = merchant.getOffers()
.stream()
.filter(offer -> offer.getResult() == mapStack)
.findFirst();
if(offerOptional.isPresent()) {
removeOffer(merchant, offerOptional.get());
} else {
ModernFix.LOGGER.warn("Failed to find merchant offer for map");
}
}
public static void removeOffer(AbstractVillager merchant, MerchantOffer offer) {
if (REMOVE_OFFER) {
merchant.getOffers().remove(offer);
} else {
((MerchantOfferAccess) offer).setMaxUses(0);
offer.setToOutOfStock();
}
}
public static void handleLocationFound(
ServerLevel level,
AbstractVillager merchant,
ItemStack mapStack,
String displayName,
MapDecoration.Type destinationType,
BlockPos pos
) {
if (pos == null) {
invalidateMap(merchant, mapStack);
} else {
CommonLogic.updateMap(mapStack, level, pos, 2, destinationType, displayName);
}
if (merchant.getTradingPlayer() instanceof ServerPlayer) {
ServerPlayer tradingPlayer = (ServerPlayer)merchant.getTradingPlayer();
tradingPlayer.sendMerchantOffers(
tradingPlayer.containerMenu.containerId,
merchant.getOffers(),
merchant instanceof Villager ? ((Villager)merchant).getVillagerData().getLevel() : 1,
merchant.getVillagerXp(),
merchant.showProgressBar(),
merchant.canRestock()
);
}
}
public static MerchantOffer updateMapAsync(
Entity pTrader,
int emeraldCost,
String displayName,
MapDecoration.Type destinationType,
int maxUses,
int villagerXp,
StructureFeature<?> destination
) {
return updateMapAsyncInternal(
pTrader,
emeraldCost,
maxUses,
villagerXp,
(level, merchant, mapStack) -> AsyncLocator.locateLevel(level, ImmutableSet.of(destination), merchant.blockPosition(), 100, true)
.thenOnServerThread(pos -> handleLocationFound(
level,
merchant,
mapStack,
displayName,
destinationType,
pos
))
);
}
private static MerchantOffer updateMapAsyncInternal(
Entity trader, int emeraldCost, int maxUses, int villagerXp, MapUpdateTask task
) {
if (trader instanceof AbstractVillager) {
AbstractVillager merchant = (AbstractVillager)trader;
ItemStack mapStack = CommonLogic.createEmptyMap();
task.apply((ServerLevel) trader.level, merchant, mapStack);
return new MerchantOffer(
new ItemStack(Items.EMERALD, emeraldCost),
new ItemStack(Items.COMPASS),
mapStack,
maxUses,
villagerXp,
0.2F
);
} else {
return null;
}
}
public interface MapUpdateTask {
void apply(ServerLevel level, AbstractVillager merchant, ItemStack mapStack);
}
}

View File

@ -1,3 +1,5 @@
{
"modernfix.jei_load": "Loading JEI..."
"modernfix.jei_load": "Loading JEI...",
"asynclocator.map.locating": "Map (Locating...)",
"asynclocator.map.none": "Map (No nearby feature found)"
}

View File

@ -27,6 +27,17 @@
"perf.cache_blockstate_cache_arrays.AbstractBlockStateCacheMixin",
"perf.datapack_reload_exceptions.LootTableManagerMixin",
"perf.datapack_reload_exceptions.RecipeManagerMixin",
"perf.async_locator.CommandSourceStackAccess",
"perf.async_locator.DolphinSwimToTreasureGoalMixin",
"perf.async_locator.EnderEyeItemMixin",
"perf.async_locator.ExplorationMapFunctionMixin",
"perf.async_locator.EyeOfEnderAccess",
"perf.async_locator.EyeOfEnderMixin",
"perf.async_locator.LocateCommandAccess",
"perf.async_locator.LocateCommandMixin",
"perf.async_locator.MapItemAccess",
"perf.async_locator.MerchantOfferAccess",
"perf.async_locator.TreasureMapForEmeraldsMixin",
"feature.measure_time.BootstrapMixin"
],
"client": [