Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ finished item.
- **Profession progression** — connect recipes and materials to profession
access and experience rewards.

## Staff equipment grants

`/ac give equipment <recipe> <ingredient.id|alloy.id> [player] [quality]`
gives one finished item using a configured recipe and compatible main material.
For example, `/ac give equipment heavy_chestplate ingredient.steel_ingot Alex 100`.
Tab completion lists loaded recipes, compatible ingredients/alloys and online players.
Omit the player to give to yourself; console must specify an online player.
Quality defaults to 100 and accepts finite values from 0 to 100 (specify the player
before quality). Alloy IDs are the loaded discovery IDs, not display names.

Secondary recipe ingredients use the lowest configured tier, then ingredient ID
alphabetically to break ties. Their stats and appearance participate normally.
The item retains normal stats, quality sockets, appearance, tier and crafting
provenance; no materials are consumed and no XP or activity rewards are granted.
Full inventories drop the item at the recipient's location.

Set `give-equipment-permission` in `config.yml` and run `/ac reload` to change
access. Missing or blank settings default to `advancedcrafting.admin` (operators).
Grant a custom permission only to staff; it does not grant other admin commands.

## Credits

Originally authored by **Drefvelin**.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class Cache {
public static String craftingStation;
public static String alloyStation;
public static String ingredientStation;
public static String giveEquipmentPermission = "advancedcrafting.admin";

public static double alloyForgeBaseSuccess = 2.0;
public static double alloyForgeBonusPerSqrtValue = 4.0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ public void load(File configFile) {
}

Cache.scrap = config.getString("scrap-path");
Cache.giveEquipmentPermission = config.getString("give-equipment-permission", "advancedcrafting.admin").trim();
if (Cache.giveEquipmentPermission.isEmpty()) {
Cache.giveEquipmentPermission = "advancedcrafting.admin";
}

Cache.craftingStation = config.getString("crafting-station", "v(ANVIL)");
Cache.alloyStation = config.getString("alloy-station", "v(BLAST_FURNACE)");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import net.tfminecraft.advancedcrafting.utils.CraftStatRefresher.RefreshResult;

public class CommandManager implements Listener, CommandExecutor, TabCompleter {
private final EquipmentGiveCommand equipmentGive = new EquipmentGiveCommand();
public String cmd1 = "ac";
public String cmd2 = "alloy";

Expand All @@ -51,6 +52,9 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String
}

private boolean handleAcCommand(CommandSender sender, String[] args) {
if (args.length >= 2 && args[0].equalsIgnoreCase("give") && args[1].equalsIgnoreCase("equipment")) {
return equipmentGive.execute(sender, args);
}
if (args.length >= 1 && args[0].equalsIgnoreCase("reload")) {
if (!AdminPermissions.require(sender)) {
return true;
Expand Down Expand Up @@ -167,7 +171,19 @@ public List<String> onTabComplete(CommandSender sender, Command cmd, String labe
}

private List<String> onAcTabComplete(CommandSender sender, String[] args) {
if (args.length >= 2 && args[0].equalsIgnoreCase("give")) {
if (args.length == 2) {
List<String> options = new ArrayList<>();
if (sender.hasPermission(AdminPermissions.PERMISSION)) options.add("alloy");
if (equipmentGive.hasPermission(sender)) options.add("equipment");
return filterPrefix(args[1], options);
}
if (args[1].equalsIgnoreCase("equipment")) return equipmentGive.complete(sender, args);
}
if (!sender.hasPermission(AdminPermissions.PERMISSION)) {
if (args.length <= 1 && equipmentGive.hasPermission(sender)) {
return filterPrefix(args.length == 0 ? "" : args[0], "give");
}
return List.of();
}
if (args.length == 0) {
Expand All @@ -181,9 +197,6 @@ private List<String> onAcTabComplete(CommandSender sender, String[] args) {
if (sub.equals("sync")) {
return filterPrefix(args[1], "recipes");
}
if (sub.equals("give")) {
return filterPrefix(args[1], "alloy");
}
if (sub.equals("alloy")) {
return filterPrefix(args[1], "info");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package net.tfminecraft.advancedcrafting.managers;

import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
import java.util.logging.Level;

import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;

import net.tfminecraft.advancedcrafting.AdvancedCrafting;
import net.tfminecraft.advancedcrafting.cache.Cache;
import net.tfminecraft.advancedcrafting.loaders.IngredientLoader;
import net.tfminecraft.advancedcrafting.loaders.RecipeLoader;
import net.tfminecraft.advancedcrafting.objects.crafting.CraftingRecipe;
import net.tfminecraft.advancedcrafting.objects.crafting.CraftingStation;
import net.tfminecraft.advancedcrafting.objects.ingredients.Ingredient;

/** Staff grants use the normal equipment builder, without crafting rewards or station state. */
final class EquipmentGiveCommand {
boolean hasPermission(CommandSender sender) {
return sender.hasPermission(Cache.giveEquipmentPermission);
}

boolean execute(CommandSender sender, String[] args) {
if (!hasPermission(sender)) {
sender.sendMessage("§cNo permission.");
return true;
}
if (args.length < 4 || args.length > 6) {
sender.sendMessage("§cUsage: /ac give equipment <recipe> <ingredient.id|alloy.id> [player] [quality 0-100]");
return true;
}
Player target;
if (args.length >= 5) {
target = Bukkit.getPlayerExact(args[4]);
if (target == null) {
sender.sendMessage("§cPlayer not found: §f" + args[4]);
return true;
}
} else if (sender instanceof Player player) {
target = player;
} else {
sender.sendMessage("§cSpecify an online player from console.");
return true;
}
double quality = 100;
if (args.length == 6) {
try {
quality = Double.parseDouble(args[5]);
} catch (NumberFormatException ex) {
sender.sendMessage("§cQuality must be a finite number from 0 to 100.");
return true;
}
if (!Double.isFinite(quality) || quality < 0 || quality > 100) {
sender.sendMessage("§cQuality must be a finite number from 0 to 100.");
return true;
}
}
CraftingRecipe recipe = RecipeLoader.getByString(args[2].toLowerCase(Locale.ROOT));
if (recipe == null) {
sender.sendMessage("§cUnknown recipe: §f" + args[2]);
return true;
}
String material = args[3].toLowerCase(Locale.ROOT);
if (!materialsFor(recipe).contains(material)) {
sender.sendMessage("§cUnknown or incompatible base material/alloy: §f" + args[3]);
return true;
}
HashMap<String, Integer> inputs = new HashMap<>();
for (var entry : recipe.getRecipe().entrySet()) {
String key = material;
if (!entry.getKey().equalsIgnoreCase(recipe.getMainType())) {
Ingredient filler = IngredientLoader.get().stream()
.filter(i -> i.getIngredientData().getType().getId().equalsIgnoreCase(entry.getKey()))
.min(Comparator.comparingInt((Ingredient i) -> i.getIngredientData().getTier())
.thenComparing(Ingredient::getId)).orElse(null);
if (filler == null) {
sender.sendMessage("§cNo configured ingredient for required type: §f" + entry.getKey());
return true;
}
key = "ingredient." + filler.getId();
}
inputs.put(key, entry.getValue());
}
try {
CraftingStation station = new CraftingStation(target.getLocation(), recipe, inputs, new HashMap<>());
ItemStack item = station.buildCompletedItem(target, quality);
for (ItemStack overflow : target.getInventory().addItem(item).values()) {
target.getWorld().dropItemNaturally(target.getLocation(), overflow);
}
} catch (RuntimeException ex) {
AdvancedCrafting.plugin.getLogger().log(Level.SEVERE, "Failed to give equipment for recipe " + recipe.getId(), ex);
sender.sendMessage("§cEquipment could not be built. Check the server log and recipe/template configuration.");
return true;
}
sender.sendMessage("§aGave §f" + recipe.getId() + "§a using §f" + material + "§a to §f" + target.getName() + "§a.");
return true;
}

private List<String> materialsFor(CraftingRecipe recipe) {
if (!recipe.getRecipe().containsKey(recipe.getMainType())) return List.of();
Stream<String> ingredients = IngredientLoader.get().stream()
.filter(i -> i.getIngredientData().getType().getId().equalsIgnoreCase(recipe.getMainType()))
.map(i -> "ingredient." + i.getId());
Stream<String> alloys = AlloyManager.getAlloyIds().stream()
.filter(id -> AlloyManager.getAlloyById(id).getData().getType().getId().equalsIgnoreCase(recipe.getMainType()))
.map(id -> "alloy." + id);
return Stream.concat(ingredients, alloys).sorted().toList();
}

List<String> complete(CommandSender sender, String[] args) {
if (!hasPermission(sender)) return List.of();
List<String> options;
switch (args.length) {
case 3 -> options = RecipeLoader.get().keySet().stream().sorted().toList();
case 4 -> {
CraftingRecipe recipe = RecipeLoader.getByString(args[2].toLowerCase(Locale.ROOT));
options = recipe == null ? List.of() : materialsFor(recipe);
}
case 5 -> options = Bukkit.getOnlinePlayers().stream().map(Player::getName).sorted().toList();
case 6 -> options = List.of("0", "25", "50", "75", "90", "100");
default -> options = List.of();
}
String prefix = args[args.length - 1].toLowerCase(Locale.ROOT);
return options.stream().filter(o -> o.toLowerCase(Locale.ROOT).startsWith(prefix)).toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,21 @@ private void warnOvershootHits(Player p) {
// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
private StationFeedback createItem(Player p, Double forcedQualityPercent) {
return createItem(p, forcedQualityPercent, true);
}

/** Builds staff equipment without consuming inventory, dropping an item, paying XP or firing progression events. */
public ItemStack buildCompletedItem(Player p, double qualityPercent) {
stats = CraftStatCalculator.compute(recipe, currentMaterials);
StationFeedback feedback = createItem(p, qualityPercent, false);
if (feedback != StationFeedback.SUCCESS) {
throw new IllegalStateException("Cannot build equipment: " + feedback);
}
return result;
}

@SuppressWarnings("deprecation")
private StationFeedback createItem(Player p, Double forcedQualityPercent, boolean deliver) {
if (!checkItems(p)) {
return StationFeedback.LACKING_ITEMS;
}
Expand Down Expand Up @@ -496,8 +511,11 @@ private StationFeedback createItem(Player p, Double forcedQualityPercent) {
if (majorityTier > 0) {
CraftTierLore.applyTierLine(finalItem, majorityTier);
}
Location dropLoc = loc.clone().add(0, 1, 0);
dropLoc.getWorld().dropItem(dropLoc, finalItem);
this.result = finalItem;
if (deliver) {
Location dropLoc = loc.clone().add(0, 1, 0);
dropLoc.getWorld().dropItem(dropLoc, finalItem);
}
if (forcedQualityPercent == null) {
CraftLifecycle.fireItemCrafted(p, recipe.getId(), recipe.getCategoryId());
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
scrap-path: crafting.scrap
branding-tool: m.tools.skin_branding_tool

# Staff-only finished equipment command. Missing/blank values use advancedcrafting.admin.
# Grant a custom node only to staff; it does not grant the other admin commands.
give-equipment-permission: advancedcrafting.admin

# TLibs block path for each station (iaf(...) for ItemsAdder furniture).
crafting-station: v(ANVIL)
alloy-station: iaf(tfmc:alloy_forge)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ void tabCompletionFiltersCommandsIdsAndOnlinePlayers() {
AlloyManager.addAlloy(alloy);
Map<List<String>, List<String>> cases = new LinkedHashMap<>();
cases.put(List.of("sync", ""), List.of("recipes"));
cases.put(List.of("give", ""), List.of("alloy"));
cases.put(List.of("give", ""), List.of("alloy", "equipment"));
cases.put(List.of("alloy", ""), List.of("info"));
cases.put(List.of("craft", "1"), List.of("100"));
cases.put(List.of("other", ""), List.of());
Expand Down
Loading
Loading