feat(commands): add admin tracker compass grant command and document it
- add /souls tracker give <player> <target> as an admin-only command - reuse the existing TrackerCompassService so the command uses the same tracker behavior and duration as kill-granted compasses - gate the command behind soulsteal.admin - update README.md command reference - update docs/PROJECT.md with tracker compass behavior and the new admin command - keep the implementation scoped to the command layer without changing the tracker service API
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
Soul Steal is a server-side Fabric mod that adds a configurable soul economy, bounties, tracker compasses, and a vanilla-compatible soul shop.
|
||||
|
||||
Full project documentation is available in [docs/PROJECT.md](docs/PROJECT.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Value |
|
||||
@@ -27,6 +29,7 @@ Players gain souls for killing other players and lose souls whenever they die, w
|
||||
| `/souls bounty list [player]` | All players | Lists active bounties globally or for one target. |
|
||||
| `/souls scoreboard [toggle|on|off]` | All players | Toggles the optional Soul Steal sidebar HUD for your player. |
|
||||
| `/souls top [page]` | All players | Shows the soul leaderboard using the configured page size. |
|
||||
| `/souls tracker give <player> <target>` | Admins / `soulsteal.admin` | Gives a tracker compass to one player that points at another player. |
|
||||
| `/souls reload` | Admins / `soulsteal.admin` or `soulsteal.admin.reload` | Reloads `config.yml` and `shop.yml` without restarting the server. |
|
||||
| `/souls set|add|take <player> <amount>` | Admins / `soulsteal.admin` or the matching `soulsteal.admin.balance.*` node | Directly manages a player's soul balance. |
|
||||
|
||||
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
# Soul Steal Project Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
Soul Steal is a server-side Fabric mod that implements a soul economy, player bounties, tracker compasses, a vanilla-style shop GUI, optional HUD elements, and permission-backed admin controls.
|
||||
|
||||
The mod is built to run entirely on the server. Players do not need a client mod to use the commands or the shop.
|
||||
|
||||
## Runtime Architecture
|
||||
|
||||
The entry point is [`SoulStealMod`](../src/main/java/com/g2806/soulsteal/SoulStealMod.java), which performs four jobs:
|
||||
|
||||
1. Loads configuration from `config/soulsteal/config.yml`.
|
||||
2. Loads persistent player data from `config/soulsteal/soulsteal-data.json`.
|
||||
3. Instantiates the feature services.
|
||||
4. Registers command, tick, join, death, and disconnect handlers.
|
||||
|
||||
The main services are:
|
||||
|
||||
- [`SoulService`](../src/main/java/com/g2806/soulsteal/service/SoulService.java): balance reads and mutations.
|
||||
- [`BountyService`](../src/main/java/com/g2806/soulsteal/service/BountyService.java): bounty placement, expiry, and kill claims.
|
||||
- [`TrackerCompassService`](../src/main/java/com/g2806/soulsteal/service/TrackerCompassService.java): temporary tracking compasses for player kills.
|
||||
- [`ShopService`](../src/main/java/com/g2806/soulsteal/service/ShopService.java): server-side shop GUI and purchases.
|
||||
- [`RewardService`](../src/main/java/com/g2806/soulsteal/service/RewardService.java): validates and grants rewards from shop entries.
|
||||
- [`PermissionService`](../src/main/java/com/g2806/soulsteal/service/PermissionService.java): permission lookups and fallback storage.
|
||||
- [`HudService`](../src/main/java/com/g2806/soulsteal/service/HudService.java): scoreboard sidebar, leaderboard, and bounty bossbar.
|
||||
|
||||
Shared utilities:
|
||||
|
||||
- [`SoulTexts`](../src/main/java/com/g2806/soulsteal/util/SoulTexts.java): formatted feedback, success, warning, and error messages.
|
||||
- [`DurationFormatter`](../src/main/java/com/g2806/soulsteal/util/DurationFormatter.java): human-readable duration strings.
|
||||
|
||||
## Feature Summary
|
||||
|
||||
### Soul Economy
|
||||
|
||||
Players have a soul balance stored per UUID.
|
||||
|
||||
- Killing another player can grant souls.
|
||||
- Dying can remove souls using configurable flat and percentage penalties.
|
||||
- Transfers between players can be enabled or disabled in config.
|
||||
- Admin commands can set, add, or remove balances directly.
|
||||
|
||||
Implemented by:
|
||||
|
||||
- [`SoulService`](../src/main/java/com/g2806/soulsteal/service/SoulService.java)
|
||||
- [`SoulCommandRegistrar`](../src/main/java/com/g2806/soulsteal/command/SoulCommandRegistrar.java)
|
||||
|
||||
### Bounties
|
||||
|
||||
Players can place timed bounties on other players using their souls as the payment source.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Placement is bounded by min/max value, min/max duration, cooldowns, and active bounty limits.
|
||||
- Killing the target claims all active bounties on that target.
|
||||
- If a bounty expires, the target receives a configured survivor payout.
|
||||
- Bounties are cleared if the target dies to non-player damage and no killer claims them.
|
||||
|
||||
Implemented by:
|
||||
|
||||
- [`BountyService`](../src/main/java/com/g2806/soulsteal/service/BountyService.java)
|
||||
- [`SoulStealMod`](../src/main/java/com/g2806/soulsteal/SoulStealMod.java)
|
||||
|
||||
### Tracker Compasses
|
||||
|
||||
When enabled, killing a player grants a temporary tracker compass.
|
||||
|
||||
Behavior:
|
||||
|
||||
- The compass tracks the killed player using lodestone-tracker data.
|
||||
- The item stores target UUID, target name, and expiration time in custom NBT.
|
||||
- Expired compasses are removed during server ticks.
|
||||
- Optional behavior can remove the compass if the target is offline.
|
||||
|
||||
Admins can also grant a tracker compass directly with `/souls tracker give <player> <target>`. This uses the same tracker data and duration as the kill-granted version.
|
||||
|
||||
Implemented by:
|
||||
|
||||
- [`TrackerCompassService`](../src/main/java/com/g2806/soulsteal/service/TrackerCompassService.java)
|
||||
|
||||
### Shop
|
||||
|
||||
The shop is a chest-based GUI rendered entirely with vanilla screen handler APIs.
|
||||
|
||||
Behavior:
|
||||
|
||||
- The home view lists categories with pagination.
|
||||
- Category views list entries with pagination.
|
||||
- Item entries can be direct purchases or quantity-select purchases.
|
||||
- Purchases can be repeatable or single-unlock.
|
||||
- Cooldowns can be stored per entry per player.
|
||||
- Rewards can include items, effects, commands, and permissions.
|
||||
|
||||
Implemented by:
|
||||
|
||||
- [`ShopService`](../src/main/java/com/g2806/soulsteal/service/ShopService.java)
|
||||
- [`RewardService`](../src/main/java/com/g2806/soulsteal/service/RewardService.java)
|
||||
- [`SoulShopScreenHandler`](../src/main/java/com/g2806/soulsteal/shop/SoulShopScreenHandler.java)
|
||||
- [`ShopCatalog`](../src/main/java/com/g2806/soulsteal/shop/ShopCatalog.java)
|
||||
|
||||
### HUD
|
||||
|
||||
The HUD layer is optional and configurable per player.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Scoreboard sidebar can be enabled globally and toggled per player.
|
||||
- Leaderboard pages are built from stored player names and balance values.
|
||||
- Wanted-player bossbars show bounty value and remaining time.
|
||||
|
||||
Implemented by:
|
||||
|
||||
- [`HudService`](../src/main/java/com/g2806/soulsteal/service/HudService.java)
|
||||
|
||||
### Permissions
|
||||
|
||||
The mod checks permissions in two layers:
|
||||
|
||||
1. External permission backends through the Fabric permissions API and optional LuckPerms integration.
|
||||
2. Internal fallback storage persisted in `soulsteal-data.json`.
|
||||
|
||||
This allows reward-granted permissions and admin nodes to keep working even without an external permission mod.
|
||||
|
||||
Implemented by:
|
||||
|
||||
- [`PermissionService`](../src/main/java/com/g2806/soulsteal/service/PermissionService.java)
|
||||
|
||||
## Command Reference
|
||||
|
||||
The root command has two aliases:
|
||||
|
||||
- `/souls`
|
||||
- `/soul`
|
||||
|
||||
### Player Commands
|
||||
|
||||
- `/souls`
|
||||
- Shows your current balance.
|
||||
- `/souls balance`
|
||||
- Same as `/souls`.
|
||||
- `/souls balance <player>`
|
||||
- Shows another player’s balance.
|
||||
- `/souls pay <player> <amount>`
|
||||
- Transfers souls to another player.
|
||||
- `/souls shop [category]`
|
||||
- Opens the shop GUI.
|
||||
- `/souls bounty place <player> <amount> [durationSeconds]`
|
||||
- Places a bounty.
|
||||
- `/souls bounty list [player]`
|
||||
- Lists active bounties.
|
||||
- `/souls scoreboard`
|
||||
- Shows scoreboard visibility state.
|
||||
- `/souls scoreboard toggle`
|
||||
- Toggles the player’s scoreboard preference.
|
||||
- `/souls scoreboard on`
|
||||
- Forces scoreboard visibility on.
|
||||
- `/souls scoreboard off`
|
||||
- Forces scoreboard visibility off.
|
||||
- `/souls top [page]`
|
||||
- Shows leaderboard pages.
|
||||
- `/souls tracker give <player> <target>`
|
||||
- Gives a tracker compass to one player that points at another player.
|
||||
|
||||
### Admin Commands
|
||||
|
||||
- `/souls reload`
|
||||
- Reloads config and shop definitions.
|
||||
- `/souls set <player> <amount>`
|
||||
- Sets a balance directly.
|
||||
- `/souls add <player> <amount>`
|
||||
- Adds souls to a balance.
|
||||
- `/souls take <player> <amount>`
|
||||
- Removes souls from a balance.
|
||||
|
||||
Permission defaults and node names are configured in `config.yml`.
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### `config/soulsteal/config.yml`
|
||||
|
||||
Primary configuration file. It controls:
|
||||
|
||||
- Economy values
|
||||
- Death penalties
|
||||
- Transfer rules
|
||||
- Bounty limits and payouts
|
||||
- Tracker compass behavior
|
||||
- Shop UI defaults
|
||||
- HUD toggles and titles
|
||||
- Permission node names
|
||||
|
||||
The schema is defined in [`SoulStealConfig`](../src/main/java/com/g2806/soulsteal/config/SoulStealConfig.java).
|
||||
|
||||
### `config/soulsteal/shop.yml`
|
||||
|
||||
Shop catalog definition. It controls:
|
||||
|
||||
- Shop title and layout
|
||||
- Category list
|
||||
- Entry pricing
|
||||
- Entry cooldowns
|
||||
- Reward definitions
|
||||
- Optional custom quantity selector behavior
|
||||
|
||||
The catalog is loaded through [`ConfigBundle`](../src/main/java/com/g2806/soulsteal/config/ConfigBundle.java).
|
||||
|
||||
### `config/soulsteal/soulsteal-data.json`
|
||||
|
||||
Persistent runtime data. It stores:
|
||||
|
||||
- Player soul balances
|
||||
- Active bounties
|
||||
- Bounty placement cooldowns
|
||||
- Shop unlocks
|
||||
- Shop purchase cooldowns
|
||||
- Permission fallback grants
|
||||
- Player name history
|
||||
- Scoreboard visibility preferences
|
||||
|
||||
Loaded and saved by [`SoulStealDataStore`](../src/main/java/com/g2806/soulsteal/data/SoulStealDataStore.java).
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Player Join
|
||||
|
||||
When a player joins:
|
||||
|
||||
1. Their name is recorded in persistent data.
|
||||
2. Their HUD state is refreshed.
|
||||
3. Any configured scoreboard or bossbar state is pushed to them.
|
||||
|
||||
### Player Kill
|
||||
|
||||
When one player kills another player:
|
||||
|
||||
1. The killer receives kill reward souls if enabled.
|
||||
2. Any matching bounties on the victim are claimed.
|
||||
3. The killer receives a tracker compass if the tracker feature is enabled.
|
||||
|
||||
### Player Death
|
||||
|
||||
When a player dies:
|
||||
|
||||
1. The configured death penalty is applied.
|
||||
2. If the death was not caused by another player, unclaimed bounties on the victim can be cleared.
|
||||
|
||||
### Server Tick
|
||||
|
||||
On server tick:
|
||||
|
||||
1. Tracker compasses are updated and expired compasses are removed.
|
||||
2. Bounty expirations are processed once per second.
|
||||
3. HUD elements are refreshed.
|
||||
|
||||
### Server Shutdown
|
||||
|
||||
On shutdown:
|
||||
|
||||
- Persistent data is saved to disk.
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `src/main/java/com/g2806/soulsteal/`
|
||||
- Bootstrap, commands, services, config, data, shop, and utilities.
|
||||
- `src/main/resources/`
|
||||
- Fabric metadata and resource files.
|
||||
- `build.gradle`
|
||||
- Fabric Loom build configuration.
|
||||
|
||||
## Development Notes
|
||||
|
||||
- The mod targets Java 21.
|
||||
- It is structured as a server-only feature set, so most behavior is driven by server lifecycle events.
|
||||
- Persistence is intentionally simple: one JSON data file and YAML-driven configuration.
|
||||
- External integrations are reflected through reflection-based checks so the mod can run without hard dependencies on permission backends.
|
||||
@@ -51,6 +51,10 @@ public final class SoulStealMod implements ModInitializer {
|
||||
private ShopService shopService;
|
||||
private HudService hudService;
|
||||
|
||||
/**
|
||||
* Initializes the mod, loads configuration and persistent state, and registers all runtime
|
||||
* event handlers.
|
||||
*/
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
LOGGER.info("Initializing Soul Steal");
|
||||
@@ -140,6 +144,12 @@ public final class SoulStealMod implements ModInitializer {
|
||||
hudService.tick(server, now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the YAML configuration bundle from disk.
|
||||
*
|
||||
* @return {@code true} if the reload succeeded; {@code false} if the config file could not be
|
||||
* loaded
|
||||
*/
|
||||
public boolean reloadConfiguration() {
|
||||
try {
|
||||
configBundle = ConfigBundle.load(configDirectory);
|
||||
@@ -158,38 +168,83 @@ public final class SoulStealMod implements ModInitializer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the loaded configuration bundle.
|
||||
*
|
||||
* @return the current configuration bundle
|
||||
*/
|
||||
public ConfigBundle bundle() {
|
||||
return configBundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active mod configuration.
|
||||
*
|
||||
* @return the current configuration tree
|
||||
*/
|
||||
public SoulStealConfig config() {
|
||||
return configBundle.config();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the service responsible for balance changes.
|
||||
*
|
||||
* @return the soul service
|
||||
*/
|
||||
public SoulService soulService() {
|
||||
return soulService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the permission service.
|
||||
*
|
||||
* @return the permission service
|
||||
*/
|
||||
public PermissionService permissionService() {
|
||||
return permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bounty service.
|
||||
*
|
||||
* @return the bounty service
|
||||
*/
|
||||
public BountyService bountyService() {
|
||||
return bountyService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the reward service.
|
||||
*
|
||||
* @return the reward service
|
||||
*/
|
||||
public RewardService rewardService() {
|
||||
return rewardService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tracker compass service.
|
||||
*
|
||||
* @return the tracker compass service
|
||||
*/
|
||||
public TrackerCompassService trackerCompassService() {
|
||||
return trackerCompassService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shop service.
|
||||
*
|
||||
* @return the shop service
|
||||
*/
|
||||
public ShopService shopService() {
|
||||
return shopService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HUD service.
|
||||
*
|
||||
* @return the HUD service
|
||||
*/
|
||||
public HudService hudService() {
|
||||
return hudService;
|
||||
}
|
||||
|
||||
@@ -25,36 +25,57 @@ public final class SoulCommandRegistrar {
|
||||
private SoulCommandRegistrar() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the public command roots exposed by the mod.
|
||||
*
|
||||
* @param dispatcher Brigadier dispatcher used by Fabric to install commands
|
||||
* @param mod active mod instance used to resolve services and configuration
|
||||
*/
|
||||
public static void register(CommandDispatcher<ServerCommandSource> dispatcher, SoulStealMod mod) {
|
||||
// Register both command roots so players can use either the full name or the short alias.
|
||||
dispatcher.register(buildRoot("souls", mod));
|
||||
dispatcher.register(buildRoot("soul", mod));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds one of the root command aliases and all nested subcommands.
|
||||
*
|
||||
* @param rootName literal command root to register, such as {@code souls} or {@code soul}
|
||||
* @param mod active mod instance used to resolve services and permissions
|
||||
* @return a fully populated command tree for the requested root
|
||||
*/
|
||||
private static com.mojang.brigadier.builder.LiteralArgumentBuilder<ServerCommandSource> buildRoot(String rootName, SoulStealMod mod) {
|
||||
return literal(rootName)
|
||||
// Running the root command alone shows the player's own balance.
|
||||
.executes(context -> showOwnBalance(context, mod))
|
||||
// /soul balance
|
||||
.then(literal("balance")
|
||||
.executes(context -> showOwnBalance(context, mod))
|
||||
.then(argument("player", EntityArgumentType.player())
|
||||
// Only privileged sources can inspect another player's balance.
|
||||
.requires(source -> mod.permissionService().hasAny(source, 2,
|
||||
mod.config().permissions().adminNode(),
|
||||
mod.config().permissions().balanceOthersNode()))
|
||||
.executes(context -> showTargetBalance(context, mod))))
|
||||
// /soul pay <player> <amount>
|
||||
.then(literal("pay")
|
||||
.requires(source -> mod.permissionService().has(source, mod.config().permissions().shopNode(), 0))
|
||||
.then(argument("player", EntityArgumentType.player())
|
||||
.then(argument("amount", LongArgumentType.longArg(1L))
|
||||
.executes(context -> transferSouls(context, mod)))))
|
||||
// /soul shop [category]
|
||||
.then(literal("shop")
|
||||
.requires(source -> mod.permissionService().has(source, mod.config().permissions().shopNode(), 0))
|
||||
.executes(context -> openShop(context, mod, null))
|
||||
.then(argument("category", StringArgumentType.word())
|
||||
.executes(context -> openShop(context, mod, StringArgumentType.getString(context, "category")))))
|
||||
// /soul bounty ...
|
||||
.then(literal("bounty")
|
||||
.requires(source -> mod.permissionService().has(source, mod.config().permissions().bountyNode(), 0))
|
||||
.then(literal("place")
|
||||
.then(argument("player", EntityArgumentType.player())
|
||||
.then(argument("amount", LongArgumentType.longArg(1L))
|
||||
// Default duration comes from config unless the caller provides one explicitly.
|
||||
.executes(context -> placeBounty(context, mod, mod.config().bounty().defaultDurationSeconds()))
|
||||
.then(argument("durationSeconds", LongArgumentType.longArg(1L))
|
||||
.executes(context -> placeBounty(context, mod, LongArgumentType.getLong(context, "durationSeconds")))))))
|
||||
@@ -62,25 +83,38 @@ public final class SoulCommandRegistrar {
|
||||
.executes(context -> listBounties(context, mod, null))
|
||||
.then(argument("player", EntityArgumentType.player())
|
||||
.executes(context -> listBounties(context, mod, EntityArgumentType.getPlayer(context, "player"))))))
|
||||
// /soul scoreboard ...
|
||||
.then(literal("scoreboard")
|
||||
.requires(source -> mod.permissionService().hasAny(source, 0, mod.config().permissions().scoreboardNode()))
|
||||
.executes(context -> showScoreboardStatus(context, mod))
|
||||
// Toggle uses the player's stored preference.
|
||||
.then(literal("toggle")
|
||||
.executes(context -> toggleScoreboard(context, mod)))
|
||||
// Explicit on/off commands are useful for scripts and exact control.
|
||||
.then(literal("on")
|
||||
.executes(context -> setScoreboardVisibility(context, mod, true)))
|
||||
.then(literal("off")
|
||||
.executes(context -> setScoreboardVisibility(context, mod, false))))
|
||||
// /soul top [page]
|
||||
.then(literal("top")
|
||||
.requires(source -> mod.permissionService().hasAny(source, 0, mod.config().permissions().leaderboardNode()))
|
||||
.executes(context -> showLeaderboard(context, mod, 1))
|
||||
.then(argument("page", IntegerArgumentType.integer(1))
|
||||
.executes(context -> showLeaderboard(context, mod, IntegerArgumentType.getInteger(context, "page")))))
|
||||
// /soul tracker give <player> <target>
|
||||
.then(literal("tracker")
|
||||
.requires(source -> mod.permissionService().hasAny(source, 2, mod.config().permissions().adminNode()))
|
||||
.then(literal("give")
|
||||
.then(argument("player", EntityArgumentType.player())
|
||||
.then(argument("target", EntityArgumentType.player())
|
||||
.executes(context -> giveTrackerCompass(context, mod))))))
|
||||
// Admin-only maintenance and balance editing commands follow.
|
||||
.then(literal("reload")
|
||||
.requires(source -> mod.permissionService().hasAny(source, 2,
|
||||
mod.config().permissions().adminNode(),
|
||||
mod.config().permissions().reloadNode()))
|
||||
.executes(context -> reload(context, mod)))
|
||||
// Set replaces the target balance outright.
|
||||
.then(literal("set")
|
||||
.requires(source -> mod.permissionService().hasAny(source, 2,
|
||||
mod.config().permissions().adminNode(),
|
||||
@@ -88,6 +122,7 @@ public final class SoulCommandRegistrar {
|
||||
.then(argument("player", EntityArgumentType.player())
|
||||
.then(argument("amount", LongArgumentType.longArg(0L))
|
||||
.executes(context -> setBalance(context, mod)))))
|
||||
// Add and take are bounded changes; they keep the balance moving up or down.
|
||||
.then(literal("add")
|
||||
.requires(source -> mod.permissionService().hasAny(source, 2,
|
||||
mod.config().permissions().adminNode(),
|
||||
@@ -147,6 +182,7 @@ public final class SoulCommandRegistrar {
|
||||
ServerPlayerEntity player = context.getSource().getPlayerOrThrow();
|
||||
boolean visible = mod.hudService().isScoreboardVisible(player.getUuid());
|
||||
String message = visible ? "Your Soul Steal scoreboard is enabled." : "Your Soul Steal scoreboard is disabled.";
|
||||
// This is a player preference; config can still disable the HUD globally.
|
||||
if (!mod.config().hud().scoreboard().enabled()) {
|
||||
message += " The server-wide HUD toggle is disabled in config.";
|
||||
}
|
||||
@@ -190,12 +226,30 @@ public final class SoulCommandRegistrar {
|
||||
context.getSource().sendFeedback(() -> SoulTexts.info("Soul leaderboard page " + leaderboardPage.page() + "/" + leaderboardPage.totalPages()), false);
|
||||
for (int index = 0; index < leaderboardPage.entries().size(); index++) {
|
||||
HudService.LeaderboardEntry entry = leaderboardPage.entries().get(index);
|
||||
// Convert the page-local index into the stable 1-based rank shown to players.
|
||||
int rank = ((leaderboardPage.page() - 1) * pageSize) + index + 1;
|
||||
context.getSource().sendFeedback(() -> Text.literal("#" + rank + " " + entry.playerName() + " - " + entry.souls() + " souls").formatted(net.minecraft.util.Formatting.GRAY), false);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives a tracker compass to one player that points at another player.
|
||||
*
|
||||
* @param context command invocation context
|
||||
* @param mod active mod instance
|
||||
* @return command result code
|
||||
* @throws com.mojang.brigadier.exceptions.CommandSyntaxException if either player argument cannot be resolved
|
||||
*/
|
||||
private static int giveTrackerCompass(CommandContext<ServerCommandSource> context, SoulStealMod mod) throws com.mojang.brigadier.exceptions.CommandSyntaxException {
|
||||
ServerPlayerEntity player = EntityArgumentType.getPlayer(context, "player");
|
||||
ServerPlayerEntity target = EntityArgumentType.getPlayer(context, "target");
|
||||
|
||||
mod.trackerCompassService().giveTrackerCompass(player, target);
|
||||
context.getSource().sendFeedback(() -> SoulTexts.success("Gave a tracker compass to " + player.getName().getString() + " for " + target.getName().getString() + "."), true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int placeBounty(CommandContext<ServerCommandSource> context, SoulStealMod mod, long durationSeconds) throws com.mojang.brigadier.exceptions.CommandSyntaxException {
|
||||
ServerPlayerEntity placer = context.getSource().getPlayerOrThrow();
|
||||
ServerPlayerEntity target = EntityArgumentType.getPlayer(context, "player");
|
||||
@@ -230,6 +284,7 @@ public final class SoulCommandRegistrar {
|
||||
|
||||
context.getSource().sendFeedback(() -> SoulTexts.info("Active bounties: " + bounties.size()), false);
|
||||
for (StoredBounty bounty : bounties) {
|
||||
// Round up so a bounty that expires in a fraction of a second still reports 1 second remaining.
|
||||
long remainingSeconds = Math.max(0L, (bounty.expiresAtEpochMillis() - System.currentTimeMillis() + 999L) / 1000L);
|
||||
context.getSource().sendFeedback(() -> Text.literal("- " + bounty.targetName() + " | " + bounty.soulValue() + " souls | by " + bounty.placerName() + " | expires in " + DurationFormatter.formatSeconds(remainingSeconds)).formatted(net.minecraft.util.Formatting.GRAY), false);
|
||||
}
|
||||
|
||||
@@ -51,34 +51,74 @@ public final class SoulStealData {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the persistent soul balance table keyed by player UUID string.
|
||||
*
|
||||
* @return mutable soul balance map
|
||||
*/
|
||||
public Map<String, Long> souls() {
|
||||
return souls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active bounty list.
|
||||
*
|
||||
* @return mutable list of active bounties
|
||||
*/
|
||||
public List<StoredBounty> activeBounties() {
|
||||
return activeBounties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of shop entries unlocked per player.
|
||||
*
|
||||
* @return mutable unlock table
|
||||
*/
|
||||
public Map<String, Set<String>> unlockedEntries() {
|
||||
return unlockedEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the per-entry purchase cooldown table.
|
||||
*
|
||||
* @return mutable cooldown map
|
||||
*/
|
||||
public Map<String, Map<String, Long>> purchaseCooldowns() {
|
||||
return purchaseCooldowns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the internal permission fallback table.
|
||||
*
|
||||
* @return mutable permission map
|
||||
*/
|
||||
public Map<String, Map<String, Boolean>> grantedPermissions() {
|
||||
return grantedPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bounty placement cooldown table.
|
||||
*
|
||||
* @return mutable placement cooldown map
|
||||
*/
|
||||
public Map<String, Long> bountyPlacementCooldowns() {
|
||||
return bountyPlacementCooldowns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last known player names table.
|
||||
*
|
||||
* @return mutable player-name map
|
||||
*/
|
||||
public Map<String, String> playerNames() {
|
||||
return playerNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the per-player scoreboard visibility table.
|
||||
*
|
||||
* @return mutable scoreboard visibility map
|
||||
*/
|
||||
public Map<String, Boolean> scoreboardVisibility() {
|
||||
return scoreboardVisibility;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ public final class SoulStealDataStore {
|
||||
this.dataFile = dataDirectory.resolve("soulsteal-data.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads persistent state from disk, creating a new file if needed.
|
||||
*
|
||||
* @throws IOException if the file cannot be read or created
|
||||
*/
|
||||
public synchronized void load() throws IOException {
|
||||
Files.createDirectories(dataDirectory);
|
||||
if (Files.notExists(dataFile)) {
|
||||
@@ -43,10 +48,20 @@ public final class SoulStealDataStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the in-memory persistent data snapshot.
|
||||
*
|
||||
* @return mutable data model used by the running server
|
||||
*/
|
||||
public synchronized SoulStealData data() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current in-memory state to disk atomically when possible.
|
||||
*
|
||||
* @throws IOException if the file cannot be written
|
||||
*/
|
||||
public synchronized void save() throws IOException {
|
||||
Files.createDirectories(dataDirectory);
|
||||
Path tempFile = dataFile.resolveSibling(dataFile.getFileName() + ".tmp");
|
||||
|
||||
@@ -18,14 +18,29 @@ public record StoredBounty(
|
||||
long createdAtEpochMillis,
|
||||
long expiresAtEpochMillis
|
||||
) {
|
||||
/**
|
||||
* Parses the bounty id as a UUID.
|
||||
*
|
||||
* @return bounty UUID
|
||||
*/
|
||||
public UUID idAsUuid() {
|
||||
return UUID.fromString(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the placer id as a UUID.
|
||||
*
|
||||
* @return placer UUID
|
||||
*/
|
||||
public UUID placerUuidAsUuid() {
|
||||
return UUID.fromString(placerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the target id as a UUID.
|
||||
*
|
||||
* @return target UUID
|
||||
*/
|
||||
public UUID targetUuidAsUuid() {
|
||||
return UUID.fromString(targetUuid);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,18 @@ public final class BountyService {
|
||||
this.soulService = soulService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to place a bounty on a target player.
|
||||
*
|
||||
* @param placerUuid player paying for the bounty
|
||||
* @param placerName display name used for messages
|
||||
* @param targetUuid target player UUID
|
||||
* @param targetName target display name used for messages
|
||||
* @param amount bounty value in souls
|
||||
* @param durationSeconds bounty lifetime in seconds
|
||||
* @param nowEpochMillis current time used for cooldown and expiry calculations
|
||||
* @return placement outcome and the created bounty when successful
|
||||
*/
|
||||
public PlaceBountyResult placeBounty(
|
||||
UUID placerUuid,
|
||||
String placerName,
|
||||
@@ -86,6 +98,13 @@ public final class BountyService {
|
||||
return new PlaceBountyResult(true, "Bounty placed successfully.", bounty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Claims all active bounties on a target after a successful kill.
|
||||
*
|
||||
* @param killerUuid player receiving the payout
|
||||
* @param targetUuid target player whose bounties may be claimed
|
||||
* @return the combined payout and the list of claimed bounties
|
||||
*/
|
||||
public ClaimBountyResult claimForKill(UUID killerUuid, UUID targetUuid) {
|
||||
List<StoredBounty> claimed = new ArrayList<>();
|
||||
Iterator<StoredBounty> iterator = dataStore.data().activeBounties().iterator();
|
||||
@@ -110,6 +129,12 @@ public final class BountyService {
|
||||
return new ClaimBountyResult(reward, claimed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all active bounties on a target without paying them out.
|
||||
*
|
||||
* @param targetUuid target player UUID
|
||||
* @return the removed bounty records
|
||||
*/
|
||||
public List<StoredBounty> clearForTarget(UUID targetUuid) {
|
||||
List<StoredBounty> removed = new ArrayList<>();
|
||||
Iterator<StoredBounty> iterator = dataStore.data().activeBounties().iterator();
|
||||
@@ -132,6 +157,12 @@ public final class BountyService {
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes expired bounties and pays the configured survivor reward where applicable.
|
||||
*
|
||||
* @param nowEpochMillis current time used to determine expiration
|
||||
* @return payout records for every expired bounty handled in this pass
|
||||
*/
|
||||
public List<ExpiredBountyPayout> processExpirations(long nowEpochMillis) {
|
||||
SoulStealConfig.BountyConfig bountyConfig = configSupplier.get().bounty();
|
||||
List<ExpiredBountyPayout> payouts = new ArrayList<>();
|
||||
@@ -157,15 +188,32 @@ public final class BountyService {
|
||||
return payouts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of all active bounties.
|
||||
*
|
||||
* @return immutable copy of the current active bounty list
|
||||
*/
|
||||
public List<StoredBounty> activeBounties() {
|
||||
return List.copyOf(dataStore.data().activeBounties());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all active bounties for a specific target player.
|
||||
*
|
||||
* @param targetUuid target player UUID
|
||||
* @return bounties currently assigned to that player
|
||||
*/
|
||||
public List<StoredBounty> activeBountiesForTarget(UUID targetUuid) {
|
||||
String targetKey = key(targetUuid);
|
||||
return dataStore.data().activeBounties().stream().filter(bounty -> bounty.targetUuid().equals(targetKey)).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next time the given placer may create another bounty.
|
||||
*
|
||||
* @param placerUuid player UUID to inspect
|
||||
* @return epoch milliseconds when the placement cooldown ends, or {@code 0} if none exists
|
||||
*/
|
||||
public long nextPlacementTime(UUID placerUuid) {
|
||||
return dataStore.data().bountyPlacementCooldowns().getOrDefault(key(placerUuid), 0L);
|
||||
}
|
||||
|
||||
@@ -53,16 +53,32 @@ public final class HudService {
|
||||
this.bountyService = bountyService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records player metadata and refreshes their HUD state when they join.
|
||||
*
|
||||
* @param player joining player
|
||||
*/
|
||||
public void handlePlayerJoin(ServerPlayerEntity player) {
|
||||
rememberPlayer(player);
|
||||
refreshPlayerDisplays(player, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any per-player HUD state when a player disconnects.
|
||||
*
|
||||
* @param player disconnecting player
|
||||
*/
|
||||
public void handlePlayerDisconnect(ServerPlayerEntity player) {
|
||||
clearSidebar(player);
|
||||
clearBossBar(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes active HUD elements for all online players and trims stale state.
|
||||
*
|
||||
* @param server current server instance
|
||||
* @param nowEpochMillis current time used for countdown calculations
|
||||
*/
|
||||
public void tick(MinecraftServer server, long nowEpochMillis) {
|
||||
Set<UUID> onlinePlayers = new HashSet<>();
|
||||
for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) {
|
||||
@@ -81,11 +97,24 @@ public final class HudService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given player's scoreboard sidebar is currently visible.
|
||||
*
|
||||
* @param playerUuid player UUID to inspect
|
||||
* @return current visibility state, falling back to the config default
|
||||
*/
|
||||
public boolean isScoreboardVisible(UUID playerUuid) {
|
||||
return dataStore.data().scoreboardVisibility()
|
||||
.getOrDefault(key(playerUuid), configSupplier.get().hud().scoreboard().defaultVisible());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets scoreboard visibility for a player and refreshes their sidebar immediately.
|
||||
*
|
||||
* @param player player to update
|
||||
* @param visible requested visibility state
|
||||
* @return the stored visibility state
|
||||
*/
|
||||
public boolean setScoreboardVisible(ServerPlayerEntity player, boolean visible) {
|
||||
rememberPlayer(player);
|
||||
Boolean previous = dataStore.data().scoreboardVisibility().put(key(player.getUuid()), visible);
|
||||
@@ -96,10 +125,22 @@ public final class HudService {
|
||||
return visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips the scoreboard visibility flag for a player.
|
||||
*
|
||||
* @param player player to update
|
||||
* @return the updated visibility state
|
||||
*/
|
||||
public boolean toggleScoreboardVisible(ServerPlayerEntity player) {
|
||||
return setScoreboardVisible(player, !isScoreboardVisible(player.getUuid()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a leaderboard page from stored names and balances.
|
||||
*
|
||||
* @param requestedPage 1-based page number requested by the caller
|
||||
* @return the clamped leaderboard page and its entries
|
||||
*/
|
||||
public LeaderboardPage leaderboard(int requestedPage) {
|
||||
Set<String> playerKeys = new HashSet<>(dataStore.data().playerNames().keySet());
|
||||
playerKeys.addAll(dataStore.data().souls().keySet());
|
||||
|
||||
@@ -26,6 +26,14 @@ public final class PermissionService {
|
||||
this.dataStore = dataStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a command source has a permission node.
|
||||
*
|
||||
* @param source permission subject
|
||||
* @param permission node to check
|
||||
* @param defaultLevel fallback operator level when no permission backend is available
|
||||
* @return {@code true} if the source is allowed to use the permission
|
||||
*/
|
||||
public boolean has(ServerCommandSource source, String permission, int defaultLevel) {
|
||||
if (source.getPlayer() != null && hasStoredPermission(source.getPlayer().getUuid(), permission)) {
|
||||
return true;
|
||||
@@ -39,6 +47,14 @@ public final class PermissionService {
|
||||
return source.getPlayer() == null || defaultLevel <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a source has any permission from the provided set.
|
||||
*
|
||||
* @param source permission subject
|
||||
* @param defaultLevel fallback operator level when no permission backend is available
|
||||
* @param permissions candidate permissions to check
|
||||
* @return {@code true} if at least one permission is granted
|
||||
*/
|
||||
public boolean hasAny(ServerCommandSource source, int defaultLevel, String... permissions) {
|
||||
for (String permission : permissions) {
|
||||
if (permission != null && !permission.isBlank() && has(source, permission, defaultLevel)) {
|
||||
@@ -48,6 +64,14 @@ public final class PermissionService {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a player has a permission node.
|
||||
*
|
||||
* @param player permission subject
|
||||
* @param permission node to check
|
||||
* @param defaultValue fallback value when no permission backend is available
|
||||
* @return {@code true} if the player is allowed to use the permission
|
||||
*/
|
||||
public boolean has(ServerPlayerEntity player, String permission, boolean defaultValue) {
|
||||
if (hasStoredPermission(player.getUuid(), permission)) {
|
||||
return true;
|
||||
@@ -61,6 +85,14 @@ public final class PermissionService {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a player has any permission from the provided set.
|
||||
*
|
||||
* @param player permission subject
|
||||
* @param defaultValue fallback value when no permission backend is available
|
||||
* @param permissions candidate permissions to check
|
||||
* @return {@code true} if at least one permission is granted
|
||||
*/
|
||||
public boolean hasAny(ServerPlayerEntity player, boolean defaultValue, String... permissions) {
|
||||
for (String permission : permissions) {
|
||||
if (permission != null && !permission.isBlank() && has(player, permission, defaultValue)) {
|
||||
@@ -70,6 +102,15 @@ public final class PermissionService {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants a permission through LuckPerms if available and optionally stores a fallback copy.
|
||||
*
|
||||
* @param playerUuid player receiving the permission
|
||||
* @param permission node to grant or revoke
|
||||
* @param value desired node value
|
||||
* @param storeFallback whether to persist the value in Soul Steal's internal store
|
||||
* @return grant outcome and backend details
|
||||
*/
|
||||
public GrantResult grantPersistentPermission(UUID playerUuid, String permission, boolean value, boolean storeFallback) {
|
||||
boolean grantedViaLuckPerms = tryGrantWithLuckPerms(playerUuid, permission, value);
|
||||
boolean storedInternally = false;
|
||||
|
||||
@@ -61,6 +61,13 @@ public final class ShopService {
|
||||
this.dataStore = dataStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the shop UI for either the home view or a specific category.
|
||||
*
|
||||
* @param player player to show the UI to
|
||||
* @param requestedCategoryKey category key to open, or {@code null} for the home view
|
||||
* @param requestedPage zero-based page index to display
|
||||
*/
|
||||
public void openShop(ServerPlayerEntity player, String requestedCategoryKey, int requestedPage) {
|
||||
if (requestedCategoryKey == null || requestedCategoryKey.isBlank()) {
|
||||
openView(player, resolveHomeView(requestedPage));
|
||||
@@ -70,6 +77,13 @@ public final class ShopService {
|
||||
openView(player, resolveCategoryView(requestedCategoryKey, requestedPage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the backing inventory for a specific shop view.
|
||||
*
|
||||
* @param player player who will interact with the inventory
|
||||
* @param view resolved shop view state
|
||||
* @return inventory contents appropriate for the supplied view
|
||||
*/
|
||||
public SimpleInventory createInventory(ServerPlayerEntity player, ShopView view) {
|
||||
return switch (view) {
|
||||
case HomeView homeView -> createHomeInventory(player, homeView);
|
||||
@@ -78,6 +92,13 @@ public final class ShopService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a click inside the shop GUI to the correct view handler.
|
||||
*
|
||||
* @param player player interacting with the shop
|
||||
* @param view current resolved view
|
||||
* @param slotIndex clicked slot index
|
||||
*/
|
||||
public void handleClick(ServerPlayerEntity player, ShopView view, int slotIndex) {
|
||||
switch (view) {
|
||||
case HomeView homeView -> handleHomeClick(player, homeView, slotIndex);
|
||||
|
||||
@@ -18,15 +18,35 @@ public final class SoulService {
|
||||
this.dataStore = dataStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the stored balance for a player UUID.
|
||||
*
|
||||
* @param playerUuid player identifier to inspect
|
||||
* @return the current balance, or the configured starting balance for new players
|
||||
*/
|
||||
public long balanceOf(UUID playerUuid) {
|
||||
SoulStealConfig.EconomyConfig economy = configSupplier.get().economy();
|
||||
return dataStore.data().souls().getOrDefault(key(playerUuid), economy.startingSouls());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a player currently has at least the requested amount of souls.
|
||||
*
|
||||
* @param playerUuid player identifier to inspect
|
||||
* @param amount amount to compare against
|
||||
* @return {@code true} when the player has enough souls
|
||||
*/
|
||||
public boolean hasSouls(UUID playerUuid, long amount) {
|
||||
return balanceOf(playerUuid) >= Math.max(0L, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds souls to a player's balance and clamps the result to the configured maximum.
|
||||
*
|
||||
* @param playerUuid player identifier to update
|
||||
* @param amount amount to add
|
||||
* @return the updated balance after clamping
|
||||
*/
|
||||
public long addSouls(UUID playerUuid, long amount) {
|
||||
if (amount <= 0L) {
|
||||
return balanceOf(playerUuid);
|
||||
@@ -39,6 +59,13 @@ public final class SoulService {
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes souls from a player's balance and clamps the result at zero.
|
||||
*
|
||||
* @param playerUuid player identifier to update
|
||||
* @param amount amount to remove
|
||||
* @return the updated balance after clamping
|
||||
*/
|
||||
public long removeSouls(UUID playerUuid, long amount) {
|
||||
if (amount <= 0L) {
|
||||
return balanceOf(playerUuid);
|
||||
@@ -50,11 +77,23 @@ public final class SoulService {
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a player's balance directly, applying the configured upper bound.
|
||||
*
|
||||
* @param playerUuid player identifier to update
|
||||
* @param amount requested new balance
|
||||
*/
|
||||
public void setSouls(UUID playerUuid, long amount) {
|
||||
SoulStealConfig.EconomyConfig economy = configSupplier.get().economy();
|
||||
updateBalance(playerUuid, Math.max(0L, Math.min(economy.maxSouls(), amount)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the configured death penalty to a player.
|
||||
*
|
||||
* @param playerUuid player identifier to penalize
|
||||
* @return the amount removed and the new balance after applying the penalty
|
||||
*/
|
||||
public SoulChange applyDeathPenalty(UUID playerUuid) {
|
||||
long current = balanceOf(playerUuid);
|
||||
if (current <= 0L) {
|
||||
@@ -73,6 +112,14 @@ public final class SoulService {
|
||||
return new SoulChange(-boundedLoss, newBalance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers souls between two players if the transfer rules allow it.
|
||||
*
|
||||
* @param senderUuid source player UUID
|
||||
* @param receiverUuid destination player UUID
|
||||
* @param amount amount to transfer
|
||||
* @return the transfer result, including balances and a human-readable message
|
||||
*/
|
||||
public TransferResult transfer(UUID senderUuid, UUID receiverUuid, long amount) {
|
||||
SoulStealConfig.TransferConfig transferConfig = configSupplier.get().economy().transfer();
|
||||
if (!transferConfig.enabled()) {
|
||||
|
||||
@@ -5,6 +5,12 @@ public final class DurationFormatter {
|
||||
private DurationFormatter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a duration in seconds into a compact human-readable string.
|
||||
*
|
||||
* @param totalSeconds duration to format
|
||||
* @return formatted duration such as {@code 2h 5m 10s}
|
||||
*/
|
||||
public static String formatSeconds(long totalSeconds) {
|
||||
if (totalSeconds <= 0L) {
|
||||
return "0s";
|
||||
|
||||
@@ -9,22 +9,52 @@ public final class SoulTexts {
|
||||
private SoulTexts() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an informational chat message.
|
||||
*
|
||||
* @param message message body without the shared prefix
|
||||
* @return formatted chat text
|
||||
*/
|
||||
public static Text info(String message) {
|
||||
return prefixed(message, Formatting.GRAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a success chat message.
|
||||
*
|
||||
* @param message message body without the shared prefix
|
||||
* @return formatted chat text
|
||||
*/
|
||||
public static Text success(String message) {
|
||||
return prefixed(message, Formatting.GREEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a warning chat message.
|
||||
*
|
||||
* @param message message body without the shared prefix
|
||||
* @return formatted chat text
|
||||
*/
|
||||
public static Text warning(String message) {
|
||||
return prefixed(message, Formatting.GOLD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an error chat message.
|
||||
*
|
||||
* @param message message body without the shared prefix
|
||||
* @return formatted chat text
|
||||
*/
|
||||
public static Text error(String message) {
|
||||
return prefixed(message, Formatting.RED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds highlighted accent text without the standard prefix.
|
||||
*
|
||||
* @param message text to accent
|
||||
* @return formatted text component
|
||||
*/
|
||||
public static MutableText accent(String message) {
|
||||
return Text.literal(message).formatted(Formatting.AQUA);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user