diff --git a/src/main/generated/resources/assets/minestuck/lang/en_us.json b/src/main/generated/resources/assets/minestuck/lang/en_us.json index 43da0f7fc7..993c8babc2 100644 --- a/src/main/generated/resources/assets/minestuck/lang/en_us.json +++ b/src/main/generated/resources/assets/minestuck/lang/en_us.json @@ -49,6 +49,7 @@ "advancements.minestuck.shady_buyer.title": "Buyer Beware", "advancements.minestuck.tree_modus.description": "Remove the root card in a tree modus with a bunch of items", "advancements.minestuck.tree_modus.title": "Uprooting", + "argument.dialogue_category.invalid": "Invaid dialogue category %s", "argument.grist_set.duplicate": "Duplicate grist type %s", "argument.grist_set.incomplete": "Incomplete (expected pairs of integers and grist types)", "argument.grist_type.invalid": "Invalid grist type %s", @@ -1048,6 +1049,8 @@ "commands.minestuck.porkhollow.receive": "Received %s boondollars from %s.", "commands.minestuck.porkhollow.send": "Successfully sent %s boondollars to %s.", "commands.minestuck.porkhollow.take": "Successfully took out %s boondollars from your porkhollow.", + "commands.minestuck.review_dialouge.invalid_type": "The summoned entity is not a dialogue entity", + "commands.minestuck.review_dialouge.success": "Summoned %d entities with dialogue ready to be reviewed", "commands.minestuck.sburbconnection.already_connected": "Those players have already been connected", "commands.minestuck.sburbconnection.success": "Successfully set %s's server player as %s", "commands.minestuck.sburbpredefine.define": "Predefined full data for %s", @@ -1059,6 +1062,9 @@ "commands.minestuck.send_grist.not_permitted": "You are not permitted to send grist to %s.", "commands.minestuck.send_grist.receive": "Received grist from %s: %s", "commands.minestuck.send_grist.success": "Successfully gave grist to %s: %s", + "commands.minestuck.set_dialouge.invalid_entity": "%s is not a dialogue entity", + "commands.minestuck.set_dialouge.invalid_id": "%s is not a registered dialogue node", + "commands.minestuck.set_dialouge.success": "Set dialogue for %s to %s", "commands.minestuck.set_rung": "Successfully changed the echeladder of %s players to rung %d with %d%% progress.", "commands.minestuck.tpz.failure": "Teleportation failed for %s", "commands.minestuck.tpz.failure_result": "Failed the teleport anything.", diff --git a/src/main/java/com/mraof/minestuck/Minestuck.java b/src/main/java/com/mraof/minestuck/Minestuck.java index 30c1a0e84e..d6cae13969 100644 --- a/src/main/java/com/mraof/minestuck/Minestuck.java +++ b/src/main/java/com/mraof/minestuck/Minestuck.java @@ -7,6 +7,7 @@ import com.mraof.minestuck.block.MSBlocks; import com.mraof.minestuck.block.SkaiaBlocks; import com.mraof.minestuck.blockentity.MSBlockEntityTypes; +import com.mraof.minestuck.command.MSSuggestionProviders; import com.mraof.minestuck.command.argument.MSArgumentTypes; import com.mraof.minestuck.computer.ProgramData; import com.mraof.minestuck.computer.editmode.DeployList; @@ -134,6 +135,7 @@ private void setup(final FMLCommonSetupEvent event) private void mainThreadSetup() { MSCriteriaTriggers.register(); + MSSuggestionProviders.register(); KindAbstratusList.registerTypes(); DeployList.registerItems(); diff --git a/src/main/java/com/mraof/minestuck/command/MSCommands.java b/src/main/java/com/mraof/minestuck/command/MSCommands.java index 8289be5de6..bd802ea655 100644 --- a/src/main/java/com/mraof/minestuck/command/MSCommands.java +++ b/src/main/java/com/mraof/minestuck/command/MSCommands.java @@ -27,5 +27,7 @@ public static void serverStarting(RegisterCommandsEvent event) PorkhollowCommand.register(dispatcher); DebugLandsCommand.register(dispatcher); EntryCommand.register(dispatcher); + ReviewDialogueCommand.register(dispatcher, event.getBuildContext()); + SetDialogueCommand.register(dispatcher); } } \ No newline at end of file diff --git a/src/main/java/com/mraof/minestuck/command/MSSuggestionProviders.java b/src/main/java/com/mraof/minestuck/command/MSSuggestionProviders.java new file mode 100644 index 0000000000..ff950fd240 --- /dev/null +++ b/src/main/java/com/mraof/minestuck/command/MSSuggestionProviders.java @@ -0,0 +1,49 @@ +package com.mraof.minestuck.command; + +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mraof.minestuck.Minestuck; +import com.mraof.minestuck.entity.MSEntityTypes; +import com.mraof.minestuck.entity.dialogue.DialogueNodes; +import net.minecraft.Util; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.synchronization.SuggestionProviders; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; + +public final class MSSuggestionProviders +{ + public final SuggestionProvider DIALOGUE_ENTITY_TYPE = SuggestionProviders.register(Minestuck.id("dialogue_entity_type"), (context, builder) -> { + //todo need a better way of identifying valid entity types + Iterable> dialogueEntities = List.of(MSEntityTypes.SALAMANDER.get(), MSEntityTypes.TURTLE.get(), MSEntityTypes.NAKAGATOR.get(), MSEntityTypes.IGUANA.get()); + return SharedSuggestionProvider.suggestResource(dialogueEntities, builder, EntityType::getKey, + type -> Component.translatable(Util.makeDescriptionId("entity", EntityType.getKey(type)))); + }); + // this suggestion provider is not registered because dialogue nodes are not available at client-side + public static final SuggestionProvider ALL_DIALOGUE_NODES = (context, builder) -> SharedSuggestionProvider.suggestResource(DialogueNodes.getInstance().allIds(), builder); + + private MSSuggestionProviders() + { + } + + @Nullable + private static MSSuggestionProviders instance; + + public static MSSuggestionProviders instance() + { + return Objects.requireNonNull(instance, "Tried to get instance before suggestions had been set up."); + } + + /** + * To be called during main thread setup as {@link net.minecraft.commands.synchronization.SuggestionProviders#register(ResourceLocation, SuggestionProvider)} does not appear to be thread-safe. + */ + public static void register() + { + instance = new MSSuggestionProviders(); + } +} diff --git a/src/main/java/com/mraof/minestuck/command/ReviewDialogueCommand.java b/src/main/java/com/mraof/minestuck/command/ReviewDialogueCommand.java new file mode 100644 index 0000000000..7ce9fb3f75 --- /dev/null +++ b/src/main/java/com/mraof/minestuck/command/ReviewDialogueCommand.java @@ -0,0 +1,105 @@ +package com.mraof.minestuck.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mraof.minestuck.command.argument.DialogueCategoryArgument; +import com.mraof.minestuck.entity.dialogue.Dialogue; +import com.mraof.minestuck.entity.dialogue.DialogueEntity; +import com.mraof.minestuck.entity.dialogue.RandomlySelectableDialogue; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.ResourceArgument; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.WallSignBlock; +import net.minecraft.world.level.block.entity.SignBlockEntity; +import net.minecraft.world.level.block.entity.SignText; + +import java.util.Collection; + +public final class ReviewDialogueCommand +{ + public static final String INVALID_TYPE_KEY = "commands.minestuck.review_dialouge.invalid_type"; + public static final String SUCCESS_KEY = "commands.minestuck.review_dialouge.success"; + private static final SimpleCommandExceptionType ERROR_FAILED = new SimpleCommandExceptionType(Component.translatable("commands.summon.failed")); + private static final SimpleCommandExceptionType INVALID_TYPE = new SimpleCommandExceptionType(Component.translatable(INVALID_TYPE_KEY)); + + public static void register(CommandDispatcher dispatcher, CommandBuildContext buildContext) + { + dispatcher.register(Commands.literal("review_dialogue").requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("entity", ResourceArgument.resource(buildContext, Registries.ENTITY_TYPE)) + .suggests(MSSuggestionProviders.instance().DIALOGUE_ENTITY_TYPE) + .then(Commands.argument("category", new DialogueCategoryArgument()) + .executes(context -> spawnDialogueEntities(context.getSource(), + ResourceArgument.getSummonableEntityType(context, "entity").value(), + DialogueCategoryArgument.getCategory(context, "category")))))); + } + + private static int spawnDialogueEntities(CommandSourceStack source, EntityType entityType, RandomlySelectableDialogue.DialogueCategory category) throws CommandSyntaxException + { + Collection dialogueCollection = RandomlySelectableDialogue.instance(category).getAll(); + + ServerLevel level = source.getLevel(); + BlockPos pos = BlockPos.containing(source.getPosition()); + + for(Dialogue.SelectableDialogue dialogue : dialogueCollection) + { + pos = pos.east(2); + level.removeBlock(pos, false); + level.setBlock(pos.below(), Blocks.BRICKS.defaultBlockState(), Block.UPDATE_ALL); + + DialogueEntity dialogueEntity = spawnEntity(entityType, level, pos); + + dialogueEntity.getDialogueComponent().setDialogue(dialogue.dialogueId(), true); + + placeSign(dialogue, pos, level); + } + + source.sendSuccess(() -> Component.translatable(SUCCESS_KEY, dialogueCollection.size()), true); + + return dialogueCollection.size(); + } + + private static DialogueEntity spawnEntity(EntityType entityType, ServerLevel level, BlockPos pos) throws CommandSyntaxException + { + Entity entity = entityType.spawn(level, pos, MobSpawnType.COMMAND); + if(entity == null) + throw ERROR_FAILED.create(); + + entity.setNoGravity(true); + if(entity instanceof Mob mob) + mob.setNoAi(true); + + if(entity instanceof DialogueEntity dialogueEntity) + return dialogueEntity; + else + throw INVALID_TYPE.create(); + } + + private static void placeSign(Dialogue.SelectableDialogue dialogue, BlockPos pos, ServerLevel level) + { + BlockPos signPos = pos.north().below(); + level.setBlock(signPos, Blocks.OAK_WALL_SIGN.defaultBlockState().setValue(WallSignBlock.FACING, Direction.NORTH), Block.UPDATE_ALL); + if(level.getBlockEntity(signPos) instanceof SignBlockEntity sign) + { + SignText signText = new SignText().setHasGlowingText(true).setColor(DyeColor.WHITE) + .setMessage(0, Component.literal(dialogue.dialogueId().getNamespace())); + String[] lines = dialogue.dialogueId().getPath().split("/", 3); + for(int i = 0; i < lines.length; i++) + signText = signText.setMessage(i + 1, Component.literal(lines[i])); + sign.setText(signText, true); + } + } +} diff --git a/src/main/java/com/mraof/minestuck/command/SetDialogueCommand.java b/src/main/java/com/mraof/minestuck/command/SetDialogueCommand.java new file mode 100644 index 0000000000..0eb5afc51e --- /dev/null +++ b/src/main/java/com/mraof/minestuck/command/SetDialogueCommand.java @@ -0,0 +1,46 @@ +package com.mraof.minestuck.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mraof.minestuck.entity.dialogue.DialogueEntity; +import com.mraof.minestuck.entity.dialogue.DialogueNodes; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.ResourceLocationArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; + +public final class SetDialogueCommand +{ + public static final String INVALID_ENTITY_KEY = "commands.minestuck.set_dialouge.invalid_entity"; + public static final String INVALID_ID_KEY = "commands.minestuck.set_dialouge.invalid_id"; + public static final String SUCCESS_KEY = "commands.minestuck.set_dialouge.success"; + private static final DynamicCommandExceptionType INVALID_ENTITY = new DynamicCommandExceptionType(entity -> Component.translatable(INVALID_ENTITY_KEY, entity)); + private static final DynamicCommandExceptionType INVALID_ID = new DynamicCommandExceptionType(id -> Component.translatable(INVALID_ID_KEY, id)); + + public static void register(CommandDispatcher dispatcher) + { + dispatcher.register(Commands.literal("set_dialogue").requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("entity", EntityArgument.entity()) + .then(Commands.argument("dialogue", ResourceLocationArgument.id()) + .suggests(MSSuggestionProviders.ALL_DIALOGUE_NODES) + .executes(context -> setDialogue(context.getSource(), + EntityArgument.getEntity(context, "entity"), ResourceLocationArgument.getId(context, "dialogue")))))); + } + + private static int setDialogue(CommandSourceStack source, Entity entity, ResourceLocation id) throws CommandSyntaxException + { + if(!(entity instanceof DialogueEntity dialogueEntity)) + throw INVALID_ENTITY.create(entity.getDisplayName()); + + if(DialogueNodes.getInstance().getDialogue(id) == null) + throw INVALID_ID.create(id); + + dialogueEntity.getDialogueComponent().setDialogue(id, true); + source.sendSuccess(() -> Component.translatable(SUCCESS_KEY, entity.getDisplayName(), id), true); + return 1; + } +} diff --git a/src/main/java/com/mraof/minestuck/command/argument/DialogueCategoryArgument.java b/src/main/java/com/mraof/minestuck/command/argument/DialogueCategoryArgument.java new file mode 100644 index 0000000000..a97192a642 --- /dev/null +++ b/src/main/java/com/mraof/minestuck/command/argument/DialogueCategoryArgument.java @@ -0,0 +1,57 @@ +package com.mraof.minestuck.command.argument; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mraof.minestuck.entity.dialogue.RandomlySelectableDialogue.DialogueCategory; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; + +import java.util.Collection; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +public final class DialogueCategoryArgument implements ArgumentType +{ + public static final String INVALID = "argument.dialogue_category.invalid"; + public static final DynamicCommandExceptionType INVALID_TYPE = new DynamicCommandExceptionType(o -> Component.translatable(INVALID, o)); + + public static final Collection CATEGORY_STRINGS = Stream.of(DialogueCategory.values()).map(DialogueCategory::folderName).toList(); + + @Override + public DialogueCategory parse(StringReader reader) throws CommandSyntaxException + { + int start = reader.getCursor(); + String name = reader.readUnquotedString(); + Optional categoryOptional = Stream.of(DialogueCategory.values()).filter(category -> category.name().equalsIgnoreCase(name)).findAny(); + if(categoryOptional.isEmpty()) + { + reader.setCursor(start); + throw INVALID_TYPE.createWithContext(reader, name); + } + return categoryOptional.get(); + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) + { + return SharedSuggestionProvider.suggest(CATEGORY_STRINGS, builder); + } + + @Override + public Collection getExamples() + { + return CATEGORY_STRINGS; + } + + public static DialogueCategory getCategory(CommandContext context, String id) + { + return context.getArgument(id, DialogueCategory.class); + } +} diff --git a/src/main/java/com/mraof/minestuck/command/argument/MSArgumentTypes.java b/src/main/java/com/mraof/minestuck/command/argument/MSArgumentTypes.java index ff56727315..cbf2c5d55e 100644 --- a/src/main/java/com/mraof/minestuck/command/argument/MSArgumentTypes.java +++ b/src/main/java/com/mraof/minestuck/command/argument/MSArgumentTypes.java @@ -25,5 +25,6 @@ public class MSArgumentTypes SingletonArgumentInfo.contextFree(TitleArgument::title))); //noinspection unchecked,rawtypes REGISTER.register("list", () -> ArgumentTypeInfos.registerByClass(ListArgument.class, new ListArgument.Info())); + REGISTER.register("dialogue_category", () -> ArgumentTypeInfos.registerByClass(DialogueCategoryArgument.class, SingletonArgumentInfo.contextFree(DialogueCategoryArgument::new))); } } diff --git a/src/main/java/com/mraof/minestuck/data/MinestuckEnUsLanguageProvider.java b/src/main/java/com/mraof/minestuck/data/MinestuckEnUsLanguageProvider.java index a025a18e16..6cfbef041f 100644 --- a/src/main/java/com/mraof/minestuck/data/MinestuckEnUsLanguageProvider.java +++ b/src/main/java/com/mraof/minestuck/data/MinestuckEnUsLanguageProvider.java @@ -2346,6 +2346,11 @@ protected void addTranslations() add(SburbPredefineCommand.SET_TITLE_LAND, "Predefined %s's title land type"); add(SburbPredefineCommand.DEFINE, "Predefined full data for %s"); add(SburbPredefineCommand.TOO_LATE, "It is too late to predefine data for this player"); + add(ReviewDialogueCommand.INVALID_TYPE_KEY, "The summoned entity is not a dialogue entity"); + add(ReviewDialogueCommand.SUCCESS_KEY, "Summoned %d entities with dialogue ready to be reviewed"); + add(SetDialogueCommand.INVALID_ENTITY_KEY, "%s is not a dialogue entity"); + add(SetDialogueCommand.INVALID_ID_KEY, "%s is not a registered dialogue node"); + add(SetDialogueCommand.SUCCESS_KEY, "Set dialogue for %s to %s"); add(GristTypeArgument.INVALID, "Invalid grist type %s"); add(GristSetArgument.INCOMPLETE, "Incomplete (expected pairs of integers and grist types)"); add(GristSetArgument.DUPLICATE, "Duplicate grist type %s"); @@ -2355,6 +2360,7 @@ protected void addTranslations() add(TitleLandTypeArgument.INVALID, "Invalid title land type %s"); add(TerrainLandTypeArgument.INVALID, "Invalid terrain land type %s"); add(LandTypePairArgument.INCOMPLETE, "Incomplete (expected two land aspects)"); + add(DialogueCategoryArgument.INVALID, "Invaid dialogue category %s"); add(PredefineData.TITLE_ALREADY_SET, "That player already has their title set to %s"); add(PredefineData.RESETTING_TERRAIN_TYPE, "The currently set terrain type %s is not compatible with land type, and will be reset"); diff --git a/src/main/java/com/mraof/minestuck/entity/dialogue/DialogueNodes.java b/src/main/java/com/mraof/minestuck/entity/dialogue/DialogueNodes.java index cccd63eca3..e780cb174d 100644 --- a/src/main/java/com/mraof/minestuck/entity/dialogue/DialogueNodes.java +++ b/src/main/java/com/mraof/minestuck/entity/dialogue/DialogueNodes.java @@ -20,6 +20,7 @@ import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; +import java.util.Collection; import java.util.Map; import java.util.Objects; @@ -49,6 +50,11 @@ public Dialogue.NodeSelector getDialogue(ResourceLocation location) return this.dialogues.get(location); } + public Collection allIds() + { + return this.dialogues.keySet(); + } + @SubscribeEvent public static void onResourceReload(AddReloadListenerEvent event) { diff --git a/src/main/java/com/mraof/minestuck/entity/dialogue/RandomlySelectableDialogue.java b/src/main/java/com/mraof/minestuck/entity/dialogue/RandomlySelectableDialogue.java index e5dff2e439..6100e26399 100644 --- a/src/main/java/com/mraof/minestuck/entity/dialogue/RandomlySelectableDialogue.java +++ b/src/main/java/com/mraof/minestuck/entity/dialogue/RandomlySelectableDialogue.java @@ -52,6 +52,11 @@ public Optional pickRandomForEntity(LivingEntity en .map(WeightedEntry.Wrapper::getData); } + public Collection getAll() + { + return this.selectableDialogueList; + } + @SubscribeEvent public static void onResourceReload(AddReloadListenerEvent event) {