From a52c873bd3c0187a8156489342320e18efeb0df0 Mon Sep 17 00:00:00 2001 From: bradsk88 Date: Fri, 8 Mar 2024 14:20:14 -0600 Subject: [PATCH] Forward Port from 1.18 More version-agnostic helpers Distributed room scanning over multiple ticks On a long-lived server with many registered doors, we were encountering crashes due to exceeding the 60 second tick limit. Instead of trying to calculate all rooms in a single tick, we now detect individual rooms per tick and utilize PendingTownRooms to collect and register them once they've all been detected. Updating RoomRecipes to debug some scanning problems Added debug mode for debugging room scans Switched debug from config to command Remove debug configs --- build.gradle | 2 +- .../questown/commands/CommandEvents.java | 2 + .../bradj/questown/commands/DebugCommand.java | 36 +++ .../questown/commands/DebugDoorsCommand.java | 50 ++++ .../java/ca/bradj/questown/core/Config.java | 19 +- .../questown/core/init/items/ItemsInit.java | 5 + .../integration/minecraft/MCContainer.java | 4 +- .../integration/minecraft/MCTownItem.java | 3 + .../questown/items/TownDoorTestItem.java | 53 ++++ .../questown/jobs/declarative/DinerWork.java | 2 +- src/main/java/ca/bradj/questown/mc/Util.java | 5 + .../questown/town/TownFlagBlockEntity.java | 255 +++++++++++------- .../ca/bradj/questown/town/TownFlagState.java | 4 +- .../bradj/questown/town/TownRoomsHandle.java | 101 ++++++- .../questown/town/interfaces/RoomsHolder.java | 9 + .../questown/town/rooms/PendingTownRooms.java | 114 ++++++++ .../questown/town/rooms/TownRoomsMap.java | 229 +++++++++++----- src/main/resources/META-INF/mods.toml | 2 +- .../resources/assets/questown/lang/en_us.json | 1 + 19 files changed, 716 insertions(+), 180 deletions(-) create mode 100644 src/main/java/ca/bradj/questown/commands/DebugCommand.java create mode 100644 src/main/java/ca/bradj/questown/commands/DebugDoorsCommand.java create mode 100644 src/main/java/ca/bradj/questown/items/TownDoorTestItem.java create mode 100644 src/main/java/ca/bradj/questown/town/rooms/PendingTownRooms.java diff --git a/build.gradle b/build.gradle index 447d919d..4f42e9a9 100644 --- a/build.gradle +++ b/build.gradle @@ -129,7 +129,7 @@ dependencies { implementation fg.deobf("mezz.jei:jei-1.19.2-forge:11.5.0.297") - implementation 'ca.bradj:RoomRecipes:1.19.2-0.0.4-alpha.2' + implementation 'ca.bradj:RoomRecipes:1.19.2-0.0.5-alpha.1' compileOnly fg.deobf("vazkii.patchouli:Patchouli:1.19.2-77:api") runtimeOnly fg.deobf("vazkii.patchouli:Patchouli:1.19.2-77") diff --git a/src/main/java/ca/bradj/questown/commands/CommandEvents.java b/src/main/java/ca/bradj/questown/commands/CommandEvents.java index 24a4824e..bd89c746 100644 --- a/src/main/java/ca/bradj/questown/commands/CommandEvents.java +++ b/src/main/java/ca/bradj/questown/commands/CommandEvents.java @@ -16,5 +16,7 @@ public static void on(RegisterCommandsEvent event) { FreezeCommand.register(event.getDispatcher()); ConfigCommand.register(event.getDispatcher()); FlagCommand.register(event.getDispatcher()); + DebugCommand.register(event.getDispatcher()); + DebugDoorsCommand.register(event.getDispatcher()); } } diff --git a/src/main/java/ca/bradj/questown/commands/DebugCommand.java b/src/main/java/ca/bradj/questown/commands/DebugCommand.java new file mode 100644 index 00000000..bc0c55f4 --- /dev/null +++ b/src/main/java/ca/bradj/questown/commands/DebugCommand.java @@ -0,0 +1,36 @@ +package ca.bradj.questown.commands; + +import ca.bradj.questown.town.TownFlagBlockEntity; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.coordinates.BlockPosArgument; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class DebugCommand { + public static void register(CommandDispatcher p_137808_) { + p_137808_.register(Commands.literal("qtdebug") + .requires((p_137812_) -> { + return p_137812_.hasPermission(2); + }) + .then(Commands.argument("pos", BlockPosArgument.blockPos()) + .executes(css -> { + return startDebug(css.getSource(), BlockPosArgument.getLoadedBlockPos(css, "pos")); + }))); + } + + private static int startDebug( + CommandSourceStack source, + BlockPos target + ) { + BlockEntity e = source.getLevel().getBlockEntity(target); + if (!(e instanceof TownFlagBlockEntity tfbe)) { + // TODO: Better error handling? + return -1; + } + + tfbe.toggleDebugMode(); + return 0; + } +} diff --git a/src/main/java/ca/bradj/questown/commands/DebugDoorsCommand.java b/src/main/java/ca/bradj/questown/commands/DebugDoorsCommand.java new file mode 100644 index 00000000..ab6ef133 --- /dev/null +++ b/src/main/java/ca/bradj/questown/commands/DebugDoorsCommand.java @@ -0,0 +1,50 @@ +package ca.bradj.questown.commands; + +import ca.bradj.questown.blocks.TownFlagBlock; +import ca.bradj.questown.core.init.items.ItemsInit; +import ca.bradj.questown.town.TownFlagBlockEntity; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.coordinates.BlockPosArgument; +import net.minecraft.core.BlockPos; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class DebugDoorsCommand { + public static void register(CommandDispatcher p_137808_) { + p_137808_.register(Commands.literal("qtdebugdoors") + .requires((p_137812_) -> { + return p_137812_.hasPermission(2); + }) + .then(Commands.argument("pos", BlockPosArgument.blockPos()) + .executes(css -> { + return giveDebug(css.getSource(), BlockPosArgument.getLoadedBlockPos(css, "pos")); + }))); + } + + private static int giveDebug( + CommandSourceStack source, + BlockPos target + ) { + BlockEntity e = source.getLevel().getBlockEntity(target); + if (!(e instanceof TownFlagBlockEntity tfbe)) { + // TODO: Better error handling? + return -1; + } + + ItemStack debugItem = ItemsInit.TOWN_DOOR_TESTER.get() + .getDefaultInstance(); + TownFlagBlock.StoreParentOnNBT( + debugItem, + target + ); + try { + source.getPlayerOrException().getInventory().add(debugItem); + } catch (CommandSyntaxException ex) { + throw new RuntimeException(ex); + } + return 0; + } +} diff --git a/src/main/java/ca/bradj/questown/core/Config.java b/src/main/java/ca/bradj/questown/core/Config.java index 3fd1b9e9..09708c41 100644 --- a/src/main/java/ca/bradj/questown/core/Config.java +++ b/src/main/java/ca/bradj/questown/core/Config.java @@ -55,6 +55,7 @@ public class Config { public static final ForgeConfigSpec.ConfigValue JOB_BOARD_ENABLED; public static final ForgeConfigSpec.ConfigValue CRASH_ON_FAILED_WARP; public static final ForgeConfigSpec.ConfigValue TIME_WARP_MAX_TICKS; + public static final ForgeConfigSpec.ConfigValue LOG_WARP_RESULT; public static final ForgeConfigSpec.ConfigValue BASE_MAX_LOOP; public static final ForgeConfigSpec.ConfigValue MAX_TICKS_WITHOUT_SUPPLIES; public static final ForgeConfigSpec.ConfigValue BASE_FULLNESS; @@ -65,7 +66,8 @@ public class Config { public static final ForgeConfigSpec.ConfigValue NEUTRAL_MOOD; public static final ForgeConfigSpec.ConfigValue MOOD_EFFECT_DURATION_ATE_UNCOMFORTABLY; public static final ForgeConfigSpec.ConfigValue MOOD_EFFECT_DURATION_ATE_COMFORTABLY; - + public static final ForgeConfigSpec.ConfigValue MAX_ROOM_DIMENSION; + public static final ForgeConfigSpec.ConfigValue MAX_ROOM_SCAN_ITERATIONS; static { // Scanning Config @@ -86,6 +88,12 @@ public class Config { BIOME_SCAN_RADIUS = BUILDER.comment( "The radius of chunks that will be scanned outward (in a plus shape) from the flag for the purpose of populating gatherer loot" ).defineInRange("BiomeScanRadius", 20, 0, 100); + MAX_ROOM_DIMENSION = BUILDER.comment( + "The maximum length or width of a room that can be detected" + ).defineInRange("MaxRoomDimension", 20, 2, 100); + MAX_ROOM_SCAN_ITERATIONS = BUILDER.comment( + "The maximum number of ticks that will be spent searching for town rooms" + ).defineInRange("MaxRoomScanIterations", 1000, 1, 24000); BUILDER.pop(); // Quests Config @@ -212,6 +220,9 @@ public class Config { "The radius of \"Meta-Rooms\" that exist around points of interest in the town. E.g." + "There is a meta room around the town flag itself, and one around each welcome mat" ).defineInRange("MetaRoomDiameter", 2, 1, 100); + BLOCK_CLAIMS_TICK_LIMIT = BUILDER.comment( + "If a job claims a block. It will hold that claim for this many ticks. (Or until they finish their work, whatever happens first)" + ).defineInRange("BlockClaimsTickLimit", 1000L, 1, 24000); // Time Warp BUILDER.push("TimeWarp").comment( @@ -223,9 +234,9 @@ public class Config { TIME_WARP_MAX_TICKS = BUILDER.comment( "Since the player can be gone for a very long time, we enforce a maximum warp to prevent the warp taking too long to compute." ).defineInRange("MaxTicks", 200000, 1, Integer.MAX_VALUE); - BLOCK_CLAIMS_TICK_LIMIT = BUILDER.comment( - "If a job claims a block. It will hold that claim for this many ticks. (Or until they finish their work, whatever happens first)" - ).defineInRange("BlockClaimsTickLimit", 1000L, 1, 24000); + LOG_WARP_RESULT = BUILDER.comment( + "Set true to enable logging of the town state after a time warp. This produces a large log message and may cause lag." + ).define("LogWarpResult", false); BUILDER.pop(); // Yep, really thrice. Getting out of nested config BUILDER.pop(); BUILDER.pop(); diff --git a/src/main/java/ca/bradj/questown/core/init/items/ItemsInit.java b/src/main/java/ca/bradj/questown/core/init/items/ItemsInit.java index 2ac90617..4c211fc2 100644 --- a/src/main/java/ca/bradj/questown/core/init/items/ItemsInit.java +++ b/src/main/java/ca/bradj/questown/core/init/items/ItemsInit.java @@ -79,6 +79,11 @@ public class ItemsInit { TownDoorItem::new ); + public static final RegistryObject TOWN_DOOR_TESTER = ITEMS.register( + TownDoorTestItem.ITEM_ID, + TownDoorTestItem::new + ); + public static final RegistryObject FALSE_DOOR = ITEMS.register( FalseDoorItem.ITEM_ID, FalseDoorItem::new diff --git a/src/main/java/ca/bradj/questown/integration/minecraft/MCContainer.java b/src/main/java/ca/bradj/questown/integration/minecraft/MCContainer.java index 5d514731..aec0af5a 100644 --- a/src/main/java/ca/bradj/questown/integration/minecraft/MCContainer.java +++ b/src/main/java/ca/bradj/questown/integration/minecraft/MCContainer.java @@ -85,9 +85,9 @@ public String toShortString() { @Override public String toString() { - Collection items = new ArrayList<>(); + Collection items = new ArrayList<>(); for (int i = 0; i < container.getContainerSize(); i++) { - items.add(getItem(i)); + items.add(getItem(i).getShortName()); } return "MCContainer{" + "container.items=" + items + diff --git a/src/main/java/ca/bradj/questown/integration/minecraft/MCTownItem.java b/src/main/java/ca/bradj/questown/integration/minecraft/MCTownItem.java index 739d1cd7..f8381e7d 100644 --- a/src/main/java/ca/bradj/questown/integration/minecraft/MCTownItem.java +++ b/src/main/java/ca/bradj/questown/integration/minecraft/MCTownItem.java @@ -114,6 +114,9 @@ public String getShortName() { if (registryName != null) { name = registryName.toString(); } + if (quantity > 1) { + name = String.format("%sx%s", quantity, name); + } return name; } diff --git a/src/main/java/ca/bradj/questown/items/TownDoorTestItem.java b/src/main/java/ca/bradj/questown/items/TownDoorTestItem.java new file mode 100644 index 00000000..d53e0b8c --- /dev/null +++ b/src/main/java/ca/bradj/questown/items/TownDoorTestItem.java @@ -0,0 +1,53 @@ +package ca.bradj.questown.items; + +import ca.bradj.questown.Questown; +import ca.bradj.questown.blocks.TownFlagBlock; +import ca.bradj.questown.town.TownFlagBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.DoorBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.DoubleBlockHalf; +import org.jetbrains.annotations.NotNull; + +public class TownDoorTestItem extends Item { + public static final String ITEM_ID = "town_door_tester"; + + public TownDoorTestItem() { + super(Questown.DEFAULT_ITEM_PROPS); + } + + @Override + public InteractionResult useOn(net.minecraft.world.item.context.UseOnContext ctx) { + return startDebug(ctx); + } + + @NotNull + private static InteractionResult startDebug(net.minecraft.world.item.context.UseOnContext ctx) { + if (ctx.getLevel() + .isClientSide()) { + return InteractionResult.PASS; + } + TownFlagBlockEntity parent = TownFlagBlock.GetParentFromNBT((ServerLevel) ctx.getLevel(), ctx.getItemInHand()); + BlockPos clickedPos = ctx.getClickedPos(); + BlockState bs = parent.getServerLevel() + .getBlockState(clickedPos); + if (bs.getBlock() instanceof DoorBlock) { + if (DoubleBlockHalf.UPPER.equals(bs.getValue(DoorBlock.HALF))) { + clickedPos = clickedPos.below(); + } + } else { + bs = parent.getServerLevel() + .getBlockState(clickedPos.above()); + if (bs.getBlock() instanceof DoorBlock) { + clickedPos = clickedPos.above(); + } + } + parent.startDebugTask(parent.getRoomHandle() + .getDebugTaskForDoor(clickedPos)); + return InteractionResult.CONSUME; + } + +} diff --git a/src/main/java/ca/bradj/questown/jobs/declarative/DinerWork.java b/src/main/java/ca/bradj/questown/jobs/declarative/DinerWork.java index d142cd62..a989e190 100644 --- a/src/main/java/ca/bradj/questown/jobs/declarative/DinerWork.java +++ b/src/main/java/ca/bradj/questown/jobs/declarative/DinerWork.java @@ -90,7 +90,7 @@ public static Work asWork( new ExpirationRules( () -> Long.MAX_VALUE, jobId -> jobId, - Config.MAX_TICKS_WITHOUT_DINING_TABLE, + Util.configGet(Config.MAX_TICKS_WITHOUT_DINING_TABLE), jobId -> DinerNoTableWork.getIdForRoot(jobId.rootId()) ) ); diff --git a/src/main/java/ca/bradj/questown/mc/Util.java b/src/main/java/ca/bradj/questown/mc/Util.java index 89a84080..e08d07cc 100644 --- a/src/main/java/ca/bradj/questown/mc/Util.java +++ b/src/main/java/ca/bradj/questown/mc/Util.java @@ -16,6 +16,7 @@ import net.minecraft.world.MenuProvider; import net.minecraft.world.inventory.MenuType; import net.minecraft.world.level.block.Block; +import net.minecraftforge.common.ForgeConfigSpec; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; import net.minecraftforge.network.NetworkEvent; import net.minecraftforge.network.NetworkHooks; @@ -122,4 +123,8 @@ public static ImmutableMap realize(ImmutableMap b.put(k, v.get())); return b.build(); } + + public static Supplier configGet(ForgeConfigSpec.ConfigValue cfg) { + return cfg::get; + } } diff --git a/src/main/java/ca/bradj/questown/town/TownFlagBlockEntity.java b/src/main/java/ca/bradj/questown/town/TownFlagBlockEntity.java index e7f53547..80ec9f11 100644 --- a/src/main/java/ca/bradj/questown/town/TownFlagBlockEntity.java +++ b/src/main/java/ca/bradj/questown/town/TownFlagBlockEntity.java @@ -45,7 +45,6 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; -import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; @@ -78,12 +77,15 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import static ca.bradj.questown.town.TownFlagState.NBT_TIME_WARP_REFERENCE_TICK; import static ca.bradj.questown.town.TownFlagState.NBT_TOWN_STATE; -public class TownFlagBlockEntity extends BlockEntity implements TownInterface, ActiveRecipes.ChangeListener>, QuestBatch.ChangeListener, TownPois.Listener { +public class TownFlagBlockEntity extends BlockEntity implements TownInterface, + ActiveRecipes.ChangeListener>, QuestBatch.ChangeListener, + TownPois.Listener { public static final String ID = "flag_base_block_entity"; // TODO: Extract serialization @@ -127,6 +129,8 @@ public class TownFlagBlockEntity extends BlockEntity implements TownInterface, A private final TownRoomsHandle roomsHandle = new TownRoomsHandle(); private final TownVillagerHandle villagerHandle = new TownVillagerHandle(); + private @Nullable Supplier debugTask; + private boolean debugMode; public TownFlagBlockEntity( BlockPos p_155229_, @@ -152,7 +156,8 @@ public static void tick( QT.FLAG_LOGGER.error("No players detected in world"); return; } - double distToPlayer = nearestPlayer.blockPosition().distSqr(e.worldPosition); + double distToPlayer = nearestPlayer.blockPosition() + .distSqr(e.worldPosition); if (distToPlayer > Config.TOWN_TICK_RADIUS.get()) { if (!stopped) { QT.FLAG_LOGGER.info( @@ -177,8 +182,20 @@ public static void tick( return; } + // Must tick sub-blocks even with debug mode enabled, + // because non-ticked sub-blocks will self-destruct. e.subBlocks.parentTick(sl); + if (e.debugMode) { + if (e.debugTask != null) { + boolean done = e.debugTask.get(); + if (done) { + e.debugTask = null; + } + } + return; + } + if (!e.mornings.empty()) { e.morningTick(e.mornings.pop()); } @@ -196,18 +213,18 @@ public static void tick( e.workHandle.tick(sl); e.quests.tick(e); - long gameTime = level.getGameTime(); - long l = gameTime % 10; - if (l != 0) { - return; - } - if (e.nearbyBiomes.isEmpty()) { computeNearbyBiomes(level, blockPos, e); } e.roomsHandle.tick(sl, blockPos); + long gameTime = level.getGameTime(); + long l = gameTime % 10; + if (l != 0) { + return; + } + Collection allRooms = e.roomsHandle.getAllRoomsIncludingMeta(); e.jobHandle.tick(sl, allRooms); e.jobHandles.forEach((k, v) -> v.tick(sl, allRooms)); @@ -229,12 +246,14 @@ private static void computeNearbyBiomes( TownFlagBlockEntity e ) { ChunkPos here = new ChunkPos(blockPos); - Biome value = level.getBiome(blockPos).value(); + Biome value = level.getBiome(blockPos) + .value(); e.nearbyBiomes.add(value); for (Direction d : Direction.Plane.HORIZONTAL) { for (int i = 0; i < Config.BIOME_SCAN_RADIUS.get(); i++) { ChunkPos there = new ChunkPos(here.x + d.getStepX() * i, here.z + d.getStepZ() * i); - Biome biome = level.getBiome(there.getMiddleBlockPosition(blockPos.getY())).value(); + Biome biome = level.getBiome(there.getMiddleBlockPosition(blockPos.getY())) + .value(); e.nearbyBiomes.add(biome); } } @@ -248,7 +267,8 @@ private void morningTick(Long newTime) { this.setChanged(); villagerHandle.forEach(LivingEntity::stopSleeping); villagerHandle.makeAllTotallyHungry(); - Util.getBlockStoredTagData(this).putLong(NBT_TIME_WARP_REFERENCE_TICK, newTime); + Util.getBlockStoredTagData(this) + .putLong(NBT_TIME_WARP_REFERENCE_TICK, newTime); } private static void profileTick( @@ -262,7 +282,10 @@ private static void profileTick( if (e.times.size() > Config.TICK_SAMPLING_RATE.get()) { QT.PROFILE_LOGGER.debug( "Average tick length: {}", - e.times.stream().mapToInt(Integer::intValue).average() + e.times.stream() + .mapToInt(Integer::intValue) + .average() + .getAsDouble() ); e.times.clear(); } @@ -322,7 +345,8 @@ private static void loadNextTick(Queue> i initializers.add(t -> { CompoundTag tag = Util.getBlockStoredTagData(t); if (tag.contains(NBT_ROOMS)) { - TownRoomsMapSerializer.INSTANCE.deserialize(tag.getCompound(NBT_ROOMS), t, t.roomsHandle.getRegisteredRooms()); + TownRoomsMapSerializer.INSTANCE.deserialize( + tag.getCompound(NBT_ROOMS), t, t.roomsHandle.getRegisteredRooms()); } return true; }); @@ -387,13 +411,17 @@ private static void loadNextTick(Queue> i t.villagerHandle.associate(t); } t.villagerHandle.addHungryListener(e -> { - if (t.getVillagerHandle().isDining(e.getUUID())) { + if (t.getVillagerHandle() + .isDining(e.getUUID())) { return; } - if (!t.getVillagerHandle().canDine(e.getUUID())) { + if (!t.getVillagerHandle() + .canDine(e.getUUID())) { return; } - t.changeJobForVisitor(e.getUUID(), DinerWork.getIdForRoot(e.getJobId().rootId())); + t.changeJobForVisitor( + e.getUUID(), DinerWork.getIdForRoot(e.getJobId() + .rootId())); }); t.villagerHandle.addStatsListener(s -> t.setChanged()); return true; @@ -419,7 +447,10 @@ public void writeTownData(CompoundTag tag) { tag.put(NBT_ROOMS, TownRoomsMapSerializer.INSTANCE.serializeNBT(roomsHandle.getRegisteredRooms())); tag.put(NBT_JOBS, TownWorkHandleSerializer.INSTANCE.serializeNBT(workHandle)); QTNBT.put(tag, NBT_KNOWLEDGE, TownKnowledgeStoreSerializer.INSTANCE.serializeNBT(knowledgeHandle)); - QTNBT.put(tag, NBT_VILLAGERS, TownVillagerHandle.SERIALIZER.serialize(villagerHandle, Util.getTick(getServerLevel()))); + QTNBT.put( + tag, NBT_VILLAGERS, + TownVillagerHandle.SERIALIZER.serialize(villagerHandle, Util.getTick(getServerLevel())) + ); // TODO: Serialization for ASAPss } @@ -479,19 +510,20 @@ private void updateWorkersAfterRequestChange() { prefix )); villagerHandle.stream() - .filter(v -> v instanceof VisitorMobEntity) - .map(v -> (VisitorMobEntity) v) - .filter(e -> { - for (WorkRequest r : workHandle.getRequestedResults()) { - if (JobsRegistry.canSatisfy(td, e.getJobId(), r.asIngredient())) { - if (e.getStatusForServer().isBusy()) { - return false; - } - } - } - return true; - }) - .forEach(e -> changeJobForVisitor(e.getUUID(), WorkSeekerJob.getIDForRoot(e.getJobId()))); + .filter(v -> v instanceof VisitorMobEntity) + .map(v -> (VisitorMobEntity) v) + .filter(e -> { + for (WorkRequest r : workHandle.getRequestedResults()) { + if (JobsRegistry.canSatisfy(td, e.getJobId(), r.asIngredient())) { + if (e.getStatusForServer() + .isBusy()) { + return false; + } + } + } + return true; + }) + .forEach(e -> changeJobForVisitor(e.getUUID(), WorkSeekerJob.getIDForRoot(e.getJobId()))); } @Override @@ -522,11 +554,12 @@ public void addImmediateReward(MCReward r) { private void grantAdvancementOnApproach() { MinecraftForge.EVENT_BUS.addListener((EntityEvent.EnteringSection event) -> { if (event.getEntity() instanceof ServerPlayer sp) { - double v = event.getEntity().distanceToSqr( - this.worldPosition.getX() + 0.5D, - this.worldPosition.getY() + 0.5D, - this.worldPosition.getZ() + 0.5D - ); + double v = event.getEntity() + .distanceToSqr( + this.worldPosition.getX() + 0.5D, + this.worldPosition.getY() + 0.5D, + this.worldPosition.getZ() + 0.5D + ); if (v < 100) { AdvancementsInit.APPROACH_TOWN_TRIGGER.trigger( sp, ApproachTownTrigger.Triggers.FirstVisit @@ -562,7 +595,9 @@ void broadcastMessage( Object... args ) { QT.FLAG_LOGGER.info("Broadcasting message: {} {}", key, args); - for (ServerPlayer p : level.getServer().getPlayerList().getPlayers()) { + for (ServerPlayer p : level.getServer() + .getPlayerList() + .getPlayers()) { p.displayClientMessage(Util.translatable(key, args), false); } } @@ -580,7 +615,8 @@ public void roomRecipeCreated( broadcastMessage( "messages.building.recipe_created", RoomRecipes.getName(match.getRecipeID()), - roomDoorPos.getDoorPos().getUIString() + roomDoorPos.getDoorPos() + .getUIString() ); // TODO: get room for rendering effect // handleRoomChange(room, ParticleTypes.HAPPY_VILLAGER); @@ -604,16 +640,22 @@ private Void swapJobBoardSign( ServerLevel level, RoomRecipeMatch room ) { - BlockPredicate predicate = BlockPredicate.Builder.block().of(BlockTags.SIGNS).build(); - for (Map.Entry e : room.getContainedBlocks().entrySet()) { + BlockPredicate predicate = BlockPredicate.Builder.block() + .of(BlockTags.SIGNS) + .build(); + for (Map.Entry e : room.getContainedBlocks() + .entrySet()) { if (!predicate.matches(level, e.getKey())) { continue; } // TODO: Get direction // Direction value = level.getBlockState(e.getKey()).getValue(HorizontalDirectionalBlock.FACING); Direction value = Direction.Plane.HORIZONTAL.getRandomDirection(level.getRandom()); - level.setBlockAndUpdate(e.getKey(), BlocksInit.JOB_BOARD_BLOCK.get().defaultBlockState() - .setValue(HorizontalDirectionalBlock.FACING, value) + level.setBlockAndUpdate( + e.getKey(), BlocksInit.JOB_BOARD_BLOCK.get() + .defaultBlockState() + .setValue( + HorizontalDirectionalBlock.FACING, value) ); registerJobsBoard(e.getKey()); jobHandle.setJobBlockState(e.getKey(), AbstractWorkStatusStore.State.freshAtState(WorkSeekerJob.MAX_STATE)); @@ -634,7 +676,8 @@ public void roomRecipeChanged( "messages.building.room_changed", Util.translatable("room." + oldMatchID.getPath()), Util.translatable("room." + newMatchID.getPath()), - newRoom.getDoorPos().getUIString() + newRoom.getDoorPos() + .getUIString() ); TownRooms.addParticles(getServerLevel(), newRoom, ParticleTypes.HAPPY_VILLAGER); if (oldMatch == null && newMatch != null) { @@ -664,8 +707,10 @@ public void roomRecipeDestroyed( ) { broadcastMessage( "messages.building.room_destroyed", - Util.translatable("room." + oldRecipeId.getRecipeID().getPath()), - roomDoorPos.getDoorPos().getUIString() + Util.translatable("room." + oldRecipeId.getRecipeID() + .getPath()), + roomDoorPos.getDoorPos() + .getUIString() ); TownRooms.addParticles(getServerLevel(), roomDoorPos, ParticleTypes.SMOKE); quests.markQuestAsLost(roomDoorPos, oldRecipeId.getRecipeID()); @@ -683,7 +728,8 @@ public void questCompleted(MCQuest quest) { getBlockPos().getX(), getBlockPos().getY() + 10, getBlockPos().getZ(), - new ItemStack(Items.FIREWORK_ROCKET.getDefaultInstance().getItem(), 3) + new ItemStack(Items.FIREWORK_ROCKET.getDefaultInstance() + .getItem(), 3) ); level.addFreshEntity(firework); } @@ -750,7 +796,9 @@ private JobID getVillagerPreferredWork( UUID uuid, Collection requestedResults ) { - Optional f = villagerHandle.stream().filter(v -> uuid.equals(v.getUUID())).findFirst(); + Optional f = villagerHandle.stream() + .filter(v -> uuid.equals(v.getUUID())) + .findFirst(); if (f.isEmpty()) { QT.BLOCK_LOGGER.error("No entities found for UUID: {}", uuid); return null; @@ -784,7 +832,9 @@ private JobID getVillagerPreferredWork( // job option. for (JobID p : preference) { - List i = requestedResults.stream().map(WorkRequest::asIngredient).toList(); + List i = requestedResults.stream() + .map(WorkRequest::asIngredient) + .toList(); for (Ingredient requestedResult : i) { // TODO: Think about how work chains work. // E.g. If a blacksmith needs iron ingots to do a requested job, @@ -814,10 +864,11 @@ public void changeJobForVisitor( boolean announce ) { Optional f = villagerHandle.stream() - .filter(v -> v instanceof VisitorMobEntity) - .map(v -> (VisitorMobEntity) v) - .filter(v -> v.getUUID().equals(visitorUUID)) - .findFirst(); + .filter(v -> v instanceof VisitorMobEntity) + .map(v -> (VisitorMobEntity) v) + .filter(v -> v.getUUID() + .equals(visitorUUID)) + .findFirst(); if (f.isEmpty()) { QT.FLAG_LOGGER.error("Could not find entity {} to apply job change: {}", visitorUUID, jobID); } else { @@ -835,7 +886,8 @@ private void doSetJob( JobID jobName, VisitorMobEntity f ) { - f.setJob(JobsRegistry.getInitializedJob(this, jobName, f.getJobJournalSnapshot().items(), visitorUUID)); + f.setJob(JobsRegistry.getInitializedJob(this, jobName, f.getJobJournalSnapshot() + .items(), visitorUUID)); } @Override @@ -844,7 +896,8 @@ private void doSetJob( return null; } List villagers = ImmutableList.copyOf(getVillagers()); - return villagers.get(getServerLevel().getRandom().nextInt(villagers.size())); + return villagers.get(getServerLevel().getRandom() + .nextInt(villagers.size())); } @Override @@ -855,11 +908,11 @@ public boolean isVillagerMissing(UUID uuid) { @Override public Collection getUnemployedVillagers() { return villagerHandle.stream() - .filter(v -> v instanceof VisitorMobEntity) - .map(v -> (VisitorMobEntity) v) - .filter(VisitorMobEntity::canAcceptJob) - .map(Entity::getUUID) - .toList(); + .filter(v -> v instanceof VisitorMobEntity) + .map(v -> (VisitorMobEntity) v) + .filter(VisitorMobEntity::canAcceptJob) + .map(Entity::getUUID) + .toList(); } @Override @@ -903,7 +956,9 @@ public void addBatchOfQuests( @Override public ImmutableSet getVillagers() { - return ImmutableSet.copyOf(villagerHandle.stream().map(Entity::getUUID).collect(Collectors.toSet())); + return ImmutableSet.copyOf(villagerHandle.stream() + .map(Entity::getUUID) + .collect(Collectors.toSet())); } @Override @@ -1001,30 +1056,33 @@ public Optional assignToFarm(UUID ownerUUID) { @Override public Optional getBiggestFarm() { - return roomsHandle.getFarms().stream().max(Comparator.comparingInt( - v -> v.getSpaces() - .stream() - .map(InclusiveSpaces::calculateArea) - .mapToInt(Double::intValue) - .sum() - )); + return roomsHandle.getFarms() + .stream() + .max(Comparator.comparingInt( + v -> v.getSpaces() + .stream() + .map(InclusiveSpaces::calculateArea) + .mapToInt(Double::intValue) + .sum() + )); } @Override public Collection getAvailableRootJobs() { // TODO: Scan villagers to make this decision - Set allJobs = JobsRegistry.getAllJobs().stream() - .map(JobID::rootId) - .collect(Collectors.toSet()); + Set allJobs = JobsRegistry.getAllJobs() + .stream() + .map(JobID::rootId) + .collect(Collectors.toSet()); Set allFilledJobs = villagerHandle.stream() - .filter(v -> v instanceof VisitorMobEntity) - .map(v -> (VisitorMobEntity) v) - .map(VisitorMobEntity::getJobId) - .map(JobID::rootId) - .collect(Collectors.toSet()); + .filter(v -> v instanceof VisitorMobEntity) + .map(v -> (VisitorMobEntity) v) + .map(VisitorMobEntity::getJobId) + .map(JobID::rootId) + .collect(Collectors.toSet()); Set allNewJobs = allJobs.stream() - .filter(v -> !allFilledJobs.contains(v)) - .collect(Collectors.toSet()); + .filter(v -> !allFilledJobs.contains(v)) + .collect(Collectors.toSet()); if (allNewJobs.isEmpty()) { allNewJobs = allJobs; } @@ -1042,7 +1100,8 @@ public ResourceLocation getRandomNearbyBiome() { if (nearbyBiomes.isEmpty()) { computeNearbyBiomes(level, getBlockPos(), this); } - Biome biome = nearbyBiomes.get(getServerLevel().getRandom().nextInt(nearbyBiomes.size())); + Biome biome = nearbyBiomes.get(getServerLevel().getRandom() + .nextInt(nearbyBiomes.size())); return ForgeRegistries.BIOMES.getKey(biome); } @@ -1081,18 +1140,22 @@ public void assumeStateFromTown( VisitorMobEntity visitorMobEntity, ServerLevel sl ) { - if (!Util.getBlockStoredTagData(this).contains(NBT_TOWN_STATE)) { + if (!Util.getBlockStoredTagData(this) + .contains(NBT_TOWN_STATE)) { QT.FLAG_LOGGER.error( "Villager entity exists but town state is missing. This is a bug and may cause unexpected behaviour."); return; } MCTownState state = TownStateSerializer.INSTANCE.load( - Util.getBlockStoredTagData(this).getCompound(NBT_TOWN_STATE), - sl, bp -> this.pois.getWelcomeMats().contains(bp) + Util.getBlockStoredTagData(this) + .getCompound(NBT_TOWN_STATE), + sl, bp -> this.pois.getWelcomeMats() + .contains(bp) ); Optional> match = state.villagers.stream() - .filter(v -> v.uuid.equals(visitorMobEntity.getUUID())) - .findFirst(); + .filter(v -> v.uuid.equals( + visitorMobEntity.getUUID())) + .findFirst(); if (match.isEmpty()) { QT.FLAG_LOGGER.error( "Villager entity exists but is not present on town state. This is a bug and may cause unexpected behaviour."); @@ -1158,11 +1221,12 @@ public Collection getKnownBiomes() { List> cs = TownContainers.getAllContainers(this, getServerLevel()); cs.forEach(v -> { v.getItems() - .stream() - .filter(i -> ItemsInit.GATHERER_MAP.get().equals(i.get())) - .map(i -> GathererMap.getBiome(i.toItemStack())) - .filter(Objects::nonNull) - .forEach(b::add); + .stream() + .filter(i -> ItemsInit.GATHERER_MAP.get() + .equals(i.get())) + .map(i -> GathererMap.getBiome(i.toItemStack())) + .filter(Objects::nonNull) + .forEach(b::add); }); nearbyBiomes.forEach(v -> { ResourceLocation key = ForgeRegistries.BIOMES.getKey(v); @@ -1181,9 +1245,9 @@ public void warpTime(int ticks) { public void freezeVillagers(Integer ticks) { villagerHandle.stream() - .filter(VisitorMobEntity.class::isInstance) - .map(VisitorMobEntity.class::cast) - .forEach(v -> v.freeze(ticks)); + .filter(VisitorMobEntity.class::isInstance) + .map(VisitorMobEntity.class::cast) + .forEach(v -> v.freeze(ticks)); } public VillagerHolder getVillagerHandle() { @@ -1193,4 +1257,13 @@ public VillagerHolder getVillagerHandle() { public int getY() { return getTownFlagBasePos().getY(); } + + public void startDebugTask(Supplier debugTask) { + this.debugTask = debugTask; + } + + public void toggleDebugMode() { + this.debugMode = !this.debugMode; + broadcastMessage("message.debug_mode", this.debugMode ? "enabled" : "disabled"); + } } diff --git a/src/main/java/ca/bradj/questown/town/TownFlagState.java b/src/main/java/ca/bradj/questown/town/TownFlagState.java index fc696d5f..d7ddd374 100644 --- a/src/main/java/ca/bradj/questown/town/TownFlagState.java +++ b/src/main/java/ca/bradj/questown/town/TownFlagState.java @@ -154,7 +154,9 @@ static MCTownState advanceTime( long after = System.currentTimeMillis(); - QT.FLAG_LOGGER.debug("State after warp of {}: {}", ticksPassed, liveState); + if (Config.LOG_WARP_RESULT.get()) { + QT.FLAG_LOGGER.info("State after warp of {}: {}", ticksPassed, liveState); + } QT.FLAG_LOGGER.debug("Warp took {} milliseconds", after - before); return new MCTownState( diff --git a/src/main/java/ca/bradj/questown/town/TownRoomsHandle.java b/src/main/java/ca/bradj/questown/town/TownRoomsHandle.java index b6fbadad..c80864a2 100644 --- a/src/main/java/ca/bradj/questown/town/TownRoomsHandle.java +++ b/src/main/java/ca/bradj/questown/town/TownRoomsHandle.java @@ -1,5 +1,6 @@ package ca.bradj.questown.town; +import ca.bradj.questown.QT; import ca.bradj.questown.core.Config; import ca.bradj.questown.roomrecipes.Spaces; import ca.bradj.questown.town.interfaces.RoomsHolder; @@ -8,6 +9,9 @@ import ca.bradj.questown.town.special.SpecialQuests; import ca.bradj.roomrecipes.adapter.Positions; import ca.bradj.roomrecipes.adapter.RoomRecipeMatch; +import ca.bradj.roomrecipes.core.Room; +import ca.bradj.roomrecipes.core.space.Position; +import ca.bradj.roomrecipes.logic.LevelRoomDetector; import ca.bradj.roomrecipes.recipes.ActiveRecipes; import ca.bradj.roomrecipes.recipes.RecipeDetection; import ca.bradj.roomrecipes.serialization.MCRoom; @@ -26,10 +30,13 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.concurrent.LinkedBlockingQueue; import java.util.function.Function; import java.util.function.Supplier; -public class TownRoomsHandle implements RoomsHolder, ActiveRecipes.ChangeListener>, Supplier { +public class TownRoomsHandle implements RoomsHolder, ActiveRecipes.ChangeListener>, + Supplier { private final TownRoomsMap roomsMap = new TownRoomsMap(this); @Nullable @@ -83,24 +90,37 @@ private List> getWelcomeMatMetaRooms(@NotNull TownFlagBl return t.getWelcomeMats() .stream() .map(p -> Spaces.metaRoomAround(p, Config.META_ROOM_DIAMETER.get())) - .map(v -> new RoomRecipeMatch<>(v, SpecialQuests.TOWN_GATE, fn.apply(v).entrySet())) + .map(v -> new RoomRecipeMatch<>( + v, SpecialQuests.TOWN_GATE, fn.apply(v) + .entrySet())) .toList(); } @Override - public void roomRecipeCreated(MCRoom mcRoom, RoomRecipeMatch mcRoomRoomRecipeMatch) { + public void roomRecipeCreated( + MCRoom mcRoom, + RoomRecipeMatch mcRoomRoomRecipeMatch + ) { @NotNull TownFlagBlockEntity t = unsafeGetTown(); t.roomRecipeCreated(mcRoom, mcRoomRoomRecipeMatch); } @Override - public void roomRecipeChanged(MCRoom mcRoom, RoomRecipeMatch mcRoomRoomRecipeMatch, MCRoom room1, RoomRecipeMatch key1) { + public void roomRecipeChanged( + MCRoom mcRoom, + RoomRecipeMatch mcRoomRoomRecipeMatch, + MCRoom room1, + RoomRecipeMatch key1 + ) { @NotNull TownFlagBlockEntity t = unsafeGetTown(); t.roomRecipeChanged(mcRoom, mcRoomRoomRecipeMatch, room1, key1); } @Override - public void roomRecipeDestroyed(MCRoom mcRoom, RoomRecipeMatch mcRoomRoomRecipeMatch) { + public void roomRecipeDestroyed( + MCRoom mcRoom, + RoomRecipeMatch mcRoomRoomRecipeMatch + ) { @NotNull TownFlagBlockEntity t = unsafeGetTown(); t.roomRecipeDestroyed(mcRoom, mcRoomRoomRecipeMatch); } @@ -114,7 +134,10 @@ public TownRoomsMap getRegisteredRooms() { return roomsMap; } - void tick(ServerLevel sl, BlockPos blockPos) { + void tick( + ServerLevel sl, + BlockPos blockPos + ) { roomsMap.tick(sl, blockPos); } @@ -136,7 +159,8 @@ public Collection getFarms() { public Collection findMatchedRecipeBlocks(TownInterface.MatchRecipe mr) { ImmutableList.Builder b = ImmutableList.builder(); for (RoomRecipeMatch i : roomsMap.getAllMatches()) { - for (Map.Entry j : i.getContainedBlocks().entrySet()) { + for (Map.Entry j : i.getContainedBlocks() + .entrySet()) { if (mr.doesMatch(j.getValue())) { b.add(j.getKey()); } @@ -147,10 +171,14 @@ public Collection findMatchedRecipeBlocks(TownInterface.MatchRecipe mr boolean hasEnoughBeds(long numVillagers) { // TODO: This returns false positives if called before entities have been loaded from tile data - long beds = roomsMap.getAllMatches().stream() - .flatMap(v -> v.getContainedBlocks().values().stream()) - .filter(v -> Ingredient.of(ItemTags.BEDS).test(new ItemStack(v.asItem()))) - .count(); + long beds = roomsMap.getAllMatches() + .stream() + .flatMap(v -> v.getContainedBlocks() + .values() + .stream()) + .filter(v -> Ingredient.of(ItemTags.BEDS) + .test(new ItemStack(v.asItem()))) + .count(); if (beds == 0 && numVillagers == 0) { return false; } @@ -168,6 +196,57 @@ public void registerDoor(BlockPos clickedPos) { t.setChanged(); } + @Override + public Supplier getDebugTaskForDoor(BlockPos clickedPos) { + @NotNull TownFlagBlockEntity t = unsafeGetTown(); + LinkedBlockingQueue flightRecorder = new LinkedBlockingQueue<>(); + Position clickedRRPOs = Positions.FromBlockPos(clickedPos); + final LevelRoomDetector d = new LevelRoomDetector( + ImmutableList.of(clickedRRPOs), + Config.MAX_ROOM_DIMENSION.get(), + Config.MAX_ROOM_SCAN_ITERATIONS.get(), + p -> WallDetection.IsWall(t.getServerLevel(), p, clickedPos.getY()), + true, + flightRecorder + ); + return () -> { + @Nullable ImmutableMap> done = d.proceed(); + if (done == null) { + return false; + } + flightRecorder.forEach(QT.FLAG_LOGGER::debug); + d.getDebugArt(true).forEach( + (k, v) -> QT.FLAG_LOGGER.debug("Art for {}\n{}", k.getUIString(), v) + ); + Optional room = done.get(clickedRRPOs); + QT.FLAG_LOGGER.debug("Room is {}", room); + room.ifPresent(r -> { + Optional> recipe = t.getRoomHandle() + .computeRecipe(new MCRoom(r.getDoorPos(), + r.getSpaces(), clickedPos.getY() + )); + QT.FLAG_LOGGER.debug("Recipe is {}", recipe); + }); + if (!t.getRoomHandle().isDoorRegistered(clickedPos)) { + QT.FLAG_LOGGER.warn("{} is not registered as a door", clickedPos); + } + return true; + }; + } + + @Override + public boolean isDoorRegistered(BlockPos clickedPos) { + @NotNull TownFlagBlockEntity t = unsafeGetTown(); + return roomsMap.isDoorRegistered(Positions.FromBlockPos(clickedPos), clickedPos.getY() - t.getY()); + } + + public Optional> computeRecipe( + MCRoom r + ) { + @NotNull TownFlagBlockEntity t = unsafeGetTown(); + return roomsMap.computeRecipe(t.getServerLevel(), r, r.yCoord - t.getY()); + } + public void registerFenceGate(BlockPos clickedPos) { @NotNull TownFlagBlockEntity t = unsafeGetTown(); roomsMap.registerFenceGate(Positions.FromBlockPos(clickedPos), clickedPos.getY() - t.getY()); diff --git a/src/main/java/ca/bradj/questown/town/interfaces/RoomsHolder.java b/src/main/java/ca/bradj/questown/town/interfaces/RoomsHolder.java index 141e435a..4e45256e 100644 --- a/src/main/java/ca/bradj/questown/town/interfaces/RoomsHolder.java +++ b/src/main/java/ca/bradj/questown/town/interfaces/RoomsHolder.java @@ -1,11 +1,14 @@ package ca.bradj.questown.town.interfaces; import ca.bradj.roomrecipes.adapter.RoomRecipeMatch; +import ca.bradj.roomrecipes.core.Room; import ca.bradj.roomrecipes.serialization.MCRoom; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceLocation; import java.util.Collection; +import java.util.Optional; +import java.util.function.Supplier; public interface RoomsHolder { Collection> getRoomsMatching(ResourceLocation roomRecipeId); @@ -19,4 +22,10 @@ public interface RoomsHolder { void registerFenceGate(BlockPos above); void registerDoor(BlockPos above); + + Supplier getDebugTaskForDoor(BlockPos clickedPos); + + boolean isDoorRegistered(BlockPos clickedPos); + + Optional> computeRecipe(MCRoom r); } diff --git a/src/main/java/ca/bradj/questown/town/rooms/PendingTownRooms.java b/src/main/java/ca/bradj/questown/town/rooms/PendingTownRooms.java new file mode 100644 index 00000000..d4f7925d --- /dev/null +++ b/src/main/java/ca/bradj/questown/town/rooms/PendingTownRooms.java @@ -0,0 +1,114 @@ +package ca.bradj.questown.town.rooms; + +import ca.bradj.questown.core.Config; +import ca.bradj.questown.town.TownRooms; +import ca.bradj.roomrecipes.adapter.RoomRecipeMatch; +import ca.bradj.roomrecipes.core.Room; +import ca.bradj.roomrecipes.core.space.Position; +import ca.bradj.roomrecipes.logic.LevelRoomDetector; +import ca.bradj.roomrecipes.recipes.ActiveRecipes; +import ca.bradj.roomrecipes.recipes.RecipeDetection; +import ca.bradj.roomrecipes.serialization.MCRoom; +import com.google.common.collect.ImmutableMap; +import net.minecraft.server.level.ServerLevel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Supplier; + +public class PendingTownRooms { + private final ServerLevel level; + private final Position scanAroundPos; + private final int y; + private final Map> foundRooms = new HashMap<>(); + private final LinkedBlockingQueue roomsToScan = new LinkedBlockingQueue<>(); + private final LevelRoomDetector roomDetector; + private final Supplier>> recipes; + private final TownRooms rooms; + private @Nullable Long trueStart; + + public PendingTownRooms( + ServerLevel level, + Position scanAroundPos, + TownRooms rooms, + Supplier>> getRecipes, + int y, + Set doorsAtLevel + ) { + this.level = level; + this.scanAroundPos = scanAroundPos; + this.recipes = getRecipes; + this.y = y; + this.rooms = rooms; + this.roomDetector = new LevelRoomDetector( + doorsAtLevel, + Config.MAX_ROOM_DIMENSION.get(), + Config.MAX_ROOM_SCAN_ITERATIONS.get(), + rooms::IsWall, + false, + null + ); + } + + public boolean proceed() { + if (this.trueStart == null) { + this.trueStart = System.currentTimeMillis(); + } + if (roomDetector.isDone() && roomsToScan.isEmpty() && foundRooms.isEmpty()) { + return true; + } + + if (!roomsToScan.isEmpty()) { + long start = System.currentTimeMillis(); + MCRoom room = roomsToScan.remove(); + Optional> recipe = RecipeDetection.getActiveRecipe( + level, + room, + rooms + ); + ActiveRecipes> rs = recipes.get(); + rs.update( + room, + room, + recipe.orElse(null) + ); + return false; + } + + @Nullable ImmutableMap> result = roomDetector.proceed(); + if (result != null) { + ImmutableMap> build = handleNewRooms(result); + rooms.update(build); + } + return false; + } + + @NotNull + private ImmutableMap> handleNewRooms( + @NotNull ImmutableMap> result + ) { + ImmutableMap.Builder> b = ImmutableMap.builder(); + result.forEach((k, v) -> { + Optional value = v.map(z -> { + MCRoom mcRoom = new MCRoom( + z.getDoorPos(), + z.getSpaces(), + y + ); + return mcRoom; + }); + b.put( + k, + value + ); + value.ifPresent(roomsToScan::add); + }); + ImmutableMap> build = b.build(); + return build; + } +} diff --git a/src/main/java/ca/bradj/questown/town/rooms/TownRoomsMap.java b/src/main/java/ca/bradj/questown/town/rooms/TownRoomsMap.java index bb189472..88869e06 100644 --- a/src/main/java/ca/bradj/questown/town/rooms/TownRoomsMap.java +++ b/src/main/java/ca/bradj/questown/town/rooms/TownRoomsMap.java @@ -1,5 +1,6 @@ package ca.bradj.questown.town.rooms; +import ca.bradj.questown.QT; import ca.bradj.questown.Questown; import ca.bradj.questown.blocks.FalseDoorBlock; import ca.bradj.questown.core.Config; @@ -30,6 +31,7 @@ import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -41,6 +43,7 @@ public class TownRoomsMap implements TownRooms.RecipeRoomChangeListener { private int scanBuffer = 0; private TownRoomsHandle changeListener; private final ArrayList times = new ArrayList<>(); + private final LinkedBlockingQueue pendingRooms = new LinkedBlockingQueue<>(); Set getRegisteredDoors() { return registeredDoors; @@ -77,24 +80,28 @@ private void updateActiveRooms( } List>> array = rooms.entrySet() - .stream() - .map(v -> new AbstractMap.SimpleEntry<>( - v.getKey(), v.getValue().map(z -> new MCRoom( - z.getDoorPos(), z.getSpaces(), scanY - )))) - .toList(); + .stream() + .map(v -> new AbstractMap.SimpleEntry<>( + v.getKey(), v.getValue() + .map(z -> new MCRoom( + z.getDoorPos(), + z.getSpaces(), + scanY + )))) + .toList(); ImmutableMap> mcRooms = ImmutableMap.copyOf(array); ars.update(mcRooms); - ars.getAll().forEach(room -> { - Optional> recipe = RecipeDetection.getActiveRecipe( - level, - room, - ars - ); - ActiveRecipes> rs = activeRecipes.get(scanLevel); - rs.update(room, room, recipe.orElse(null)); - }); + ars.getAll() + .forEach(room -> { + Optional> recipe = RecipeDetection.getActiveRecipe( + level, + room, + ars + ); + ActiveRecipes> rs = activeRecipes.get(scanLevel); + rs.update(room, room, recipe.orElse(null)); + }); } private void updateActiveFarms( @@ -109,12 +116,15 @@ private void updateActiveFarms( registeredDoors, 20, (Position p) -> isFence(level, Positions.ToBlock(p, scanY)) ); List>> array = rooms.entrySet() - .stream() - .map(v -> new AbstractMap.SimpleEntry<>( - v.getKey(), v.getValue().map(z -> new MCRoom( - z.getDoorPos(), z.getSpaces(), scanY - )))) - .toList(); + .stream() + .map(v -> new AbstractMap.SimpleEntry<>( + v.getKey(), v.getValue() + .map(z -> new MCRoom( + z.getDoorPos(), + z.getSpaces(), + scanY + )))) + .toList(); ImmutableMap> mcRooms = ImmutableMap.copyOf(array); ars.update(mcRooms); } @@ -124,8 +134,12 @@ private static boolean isFence( BlockPos bPos ) { BlockState bs = level.getBlockState(bPos); - return Ingredient.of(Tags.Items.FENCES).test(new ItemStack(bs.getBlock().asItem(), 1)) || - Ingredient.of(Tags.Items.FENCE_GATES).test(new ItemStack(bs.getBlock().asItem(), 1)); + return Ingredient.of(Tags.Items.FENCES) + .test(new ItemStack(bs.getBlock() + .asItem(), 1)) || + Ingredient.of(Tags.Items.FENCE_GATES) + .test(new ItemStack(bs.getBlock() + .asItem(), 1)); } private TownRooms getOrCreateRooms(int scanLevel) { @@ -176,18 +190,30 @@ public void tick( BlockPos blockPos ) { registeredDoors.stream() - .filter(tp -> { - BlockPos bp = new BlockPos(tp.x, blockPos.getY() + tp.scanLevel, tp.z); - BlockState bs = level.getBlockState(bp); - return !(bs.getBlock() instanceof DoorBlock || bs.getBlock() instanceof FalseDoorBlock); - }) - .toList() - .forEach(tp -> { - registeredDoors.remove(tp); - Questown.LOGGER.debug("Door was de-registered due to not existing anymore"); - }); + .filter(tp -> { + BlockPos bp = new BlockPos(tp.x, blockPos.getY() + tp.scanLevel, tp.z); + BlockState bs = level.getBlockState(bp); + return !(bs.getBlock() instanceof DoorBlock || bs.getBlock() instanceof FalseDoorBlock); + }) + .toList() + .forEach(tp -> { + registeredDoors.remove(tp); + Questown.LOGGER.debug("Door was de-registered due to not existing anymore"); + }); long start = System.currentTimeMillis(); + + + if (!this.pendingRooms.isEmpty()) { + PendingTownRooms next = this.pendingRooms.remove(); + boolean finished = next.proceed(); + if (!finished) { + this.pendingRooms.add(next); + } + profileTick("PTR.proceed", start); + return; + } + // scanBuffer = (scanBuffer + 1) % 2; // if (scanBuffer == 0) { // TODO: Use a FIFO queue and only run one iteration (y level) per tick @@ -195,44 +221,73 @@ public void tick( // } Position scanAroundPos = Positions.FromBlockPos(blockPos); Set doorsAtZero = registeredDoors.stream() - .filter(v -> v.scanLevel == 0) - .map(p -> new Position(p.x, p.z)) - .collect(Collectors.toSet()); - updateActiveRooms(level, scanAroundPos, 0, blockPos.getY(), doorsAtZero); + .filter(v -> v.scanLevel == 0) + .map(p -> new Position(p.x, p.z)) + .collect(Collectors.toSet()); + + this.pendingRooms.add(new PendingTownRooms( + level, scanAroundPos, + getOrCreateRooms(0), + () -> activeRecipes.get(0), + blockPos.getY(), doorsAtZero + )); if (scanLevel != 0) { Set doorsAtLevel = registeredDoors.stream() - .filter(v -> v.scanLevel == scanLevel) - .map(p -> new Position(p.x, p.z)) - .collect(Collectors.toSet()); - int y = blockPos.offset(0, scanLevel, 0).getY(); - updateActiveRooms(level, scanAroundPos, scanLevel, y, doorsAtLevel); + .filter(v -> v.scanLevel == scanLevel) + .map(p -> new Position(p.x, p.z)) + .collect(Collectors.toSet()); + int y = blockPos.offset(0, scanLevel, 0) + .getY(); + this.pendingRooms.add(new PendingTownRooms( + level, scanAroundPos, + getOrCreateRooms(scanLevel), + () -> activeRecipes.get(scanLevel), + y, doorsAtLevel + )); } - for (int scanLev : registeredDoors.stream().map(v -> v.scanLevel).distinct().toList()) { + for (int scanLev : registeredDoors.stream() + .map(v -> v.scanLevel) + .distinct() + .toList()) { if (scanLev == scanLevel || scanLev == 0) { continue; } Set doorsAtLevel = registeredDoors.stream() - .filter(v -> v.scanLevel == scanLev) - .map(p -> new Position(p.x, p.z)) - .collect(Collectors.toSet()); - int y1 = blockPos.offset(0, scanLev, 0).getY(); - updateActiveRooms(level, null, scanLev, y1, doorsAtLevel); + .filter(v -> v.scanLevel == scanLev) + .map(p -> new Position(p.x, p.z)) + .collect(Collectors.toSet()); + int y1 = blockPos.offset(0, scanLev, 0) + .getY(); + this.pendingRooms.add(new PendingTownRooms( + level, null, + getOrCreateRooms(scanLev), + () -> activeRecipes.get(scanLev), + y1, doorsAtLevel + )); } - for (int scanLev : registeredFenceGates.stream().map(v -> v.scanLevel).distinct().toList()) { + for (int scanLev : registeredFenceGates.stream() + .map(v -> v.scanLevel) + .distinct() + .toList()) { Set doorsAtLevel = registeredFenceGates.stream() - .filter(v -> v.scanLevel == scanLev) - .map(p -> new Position(p.x, p.z)) - .collect(Collectors.toSet()); - int y1 = blockPos.offset(0, scanLev, 0).getY(); + .filter(v -> v.scanLevel == scanLev) + .map(p -> new Position(p.x, p.z)) + .collect(Collectors.toSet()); + int y1 = blockPos.offset(0, scanLev, 0) + .getY(); + // FIXME: Do the farms too updateActiveFarms(level, scanLev, y1, doorsAtLevel); } - profileTick(start); + profileTick("queue+farm", start); } - private void profileTick(long start) { + private void profileTick( + String prefix, + long start + ) { if (Config.TICK_SAMPLING_RATE.get() == 0) { return; } @@ -240,10 +295,17 @@ private void profileTick(long start) { times.add((int) (end - start)); if (times.size() > Config.TICK_SAMPLING_RATE.get()) { - Questown.LOGGER.debug( - "[TownRoomsMap] Average tick length: {}", - times.stream().mapToInt(Integer::intValue).average() - ); + String msg = "[TownRoomsMap:{}] Average tick length: {}"; + OptionalDouble val = times.stream() + .mapToInt(Integer::intValue) + .average(); + if (val.isPresent() && val.getAsDouble() > 10) { + QT.PROFILE_LOGGER.error(msg, prefix, val); + } else if (val.isPresent() && val.getAsDouble() > 1) { + QT.PROFILE_LOGGER.warn(msg, prefix, val); + } else { + QT.PROFILE_LOGGER.debug(msg, prefix, val); + } times.clear(); } } @@ -266,7 +328,11 @@ public void initialize( } public Collection getAllRooms() { - return this.activeRooms.values().stream().map(TownRooms::getAll).flatMap(Collection::stream).toList(); + return this.activeRooms.values() + .stream() + .map(TownRooms::getAll) + .flatMap(Collection::stream) + .toList(); } @Override @@ -277,7 +343,8 @@ public void updateRecipeForRoom( @Nullable RoomRecipeMatch resourceLocation ) { getOrCreateRooms(scanLevel); - activeRecipes.get(scanLevel).update(oldRoom, newRoom, resourceLocation); + activeRecipes.get(scanLevel) + .update(oldRoom, newRoom, resourceLocation); } public int numRecipes() { @@ -286,9 +353,10 @@ public int numRecipes() { public Collection> getAllMatches() { Stream> objectStream = this.activeRecipes.values() - .stream() - .map(ActiveRecipes::entrySet) - .flatMap(v -> v.stream().map(Map.Entry::getValue)); + .stream() + .map(ActiveRecipes::entrySet) + .flatMap(v -> v.stream() + .map(Map.Entry::getValue)); return objectStream.collect(Collectors.toSet()); } @@ -313,13 +381,20 @@ public Collection> getRoomsMatching(ResourceLocation rec for (TownPosition p : registeredDoors) { Position pz = new Position(p.x, p.z); TownRooms rooms = getOrCreateRooms(p.scanLevel); - Optional room = rooms.getAll().stream().filter(v -> v.getDoorPos().equals(pz)).findFirst(); + Optional room = rooms.getAll() + .stream() + .filter(v -> v.getDoorPos() + .equals(pz)) + .findFirst(); if (room.isEmpty()) { continue; } ActiveRecipes> recipes = activeRecipes.get(p.scanLevel); for (Map.Entry> m : recipes.entrySet()) { - if (m.getKey().equals(room.get()) && m.getValue().getRecipeID().equals(recipeId)) { + if (m.getKey() + .equals(room.get()) && m.getValue() + .getRecipeID() + .equals(recipeId)) { b.add(m.getValue()); } } @@ -329,9 +404,27 @@ public Collection> getRoomsMatching(ResourceLocation rec public Collection getFarms() { return activeFarms.entrySet() + .stream() + .flatMap(v -> v.getValue() + .getAll() + .stream()) + .toList(); + } + + public boolean isDoorRegistered( + Position position, + int y + ) { + return registeredDoors .stream() - .flatMap(v -> v.getValue().getAll().stream()) - .toList(); + .anyMatch(v -> v.scanLevel == y && position.x == v.x && position.z == v.z); } + public Optional> computeRecipe( + ServerLevel serverLevel, + MCRoom r, + int scanLevel + ) { + return RecipeDetection.getActiveRecipe(serverLevel, r, getOrCreateRooms(scanLevel)); + } } diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 5d7b7188..3c45d33b 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -60,7 +60,7 @@ Questown is a new mod that intends to add a quest-based component to village bui modId="roomrecipes" mandatory=true ordering="BEFORE" - versionRange="[1.19.2-0.0.4-alpha.2]" + versionRange="[1.19.2-0.0.5-alpha.1]" side="BOTH" [[dependencies.questown]] modId="jei" diff --git a/src/main/resources/assets/questown/lang/en_us.json b/src/main/resources/assets/questown/lang/en_us.json index 50f55e2e..61b98461 100644 --- a/src/main/resources/assets/questown/lang/en_us.json +++ b/src/main/resources/assets/questown/lang/en_us.json @@ -17,6 +17,7 @@ "message.baker.villagers_will_add_wheat": "Baker villagers can add wheat and coal to bake bread", "message.baker.villagers_will_add_coal": "Baker villagers can add coal to bake bread", "messages.jobs.changed": "Job changed to %s for villager %s", + "message.debug_mode": "Debug Mode: %s", "tooltips.quests": "Quests", "tooltips.stats": "Stats", "tooltips.villagers.job.inventory.locked": "This item was given by a player. Gatherer will not drop it.",