Skip to content

Commit cbc3702

Browse files
committed
feat(commands): Add admin commands for managing research progress and drives
1 parent 0487061 commit cbc3702

3 files changed

Lines changed: 294 additions & 1 deletion

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,19 @@ A Minecraft NeoForge mod (1.21.1) that adds a research-gated crafting system. Pe
5555
| Research Chip | Transfer completed research between players |
5656
| Fluid Buckets | Fill the Research Station's tank |
5757

58+
## Commands
59+
60+
All commands require OP level 2. See the [Commands Reference](wiki/Commands.md) for full details.
61+
62+
| Command | Description |
63+
|---------|-------------|
64+
| `/researchcube unlock <player> <research> [force]` | Unlock a research for a player |
65+
| `/researchcube unlockAll <player>` | Unlock all research for a player |
66+
| `/researchcube lock <player> <research>` | Lock a completed research |
67+
| `/researchcube getDrive <player> <research>` | Give an imprinted drive for a research |
68+
| `/researchcube addToDrive <research> [force]` | Add research recipes to held drive |
69+
| `/researchcube help` | Show in-game help |
70+
5871
## Quick Start for Pack Developers
5972

6073
### Adding Custom Research
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
package com.researchcube.command;
2+
3+
import com.mojang.brigadier.CommandDispatcher;
4+
import com.mojang.brigadier.arguments.BoolArgumentType;
5+
import com.mojang.brigadier.context.CommandContext;
6+
import com.mojang.brigadier.exceptions.CommandSyntaxException;
7+
import com.mojang.brigadier.suggestion.SuggestionProvider;
8+
import com.researchcube.item.DriveItem;
9+
import com.researchcube.registry.ModItems;
10+
import com.researchcube.research.ResearchDefinition;
11+
import com.researchcube.research.ResearchRegistry;
12+
import com.researchcube.research.ResearchSavedData;
13+
import com.researchcube.research.ResearchTier;
14+
import com.researchcube.research.WeightedRecipe;
15+
import com.researchcube.util.NbtUtil;
16+
import net.minecraft.ChatFormatting;
17+
import net.minecraft.commands.CommandSourceStack;
18+
import net.minecraft.commands.Commands;
19+
import net.minecraft.commands.SharedSuggestionProvider;
20+
import net.minecraft.commands.arguments.EntityArgument;
21+
import net.minecraft.commands.arguments.ResourceLocationArgument;
22+
import net.minecraft.network.chat.Component;
23+
import net.minecraft.resources.ResourceLocation;
24+
import net.minecraft.server.level.ServerPlayer;
25+
import net.minecraft.world.item.ItemStack;
26+
27+
import java.util.Collection;
28+
import java.util.Set;
29+
30+
/**
31+
* Admin command for managing research progress and drives.
32+
* All subcommands require OP level 2.
33+
*/
34+
public class ResearchCubeCommand {
35+
36+
private static final SuggestionProvider<CommandSourceStack> SUGGEST_RESEARCH = (context, builder) -> {
37+
Collection<ResearchDefinition> all = ResearchRegistry.getAll();
38+
return SharedSuggestionProvider.suggestResource(
39+
all.stream().map(ResearchDefinition::getId),
40+
builder
41+
);
42+
};
43+
44+
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
45+
dispatcher.register(Commands.literal("researchcube")
46+
.requires(src -> src.hasPermission(2))
47+
.then(Commands.literal("unlock")
48+
.then(Commands.argument("player", EntityArgument.player())
49+
.then(Commands.argument("research", ResourceLocationArgument.id())
50+
.suggests(SUGGEST_RESEARCH)
51+
.executes(ctx -> unlock(ctx, false))
52+
.then(Commands.argument("force", BoolArgumentType.bool())
53+
.executes(ctx -> unlock(ctx, BoolArgumentType.getBool(ctx, "force")))))))
54+
.then(Commands.literal("unlockAll")
55+
.then(Commands.argument("player", EntityArgument.player())
56+
.executes(ResearchCubeCommand::unlockAll)))
57+
.then(Commands.literal("lock")
58+
.then(Commands.argument("player", EntityArgument.player())
59+
.then(Commands.argument("research", ResourceLocationArgument.id())
60+
.suggests(SUGGEST_RESEARCH)
61+
.executes(ResearchCubeCommand::lock))))
62+
.then(Commands.literal("getDrive")
63+
.then(Commands.argument("player", EntityArgument.player())
64+
.then(Commands.argument("research", ResourceLocationArgument.id())
65+
.suggests(SUGGEST_RESEARCH)
66+
.executes(ResearchCubeCommand::getDrive))))
67+
.then(Commands.literal("addToDrive")
68+
.then(Commands.argument("research", ResourceLocationArgument.id())
69+
.suggests(SUGGEST_RESEARCH)
70+
.executes(ctx -> addToDrive(ctx, false))
71+
.then(Commands.argument("force", BoolArgumentType.bool())
72+
.executes(ctx -> addToDrive(ctx, BoolArgumentType.getBool(ctx, "force"))))))
73+
.then(Commands.literal("help")
74+
.executes(ResearchCubeCommand::help))
75+
);
76+
}
77+
78+
private static int unlock(CommandContext<CommandSourceStack> ctx, boolean force) throws CommandSyntaxException {
79+
ServerPlayer player = EntityArgument.getPlayer(ctx, "player");
80+
ResourceLocation researchId = ResourceLocationArgument.getId(ctx, "research");
81+
CommandSourceStack source = ctx.getSource();
82+
83+
ResearchDefinition def = ResearchRegistry.get(researchId);
84+
if (def == null) {
85+
source.sendFailure(Component.literal("Unknown research: " + researchId));
86+
return 0;
87+
}
88+
89+
ResearchSavedData data = ResearchSavedData.get(source.getServer());
90+
String key = ResearchSavedData.getResearchKey(player);
91+
92+
if (data.hasCompleted(key, researchId)) {
93+
source.sendFailure(Component.literal("Player " + player.getName().getString()
94+
+ " has already completed " + researchId));
95+
return 0;
96+
}
97+
98+
if (!force) {
99+
Set<String> completed = data.getCompletedResearchStrings(key);
100+
if (!def.getPrerequisites().isSatisfied(completed)) {
101+
source.sendFailure(Component.literal("Prerequisites not met for " + researchId
102+
+ ". Use force=true to override."));
103+
return 0;
104+
}
105+
}
106+
107+
data.addCompleted(key, researchId);
108+
source.sendSuccess(() -> Component.literal("Unlocked " + researchId + " for "
109+
+ player.getName().getString()), true);
110+
return 1;
111+
}
112+
113+
private static int unlockAll(CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
114+
ServerPlayer player = EntityArgument.getPlayer(ctx, "player");
115+
CommandSourceStack source = ctx.getSource();
116+
117+
ResearchSavedData data = ResearchSavedData.get(source.getServer());
118+
String key = ResearchSavedData.getResearchKey(player);
119+
120+
int count = 0;
121+
for (ResearchDefinition def : ResearchRegistry.getAll()) {
122+
if (!data.hasCompleted(key, def.getId())) {
123+
data.addCompleted(key, def.getId());
124+
count++;
125+
}
126+
}
127+
128+
int finalCount = count;
129+
source.sendSuccess(() -> Component.literal("Unlocked " + finalCount + " research(es) for "
130+
+ player.getName().getString()), true);
131+
return count;
132+
}
133+
134+
private static int lock(CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
135+
ServerPlayer player = EntityArgument.getPlayer(ctx, "player");
136+
ResourceLocation researchId = ResourceLocationArgument.getId(ctx, "research");
137+
CommandSourceStack source = ctx.getSource();
138+
139+
ResearchDefinition def = ResearchRegistry.get(researchId);
140+
if (def == null) {
141+
source.sendFailure(Component.literal("Unknown research: " + researchId));
142+
return 0;
143+
}
144+
145+
ResearchSavedData data = ResearchSavedData.get(source.getServer());
146+
String key = ResearchSavedData.getResearchKey(player);
147+
148+
if (!data.hasCompleted(key, researchId)) {
149+
source.sendFailure(Component.literal("Player " + player.getName().getString()
150+
+ " has not completed " + researchId));
151+
return 0;
152+
}
153+
154+
data.removeCompleted(key, researchId);
155+
source.sendSuccess(() -> Component.literal("Locked " + researchId + " for "
156+
+ player.getName().getString()), true);
157+
return 1;
158+
}
159+
160+
private static int getDrive(CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
161+
ServerPlayer player = EntityArgument.getPlayer(ctx, "player");
162+
ResourceLocation researchId = ResourceLocationArgument.getId(ctx, "research");
163+
CommandSourceStack source = ctx.getSource();
164+
165+
ResearchDefinition def = ResearchRegistry.get(researchId);
166+
if (def == null) {
167+
source.sendFailure(Component.literal("Unknown research: " + researchId));
168+
return 0;
169+
}
170+
171+
ItemStack driveStack = getDriveItemForTier(def.getTier());
172+
if (driveStack.isEmpty()) {
173+
source.sendFailure(Component.literal("No drive available for tier " + def.getTier().getDisplayName()));
174+
return 0;
175+
}
176+
177+
for (WeightedRecipe wr : def.getWeightedRecipePool()) {
178+
NbtUtil.addRecipe(driveStack, wr.id().toString());
179+
}
180+
181+
// Mark research as completed
182+
ResearchSavedData data = ResearchSavedData.get(source.getServer());
183+
String key = ResearchSavedData.getResearchKey(player);
184+
data.addCompleted(key, researchId);
185+
186+
// Give the drive to the player
187+
if (!player.getInventory().add(driveStack)) {
188+
player.drop(driveStack, false);
189+
}
190+
191+
source.sendSuccess(() -> Component.literal("Gave " + def.getTier().getDisplayName()
192+
+ " drive with " + def.getWeightedRecipePool().size() + " recipe(s) to "
193+
+ player.getName().getString()), true);
194+
return 1;
195+
}
196+
197+
private static int addToDrive(CommandContext<CommandSourceStack> ctx, boolean force) throws CommandSyntaxException {
198+
CommandSourceStack source = ctx.getSource();
199+
ServerPlayer player = source.getPlayerOrException();
200+
ResourceLocation researchId = ResourceLocationArgument.getId(ctx, "research");
201+
202+
ResearchDefinition def = ResearchRegistry.get(researchId);
203+
if (def == null) {
204+
source.sendFailure(Component.literal("Unknown research: " + researchId));
205+
return 0;
206+
}
207+
208+
ItemStack heldItem = player.getMainHandItem();
209+
if (!(heldItem.getItem() instanceof DriveItem driveItem)) {
210+
source.sendFailure(Component.literal("You must hold a drive in your main hand."));
211+
return 0;
212+
}
213+
214+
if (!force && driveItem.getTier() != def.getTier()) {
215+
source.sendFailure(Component.literal("Drive tier (" + driveItem.getTier().getDisplayName()
216+
+ ") does not match research tier (" + def.getTier().getDisplayName()
217+
+ "). Use force=true to override."));
218+
return 0;
219+
}
220+
221+
int added = 0;
222+
for (WeightedRecipe wr : def.getWeightedRecipePool()) {
223+
String recipeId = wr.id().toString();
224+
if (!NbtUtil.hasRecipe(heldItem, recipeId)) {
225+
NbtUtil.addRecipe(heldItem, recipeId);
226+
added++;
227+
}
228+
}
229+
230+
int finalAdded = added;
231+
source.sendSuccess(() -> Component.literal("Added " + finalAdded + " recipe(s) from "
232+
+ researchId + " to held drive."), true);
233+
return 1;
234+
}
235+
236+
private static int help(CommandContext<CommandSourceStack> ctx) {
237+
CommandSourceStack source = ctx.getSource();
238+
source.sendSuccess(() -> Component.literal("=== ResearchCube Commands ===")
239+
.withStyle(ChatFormatting.GOLD), false);
240+
source.sendSuccess(() -> Component.literal("/researchcube unlock <player> <research> [force]")
241+
.withStyle(ChatFormatting.YELLOW)
242+
.append(Component.literal(" - Unlock a research for a player").withStyle(ChatFormatting.GRAY)), false);
243+
source.sendSuccess(() -> Component.literal("/researchcube unlockAll <player>")
244+
.withStyle(ChatFormatting.YELLOW)
245+
.append(Component.literal(" - Unlock all research for a player").withStyle(ChatFormatting.GRAY)), false);
246+
source.sendSuccess(() -> Component.literal("/researchcube lock <player> <research>")
247+
.withStyle(ChatFormatting.YELLOW)
248+
.append(Component.literal(" - Lock a research for a player").withStyle(ChatFormatting.GRAY)), false);
249+
source.sendSuccess(() -> Component.literal("/researchcube getDrive <player> <research>")
250+
.withStyle(ChatFormatting.YELLOW)
251+
.append(Component.literal(" - Give an imprinted drive for a research").withStyle(ChatFormatting.GRAY)), false);
252+
source.sendSuccess(() -> Component.literal("/researchcube addToDrive <research> [force]")
253+
.withStyle(ChatFormatting.YELLOW)
254+
.append(Component.literal(" - Add research recipes to held drive").withStyle(ChatFormatting.GRAY)), false);
255+
source.sendSuccess(() -> Component.literal("/researchcube help")
256+
.withStyle(ChatFormatting.YELLOW)
257+
.append(Component.literal(" - Show this help message").withStyle(ChatFormatting.GRAY)), false);
258+
return 1;
259+
}
260+
261+
private static ItemStack getDriveItemForTier(ResearchTier tier) {
262+
return switch (tier) {
263+
case IRRECOVERABLE -> new ItemStack(ModItems.METADATA_IRRECOVERABLE.get());
264+
case UNSTABLE -> new ItemStack(ModItems.METADATA_UNSTABLE.get());
265+
case BASIC -> new ItemStack(ModItems.METADATA_RECLAIMED.get());
266+
case ADVANCED -> new ItemStack(ModItems.METADATA_ENHANCED.get());
267+
case PRECISE -> new ItemStack(ModItems.METADATA_ELABORATE.get());
268+
case FLAWLESS -> new ItemStack(ModItems.METADATA_CYBERNETIC.get());
269+
case SELF_AWARE -> new ItemStack(ModItems.METADATA_SELF_AWARE.get());
270+
};
271+
}
272+
}

src/main/java/com/researchcube/event/ModServerEvents.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package com.researchcube.event;
22

33
import com.researchcube.ResearchCubeMod;
4+
import com.researchcube.command.ResearchCubeCommand;
45
import com.researchcube.research.ResearchManager;
56
import net.neoforged.bus.api.SubscribeEvent;
67
import net.neoforged.fml.common.EventBusSubscriber;
78
import net.neoforged.neoforge.event.AddReloadListenerEvent;
9+
import net.neoforged.neoforge.event.RegisterCommandsEvent;
810

911
/**
10-
* Registers server-side reload listeners (datapack loading).
12+
* Registers server-side reload listeners (datapack loading) and commands.
1113
* This hooks ResearchManager into the datapack reload lifecycle.
1214
*/
1315
@EventBusSubscriber(modid = ResearchCubeMod.MOD_ID)
@@ -18,4 +20,10 @@ public static void onAddReloadListeners(AddReloadListenerEvent event) {
1820
event.addListener(new ResearchManager());
1921
ResearchCubeMod.LOGGER.debug("Registered ResearchManager reload listener.");
2022
}
23+
24+
@SubscribeEvent
25+
public static void onRegisterCommands(RegisterCommandsEvent event) {
26+
ResearchCubeCommand.register(event.getDispatcher());
27+
ResearchCubeMod.LOGGER.debug("Registered /researchcube command.");
28+
}
2129
}

0 commit comments

Comments
 (0)