Skip to content

Commit

Permalink
Add dialogue commands for testing and reviewing dialogue (#590)
Browse files Browse the repository at this point in the history
Two new commands:
- `/review_dialogue <entity_type> <dialogue_category>` spawns entities
of the given type for every dialogue in the given category (going by the
starting node of all randomly selectable dialogue)
- `/set_dialogue <entity> <dialogue_id>` sets the dialogue node for the
given entity

Both of these commands sets the dialogue such that it is kept on reset.
  • Loading branch information
kirderf1 authored Apr 5, 2024
1 parent 0a86742 commit f2e71e4
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/main/generated/resources/assets/minestuck/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/mraof/minestuck/Minestuck.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -134,6 +135,7 @@ private void setup(final FMLCommonSetupEvent event)
private void mainThreadSetup()
{
MSCriteriaTriggers.register();
MSSuggestionProviders.register();

KindAbstratusList.registerTypes();
DeployList.registerItems();
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/mraof/minestuck/command/MSCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<CommandSourceStack> DIALOGUE_ENTITY_TYPE = SuggestionProviders.register(Minestuck.id("dialogue_entity_type"), (context, builder) -> {
//todo need a better way of identifying valid entity types
Iterable<EntityType<?>> 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<CommandSourceStack> 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();
}
}
105 changes: 105 additions & 0 deletions src/main/java/com/mraof/minestuck/command/ReviewDialogueCommand.java
Original file line number Diff line number Diff line change
@@ -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<CommandSourceStack> 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<Dialogue.SelectableDialogue> 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);
}
}
}
46 changes: 46 additions & 0 deletions src/main/java/com/mraof/minestuck/command/SetDialogueCommand.java
Original file line number Diff line number Diff line change
@@ -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<CommandSourceStack> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<DialogueCategory>
{
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<String> 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<DialogueCategory> 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 <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder)
{
return SharedSuggestionProvider.suggest(CATEGORY_STRINGS, builder);
}

@Override
public Collection<String> getExamples()
{
return CATEGORY_STRINGS;
}

public static DialogueCategory getCategory(CommandContext<CommandSourceStack> context, String id)
{
return context.getArgument(id, DialogueCategory.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;

Expand Down Expand Up @@ -49,6 +50,11 @@ public Dialogue.NodeSelector getDialogue(ResourceLocation location)
return this.dialogues.get(location);
}

public Collection<ResourceLocation> allIds()
{
return this.dialogues.keySet();
}

@SubscribeEvent
public static void onResourceReload(AddReloadListenerEvent event)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public Optional<Dialogue.SelectableDialogue> pickRandomForEntity(LivingEntity en
.map(WeightedEntry.Wrapper::getData);
}

public Collection<Dialogue.SelectableDialogue> getAll()
{
return this.selectableDialogueList;
}

@SubscribeEvent
public static void onResourceReload(AddReloadListenerEvent event)
{
Expand Down

0 comments on commit f2e71e4

Please sign in to comment.