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
80 changes: 80 additions & 0 deletions ADMIN-COMMANDS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Magic administration

These commands require `magic.admin` (the existing `magic.admin.reload` alias
also grants administration). Character targets must be online and have a loaded
active character. Commands affect that character only, save immediately, and
resync spell modifiers. If saving fails, the edit is rolled back and the command
reports failure. Console may use character commands; held-item commands
must be used by a player.

```text
/magic player get <player>
/magic player reset <player>
/magic resonance get <player>
/magic resonance <set|add|remove> <player> <element|all> <amount>
/magic resonance reset <player> [element|all]
/magic equilibrium <get|set|add|remove|reset> <player> [amount]
/magic castmode <get|set|reset> <player> [flow|surge]
```

Amounts are required for set/add/remove and omitted for get/reset. `add` accepts
signed deltas; `remove` takes a nonnegative magnitude. Resonance is clamped to
zero through the configured school maximum; equilibrium uses the configured
minimum and maximum. Both keep two decimals. Positive equilibrium is tranquility,
negative is corruption, and zero is neutral. For example, `equilibrium set Alice
-30` gives 30 corruption; `equilibrium add Alice 10` leaves 20 corruption.

`player get` shows character ID, casting mode, equilibrium, Focus and all school
resonances. `player reset` resets **only Magic's character values** to configured
defaults; Focus is unchanged. Resetting a single value also restores its configured
default. Fractional passive drift is cleared when its value is edited.

Focus belongs to RPCharacters; use `/focus get`, `/focus set`, `/focus add`,
`/focus remove`, `/focus reset` or `/focus restore` with `rpchar.focus.admin`.
Mana and cooldowns belong to their provider plugins. Spell modifiers are derived
from resonance, equilibrium and gear, so are recalculated rather than edited.
School unlocks remain permissions.

## Held artifact and weapon values

```text
/magic artifact inspect
/magic artifact fill <get|set|add|remove|reset> <element|all> [amount]
/magic artifact muffle <get|set|add|remove|reset> [percent]
/magic artifact wear <get|set|add|remove|reset> [amount]
/magic gear inspect
/magic gear rift <get|set|add|remove|reset> [percent]
/magic gear durability <get|set|add|remove|reset> [points]
```

Edits require exactly one appropriate item in the main hand. Artifacts must have
an ID. Fill targets existing aura slots and clamps to each slot's capacity;
reset empties it. Muffle is a percentage from zero to 100; reset clears it and
editing re-anchors the care clock. Wear is accumulated lifetime wear, not remaining
integrity: reset repairs a still-existing artifact; reaching its wear budget
crumbles it into the normal spent husk. A husk cannot be repaired by these commands.
Wear edits are unavailable while wear is disabled. Artifact inspection also shows
its ID, aura capacities, wear budget and active sharing count.

Rift is the weapon's permanent Rift, clamped to the configured cap. Temporary
casting mismatch is derived from equipment and character state. Durability means
remaining points, uses custom max durability when present, and stops at one point
to preserve MMOItems weapons. Reset repairs to maximum. Rift/durability require
whole numbers; unbreakable or nondamageable weapons have no editable durability.
Gear inspection also shows elemental attunement. Edits preserve item identity,
parts and sockets and update affected lore. They are logged to Magic's aura log
when logging is enabled. Artifact fill edits preserve capacity, rarity, sharing
history and identity.

Existing `/magic artifact setfill`, `setmuffle`, `unmuffle`, `/magic weapon give`,
`/magic shrine fill`, `/magic artifact create` and `/magic refresh` still work.
Tab completion lists the new routes and arguments for permitted senders.

## Validation

Run `mvn -B --no-transfer-progress clean verify` with Java 21 and the pinned
dependencies. The existing durable gear-storage tests require Linux filesystem
semantics. `tools/dev-admin-values.cjs` exercises both this plugin and the
RPCharacters Focus commands on TFMCDev01 through its existing Dev bot helpers.
It uses only the dedicated `AdminValueProbe` character, removes its temporary
operator status, resets its test values and clears its test inventory afterward.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Magic keeps resonance profiles tied to roleplay characters through RPCharacters.

## Documentation

[Staff value commands](ADMIN-COMMANDS.md)

[Project documentation](https://github.com/TF-Minecraft/Docs/blob/main/projects/Magic/README.md)

Technical documentation is maintained in [TF-Minecraft/Docs](https://github.com/TF-Minecraft/Docs).
Expand Down
178 changes: 178 additions & 0 deletions src/main/java/net/tfminecraft/magic/command/HeldValuesCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package net.tfminecraft.magic.command;

import java.util.List;
import java.util.Locale;

import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.persistence.PersistentDataType;

import net.tfminecraft.magic.Magic;
import net.tfminecraft.magic.Messages;
import net.tfminecraft.magic.artifact.Artifact;
import net.tfminecraft.magic.artifact.ArtifactCareStore;
import net.tfminecraft.magic.artifact.ArtifactIds;
import net.tfminecraft.magic.artifact.ArtifactKeys;
import net.tfminecraft.magic.artifact.ArtifactLore;
import net.tfminecraft.magic.artifact.ArtifactWear;
import net.tfminecraft.magic.attunement.AuraLog;
import net.tfminecraft.magic.gear.GearProvenance;
import net.tfminecraft.magic.gear.WeaponLore;
import net.tfminecraft.magic.gear.WeaponRequirement;
import net.tfminecraft.magic.gear.WeaponRift;
import net.tfminecraft.magic.gear.WeaponWear;
import net.tfminecraft.magic.gear.orb.OrbCache;
import net.tfminecraft.magic.registry.ElementRegistry;
import net.tfminecraft.magic.util.MagicNumbers;

final class HeldValuesCommand {
private HeldValuesCommand() {}

static final List<String> ARTIFACT = List.of("inspect", "fill", "muffle", "wear");
static final List<String> GEAR = List.of("inspect", "rift", "durability");

static boolean execute(CommandSender sender, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Messages.get("values.players_only"));
return true;
}
boolean gear = args[0].equalsIgnoreCase("gear");
String field = args.length > 1 ? args[1].toLowerCase(Locale.ROOT) : "";
boolean inspect = field.equals("inspect");
ValueAction action = args.length > 2 ? ValueAction.parse(args[2]) : null;
boolean fill = field.equals("fill");
int amountIndex = fill ? 4 : 3;
if (!(gear ? GEAR : ARTIFACT).contains(field)
|| (inspect ? args.length != 2 : action == null
|| args.length != amountIndex + (action.needsAmount() ? 1 : 0))) {
sender.sendMessage(Messages.get("values.item_usage"));
return true;
}
ItemStack item = player.getInventory().getItemInMainHand().clone();
Artifact artifact = gear ? null : Artifact.fromItem(item);
if (gear ? !GearProvenance.isGear(item) : artifact == null || ArtifactIds.read(item) == null) {
sender.sendMessage(Messages.get("values.wrong_item"));
return true;
}
if (item.getAmount() != 1 && !inspect && action != ValueAction.GET) {
sender.sendMessage(Messages.get("values.single_item"));
return true;
}
if (inspect) {
if (gear) {
report(sender, "rift", WeaponRift.get(item));
report(sender, "durability", WeaponWear.remaining(item));
var aura = WeaponRequirement.fromItem(item).aura();
for (String element : aura.getCappedElementIds()) {
report(sender, "attunement." + element, aura.getFill(element));
}
} else {
sender.sendMessage(Messages.get("values.item_id", "id", ArtifactIds.read(item).toString()));
for (String element : artifact.getCappedElementIds()) reportFill(sender, artifact, element);
report(sender, "muffle", ArtifactCareStore.readMuffle(item) * 100);
report(sender, "wear", ArtifactWear.readWear(item));
report(sender, "wear_budget", ArtifactWear.budget(artifact));
report(sender, "active_users", ArtifactCareStore.activeUserCount(item, System.currentTimeMillis()));
}
return true;
}
Double amount = action.needsAmount() ? ValueAction.amount(args[amountIndex]) : Double.valueOf(0.0);
if (amount == null || (action == ValueAction.REMOVE && amount < 0)
|| (gear && (amount != Math.rint(amount) || Math.abs(amount) > Integer.MAX_VALUE))) {
sender.sendMessage(Messages.get("values.invalid_amount"));
return true;
}
if (fill) {
String element = args[3].toLowerCase(Locale.ROOT);
if (!element.equals("all") && (!ElementRegistry.contains(element) || artifact.getCap(element) <= 0)) {
sender.sendMessage(Messages.get("artifact.setfill.no_cap", "element", element));
return true;
}
for (String id : element.equals("all") ? artifact.getCappedElementIds() : List.of(element)) {
double before = artifact.getFill(id);
if (action != ValueAction.GET) artifact.setFill(id, action.apply(before, amount, 0));
reportFill(sender, artifact, id);
}
if (action != ValueAction.GET) artifact.write(item);
} else {
double before;
double max;
double reset = 0;
switch (field) {
case "muffle" -> { before = ArtifactCareStore.readMuffle(item) * 100; max = 100; }
case "wear" -> {
if (!ArtifactWear.enabled()) {
sender.sendMessage(Messages.get("values.wear_disabled"));
return true;
}
before = ArtifactWear.readWear(item);
max = ArtifactWear.budget(artifact);
}
case "rift" -> { before = WeaponRift.get(item); max = OrbCache.riftCap; }
default -> {
before = WeaponWear.remaining(item);
if (before < 0) {
sender.sendMessage(Messages.get("values.no_durability"));
return true;
}
Damageable meta = (Damageable) item.getItemMeta();
max = meta.hasMaxDamage() ? meta.getMaxDamage() : item.getType().getMaxDurability();
reset = max;
}
}
// Durability stops at one: MMOItems may delete a zero-durability weapon.
double next = action == ValueAction.GET ? before : MagicNumbers.clamp(
action.apply(before, amount, reset), field.equals("durability") ? 1 : 0, max);
if (action != ValueAction.GET) {
switch (field) {
case "muffle" -> ArtifactCareStore.setMuffle(item, next / 100, System.currentTimeMillis());
case "wear" -> {
if (next >= max) ArtifactWear.crumble(item);
else {
var meta = item.getItemMeta();
meta.getPersistentDataContainer().set(ArtifactKeys.artifactWear(), PersistentDataType.DOUBLE, next);
item.setItemMeta(meta);
ArtifactLore.refreshAttune(item);
}
}
case "rift" -> { WeaponRift.set(item, (int) next); WeaponLore.apply(item); }
default -> {
Damageable meta = (Damageable) item.getItemMeta();
meta.setDamage((int) (max - next));
item.setItemMeta(meta);
}
}
}
report(sender, field, next);
}
if (action != ValueAction.GET) {
player.getInventory().setItemInMainHand(item);
AuraLog.append("admin-item player=%s field=%s action=%s amount=%s", player.getName(), field, action, AuraLog.n(amount));
if (gear) Magic.plugin.syncSpellModifiers(player,
Magic.plugin.getResonanceGuiManager().getSessionManager().get(player));
}
return true;
}

private static void report(CommandSender sender, String field, double value) {
sender.sendMessage(Messages.get("values.item", "field", field, "value", MagicNumbers.format(value)));
}

private static void reportFill(CommandSender sender, Artifact artifact, String element) {
sender.sendMessage(Messages.get("values.fill", "element", element,
"value", MagicNumbers.format(artifact.getFill(element)), "max", MagicNumbers.format(artifact.getCap(element))));
}

static List<String> complete(String[] args) {
if (args.length == 2) return GEAR;
if (args.length == 3 && !args[1].equalsIgnoreCase("inspect")) return ValueAction.NAMES;
if (args.length == 4 && args[1].equalsIgnoreCase("fill")) {
var options = new java.util.ArrayList<>(ElementRegistry.getAllIds());
options.add("all");
return options;
}
return List.of();
}
}
Loading
Loading