diff --git a/api/src/main/java/dev/jsinco/brewery/api/brew/Brew.java b/api/src/main/java/dev/jsinco/brewery/api/brew/Brew.java index 9bcee0da..fa11512c 100644 --- a/api/src/main/java/dev/jsinco/brewery/api/brew/Brew.java +++ b/api/src/main/java/dev/jsinco/brewery/api/brew/Brew.java @@ -7,8 +7,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.function.Function; import java.util.function.Supplier; @@ -52,6 +51,26 @@ public interface Brew extends MetaContainer { */ Brew withStep(BrewingStep step); + /** + * @param steps A collection brewing steps + * @return A new brew instance with the brewing step added in the chain + */ + Brew withSteps(Collection steps); + + /** + * @param steps A collection of brewing steps + * @return A new brew instance with the brewing steps replacing the existing steps + */ + Brew withStepsReplaced(Collection steps); + + /** + * @param index Index of the step in {@link #getSteps()} + * @param modifier Function that modifies the step + * @return A new brew instance with the modified step + * @throws IndexOutOfBoundsException If the index is out of bounds + */ + Brew withModifiedStep(int index, Function modifier); + /** * @param modifier Function that modifies the last step * @return A new brew instance with the modified last step @@ -79,6 +98,18 @@ public interface Brew extends MetaContainer { */ List getSteps(); + /** + * All players who contributed to this brew, in their order of contribution. + * + * @return All brewers, may be empty + */ + SequencedSet getBrewers(); + + /** + * @return The amount of steps in this brew + */ + int stepAmount(); + /** * A state of a brew, mainly indicates how the data should be written when converting into an item */ diff --git a/api/src/main/java/dev/jsinco/brewery/api/brew/BrewingStep.java b/api/src/main/java/dev/jsinco/brewery/api/brew/BrewingStep.java index afbcc6fc..0aeaf549 100644 --- a/api/src/main/java/dev/jsinco/brewery/api/brew/BrewingStep.java +++ b/api/src/main/java/dev/jsinco/brewery/api/brew/BrewingStep.java @@ -8,8 +8,7 @@ import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.text.minimessage.translation.Argument; -import java.util.Locale; -import java.util.Map; +import java.util.*; public interface BrewingStep { @@ -37,6 +36,12 @@ public interface BrewingStep { */ Map failedScores(); + /** + * All players who contributed to this brewing step, in their order of contribution. + * @return All brewers, may be empty + */ + SequencedSet brewers(); + /** * @param state The state of the brew * @param resolver A tag resolver for this step @@ -50,21 +55,38 @@ default Component infoDisplay(Brew.State state, TagResolver resolver) { }, Argument.tagResolver(resolver)); } - interface TimedStep { + interface TimedStep extends BrewingStep { /** * @return The time for this step (ticks) */ Moment time(); } - interface IngredientsStep { + interface IngredientsStep extends BrewingStep { /** * @return The ingredients for this step */ Map ingredients(); } - interface Cook extends BrewingStep, TimedStep, IngredientsStep { + interface AuthoredStep> extends BrewingStep { + + /** + * @param brewer A brewer's UUID + * @return A new instance of this step with the specified brewer + */ + SELF withBrewer(UUID brewer); + + /** + * @param brewers A collection of brewer UUIDs + * @return A new instance of this step with the specified brewers + */ + SELF withBrewers(SequencedCollection brewers); + + SELF withBrewersReplaced(SequencedCollection brewers); + } + + interface Cook extends TimedStep, IngredientsStep, AuthoredStep { /** * @return The type of the cauldron @@ -84,7 +106,7 @@ interface Cook extends BrewingStep, TimedStep, IngredientsStep { Cook withIngredients(Map ingredients); } - interface Distill extends BrewingStep { + interface Distill extends AuthoredStep { /** * @return The amount of distill runs for this step @@ -97,7 +119,7 @@ interface Distill extends BrewingStep { Distill incrementRuns(); } - interface Age extends BrewingStep, TimedStep { + interface Age extends TimedStep, AuthoredStep { /** * @return The type of the barrel @@ -111,7 +133,7 @@ interface Age extends BrewingStep, TimedStep { Age withAge(Moment age); } - interface Mix extends BrewingStep, TimedStep, IngredientsStep { + interface Mix extends TimedStep, IngredientsStep, AuthoredStep { /** * @param ingredients A map of ingredients with amount diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts index 970a1585..8c3a3d01 100644 --- a/bukkit/build.gradle.kts +++ b/bukkit/build.gradle.kts @@ -232,6 +232,9 @@ bukkit { register("brewery.command.seal") { description = "Allows the user to use the /tbp seal command." } + register("brewery.command.brewer") { + description = "Allows the user to use the /tbp brewer command." + } register("brewery.command.other") { description = "Allows the user to use other /tbp commands." } @@ -253,6 +256,7 @@ bukkit { "brewery.command.info" to true, "brewery.command.debug" to true, "brewery.command.seal" to true, + "brewery.command.brewer" to true, "brewery.command.other" to true, "brewery.command.replicate" to true, "brewery.command.version" to true, diff --git a/bukkit/src/main/java/dev/jsinco/brewery/bukkit/brew/BrewingStepPdcType.java b/bukkit/src/main/java/dev/jsinco/brewery/bukkit/brew/BrewingStepPdcType.java index 3985e240..b6badafb 100644 --- a/bukkit/src/main/java/dev/jsinco/brewery/bukkit/brew/BrewingStepPdcType.java +++ b/bukkit/src/main/java/dev/jsinco/brewery/bukkit/brew/BrewingStepPdcType.java @@ -32,7 +32,7 @@ public class BrewingStepPdcType implements PersistentDataType throw new IllegalStateException("Unexpected value: " + complex); } + encodeBrewers(complex.brewers(), dataOutputStream); } @Override @@ -118,13 +119,13 @@ private void writePayload(@NotNull BrewingStep complex, @NotNull DataOutputStrea if (Arrays.equals(magic, MAGIC)) { int version = headerIn.readUnsignedByte(); - if (version != 1) throw new RuntimeException("Unsupported version: " + version); + if (version < 1 || version > VERSION) throw new RuntimeException("Unsupported version: " + version); int ivLen = headerIn.readUnsignedByte(); byte[] iv = headerIn.readNBytes(ivLen); if (ivLen == 0) { // brew isn't encrypted try (DataInputStream dis = new DataInputStream(in)) { - return readPayload(dis); + return readPayload(dis, version); } } @@ -139,7 +140,7 @@ private void writePayload(@NotNull BrewingStep complex, @NotNull DataOutputStrea cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv)); try (CipherInputStream cis = new CipherInputStream(inDup(primitive, MAGIC.length + 1 + 1 + ivLen), cipher); DataInputStream dis = new DataInputStream(cis)) { - return readPayload(dis); + return readPayload(dis, version); } } catch (IOException | GeneralSecurityException e) { last = e; // wrong key or tampered data @@ -157,22 +158,28 @@ private static ByteArrayInputStream inDup(byte[] all, int offset) { return new ByteArrayInputStream(all, offset, all.length - offset); } - private BrewingStep readPayload(@NotNull DataInputStream dataInputStream) throws IOException { + private BrewingStep readPayload(@NotNull DataInputStream dataInputStream, int version) throws IOException { BrewingStep.StepType stepType = BrewingStep.StepType.valueOf(dataInputStream.readUTF()); return switch (stepType) { case COOK -> new CookStepImpl( decodeMoment(dataInputStream), decodeIngredients(dataInputStream), - BreweryRegistry.CAULDRON_TYPE.get(BreweryKey.parse(dataInputStream.readUTF())) + BreweryRegistry.CAULDRON_TYPE.get(BreweryKey.parse(dataInputStream.readUTF())), + decodeBrewers(dataInputStream, version) + ); + case DISTILL -> new DistillStepImpl( + dataInputStream.readInt(), + decodeBrewers(dataInputStream, version) ); - case DISTILL -> new DistillStepImpl(dataInputStream.readInt()); case AGE -> new AgeStepImpl( decodeMoment(dataInputStream), - BreweryRegistry.BARREL_TYPE.get(BreweryKey.parse(dataInputStream.readUTF())) + BreweryRegistry.BARREL_TYPE.get(BreweryKey.parse(dataInputStream.readUTF())), + decodeBrewers(dataInputStream, version) ); case MIX -> new MixStepImpl( decodeMoment(dataInputStream), - decodeIngredients(dataInputStream) + decodeIngredients(dataInputStream), + decodeBrewers(dataInputStream, version) ); }; } @@ -202,7 +209,7 @@ private BrewingStep attemptDecryptDES(byte[] primitive, SecretKey key) throws IO CipherInputStream cis = new CipherInputStream(input, getLegacyDESCipher(Cipher.DECRYPT_MODE, key)); DataInputStream dis = new DataInputStream(cis) ) { - return readPayload(dis); + return readPayload(dis, 1); } } @@ -253,6 +260,26 @@ public Map decodeIngredients(InputStream inputStr return ingredients; } + private void encodeBrewers(@NotNull SequencedSet brewers, OutputStream outputStream) throws IOException { + DecoderEncoder.writeVarInt(brewers.size(), outputStream); + for (UUID uuid : brewers) { + outputStream.write(DecoderEncoder.asBytes(uuid)); + } + } + + private SequencedSet decodeBrewers(InputStream inputStream, int version) throws IOException { + if (version == 1) { + return new LinkedHashSet<>(); + } + int length = DecoderEncoder.readVarInt(inputStream); + SequencedSet brewers = new LinkedHashSet<>(); + for (int i = 0; i < length; i++) { + UUID uuid = DecoderEncoder.asUuid(inputStream.readNBytes(16)); + brewers.add(uuid); + } + return brewers; + } + private Cipher getLegacyDESCipher(int operationMode, SecretKey key) { try { Cipher cipher = (Config.config().encryptSensitiveData() && useCipher) @@ -265,4 +292,4 @@ private Cipher getLegacyDESCipher(int operationMode, SecretKey key) { } } -} +} \ No newline at end of file diff --git a/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/BrewerCommand.java b/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/BrewerCommand.java new file mode 100644 index 00000000..04527457 --- /dev/null +++ b/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/BrewerCommand.java @@ -0,0 +1,278 @@ +package dev.jsinco.brewery.bukkit.command; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import dev.jsinco.brewery.api.brew.Brew; +import dev.jsinco.brewery.api.brew.BrewingStep; +import dev.jsinco.brewery.brew.BrewImpl; +import dev.jsinco.brewery.bukkit.brew.BrewAdapter; +import dev.jsinco.brewery.bukkit.command.argument.EnumArgument; +import dev.jsinco.brewery.bukkit.command.argument.OfflinePlayerArgument; +import dev.jsinco.brewery.bukkit.command.argument.OfflinePlayerSelectorArgumentResolver; +import dev.jsinco.brewery.bukkit.util.BukkitMessageUtil; +import dev.jsinco.brewery.util.MessageUtil; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.tag.resolver.Formatter; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class BrewerCommand { + + private static DynamicCommandExceptionType INDEX_OUT_OF_BOUNDS = new DynamicCommandExceptionType(object -> + BukkitMessageUtil.toBrigadier("tbp.command.brewer.step-out-of-bounds", Formatter.number("max_index", (Number) object)) + ); + + private static final int PLAYER_INVENTORY_SIZE = 41; + + public static ArgumentBuilder command() { + ArgumentBuilder withIndex = Commands.argument("inventory_slot", + IntegerArgumentType.integer(0, PLAYER_INVENTORY_SIZE - 1)); + appendBrewerModification(withIndex); + ArgumentBuilder withNamedSlot = Commands.argument("equipment_slot", + new EnumArgument<>(EquipmentSlot.class)); + appendBrewerModification(withNamedSlot); + + ArgumentBuilder command = Commands.literal("brewer"); + command.then(withNamedSlot); // /tbp brewer ... + command.then(withIndex); // /tbp brewer ... + command.then(BreweryCommand.playerBranch(argument -> { + argument.then(withNamedSlot); // /tbp brewer for ... + argument.then(withIndex); // /tbp brewer for ... + appendBrewerModification(argument); // /tbp brewer for ... + })); + appendBrewerModification(command); // /tbp brewer ... + return command; + } + + private static void appendBrewerModification(ArgumentBuilder builder) { + builder.then(Commands.literal("add") + .then(Commands.argument("brewers", OfflinePlayerArgument.MULTIPLE) + .then(Commands.argument("step", IntegerArgumentType.integer(0)) + .executes(context -> modifyBrew(context, + BrewerCommand::addBrewers, + BrewerCommand::addBrewersToLast, + "tbp.command.brewer.add" + )) + ) + .executes(context -> modifyBrew(context, + BrewerCommand::addBrewers, + BrewerCommand::addBrewersToLast, + "tbp.command.brewer.add" + )) + ) + ).then(Commands.literal("remove") + .then(Commands.argument("brewers", OfflinePlayerArgument.MULTIPLE) + .then(Commands.argument("step", IntegerArgumentType.integer(0)) + .executes(context -> modifyBrew(context, + BrewerCommand::removeBrewers, + "tbp.command.brewer.remove" + )) + ) + .executes(context -> modifyBrew(context, + BrewerCommand::removeBrewers, + "tbp.command.brewer.remove" + )) + ) + ).then(Commands.literal("set") + .then(Commands.argument("brewers", OfflinePlayerArgument.MULTIPLE) + .then(Commands.argument("step", IntegerArgumentType.integer(0)) + .executes(context -> modifyBrew(context, + BrewerCommand::setBrewers, + BrewerCommand::setBrewersClearingOtherSteps, + "tbp.command.brewer.set" + )) + ) + .executes(context -> modifyBrew(context, + BrewerCommand::setBrewers, + BrewerCommand::setBrewersClearingOtherSteps, + "tbp.command.brewer.set" + )) + ) + ).then(Commands.literal("clear") + .then(Commands.argument("step", IntegerArgumentType.integer(0)) + .executes(context -> modifyBrew(context, + BrewerCommand::clearBrewers, + "tbp.command.brewer.clear" + )) + ) + .executes(context -> modifyBrew(context, + BrewerCommand::clearBrewers, + "tbp.command.brewer.clear" + )) + ); + } + + private static BrewingStep addBrewers(BrewingStep step, List brewers) { + if (step instanceof BrewingStep.AuthoredStep authoredStep) { + return authoredStep.withBrewers(brewers); + } + return step; + } + private static Brew addBrewersToLast(Brew brew, List brewers) { + return lastAuthoredStepIndex(brew).map(index -> + brew.withModifiedStep(index, ignored -> addBrewers(brew.getSteps().get(index), brewers)) + ).orElse(brew); + } + + private static BrewingStep removeBrewers(BrewingStep step, List brewers) { + if (step instanceof BrewingStep.AuthoredStep authoredStep) { + return authoredStep.withBrewersReplaced( + authoredStep.brewers().stream() + .filter(uuid -> brewers.stream().noneMatch(uuid::equals)) + .toList() + ); + } + return step; + } + + private static BrewingStep setBrewers(BrewingStep step, List brewers) { + if (step instanceof BrewingStep.AuthoredStep authoredStep) { + return authoredStep.withBrewersReplaced(brewers); + } + return step; + } + private static Brew setBrewersClearingOtherSteps(Brew brew, List brewers) { + Brew cleared = modifyAllBrewSteps(brew, BrewerCommand::clearBrewers, brewers); + return lastAuthoredStepIndex(cleared).map(index -> + brew.withModifiedStep(index, ignored -> addBrewers(cleared.getSteps().get(index), brewers)) + ).orElse(brew); + } + + private static BrewingStep clearBrewers(BrewingStep step, List ignored) { + if (step instanceof BrewingStep.AuthoredStep authoredStep) { + return authoredStep.withBrewersReplaced(List.of()); + } + return step; + } + + private static int modifyBrew(CommandContext context, + BiFunction, BrewingStep> stepOperation, + String successMessageKey) throws CommandSyntaxException { + return modifyBrew( + context, + stepOperation, + (brew, brewers) -> modifyAllBrewSteps(brew, stepOperation, brewers), + successMessageKey + ); + } + private static int modifyBrew(CommandContext context, + BiFunction, BrewingStep> stepOperation, + BiFunction, Brew> brewOperation, + String successMessageKey) throws CommandSyntaxException { + CommandSender sender = context.getSource().getSender(); + Slot targetSlot = getTargetSlot(context, BreweryCommand.getPlayer(context)); + ItemStack itemStack = targetSlot.itemGetter.get(); + if (itemStack.isEmpty()) { + MessageUtil.message(sender, "tbp.command.info.not-a-brew"); + return 1; + } + Optional stepIndex = getArgument(context, "step", int.class); + List brewers = getBrewers(context); + List brewerUuids = brewers + .stream() + .map(OfflinePlayer::getUniqueId) + .toList(); + Optional brewOptional = BrewAdapter.fromItem(itemStack); + if (stepIndex.isPresent() && brewOptional.isPresent() && brewOptional.get().stepAmount() <= stepIndex.get()) { + throw INDEX_OUT_OF_BOUNDS.create(brewOptional.get().stepAmount() - 1); + } + if(brewOptional.map(Brew::stepAmount).filter(stepAmount -> stepAmount <= 0).isPresent()){ + MessageUtil.message(sender, "tbp.command.brewer.empty"); + return 1; + } + brewOptional + .map(brew -> stepIndex.map(index -> + brew.withModifiedStep(index, brewingStep -> stepOperation.apply(brewingStep, brewerUuids)) + ).orElseGet(() -> brewOperation.apply(brew, brewerUuids)) + ).ifPresentOrElse(brew -> { + targetSlot.itemSetter().accept(BrewAdapter.toItem(brew, new Brew.State.Other())); + MessageUtil.message(sender, successMessageKey, Placeholder.component("brewers", + brewers.stream() + .map(BrewerCommand::getName) + .map(Component::text) + .collect(Component.toComponent(Component.text(", "))) + )); + }, () -> MessageUtil.message(sender, "tbp.command.info.not-a-brew")); + return 1; + } + + private static Brew modifyAllBrewSteps(Brew brew, BiFunction, BrewingStep> operation, List brewers) { + return new BrewImpl( + brew.getSteps().stream() + .map(brewingStep -> operation.apply(brewingStep, brewers)) + .toList() + ); + } + + private static Optional lastAuthoredStepIndex(Brew brew) { + for (int i = brew.getSteps().size() - 1; i >= 0; i--) { + BrewingStep step = brew.getSteps().get(i); + if (step instanceof BrewingStep.AuthoredStep) { + return Optional.of(i); + } + } + return Optional.empty(); + } + + private static Slot getTargetSlot(CommandContext context, Player targetPlayer) { + PlayerInventory inventory = targetPlayer.getInventory(); + return getArgument(context, "equipment_slot", EquipmentSlot.class).map(equipmentSlot -> new Slot( + () -> inventory.getItem(equipmentSlot), + itemStack -> inventory.setItem(equipmentSlot, itemStack) + )) + .or(() -> getArgument(context, "inventory_slot", int.class).map(inventorySlot -> new Slot( + () -> inventory.getItem(inventorySlot), + itemStack -> inventory.setItem(inventorySlot, itemStack) + ))) + .orElseGet(() -> new Slot( + inventory::getItemInMainHand, + inventory::setItemInMainHand + )); + } + + private record Slot(Supplier itemGetter, Consumer itemSetter) { + } + + private static List getBrewers(CommandContext context) throws CommandSyntaxException { + try { + return context.getArgument("brewers", OfflinePlayerSelectorArgumentResolver.class) + .resolve(context.getSource()); + } catch (IllegalArgumentException e) { + return List.of(); + } + } + + private static Optional getArgument(CommandContext context, String name, Class resolver) { + try { + return Optional.of(context.getArgument(name, resolver)); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + } + + private static String getName(OfflinePlayer player) { + String name = player.getName(); + if (name != null) { + return name; + } + return player.getUniqueId().toString(); + } + +} diff --git a/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/BreweryCommand.java b/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/BreweryCommand.java index 019e86ee..18e87a0d 100644 --- a/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/BreweryCommand.java +++ b/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/BreweryCommand.java @@ -48,6 +48,8 @@ public static void register(ReloadableRegistrarEvent commands) { .requires(commandSourceStack -> commandSourceStack.getSender().hasPermission("brewery.command.debug"))) .then(SealCommand.command() .requires(commandSourceStack -> commandSourceStack.getSender().hasPermission("brewery.command.seal"))) + .then(BrewerCommand.command() + .requires(commandSourceStack -> commandSourceStack.getSender().hasPermission("brewery.command.brewer"))) .then(StatusCommand.command() .requires(commandSourceStack -> commandSourceStack.getSender().hasPermission("brewery.command.status"))) .then(Commands.literal("reload") @@ -89,7 +91,7 @@ public static void register(ReloadableRegistrarEvent commands) { } public static ArgumentBuilder offlinePlayerBranch(Consumer> childAction) { - ArgumentBuilder child = Commands.argument("player", new OfflinePlayerArgument()); + ArgumentBuilder child = Commands.argument("player", OfflinePlayerArgument.SINGLE); childAction.accept(child); return Commands.literal("for") .then(child) @@ -99,8 +101,10 @@ public static void register(ReloadableRegistrarEvent commands) { public static OfflinePlayer getOfflinePlayer(CommandContext context) throws CommandSyntaxException { try { - return context.getArgument("player", OfflinePlayerSelectorArgumentResolver.class) + List resolved = context.getArgument("player", OfflinePlayerSelectorArgumentResolver.class) .resolve(context.getSource()); + if (resolved.isEmpty()) throw ERROR_UNDEFINED_PLAYER.create(); + return resolved.getFirst(); } catch (IllegalArgumentException ignored) {} if (context.getSource().getSender() instanceof OfflinePlayer player) return player; throw ERROR_UNDEFINED_PLAYER.create(); @@ -108,9 +112,8 @@ public static OfflinePlayer getOfflinePlayer(CommandContext public static Player getPlayer(CommandContext context) throws CommandSyntaxException { try { - PlayerSelectorArgumentResolver resolver = - context.getArgument("player", PlayerSelectorArgumentResolver.class); - List resolved = resolver.resolve(context.getSource()); + List resolved = context.getArgument("player", PlayerSelectorArgumentResolver.class) + .resolve(context.getSource()); if (resolved.isEmpty()) throw ERROR_UNDEFINED_PLAYER.create(); return resolved.getFirst(); } catch (IllegalArgumentException e) { diff --git a/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/InfoCommand.java b/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/InfoCommand.java index 86c07d7f..4c5ba412 100644 --- a/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/InfoCommand.java +++ b/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/InfoCommand.java @@ -27,6 +27,8 @@ import org.jetbrains.annotations.Nullable; import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; public class InfoCommand { private static final int PLAYER_INVENTORY_SIZE = 41; @@ -78,20 +80,28 @@ private static void showInfo(@Nullable ItemStack itemStack, CommandSender sender ); return; } - brewOptional - .ifPresent(brew -> MessageUtil.message(sender, - "tbp.command.info.message", - MessageUtil.getScoreTagResolver(brew.closestRecipe(TheBrewingProject.getInstance().getRecipeRegistry()) - .map(brew::score) - .orElse(BrewScoreImpl.failed(brew))), - Placeholder.component("brewing_step_info", MessageUtil.compileBrewInfo(brew, true, TheBrewingProject.getInstance().getRecipeRegistry()) - .collect(Component.toComponent(Component.text("\n"))) - )) - ); + brewOptional.ifPresent(brew -> MessageUtil.message(sender, + "tbp.command.info.message", + MessageUtil.getScoreTagResolver(brew.closestRecipe(TheBrewingProject.getInstance().getRecipeRegistry()) + .map(brew::score) + .orElse(BrewScoreImpl.failed(brew))), + Placeholder.component("brewing_step_info", MessageUtil.compileBrewInfo(brew, true, TheBrewingProject.getInstance().getRecipeRegistry()) + .collect(Component.toComponent(Component.text("\n"))) + )) + ); Optional recipeEffectsOptional = RecipeEffects.fromItem(itemStack); recipeEffectsOptional.ifPresent(effects -> { MessageUtil.message(sender, "tbp.command.info.effect-message", BukkitMessageUtil.recipeEffectResolver(effects)); }); + brewOptional.ifPresent(brew -> { + if (brew.getBrewers().isEmpty()) { + return; + } + String brewers = brew.getBrewers().stream() + .map(InfoCommand::uuidToPlayerName) + .collect(Collectors.joining(", ")); + MessageUtil.message(sender, "tbp.command.info.brewer-message", Placeholder.unparsed("brewers", brewers)); + }); if (brewOptional.isEmpty() && recipeEffectsOptional.isEmpty()) { MessageUtil.message(sender, "tbp.command.info.not-a-brew"); } @@ -106,4 +116,12 @@ private static Component debugInfo(Brew brew) { Component.text(")", NamedTextColor.GRAY) ); } + + private static String uuidToPlayerName(UUID uuid) { + String name = TheBrewingProject.getInstance().getServer().getOfflinePlayer(uuid).getName(); + if (name != null) { + return name; + } + return uuid.toString(); + } } diff --git a/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/argument/OfflinePlayerArgument.java b/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/argument/OfflinePlayerArgument.java index 5b2d4de6..8cdb665c 100644 --- a/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/argument/OfflinePlayerArgument.java +++ b/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/argument/OfflinePlayerArgument.java @@ -16,14 +16,22 @@ import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; +import java.util.List; import java.util.concurrent.CompletableFuture; public class OfflinePlayerArgument implements CustomArgumentType { + public static final OfflinePlayerArgument SINGLE = new OfflinePlayerArgument(ArgumentTypes.player()); + public static final OfflinePlayerArgument MULTIPLE = new OfflinePlayerArgument(ArgumentTypes.players()); + private static final DynamicCommandExceptionType ERROR_INVALID_PLAYER = new DynamicCommandExceptionType(invalidPlayer -> BukkitMessageUtil.toBrigadier("tbp.command.unknown-player", Placeholder.unparsed("player_name", invalidPlayer.toString())) ); - private final ArgumentType backing = ArgumentTypes.player(); + private final ArgumentType backing; + + private OfflinePlayerArgument(ArgumentType backing) { + this.backing = backing; + } @Override public CompletableFuture listSuggestions(@NotNull CommandContext context, SuggestionsBuilder builder) { @@ -37,18 +45,20 @@ public OfflinePlayerSelectorArgumentResolver parse(StringReader reader) throws C PlayerSelectorArgumentResolver playerSelectorArgumentResolver = backing.parse(reader); return commandSourceStack -> playerSelectorArgumentResolver .resolve(commandSourceStack) - .getFirst(); + .stream() + .map(player -> (OfflinePlayer) player) + .toList(); } String playerName = reader.readString(); OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayerIfCached(playerName); if(offlinePlayer == null) { throw ERROR_INVALID_PLAYER.create(playerName); } - return ignored -> offlinePlayer; + return ignored -> List.of(offlinePlayer); } @Override public @NotNull ArgumentType getNativeType() { - return ArgumentTypes.player(); + return backing; } } diff --git a/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/argument/OfflinePlayerSelectorArgumentResolver.java b/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/argument/OfflinePlayerSelectorArgumentResolver.java index 25876489..d5d20336 100644 --- a/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/argument/OfflinePlayerSelectorArgumentResolver.java +++ b/bukkit/src/main/java/dev/jsinco/brewery/bukkit/command/argument/OfflinePlayerSelectorArgumentResolver.java @@ -4,7 +4,9 @@ import io.papermc.paper.command.brigadier.CommandSourceStack; import org.bukkit.OfflinePlayer; +import java.util.List; + public interface OfflinePlayerSelectorArgumentResolver { - OfflinePlayer resolve(CommandSourceStack stack) throws CommandSyntaxException; + List resolve(CommandSourceStack stack) throws CommandSyntaxException; } diff --git a/bukkit/src/test/java/dev/jsinco/brewery/bukkit/brew/BrewTest.java b/bukkit/src/test/java/dev/jsinco/brewery/bukkit/brew/BrewTest.java index e96021eb..a0a433a7 100644 --- a/bukkit/src/test/java/dev/jsinco/brewery/bukkit/brew/BrewTest.java +++ b/bukkit/src/test/java/dev/jsinco/brewery/bukkit/brew/BrewTest.java @@ -1,5 +1,7 @@ package dev.jsinco.brewery.bukkit.brew; +import dev.jsinco.brewery.api.brew.Brew; +import dev.jsinco.brewery.api.brew.BrewingStep; import dev.jsinco.brewery.brew.AgeStepImpl; import dev.jsinco.brewery.brew.BrewImpl; import dev.jsinco.brewery.brew.CookStepImpl; @@ -8,19 +10,140 @@ import dev.jsinco.brewery.api.breweries.CauldronType; import dev.jsinco.brewery.bukkit.ingredient.SimpleIngredient; import dev.jsinco.brewery.api.moment.PassedMoment; +import dev.jsinco.brewery.util.CollectionUtil; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockbukkit.mockbukkit.MockBukkitExtension; -import java.util.List; -import java.util.Map; +import java.util.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.*; @ExtendWith(MockBukkitExtension.class) public class BrewTest { + @Test + void brewers_empty() { + Brew brew = new BrewImpl(List.of()); + assertTrue(brew.getBrewers().isEmpty()); + } + + @Test + void brewers_collectFromSteps() { + Brew brew = new BrewImpl( + List.of( + new CookStepImpl( + new PassedMoment(20), + Map.of(SimpleIngredient.from("wheat").get(), 1), + CauldronType.LAVA, + CollectionUtil.sequencedSetOf(UUID.fromString("f6489b79-7a9f-49e2-980e-265a05dbc3af")) + ), + new DistillStepImpl( + 3, + CollectionUtil.sequencedSetOf(UUID.fromString("144ce39d-301b-40a9-9788-0ca8cb23daf4")) + ), + new AgeStepImpl( + new PassedMoment(20), + BarrelType.ACACIA, + CollectionUtil.sequencedSetOf(UUID.fromString("d2b440c3-edde-4443-899e-6825c31d0919")) + ) + ) + ); + SequencedSet expected = CollectionUtil.sequencedSetOf( + UUID.fromString("f6489b79-7a9f-49e2-980e-265a05dbc3af"), + UUID.fromString("144ce39d-301b-40a9-9788-0ca8cb23daf4"), + UUID.fromString("d2b440c3-edde-4443-899e-6825c31d0919") + ); + // converting to list tests iteration order instead of set equality + assertEquals(new ArrayList<>(expected), new ArrayList<>(brew.getBrewers())); + } + + @Test + void brewers_respectsStepOrder() { + Brew brew = new BrewImpl( + List.of( + new CookStepImpl( + new PassedMoment(20), + Map.of(SimpleIngredient.from("wheat").get(), 1), + CauldronType.LAVA, + CollectionUtil.sequencedSetOf( + UUID.fromString("f6489b79-7a9f-49e2-980e-265a05dbc3af"), + UUID.fromString("d2b440c3-edde-4443-899e-6825c31d0919") + ) + ), + new DistillStepImpl( + 3, + CollectionUtil.sequencedSetOf(UUID.fromString("144ce39d-301b-40a9-9788-0ca8cb23daf4")) + ), + new AgeStepImpl( + new PassedMoment(20), + BarrelType.ACACIA, + CollectionUtil.sequencedSetOf(UUID.fromString("d2b440c3-edde-4443-899e-6825c31d0919")) + ) + ) + ); + SequencedSet expected = CollectionUtil.sequencedSetOf( + UUID.fromString("f6489b79-7a9f-49e2-980e-265a05dbc3af"), + UUID.fromString("d2b440c3-edde-4443-899e-6825c31d0919"), + UUID.fromString("144ce39d-301b-40a9-9788-0ca8cb23daf4") + ); + // converting to list tests iteration order instead of set equality + assertEquals(new ArrayList<>(expected), new ArrayList<>(brew.getBrewers())); + } + + @Test + void withStepsReplaced() { + List steps = List.of( + new CookStepImpl( + new PassedMoment(20), + Map.of(SimpleIngredient.from("wheat").get(), 1), + CauldronType.LAVA + ), + new DistillStepImpl( + 3 + ), + new AgeStepImpl( + new PassedMoment(20), + BarrelType.ACACIA + ) + ); + Brew brew = new BrewImpl(List.of(new DistillStepImpl(1))); + assertEquals(steps, brew.withStepsReplaced(steps).getSteps()); + } + + void withModifiedStep() { + List expected = List.of( + new CookStepImpl( + new PassedMoment(20), + Map.of(SimpleIngredient.from("wheat").get(), 1), + CauldronType.LAVA + ), + new DistillStepImpl( + 3 + ), + new AgeStepImpl( + new PassedMoment(20), + BarrelType.ACACIA + ) + ); + List steps = List.of( + new CookStepImpl( + new PassedMoment(20), + Map.of(SimpleIngredient.from("wheat").get(), 1), + CauldronType.LAVA + ), + new DistillStepImpl( + 1 + ), + new AgeStepImpl( + new PassedMoment(20), + BarrelType.ACACIA + ) + ); + Brew brew = new BrewImpl(steps); + assertEquals(expected, brew.withModifiedStep(1, step -> new DistillStepImpl(3))); + } + @Test void equals() { BrewImpl brew1 = new BrewImpl( @@ -58,6 +181,45 @@ void equals() { assertEquals(brew1, brew2); } + @Test + void equals2() { + BrewImpl brew1 = new BrewImpl( + List.of( + new CookStepImpl( + new PassedMoment(20), + Map.of(SimpleIngredient.from("wheat").get(), 1), + CauldronType.LAVA, + CollectionUtil.sequencedSetOf(UUID.fromString("f6489b79-7a9f-49e2-980e-265a05dbc3af")) + ), + new DistillStepImpl( + 3 + ), + new AgeStepImpl( + new PassedMoment(20), + BarrelType.ACACIA + ) + ) + ); + BrewImpl brew2 = new BrewImpl( + List.of( + new CookStepImpl( + new PassedMoment(20), + Map.of(SimpleIngredient.from("wheat").get(), 1), + CauldronType.LAVA, + CollectionUtil.sequencedSetOf(UUID.fromString("f6489b79-7a9f-49e2-980e-265a05dbc3af")) + ), + new DistillStepImpl( + 3 + ), + new AgeStepImpl( + new PassedMoment(20), + BarrelType.ACACIA + ) + ) + ); + assertEquals(brew1, brew2); + } + @Test void equals_notEqual() { BrewImpl brew1 = new BrewImpl( @@ -96,7 +258,7 @@ void equals_notEqual() { } @Test - void equals_notEquals2() { + void equals_notEqual2() { BrewImpl brew1 = new BrewImpl( List.of( new CookStepImpl( @@ -131,4 +293,74 @@ void equals_notEquals2() { ); assertNotEquals(brew1, brew2); } + + @Test + void equals_notEqual3() { + BrewImpl brew1 = new BrewImpl( + List.of( + new CookStepImpl( + new PassedMoment(20), + Map.of(SimpleIngredient.from("wheat").get(), 1), + CauldronType.LAVA + ), + new DistillStepImpl( + 3, + CollectionUtil.sequencedSetOf(UUID.fromString("f6489b79-7a9f-49e2-980e-265a05dbc3af")) + ), + new AgeStepImpl( + new PassedMoment(20), + BarrelType.ACACIA + ) + ) + ); + BrewImpl brew2 = new BrewImpl( + List.of( + new CookStepImpl( + new PassedMoment(20), + Map.of(SimpleIngredient.from("wheat").get(), 1), + CauldronType.LAVA + ), + new DistillStepImpl( + 3 + ), + new AgeStepImpl( + new PassedMoment(20), + BarrelType.ACACIA + ) + ) + ); + assertNotEquals(brew1, brew2); + } + + @Test + void equals_notEqual4() { + BrewImpl brew1 = new BrewImpl( + List.of( + new CookStepImpl( + new PassedMoment(20), + Map.of(SimpleIngredient.from("wheat").get(), 1), + CauldronType.LAVA, + CollectionUtil.sequencedSetOf( + UUID.fromString("f6489b79-7a9f-49e2-980e-265a05dbc3af"), + UUID.fromString("144ce39d-301b-40a9-9788-0ca8cb23daf4") + ) + ) + ) + ); + BrewImpl brew2 = new BrewImpl( + List.of( + new CookStepImpl( + new PassedMoment(20), + Map.of(SimpleIngredient.from("wheat").get(), 1), + CauldronType.LAVA, + CollectionUtil.sequencedSetOf( + // reversed order + UUID.fromString("144ce39d-301b-40a9-9788-0ca8cb23daf4"), + UUID.fromString("f6489b79-7a9f-49e2-980e-265a05dbc3af") + ) + ) + ) + ); + assertNotEquals(brew1, brew2); + } } diff --git a/core/src/main/java/dev/jsinco/brewery/brew/AgeStepImpl.java b/core/src/main/java/dev/jsinco/brewery/brew/AgeStepImpl.java index 0f88a6b3..66936a32 100644 --- a/core/src/main/java/dev/jsinco/brewery/brew/AgeStepImpl.java +++ b/core/src/main/java/dev/jsinco/brewery/brew/AgeStepImpl.java @@ -5,12 +5,13 @@ import dev.jsinco.brewery.api.brew.ScoreType; import dev.jsinco.brewery.api.breweries.BarrelType; import dev.jsinco.brewery.api.moment.Moment; +import dev.jsinco.brewery.util.CollectionUtil; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; -public record AgeStepImpl(Moment time, BarrelType barrelType) implements BrewingStep.Age { +public record AgeStepImpl(Moment time, BarrelType barrelType, SequencedSet brewers) implements BrewingStep.Age { private static final Map BREW_STEP_MISMATCH = Stream.of( new PartialBrewScore(0, ScoreType.TIME), @@ -18,13 +19,17 @@ public record AgeStepImpl(Moment time, BarrelType barrelType) implements Brewing ) .collect(Collectors.toUnmodifiableMap(PartialBrewScore::type, partial -> partial)); + public AgeStepImpl(Moment time, BarrelType barrelType) { + this(time, barrelType, Collections.emptySortedSet()); + } + public AgeStepImpl withAge(Moment age) { - return new AgeStepImpl(age, this.barrelType); + return new AgeStepImpl(age, this.barrelType, this.brewers); } @Override public Map proximityScores(BrewingStep other) { - if (!(other instanceof AgeStepImpl(Moment otherAge, BarrelType otherType))) { + if (!(other instanceof AgeStepImpl(Moment otherAge, BarrelType otherType, SequencedSet ignored))) { return BREW_STEP_MISMATCH; } double barrelTypeScore = barrelType.equals(BarrelType.ANY) || barrelType.equals(otherType) ? 1D : 0.9D; @@ -42,7 +47,7 @@ public StepType stepType() { @Override public Map maximumScores(BrewingStep other) { - if (!(other instanceof AgeStepImpl(Moment otherAge, BarrelType otherType))) { + if (!(other instanceof AgeStepImpl(Moment otherAge, BarrelType otherType, SequencedSet ignored))) { return BREW_STEP_MISMATCH; } double barrelTypeScore = barrelType.equals(BarrelType.ANY) || barrelType.equals(otherType) ? 1D : 0.9D; @@ -57,4 +62,35 @@ public Map maximumScores(BrewingStep other) { public Map failedScores() { return BREW_STEP_MISMATCH; } + + @Override + public Age withBrewer(UUID brewer) { + return new AgeStepImpl(this.time, this.barrelType, Stream.concat( + this.brewers.stream(), + Stream.of(brewer) + ).collect(Collectors.toCollection(LinkedHashSet::new))); + } + + @Override + public Age withBrewers(SequencedCollection brewers) { + return new AgeStepImpl(this.time, this.barrelType, Stream.concat( + this.brewers.stream(), + brewers.stream() + ).collect(Collectors.toCollection(LinkedHashSet::new))); + } + + @Override + public Age withBrewersReplaced(SequencedCollection brewers) { + return new AgeStepImpl(this.time, this.barrelType, new LinkedHashSet<>(brewers)); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AgeStepImpl(Moment otherTime, BarrelType otherType, SequencedSet otherBrewers))) { + return false; + } + return Objects.equals(time, otherTime) + && barrelType == otherType + && CollectionUtil.isEqualWithOrdering(brewers, otherBrewers); + } } diff --git a/core/src/main/java/dev/jsinco/brewery/brew/BrewImpl.java b/core/src/main/java/dev/jsinco/brewery/brew/BrewImpl.java index 0f886318..3a6ece3e 100644 --- a/core/src/main/java/dev/jsinco/brewery/brew/BrewImpl.java +++ b/core/src/main/java/dev/jsinco/brewery/brew/BrewImpl.java @@ -14,6 +14,7 @@ import java.util.*; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; import java.util.stream.Stream; public class BrewImpl implements Brew { @@ -44,6 +45,25 @@ public BrewImpl withStep(BrewingStep step) { return new BrewImpl(Stream.concat(steps.stream(), Stream.of(step)).toList(), meta); } + public BrewImpl withSteps(Collection steps) { + return new BrewImpl(Stream.concat(this.steps.stream(), steps.stream()).toList(), meta); + } + + public BrewImpl withStepsReplaced(Collection steps) { + return new BrewImpl(List.copyOf(steps), meta); + } + + public BrewImpl withModifiedStep(int index, Function modifier) { + BrewingStep newStep = modifier.apply(steps.get(index)); + return new BrewImpl( + Stream.concat( + steps.subList(0, steps.size() - 1).stream(), + Stream.of(newStep) + ).toList(), + meta + ); + } + public BrewImpl witModifiedLastStep(Function modifier) { BrewingStep newStep = modifier.apply(steps.getLast()); return new BrewImpl( @@ -76,6 +96,19 @@ public List getCompletedSteps() { .toList(); } + @Override + public SequencedSet getBrewers() { + return steps.stream() + .map(BrewingStep::brewers) + .flatMap(Collection::stream) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + @Override + public int stepAmount() { + return steps.size(); + } + private boolean isCompleted(BrewingStep step) { return !(step instanceof BrewingStep.Age age) || age.time().moment() > Config.config().barrels().agingYearTicks() / 2; } diff --git a/core/src/main/java/dev/jsinco/brewery/brew/BrewingStepSerializer.java b/core/src/main/java/dev/jsinco/brewery/brew/BrewingStepSerializer.java index 06bdb8d0..15d33c7c 100644 --- a/core/src/main/java/dev/jsinco/brewery/brew/BrewingStepSerializer.java +++ b/core/src/main/java/dev/jsinco/brewery/brew/BrewingStepSerializer.java @@ -1,5 +1,6 @@ package dev.jsinco.brewery.brew; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import dev.jsinco.brewery.api.brew.BrewingStep; @@ -12,8 +13,7 @@ import dev.jsinco.brewery.api.util.BreweryKey; import dev.jsinco.brewery.api.util.BreweryRegistry; -import java.util.Locale; -import java.util.Map; +import java.util.*; import java.util.concurrent.CompletableFuture; public class BrewingStepSerializer { @@ -24,30 +24,51 @@ public JsonObject serialize(BrewingStep step) { JsonObject object = new JsonObject(); object.addProperty("type", step.stepType().name().toLowerCase(Locale.ROOT)); switch (step) { - case AgeStepImpl(Moment age, BarrelType type) -> { + case AgeStepImpl(Moment age, BarrelType type, SequencedSet brewers) -> { object.add("age", Moment.SERIALIZER.serialize(age)); object.addProperty("barrel_type", type.key().toString()); + if (!brewers.isEmpty()) { + object.add("brewers", brewersToJson(brewers)); + } } case CookStepImpl( Moment brewTime, Map ingredients, - CauldronType cauldronType + CauldronType cauldronType, + SequencedSet brewers ) -> { object.add("brew_time", Moment.SERIALIZER.serialize(brewTime)); object.addProperty("cauldron_type", cauldronType.key().toString()); object.add("ingredients", IngredientUtil.ingredientsToJson((Map) ingredients)); + if (!brewers.isEmpty()) { + object.add("brewers", brewersToJson(brewers)); + } } - case DistillStepImpl(int runs) -> { + case DistillStepImpl(int runs, SequencedSet brewers) -> { object.addProperty("runs", runs); + if (!brewers.isEmpty()) { + object.add("brewers", brewersToJson(brewers)); + } } - case MixStepImpl(Moment time, Map ingredients) -> { + case MixStepImpl(Moment time, Map ingredients, SequencedSet brewers) -> { object.add("ingredients", IngredientUtil.ingredientsToJson((Map) ingredients)); object.add("mix_time", Moment.SERIALIZER.serialize(time)); + if (!brewers.isEmpty()) { + object.add("brewers", brewersToJson(brewers)); + } } default -> throw new IllegalStateException("Unexpected value: " + step); } return object; } + private static JsonArray brewersToJson(SequencedSet brewers) { + JsonArray arr = new JsonArray(); + for (UUID brewer : brewers) { + arr.add(brewer.toString()); + } + return arr; + } + public CompletableFuture deserialize(JsonElement jsonElement, IngredientManager ingredientManager) { JsonObject object = jsonElement.getAsJsonObject(); BrewingStep.StepType stepType = BrewingStep.StepType.valueOf(object.get("type").getAsString().toUpperCase(Locale.ROOT)); @@ -57,17 +78,39 @@ public CompletableFuture deserialize(JsonElement jsonElement, Ingre .thenApplyAsync(ingredients -> new CookStepImpl( Moment.SERIALIZER.deserialize(object.get("brew_time")), ingredients, - BreweryRegistry.CAULDRON_TYPE.get(BreweryKey.parse(object.get("cauldron_type").getAsString())) + BreweryRegistry.CAULDRON_TYPE.get(BreweryKey.parse(object.get("cauldron_type").getAsString())), + jsonToBrewers(object) )); - case DISTILL -> CompletableFuture.completedFuture(new DistillStepImpl(object.get("runs").getAsInt())); + case DISTILL -> + CompletableFuture.completedFuture(new DistillStepImpl( + object.get("runs").getAsInt(), + jsonToBrewers(object) + )); case AGE -> - CompletableFuture.completedFuture(new AgeStepImpl(Moment.SERIALIZER.deserialize(object.get("age")), BreweryRegistry.BARREL_TYPE.get(BreweryKey.parse(object.get("barrel_type").getAsString())))); + CompletableFuture.completedFuture(new AgeStepImpl( + Moment.SERIALIZER.deserialize(object.get("age")), + BreweryRegistry.BARREL_TYPE.get(BreweryKey.parse(object.get("barrel_type").getAsString())), + jsonToBrewers(object) + )); case MIX -> IngredientUtil.ingredientsFromJson(object.get("ingredients").getAsJsonObject(), ingredientManager) .thenApplyAsync(ingredients -> new MixStepImpl( Moment.SERIALIZER.deserialize(object.get("mix_time")), - ingredients + ingredients, + jsonToBrewers(object) )); }; } -} + + private static SequencedSet jsonToBrewers(JsonObject obj) { + if (!obj.has("brewers")) { + return Collections.emptySortedSet(); + } + SequencedSet brewers = new LinkedHashSet<>(); + for (JsonElement element : obj.get("brewers").getAsJsonArray()) { + UUID brewer = UUID.fromString(element.getAsString()); + brewers.add(brewer); + } + return brewers; + } +} \ No newline at end of file diff --git a/core/src/main/java/dev/jsinco/brewery/brew/CookStepImpl.java b/core/src/main/java/dev/jsinco/brewery/brew/CookStepImpl.java index 2b098c12..37fc5a4b 100644 --- a/core/src/main/java/dev/jsinco/brewery/brew/CookStepImpl.java +++ b/core/src/main/java/dev/jsinco/brewery/brew/CookStepImpl.java @@ -6,33 +6,40 @@ import dev.jsinco.brewery.api.breweries.CauldronType; import dev.jsinco.brewery.api.ingredient.Ingredient; import dev.jsinco.brewery.api.moment.Moment; +import dev.jsinco.brewery.util.CollectionUtil; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; public record CookStepImpl(Moment time, Map ingredients, - CauldronType cauldronType) implements BrewingStep.Cook { + CauldronType cauldronType, SequencedSet brewers) implements BrewingStep.Cook { private static final Map BREW_STEP_MISMATCH = Stream.of( new PartialBrewScore(0, ScoreType.TIME), new PartialBrewScore(0, ScoreType.INGREDIENTS) ).collect(Collectors.toUnmodifiableMap(PartialBrewScore::type, partial -> partial)); + public CookStepImpl(Moment time, Map ingredients, + CauldronType cauldronType) { + this(time, ingredients, cauldronType, Collections.emptySortedSet()); + } + @Override public CookStepImpl withBrewTime(Moment brewTime) { - return new CookStepImpl(brewTime, this.ingredients, this.cauldronType); + return new CookStepImpl(brewTime, this.ingredients, this.cauldronType, this.brewers); } @Override public CookStepImpl withIngredients(Map ingredients) { - return new CookStepImpl(this.time, ingredients, this.cauldronType); + return new CookStepImpl(this.time, ingredients, this.cauldronType, this.brewers); } @Override public Map proximityScores(BrewingStep other) { if (!(other instanceof CookStepImpl( - Moment otherTime, Map otherIngredients, CauldronType otherType + Moment otherTime, Map otherIngredients, + CauldronType otherType, SequencedSet ignored ))) { return BREW_STEP_MISMATCH; } @@ -59,4 +66,39 @@ public Map maximumScores(BrewingStep other) { public Map failedScores() { return BREW_STEP_MISMATCH; } + + @Override + public Cook withBrewer(UUID brewer) { + return new CookStepImpl(this.time, this.ingredients, this.cauldronType, Stream.concat( + this.brewers.stream(), + Stream.of(brewer) + ).collect(Collectors.toCollection(LinkedHashSet::new))); + } + + @Override + public Cook withBrewers(SequencedCollection brewers) { + return new CookStepImpl(this.time, this.ingredients, this.cauldronType, Stream.concat( + this.brewers.stream(), + brewers.stream() + ).collect(Collectors.toCollection(LinkedHashSet::new))); + } + + @Override + public Cook withBrewersReplaced(SequencedCollection brewers) { + return new CookStepImpl(this.time, this.ingredients, this.cauldronType, new LinkedHashSet<>(brewers)); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof CookStepImpl( + Moment otherTime, Map otherIngredients, + CauldronType otherType, SequencedSet otherBrewers + ))) { + return false; + } + return Objects.equals(time, otherTime) + && Objects.equals(ingredients, otherIngredients) + && cauldronType == otherType + && CollectionUtil.isEqualWithOrdering(brewers, otherBrewers); + } } diff --git a/core/src/main/java/dev/jsinco/brewery/brew/DistillStepImpl.java b/core/src/main/java/dev/jsinco/brewery/brew/DistillStepImpl.java index 8e968443..62899b6c 100644 --- a/core/src/main/java/dev/jsinco/brewery/brew/DistillStepImpl.java +++ b/core/src/main/java/dev/jsinco/brewery/brew/DistillStepImpl.java @@ -3,24 +3,30 @@ import dev.jsinco.brewery.api.brew.BrewingStep; import dev.jsinco.brewery.api.brew.PartialBrewScore; import dev.jsinco.brewery.api.brew.ScoreType; +import dev.jsinco.brewery.util.CollectionUtil; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; -public record DistillStepImpl(int runs) implements BrewingStep.Distill { +public record DistillStepImpl(int runs, SequencedSet brewers) implements BrewingStep.Distill { + private static final Map BREW_STEP_MISMATCH = Stream.of( new PartialBrewScore(0, ScoreType.DISTILL_AMOUNT) ).collect(Collectors.toUnmodifiableMap(PartialBrewScore::type, partial -> partial)); + public DistillStepImpl(int runs) { + this(runs, Collections.emptySortedSet()); + } + @Override public DistillStepImpl incrementRuns() { - return new DistillStepImpl(this.runs + 1); + return new DistillStepImpl(this.runs + 1, this.brewers); } @Override public Map proximityScores(BrewingStep other) { - if (!(other instanceof DistillStepImpl(int otherRuns))) { + if (!(other instanceof DistillStepImpl(int otherRuns, SequencedSet ignored))) { return BREW_STEP_MISMATCH; } double distillScore = Math.sqrt(BrewingStepUtil.nearbyValueScore(this.runs, otherRuns)); @@ -35,7 +41,7 @@ public StepType stepType() { @Override public Map maximumScores(BrewingStep other) { - if (!(other instanceof DistillStepImpl(int runs1))) { + if (!(other instanceof DistillStepImpl(int runs1, SequencedSet ignored))) { return BREW_STEP_MISMATCH; } double maximumDistillScore = runs1 < this.runs ? 1D : BrewingStepUtil.nearbyValueScore(this.runs, runs1); @@ -47,4 +53,33 @@ public Map maximumScores(BrewingStep other) { public Map failedScores() { return BREW_STEP_MISMATCH; } + + @Override + public Distill withBrewer(UUID brewer) { + return new DistillStepImpl(this.runs, Stream.concat( + this.brewers.stream(), + Stream.of(brewer) + ).collect(Collectors.toCollection(LinkedHashSet::new))); + } + + @Override + public Distill withBrewers(SequencedCollection brewers) { + return new DistillStepImpl(this.runs, Stream.concat( + this.brewers.stream(), + brewers.stream() + ).collect(Collectors.toCollection(LinkedHashSet::new))); + } + + @Override + public Distill withBrewersReplaced(SequencedCollection brewers) { + return new DistillStepImpl(this.runs, new LinkedHashSet<>(brewers)); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof DistillStepImpl(int otherRuns, SequencedSet otherBrewers))) { + return false; + } + return runs == otherRuns && CollectionUtil.isEqualWithOrdering(brewers, otherBrewers); + } } diff --git a/core/src/main/java/dev/jsinco/brewery/brew/MixStepImpl.java b/core/src/main/java/dev/jsinco/brewery/brew/MixStepImpl.java index 817ff18d..ecccfc55 100644 --- a/core/src/main/java/dev/jsinco/brewery/brew/MixStepImpl.java +++ b/core/src/main/java/dev/jsinco/brewery/brew/MixStepImpl.java @@ -5,26 +5,34 @@ import dev.jsinco.brewery.api.brew.ScoreType; import dev.jsinco.brewery.api.ingredient.Ingredient; import dev.jsinco.brewery.api.moment.Moment; +import dev.jsinco.brewery.util.CollectionUtil; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; -public record MixStepImpl(Moment time, Map ingredients) implements BrewingStep.Mix { +public record MixStepImpl(Moment time, Map ingredients, + SequencedSet brewers) implements BrewingStep.Mix { private static final Map BREW_STEP_MISMATCH = Stream.of( new PartialBrewScore(0, ScoreType.TIME), new PartialBrewScore(0, ScoreType.INGREDIENTS) ).collect(Collectors.toUnmodifiableMap(PartialBrewScore::type, partial -> partial)); + public MixStepImpl(Moment time, Map ingredients) { + this(time, ingredients, Collections.emptySortedSet()); + } + @Override public MixStepImpl withIngredients(Map ingredients) { - return new MixStepImpl(this.time, ingredients); + return new MixStepImpl(this.time, ingredients, this.brewers); } @Override public Map proximityScores(BrewingStep other) { - if (!(other instanceof MixStepImpl(Moment otherTime, Map otherIngredients))) { + if (!(other instanceof MixStepImpl( + Moment otherTime, Map otherIngredients, SequencedSet ignored + ))) { return BREW_STEP_MISMATCH; } double timeScore = BrewingStepUtil.nearbyValueScore(this.time.moment(), otherTime.moment()); @@ -52,6 +60,39 @@ public Map failedScores() { @Override public MixStepImpl withTime(Moment time) { - return new MixStepImpl(time, this.ingredients); + return new MixStepImpl(time, this.ingredients, this.brewers); + } + + @Override + public Mix withBrewer(UUID brewer) { + return new MixStepImpl(this.time, this.ingredients, Stream.concat( + this.brewers.stream(), + Stream.of(brewer) + ).collect(Collectors.toCollection(LinkedHashSet::new))); + } + + @Override + public Mix withBrewers(SequencedCollection brewers) { + return new MixStepImpl(this.time, this.ingredients, Stream.concat( + this.brewers.stream(), + brewers.stream() + ).collect(Collectors.toCollection(LinkedHashSet::new))); + } + + @Override + public Mix withBrewersReplaced(SequencedCollection brewers) { + return new MixStepImpl(this.time, this.ingredients, new LinkedHashSet<>(brewers)); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof MixStepImpl( + Moment otherTime, Map otherIngredients, SequencedSet otherBrewers + ))) { + return false; + } + return Objects.equals(time, otherTime) + && Objects.equals(ingredients, otherIngredients) + && CollectionUtil.isEqualWithOrdering(brewers, otherBrewers); } } diff --git a/core/src/main/java/dev/jsinco/brewery/util/CollectionUtil.java b/core/src/main/java/dev/jsinco/brewery/util/CollectionUtil.java new file mode 100644 index 00000000..0fe52b5b --- /dev/null +++ b/core/src/main/java/dev/jsinco/brewery/util/CollectionUtil.java @@ -0,0 +1,41 @@ +package dev.jsinco.brewery.util; + +import java.util.*; + +public class CollectionUtil { + private CollectionUtil() {} + + /** + * Returns an unmodifiable sequenced set containing an arbitrary number of elements. + * @param elements The elements to be contained in the sequenced set. + * @return A new unmodifiable sequenced set. + * @param Set element type + */ + @SafeVarargs + public static SequencedSet sequencedSetOf(E... elements) { + return Collections.unmodifiableSequencedSet(new LinkedHashSet<>(Arrays.asList(elements))); + } + + /** + * Checks if two sequenced sets are equal, and that their ordering is the same. + * @param set1 The first set + * @param set2 The second set + * @return True if both sets are the same size, and element 1 of both sets are equal, + * element 2 of both sets are equal, and so on + * @param Set element type + */ + public static boolean isEqualWithOrdering(SequencedSet set1, SequencedSet set2) { + if (set1.size() != set2.size()) { + return false; + } + Iterator iter1 = set1.iterator(); + Iterator iter2 = set2.iterator(); + while (iter1.hasNext() && iter2.hasNext()) { + if (!Objects.equals(iter1.next(), iter2.next())) { + return false; + } + } + return true; + } + +} \ No newline at end of file diff --git a/core/src/main/resources/locale/en-US.lang.properties b/core/src/main/resources/locale/en-US.lang.properties index 6b7e87d3..511713f6 100644 --- a/core/src/main/resources/locale/en-US.lang.properties +++ b/core/src/main/resources/locale/en-US.lang.properties @@ -57,12 +57,19 @@ tbp.cauldron.type.snow=snow tbp.cauldron.type.water=water tbp.command.copy=Click to copy tbp.command.encryption.rotate_key=Successfully migrated to a new 256-bit encryption key! +tbp.command.brewer.add=Successfully added to brewers +tbp.command.brewer.remove=Successfully removed from brewers +tbp.command.brewer.set=Successfully set brewers to +tbp.command.brewer.clear=Successfully cleared brewers +tbp.command.brewer.empty=Cannot modify brewers of an empty brew +tbp.command.brewer.step-out-of-bounds=Step index out of bounds, has to be less than tbp.command.create.missing-mandatory-argument=Missing mandatory argument(s) tbp.command.create.success=Successfully created tbp.command.illegal-argument=Illegal argument tbp.command.illegal-argument-detailed=Unknown argument(s) '' tbp.command.info.effect-message=Potion effects:
:
Title message:
Effect message:
Effect action bar:
Effect events: tbp.command.info.message=--------
+tbp.command.info.brewer-message=Brewers: tbp.command.info.not-a-brew=You're not holding a brew! tbp.command.missing-argument=Missing argument tbp.command.not-enough-permissions=Not enough permissions