Compare commits

...
Author SHA1 Message Date
Adrian Gonzales 1a31d98149 feat: Added RelationalPlaceholder builder 2025-10-04 22:34:51 -05:00
Adrian Gonzales ae5a8e235e Merge branch 'main' into feat/complex-filter 2025-10-04 22:02:33 -05:00
Adrian f522456b76 Merge pull request #301 from MiniPlaceholders/feat/newparsesubcommands
Implement new parserel subcommand
2025-10-03 01:16:17 -05:00
Adrian Gonzales de7d14d55d fix: Fixed command formatting 2025-10-02 23:19:00 -05:00
Adrian Gonzales c0230efb3b misc: Some component creation cleanup 2025-10-02 22:41:53 -05:00
Adrian Gonzales 3cdb7de9f2 feat: Add --null argument to parse node 2025-10-02 19:41:57 -05:00
Adrian Gonzales 9c954834cb misc: Simplify brigadier suggestions 2025-10-02 16:18:18 -05:00
Adrian Gonzales 55e493c013 feat: Implement parseglobal subcommand 2025-10-02 16:08:52 -05:00
Adrian Gonzales ea725a8121 feat: Implement parserel subcommand 2025-10-02 15:48:22 -05:00
Adrian Gonzales d138d00b26 misc: Renamed #presentClass to #requiredClass 2025-10-02 14:54:17 -05:00
Adrian d63397fc96 Merge pull request #299 from MiniPlaceholders/fix/sponge/commands
Implement Sponge commands
2025-10-02 14:50:02 -05:00
Adrian Gonzales fe6ea73bb0 misc: Some cleanup 2025-10-02 13:20:58 -05:00
Adrian Gonzales be368a7690 feat: Allow operator execution 2025-10-02 12:38:52 -05:00
Adrian Gonzales b31b91e9b8 fix: Fixed Fabric permissions check 2025-10-02 12:28:31 -05:00
Adrian Gonzales 7d8243711a fix(sponge): Fixed console command execution 2025-10-01 21:11:09 -05:00
Adrian Gonzales 7633ada70e feat: Initial Sponge commands implementation 2025-10-01 19:58:08 -05:00
Adrian Gonzales da6256def1 feat: Implement AudiencePlaceholder Builder 2025-10-01 15:48:02 -05:00
Adrian 6d8267684a Merge pull request #296 from MiniPlaceholders/renovate/major-junit-framework-monorepo
fix(deps): update dependency org.junit:junit-bom to v6
2025-09-30 13:05:44 -05:00
renovate[bot] a1d5bb7eed fix(deps): update dependency org.junit:junit-bom to v6 2025-09-30 09:53:12 +00:00
Adrian 161a254613 Merge pull request #294 from MiniPlaceholders/renovate/fabric.api
fix(deps): update dependency net.fabricmc.fabric-api:fabric-api to v0.133.14+1.21.9
2025-09-29 18:16:30 -05:00
renovate[bot] d957269743 fix(deps): update dependency net.fabricmc.fabric-api:fabric-api to v0.133.14+1.21.9 2025-09-29 21:10:18 +00:00
Adrian 7ac5aec82d Merge pull request #292 from MiniPlaceholders/renovate/shadow
fix(deps): update shadow to v9.2.2
2025-09-27 23:28:40 -05:00
Adrian 9713704352 Merge pull request #293 from MiniPlaceholders/renovate/fabric.api
fix(deps): update dependency net.fabricmc.fabric-api:fabric-api to v0.133.13+1.21.9
2025-09-27 23:28:19 -05:00
renovate[bot] 4240fb556c fix(deps): update dependency net.fabricmc.fabric-api:fabric-api to v0.133.13+1.21.9 2025-09-26 21:53:12 +00:00
renovate[bot] 5a9ac8af87 fix(deps): update shadow to v9.2.2 2025-09-26 10:00:24 +00:00
Adrian 97b68a3616 Merge pull request #290 from MiniPlaceholders/renovate/fabric.api
fix(deps): update dependency net.fabricmc.fabric-api:fabric-api to v0.133.12+1.21.9
2025-09-25 16:08:43 -05:00
renovate[bot] afde5e6e2c fix(deps): update dependency net.fabricmc.fabric-api:fabric-api to v0.133.12+1.21.9 2025-09-25 20:50:11 +00:00
Adrian 8a37136565 Merge pull request #291 from MiniPlaceholders/renovate/shadow
fix(deps): update shadow to v9.2.1
2025-09-24 11:46:24 -05:00
renovate[bot] 8d5378ff3e fix(deps): update shadow to v9.2.1 2025-09-24 10:30:48 +00:00
Adrian Gonzales 40fd7e7873 test: Improved Placeholders tests 2025-09-23 14:12:56 -05:00
Adrian Gonzales aa47088828 Merge branch 'main' into feat/complex-filter
# Conflicts:
#	api/src/main/java/io/github/miniplaceholders/api/types/RelationalAudience.java
2025-09-23 12:58:47 -05:00
Adrian Gonzales 31ee16e412 feat: Added LoadRequirement#presentClass and LoadRequirement#anyOf 2025-09-23 12:48:57 -05:00
Adrian Gonzales 8aec3e592f fix: Added nullability checks to RelationalAudience 2025-09-23 12:05:45 -05:00
Adrian 88c26dee51 Merge pull request #281 from MiniPlaceholders/renovate/fabric.api
fix(deps): update dependency net.fabricmc.fabric-api:fabric-api to v0.133.10+1.21.9
2025-09-22 12:49:22 -05:00
renovate[bot] 659be54911 fix(deps): update dependency net.fabricmc.fabric-api:fabric-api to v0.133.10+1.21.9 2025-09-20 21:08:10 +00:00
Adrian 3808d660d9 Merge pull request #289 from MiniPlaceholders/renovate/gradle-9.x
chore(deps): update gradle to v9.1.0
2025-09-18 12:09:59 -05:00
renovate[bot] c6ecdacdbc chore(deps): update gradle to v9.1.0 2025-09-18 13:53:15 +00:00
Adrian 25a9b435ff Update outdated README 2025-09-15 22:03:51 -05:00
Adrian d95b9fddac Merge pull request #287 from MiniPlaceholders/renovate/kotlin-monorepo
fix(deps): update kotlin monorepo to v2.2.20
2025-09-10 23:52:26 -05:00
renovate[bot] 78d1b83513 fix(deps): update kotlin monorepo to v2.2.20 2025-09-10 09:43:54 +00:00
Adrian 56a1e9afca Merge pull request #288 from MiniPlaceholders/renovate/major-runtask
chore(deps): update runtask to v3 (major)
2025-09-09 12:27:22 -05:00
Adrian ba1438457b Merge pull request #286 from MiniPlaceholders/renovate/quiet-fabric-loom-1.x
chore(deps): update plugin quiet-fabric-loom to v1.11.325
2025-09-09 01:09:53 -05:00
renovate[bot] ad36fab536 chore(deps): update runtask to v3 2025-09-08 01:08:10 +00:00
renovate[bot] f2ff2c0bf3 chore(deps): update plugin quiet-fabric-loom to v1.11.325 2025-09-07 23:34:41 +00:00
Adrian Gonzales fff5acdb7b feat: Add complex-audience-filter 2025-09-05 00:50:10 -05:00
renovate[bot] 6e28786b09 fix(deps): update shadow to v9.1.0 (#283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-30 16:14:56 -05:00
Adrian Gonzales f67690f413 fix: Fixed multiple loading expansions 2025-08-30 11:18:49 -05:00
Adrian cc8646cb4e [ci skip] Fix link formatting in README.md 2025-08-24 16:45:14 -05:00
Adrian 4db80d7583 [ci skip] Updated README
Updated compatibility section and command examples in README.md.
2025-08-24 16:44:20 -05:00
renovate[bot] 67d480c969 chore(deps): update plugin idea-ext to v1.3 (#280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 00:39:51 -05:00
53 changed files with 2062 additions and 1280 deletions

No files matched your search

+17 -34
View File
@@ -1,45 +1,30 @@
# MiniPlaceholders
[![WorkFlow](https://img.shields.io/github/actions/workflow/status/MiniPlaceholders/MiniPlaceholders/build.yml?style=flat-square)](https://github.com/MiniPlaceholders/MiniPlaceholders/actions)
[![GitHub WorkFlow](https://img.shields.io/github/actions/workflow/status/MiniPlaceholders/MiniPlaceholders/build.yml?logo=GitHub&style=flat-square)](https://github.com/MiniPlaceholders/MiniPlaceholders/actions)
[![Jenkins Build](https://img.shields.io/jenkins/build?jobUrl=https%3A%2F%2Fci.codemc.io%2Fjob%2FMiniPlaceholders%2Fjob%2FMiniPlaceholders%2F&style=flat-square&logo=jenkins)](https://ci.codemc.io/job/MiniPlaceholders/job/MiniPlaceholders/)
![Latest Version](https://img.shields.io/github/v/release/MiniPlaceholders/MiniPlaceholders?style=flat-square)
[![Discord](https://img.shields.io/discord/899740810956910683?color=7289da&logo=Discord&label=Discord&style=flat-square)](https://discord.gg/5NMMzK5mAn)
![Modrinth Downloads](https://img.shields.io/modrinth/dt/HQyibRsN?logo=Modrinth&style=flat-square)
![GitHub Downloads](https://img.shields.io/github/downloads/MiniPlaceholders/MiniPlaceholders/total?logo=GitHub&style=flat-square)
MiniMessage Component-based Placeholders API for Minecraft Platforms.
## Compatibility
- Paper 1.19.3, 1.19.4+
- Folia 1.19.4+
- Velocity 3.1.2+
- Fabric 1.19.4+
- Sponge API 8+
## Commands
### Velocity
- `/vminiplaceholders parse me "[message with placeholders]"`
- `/vminiplaceholders parse player [some-player] "[message with placeholders]"`
### Paper | Folia | Fabric
- `/miniplaceholders <parse|help> player [some-player] "[message with placeholders]`
- `/miniplaceholders parse player [some-player] "[message with placeholders]"`
#### Example:
- `/miniplaceholders parse me "<player_xp>"`
- `/vminiplaceholders parse player 4drian3d "<player_name>"`
- Paper 1.21+
- Velocity 3.4+
- Fabric 1.21.6+
- Sponge API 12+
## User Usage
Check our user usage wiki [here](https://github.com/MiniPlaceholders/MiniPlaceholders/wiki/User-Getting-Started)
Check our user usage wiki [here](https://miniplaceholders.github.io/docs/user-guide/User-Getting-Started)
## API
Check the available [Javadocs](https://javadoc.io/doc/io.github.miniplaceholders/miniplaceholders-api)
Or check the [Developer Wiki](https://github.com/MiniPlaceholders/MiniPlaceholders/wiki/Developer-Getting-Started)
Or check the [Developer Wiki](https://miniplaceholders.github.io/docs/developer-guide/Developer-Getting-Started)
### Java
```java
@@ -47,9 +32,7 @@ Or check the [Developer Wiki](https://github.com/MiniPlaceholders/MiniPlaceholde
class Main {
public static void registerExpansion() {
final Expansion expansion = Expansion.builder("my-expansion")
.filter(Player.class)
.audiencePlaceholder("name", (audience, ctx, queue) -> {
final Player player = (player) audience;
.audiencePlaceholder(Player.class, "name", (player, ctx, queue) -> {
return Tag.selfClosingInserting(player.getName());
})
.globalPlaceholder("tps", (ctx, queue) ->
@@ -59,8 +42,8 @@ class Main {
expansion.register();
Player player;
final TagResolver playerResolver = MiniPlaceholders.getAudiencePlaceholders(player);
player.sendMessage(miniMessage().deserialize("Player Name: <my-expansion_name>", playerResolver));
final TagResolver playerResolver = MiniPlaceholders.audiencePlaceholders();
player.sendMessage(miniMessage().deserialize("Player Name: <my-expansion_name>", player, playerResolver));
}
}
@@ -70,10 +53,10 @@ class Main {
```kotlin
fun register() {
val expansion = expansion("my-expansion") {
audiencePlaceholder("name") { aud, _, _ ->
audience<Player>("name") { player, _, _ ->
aud.getName().asClosingTag()
}
globalPlaceholder("tps") { _, _ ->
global("tps") { _, _ ->
Component.text(Bukkit.getTps()[0]).asInsertingTag()
}
}
@@ -81,7 +64,7 @@ fun register() {
expansion.register()
val player: Player
val playerResolver = MiniPlaceholders.getAudiencePlaceholders(player)
player.sendMessage(miniMessage().deserialize("Player Name: <my-expansion_name>", playerResolver))
val playerResolver = MiniPlaceholders.audiencePlaceholders()
player.sendMessage(miniMessage().deserialize("Player Name: <my-expansion_name>", player, playerResolver))
}
```
@@ -13,7 +13,6 @@ import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NullMarked;
@@ -35,360 +34,390 @@ import java.util.Collection;
* TagResolver resolver = expansion.resolver();
* player.sendMessage(MiniMessage.miniMessage().deserialize("Hello {@literal <luckperms_prefix> <player_name>}", player, resolver));
* </pre>
*
*
* @since 1.0.0
*/
@NullMarked
public sealed interface Expansion permits ExpansionImpl {
/**
* Get the expansion name
*
* @return the expansion name
* @since 1.0.0
*/
String name();
/**
* Get the expansion name
*
* @return the expansion name
* @since 1.0.0
*/
String name();
/**
* Get the author of this expansion
*
* @return the author or null
* @since 2.3.0
*/
@Nullable String author();
/**
* Get the version of this expansion
*
* @return the version or null
* @since 2.3.0
*/
@Nullable String version();
/**
* Get the {@link TagResolver} of an {@link Audience}
*
* @return A TagResolver with variable placeholders of an Audience
* @since 1.0.0
*/
TagResolver audiencePlaceholders();
/**
* Get the relational placeholders based on two audiences
*
* <p>The resulting TagResolver will give results according
* to the 2 {@link Audience} provided and when called
* at any time will retrieve the required values</p>
*
* @return A TagResolver with variable placeholders between 2 {@link Audience}s
* @since 1.0.0
*/
TagResolver relationalPlaceholders();
/**
* Get global placeholders
*
* @return the global placeholders
* @since 1.0.0
*/
TagResolver globalPlaceholders();
/**
* Register this expansion
*
* @throws IllegalStateException if the expansion you are trying to register is already registered
* @see Expansion#registered()
* @since 1.0.0
*/
void register();
/**
* Unregisters this expansion
*
* @throws IllegalStateException if the expansion you are trying to unregister is not registered
* @see Expansion#registered()
* @since 2.1.0
*/
void unregister();
/**
* Get the registration status of this expansion
*
* @return true if the expansion is registered, otherwise false
* @since 2.1.0
*/
boolean registered();
/**
* Get the registered audience placeholders
*
* @return registered audience placeholders
*/
Collection<AudiencePlaceholder<?>> registeredAudiencePlaceholders();
/**
* Get the registered relational placeholders
*
* @return registered relational placeholders
*/
Collection<RelationalPlaceholder<?>> registeredRelationalPlaceholders();
/**
* Get an Audience Placeholder by name
*
* <pre>{@code
* Expansion expansion = MiniPlaceholders.expansionByName("player")
* AudiencePlaceholder<?> placeholder = expansion.audiencePlaceholderByName("name");
* }</pre>
*
* @param name the name of the placeholder to search
* @return the desired placeholder or null
*/
@Nullable AudiencePlaceholder<?> audiencePlaceholderByName(final String name);
/**
* Get a Relational Placeholder by name
*
* <pre>{@code
* Expansion expansion = MiniPlaceholders.expansionByName("expansion")
* RelationalPlaceholder<?> placeholder = expansion.relationalPlaceholderByName("placeholder");
* }</pre>
*
* @param name the name of the placeholder to search
* @return the desired placeholder or null
* @apiNote The name of a relational placeholder does not contain the prefix “rel”,
* just the name assigned when it was created
*/
@Nullable RelationalPlaceholder<?> relationalPlaceholderByName(final String name);
/**
* Get a Global Placeholder by name
*
* <pre>{@code
* Expansion expansion = MiniPlaceholders.expansionByName("player")
* GlobalPlaceholder placeholder = expansion.globalPlaceholderByName("name");
* }</pre>
*
* @param name the name of the placeholder to search
* @return the desired placeholder or null
*/
@Nullable GlobalPlaceholder globalPlaceholderByName(final String name);
/**
* Checks whether an audience placeholder with the specified name has been registered in this expansion.
*
* @param name the name of the placeholder to check
* @return true if the placeholder is registered, else false
*/
default boolean hasAudiencePlaceholder(final String name) {
return audiencePlaceholderByName(name) != null;
}
/**
* Checks whether a relational placeholder with the specified name has been registered in this expansion.
*
* @param name the name of the placeholder to check
* @return true if the placeholder is registered, else false
*/
default boolean hasRelationalPlaceholder(final String name) {
return relationalPlaceholderByName(name) != null;
}
/**
* Checks whether a global placeholder with the specified name has been registered in this expansion.
*
* @param name the name of the placeholder to check
* @return true if the placeholder is registered, else false
*/
default boolean hasGlobalPlaceholder(final String name) {
return globalPlaceholderByName(name) != null;
}
/**
* Get a TagResolver that contains all placeholders of a specific type in this expansion.
*
* @param type the desired type
* @return a TagResolver containing all placeholder of a specific type
*/
TagResolver placeholdersByType(PlaceholderType type);
/**
* Creates a new Expansion Builder
*
* @param name the expansion name
* @return a new expansion builder
* @since 1.0.0
*/
static Expansion.Builder builder(final String name) {
return new ExpansionImpl.Builder(name);
}
/**
* Get a shorter ToString with the details of this expansion.
*
* @return a shorter ToString
*/
@ApiStatus.Internal
@Deprecated(forRemoval = true, since = "3.1.0")
default String shortToString() {
final StringBuilder builder = new StringBuilder(name()).append('[');
if (author() != null) {
builder.append("Author: ").append(author());
}
if (version() != null) {
if (author() != null) {
builder.append(", ");
}
builder.append("Version: ").append(version());
}
builder.append(']');
return builder.toString();
}
/**
* Expansion Builder
* <p>Example use:</p>
* <pre>
* Expansion.Builder builder = Expansion.builder("player");
* builder
* // Thanks to this filter, a cast can be performed without the probability of a ClassCastException
* .filter(Player.class)
* .resolver("name", (audience, queue, ctx) -> Tag.selfClosingInserting(Component.text(((Player)audience).getUsername())));
* Expansion expansion = builder.build();
* </pre>
* <p>
* then use it:
*
* <pre>
* Player player = event.getPlayer();
* TagResolver resolver = expansion.resolver(player);
* Component messageReplaced = MiniMessage.deserialize({@link String}, resolver);
* </pre>
*
* @since 1.0.0
*/
@NullMarked
interface Builder extends AbstractBuilder<Expansion> {
/**
* Get the author of this expansion
* Adds an audience placeholder
*
* @return the author or null
* <p>This type of Placeholder depends on a specific audience to obtain its values</p>
*
* <p>The content of this Placeholder is cached
* and can mutate depending on when it is invoked</p>
*
* @param targetClass the target class
* @param key the placeholder key, cannot be an empty or black string
* @param audiencePlaceholder the single placeholder
* @param <A> the type of audience that this placeholder supports, must be the same as targetClass
* @return the {@link Builder} itself
* @since 1.0.0
*/
<A extends Audience> Builder audiencePlaceholder(
final @Nullable Class<A> targetClass,
final String key,
final AudienceTagResolver<A> audiencePlaceholder
);
/**
* Adds an audience placeholder
*
* <p>This type of Placeholder depends on a specific audience to obtain its values</p>
*
* <p>The content of this Placeholder is cached
* and can mutate depending on when it is invoked</p>
*
* @param key the placeholder key, cannot be an empty or black string
* @param audiencePlaceholder the single placeholder
* @return the {@link Builder} itself
* @since 3.0.0
*/
default Builder audiencePlaceholder(final String key, final AudienceTagResolver<Audience> audiencePlaceholder) {
return this.audiencePlaceholder(null, key, audiencePlaceholder);
}
<A extends Audience> Builder audiencePlaceholder(
final Class<A> targetClass,
final AudiencePlaceholder.Builder.Provider<A> builderProvider
);
default Builder audiencePlaceholder(final AudiencePlaceholder.Builder.Provider<Audience> builderProvider) {
return audiencePlaceholder(Audience.class, builderProvider);
}
/**
* Adds a Relational Placeholder based on two audiences
*
* <p>This type of placeholder allows you to create
* components based on a 2-audiences relationship,
* one is the one on which the placeholder
* is based and the other is the one on which
* the placeholder is displayed.</p>
*
* <p>The content of this Placeholder is cached
* and can mutate depending on when it is invoked</p>
*
* @param targetClass the target class
* @param key the placeholder key, cannot be an empty or black string
* @param relationalPlaceholder the relational placeholder
* @param <A> the type of audience that this placeholder supports, must be the same as targetClass
* @return the {@link Builder} itself
* @since 3.0.0
*/
<A extends Audience> Builder relationalPlaceholder(
final @Nullable Class<A> targetClass,
final String key,
final RelationalTagResolver<A> relationalPlaceholder
);
/**
* Adds a Relational Placeholder based on two audiences
*
* <p>This type of placeholder allows you to create
* components based on a 2-audiences relationship,
* one is the one on which the placeholder
* is based and the other is the one on which
* the placeholder is displayed.</p>
*
* <p>The content of this Placeholder is cached
* and can mutate depending on when it is invoked</p>
*
* @param key the placeholder key, cannot be an empty or black string
* @param relationalResolver the relational placeholder
* @return the {@link Builder} itself
* @since 3.0.0
*/
default Builder relationalPlaceholder(
final String key,
final RelationalTagResolver<Audience> relationalResolver
) {
return this.relationalPlaceholder(null, key, relationalResolver);
}
<A extends Audience> Builder relationalPlaceholder(
final Class<A> targetClass,
final RelationalPlaceholder.Builder.Provider<A> builderProvider
);
default Builder relationalPlaceholder(final RelationalPlaceholder.Builder.Provider<Audience> builderProvider) {
return relationalPlaceholder(Audience.class, builderProvider);
}
/**
* Adds a global placeholder
*
* <p>The content of this Placeholder is cached
* and can mutate depending on when it is invoked</p>
*
* @param key the placeholder key, cannot be an empty or black string
* @param resolver the function
* @return the {@link Builder} itself
* @since 3.0.0
*/
Builder globalPlaceholder(final String key, final GlobalTagResolver resolver);
/**
* Adds a global placeholder
*
* <p>This placeholder is not cached and is immutable</p>
*
* @param key the placeholder key, cannot be an empty or black string
* @param tag the tag of this placeholder
* @return the {@link Builder} itself
* @since 1.1.0
*/
default Builder globalPlaceholder(final String key, final Tag tag) {
return this.globalPlaceholder(key, (queue, context) -> tag);
}
/**
* Sets the author of this expansion
*
* @param author the author of this expansion
* @return the {@link Builder} itself
* @since 2.3.0
*/
@Nullable String author();
@Contract("_ -> this")
Builder author(final @Nullable String author);
/**
* Get the version of this expansion
* Sets the version of this expansion
*
* @return the version or null
* @param version the version of this expansion
* @return the {@link Builder} itself
* @since 2.3.0
*/
@Nullable String version();
/**
* Get the {@link TagResolver} of an {@link Audience}
* @return A TagResolver with variable placeholders of an Audience
* @since 1.0.0
*/
TagResolver audiencePlaceholders();
/**
* Get the relational placeholders based on two audiences
*
* <p>The resulting TagResolver will give results according
* to the 2 {@link Audience} provided and when called
* at any time will retrieve the required values</p>
* @return A TagResolver with variable placeholders between 2 {@link Audience}s
* @since 1.0.0
*/
TagResolver relationalPlaceholders();
/**
* Get global placeholders
* @return the global placeholders
* @since 1.0.0
*/
TagResolver globalPlaceholders();
/**
* Register this expansion
* @since 1.0.0
* @throws IllegalStateException if the expansion you are trying to register is already registered
* @see Expansion#registered()
*/
void register();
/**
* Unregisters this expansion
* @since 2.1.0
* @throws IllegalStateException if the expansion you are trying to unregister is not registered
* @see Expansion#registered()
*/
void unregister();
/**
* Get the registration status of this expansion
* @return true if the expansion is registered, otherwise false
* @since 2.1.0
*/
boolean registered();
// TODO: Docs and Tests
/**
* Get the registered audience placeholders
*
* @return registered audience placeholders
*/
Collection<AudiencePlaceholder<?>> registeredAudiencePlaceholders();
/**
* Get the registered relational placeholders
*
* @return registered relational placeholders
*/
Collection<RelationalPlaceholder<?>> registeredRelationalPlaceholders();
/**
* Get an Audience Placeholder by name
*
* <pre>{@code
* Expansion expansion = MiniPlaceholders.expansionByName("player")
* AudiencePlaceholder<?> placeholder = expansion.audiencePlaceholderByName("name");
* }</pre>
* @param name the name of the placeholder to search
* @return the desired placeholder or null
*/
@Nullable AudiencePlaceholder<?> audiencePlaceholderByName(final String name);
/**
* Get a Relational Placeholder by name
*
* <pre>{@code
* Expansion expansion = MiniPlaceholders.expansionByName("expansion")
* RelationalPlaceholder<?> placeholder = expansion.relationalPlaceholderByName("placeholder");
* }</pre>
* @param name the name of the placeholder to search
* @return the desired placeholder or null
* @apiNote The name of a relational placeholder does not contain the prefix “rel”,
* just the name assigned when it was created
*/
@Nullable RelationalPlaceholder<?> relationalPlaceholderByName(final String name);
/**
* Get a Global Placeholder by name
*
* <pre>{@code
* Expansion expansion = MiniPlaceholders.expansionByName("player")
* GlobalPlaceholder placeholder = expansion.globalPlaceholderByName("name");
* }</pre>
* @param name the name of the placeholder to search
* @return the desired placeholder or null
*/
@Nullable GlobalPlaceholder globalPlaceholderByName(final String name);
/**
* Checks whether an audience placeholder with the specified name has been registered in this expansion.
*
* @param name the name of the placeholder to check
* @return true if the placeholder is registered, else false
*/
default boolean hasAudiencePlaceholder(final String name) {
return audiencePlaceholderByName(name) != null;
}
/**
* Checks whether a relational placeholder with the specified name has been registered in this expansion.
*
* @param name the name of the placeholder to check
* @return true if the placeholder is registered, else false
*/
default boolean hasRelationalPlaceholder(final String name) {
return relationalPlaceholderByName(name) != null;
}
/**
* Checks whether a global placeholder with the specified name has been registered in this expansion.
*
* @param name the name of the placeholder to check
* @return true if the placeholder is registered, else false
*/
default boolean hasGlobalPlaceholder(final String name) {
return globalPlaceholderByName(name) != null;
}
/**
* Get a TagResolver that contains all placeholders of a specific type in this expansion.
*
* @param type the desired type
* @return a TagResolver containing all placeholder of a specific type
*/
TagResolver placeholdersByType(PlaceholderType type);
/**
* Creates a new Expansion Builder
* @param name the expansion name
* @return a new expansion builder
* @since 1.0.0
*/
static Expansion.Builder builder(final String name){
return new ExpansionImpl.Builder(name);
}
/**
* Get a shorter ToString with the details of this expansion.
* @return a shorter ToString
*/
@ApiStatus.Internal
default String shortToString() {
final StringBuilder builder = new StringBuilder(name()).append('[');
if (author() != null) {
builder.append("Author: ").append(author());
}
if (version() != null) {
if (author() != null) {
builder.append(", ");
}
builder.append("Version: ").append(version());
}
builder.append(']');
return builder.toString();
}
/**
* Expansion Builder
* <p>Example use:</p>
* <pre>
* Expansion.Builder builder = Expansion.builder("player");
* builder
* // Thanks to this filter, a cast can be performed without the probability of a ClassCastException
* .filter(Player.class)
* .resolver("name", (audience, queue, ctx) -> Tag.selfClosingInserting(Component.text(((Player)audience).getUsername())));
* Expansion expansion = builder.build();
* </pre>
*
* then use it:
*
* <pre>
* Player player = event.getPlayer();
* TagResolver resolver = expansion.resolver(player);
* Component messageReplaced = MiniMessage.deserialize({@link String}, resolver);
* </pre>
*
* @since 1.0.0
*/
@NullMarked
interface Builder extends AbstractBuilder<Expansion> {
/**
* Adds an audience placeholder
*
* <p>This type of Placeholder depends on a specific audience to obtain its values</p>
*
* <p>The content of this Placeholder is cached
* and can mutate depending on when it is invoked</p>
*
* @param targetClass the target class
* @param key the placeholder key, cannot be an empty or black string
* @param audiencePlaceholder the single placeholder
* @param <A> the type of audience that this placeholder supports, must be the same as targetClass
* @return the {@link Builder} itself
* @since 1.0.0
*/
<A extends Audience> Builder audiencePlaceholder(
final @Nullable Class<A> targetClass,
final String key,
final AudienceTagResolver<A> audiencePlaceholder
);
/**
* Adds an audience placeholder
*
* <p>This type of Placeholder depends on a specific audience to obtain its values</p>
*
* <p>The content of this Placeholder is cached
* and can mutate depending on when it is invoked</p>
*
* @param key the placeholder key, cannot be an empty or black string
* @param audiencePlaceholder the single placeholder
* @return the {@link Builder} itself
* @since 3.0.0
*/
default Builder audiencePlaceholder(final String key, final AudienceTagResolver<Audience> audiencePlaceholder) {
return this.audiencePlaceholder(null, key, audiencePlaceholder);
}
/**
* Adds a Relational Placeholder based on two audiences
*
* <p>This type of placeholder allows you to create
* components based on a 2-audiences relationship,
* one is the one on which the placeholder
* is based and the other is the one on which
* the placeholder is displayed.</p>
*
* <p>The content of this Placeholder is cached
* and can mutate depending on when it is invoked</p>
*
* @param targetClass the target class
* @param key the placeholder key, cannot be an empty or black string
* @param relationalPlaceholder the relational placeholder
* @param <A> the type of audience that this placeholder supports, must be the same as targetClass
* @return the {@link Builder} itself
* @since 3.0.0
*/
<A extends Audience> Builder relationalPlaceholder(
final @Nullable Class<A> targetClass,
final String key,
final RelationalTagResolver<A> relationalPlaceholder
);
/**
* Adds a Relational Placeholder based on two audiences
*
* <p>This type of placeholder allows you to create
* components based on a 2-audiences relationship,
* one is the one on which the placeholder
* is based and the other is the one on which
* the placeholder is displayed.</p>
*
* <p>The content of this Placeholder is cached
* and can mutate depending on when it is invoked</p>
*
* @param key the placeholder key, cannot be an empty or black string
* @param relationalResolver the relational placeholder
* @return the {@link Builder} itself
* @since 3.0.0
*/
default Builder relationalPlaceholder(
final String key,
final RelationalTagResolver<Audience> relationalResolver
) {
return this.relationalPlaceholder(null, key, relationalResolver);
}
/**
* Adds a global placeholder
*
* <p>The content of this Placeholder is cached
* and can mutate depending on when it is invoked</p>
* @param key the placeholder key, cannot be an empty or black string
* @param resolver the function
* @return the {@link Builder} itself
* @since 3.0.0
*/
Builder globalPlaceholder(final String key, final GlobalTagResolver resolver);
/**
* Adds a global placeholder
*
* <p>This placeholder is not cached and is immutable</p>
* @param key the placeholder key, cannot be an empty or black string
* @param tag the tag of this placeholder
* @return the {@link Builder} itself
* @since 1.1.0
*/
default Builder globalPlaceholder(final String key, final Tag tag) {
return this.globalPlaceholder(key, (queue, context) -> tag);
}
/**
* Sets the author of this expansion
*
* @param author the author of this expansion
* @return the {@link Builder} itself
* @since 2.3.0
*/
@Contract("_ -> this")
@NotNull Builder author(final @Nullable String author);
/**
* Sets the version of this expansion
*
* @param version the version of this expansion
* @return the {@link Builder} itself
* @since 2.3.0
*/
@Contract("_ -> this")
@NotNull Builder version(final @Nullable String version);
}
@Contract("_ -> this")
Builder version(final @Nullable String version);
}
}
@@ -164,7 +164,7 @@ final class ExpansionImpl implements Expansion {
}
@NullUnmarked
static final class Builder implements Expansion.Builder {
public static final class Builder implements Expansion.Builder {
private final String expansionName;
@Subst("server")
private final String expansionPrefix;
@@ -179,18 +179,36 @@ final class ExpansionImpl implements Expansion {
this.expansionPrefix = name.toLowerCase(Locale.ROOT).concat("_");
}
@Override
public <A extends Audience> Expansion.Builder audiencePlaceholder(
final Class<A> targetClass,
final @NotNull @Subst("placeholder") String key,
final @NotNull AudienceTagResolver<@NotNull A> audiencePlaceholder
) {
nonNullOrEmptyString(key, "Placeholder key");
requireNonNull(audiencePlaceholder, "the audience placeholder cannot be null");
if (this.audiencePlaceholders == null) this.audiencePlaceholders = new HashSet<>();
this.audiencePlaceholders.add(AudiencePlaceholder.single(targetClass, expansionPrefix + key, key, audiencePlaceholder));
return this;
}
@Override
public <A extends Audience> @NotNull Builder audiencePlaceholder(
final @Nullable Class<A> targetClass,
@Subst("name") final @NotNull String key,
final @NotNull AudienceTagResolver<@NotNull A> audiencePlaceholder
public <A extends Audience> Expansion.Builder audiencePlaceholder(
final @NotNull Class<A> targetClass,
final @NotNull AudiencePlaceholder.Builder.Provider<A> builderProvider
) {
nonNullOrEmptyString(key, "Placeholder key");
requireNonNull(audiencePlaceholder, "the audience placeholder cannot be null");
requireNonNull(targetClass, "target class");
requireNonNull(builderProvider, "builder");
if (this.audiencePlaceholders == null) this.audiencePlaceholders = new HashSet<>();
this.audiencePlaceholders.add(AudiencePlaceholder.single(targetClass, expansionPrefix + key, key, audiencePlaceholder));
final AudiencePlaceholder.Builder<A> builder = AudiencePlaceholder.builder(targetClass);
builderProvider.provide(builder);
final AudiencePlaceholder<@NotNull A> placeholder = builder.build(expansionPrefix);
this.audiencePlaceholders.add(placeholder);
return this;
}
@@ -210,6 +228,24 @@ final class ExpansionImpl implements Expansion {
return this;
}
@Override
public <A extends Audience> Expansion.Builder relationalPlaceholder(
final @NotNull Class<A> targetClass,
final @NotNull RelationalPlaceholder.Builder.Provider<A> builderProvider
) {
requireNonNull(targetClass, "target class");
requireNonNull(builderProvider, "builder");
if (this.relationalPlaceholders == null) this.relationalPlaceholders = new HashSet<>();
final RelationalPlaceholder.Builder<A> builder = RelationalPlaceholder.builder(targetClass);
builderProvider.provide(builder);
final RelationalPlaceholder<@NotNull A> placeholder = builder.build(expansionPrefix);
this.relationalPlaceholders.add(placeholder);
return this;
}
@Override
public @NotNull Builder globalPlaceholder(
@Subst("time") @NotNull final String key,
@@ -8,6 +8,7 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.Collection;
import java.util.Collections;
@@ -236,6 +237,7 @@ public final class MiniPlaceholders {
* @return all available registered expansions
* @since 3.0.0
*/
@Unmodifiable
public static Collection<Expansion> expansionsAvailable() {
return Collections.unmodifiableSet(expansions);
}
@@ -8,47 +8,90 @@ import net.kyori.adventure.text.minimessage.Context;
import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.TagPattern;
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;
import org.intellij.lang.annotations.Subst;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
import java.util.Objects;
import java.util.function.Predicate;
import static java.util.Objects.requireNonNull;
/**
* A placeholder that can be applied based on an audience of the specified type.
*
* @param targetClass the type of audience from which this placeholder can obtain information
* @param key the full placeholder key
* @param name the name of this placeholder
* @param resolver the object responsible for obtaining audience information
* @param <A> the parametrized type
* @param targetClass the type of audience from which this placeholder can obtain information
* @param targetFilter the filter that will be applied to the audience before being able to evaluate its return tag
* @param key the full placeholder key
* @param name the name of this placeholder
* @param resolver the object responsible for obtaining audience information
* @param <A> the parametrized type
*/
@NullMarked
public record AudiencePlaceholder<A extends Audience>(
@Nullable Class<A> targetClass,
@TagPattern String key,
String name,
AudienceTagResolver<A> resolver
@Nullable Class<A> targetClass,
@Nullable Predicate<A> targetFilter,
@TagPattern String key,
@TagPattern String name,
AudienceTagResolver<A> resolver,
@Nullable PlaceholderMetadata placeholderMetadata
) implements Placeholder {
/**
* A placeholder that can be applied based on an audience of the specified type.
*
* @param targetClass the type of audience from which this placeholder can obtain information
* @param key the full placeholder key
* @param name the name of this placeholder
* @param resolver the object responsible for obtaining audience information
* @since 3.0.0
*/
public AudiencePlaceholder(
final @Nullable Class<A> targetClass,
final @TagPattern String key,
final @TagPattern String name,
final AudienceTagResolver<A> resolver
) {
this(targetClass, null, key, name, resolver, null);
}
/**
* Creates a new AudiencePlaceholder.
*
* @param targetClass the type of audience from which this placeholder can obtain information
* @param key the placeholder key
* @param name the name of this placeholder
* @param resolver the object responsible for obtaining audience information
* @param key the placeholder key
* @param name the name of this placeholder
* @param resolver the object responsible for obtaining audience information
* @param <A> the parametrized type
* @return a new AudiencePlaceholder
* @param <A> the parametrized type
* @since 3.0.0
*/
public static <A extends Audience> AudiencePlaceholder<A> single(
@Nullable Class<A> targetClass,
@TagPattern String key,
String name,
AudienceTagResolver<A> resolver
@Nullable Class<A> targetClass,
@TagPattern String key,
@TagPattern String name,
AudienceTagResolver<A> resolver
) {
return new AudiencePlaceholder<>(targetClass, requireNonNull(key), requireNonNull(name), requireNonNull(resolver));
return new AudiencePlaceholder<>(
targetClass,
null,
requireNonNull(key),
requireNonNull(name),
requireNonNull(resolver),
null
);
}
public static <A extends Audience> Builder<A> builder(final Class<A> clazz) {
requireNonNull(clazz);
return new Builder<>(clazz);
}
public static Builder<Audience> builder() {
return new Builder<>(Audience.class);
}
@Override
@@ -65,13 +108,16 @@ public record AudiencePlaceholder<A extends Audience>(
if (audience == null) {
return null;
}
if (targetFilter != null && !targetFilter.test(audience)) {
return null;
}
return resolveA(name, audience, arguments, ctx);
}
private @Nullable Tag resolveA(String key, A audience, ArgumentQueue queue, Context ctx) {
return this.has(key)
? resolver.tag(audience, queue, ctx)
: null;
? resolver.tag(audience, queue, ctx)
: null;
}
@SuppressWarnings("OverrideOnly")
@@ -106,7 +152,63 @@ public record AudiencePlaceholder<A extends Audience>(
@Override
public String toString() {
return "AudiencePlaceholder{" +
"key='" + key + '\'' +
'}';
"key='" + key + '\'' +
'}';
}
@NullUnmarked
public static final class Builder<A extends Audience> {
@NotNull
private final Class<A> targetClass;
@TagPattern
private String name;
@Nullable
private Predicate<A> targetFilter;
private AudienceTagResolver<@NotNull A> resolver;
@Nullable
private PlaceholderMetadata metadata;
Builder(final @NotNull Class<A> targetClass) {
this.targetClass = targetClass;
}
public Builder<A> name(final @NonNull @TagPattern String name) {
this.name = requireNonNull(name, "placeholder name");
return this;
}
public Builder<A> targetFilter(final Predicate<A> targetFilter) {
this.targetFilter = targetFilter;
return this;
}
public Builder<A> resolver(final @NotNull AudienceTagResolver<@NotNull A> resolver) {
this.resolver = requireNonNull(resolver);
return this;
}
public Builder<A> metadata(final PlaceholderMetadata metadata) {
this.metadata = metadata;
return this;
}
public @NonNull AudiencePlaceholder<@NonNull A> build(final @NotNull @Subst("expansion") String expansionPrefix) {
requireNonNull(this.name, "name");
requireNonNull(this.resolver, "resolver");
return new AudiencePlaceholder<>(
targetClass,
targetFilter,
expansionPrefix+name,
name,
resolver,
metadata
);
}
@FunctionalInterface
public interface Provider<A extends Audience> {
void provide(Builder<A> builderProvider);
}
}
}
@@ -0,0 +1,17 @@
package io.github.miniplaceholders.api.placeholder;
import org.jspecify.annotations.NullMarked;
import static java.util.Objects.requireNonNull;
@NullMarked
public record PlaceholderMetadata(String displayName, String description) {
public PlaceholderMetadata {
requireNonNull(displayName);
requireNonNull(description);
}
public static PlaceholderMetadata data(final String displayName, final String description) {
return new PlaceholderMetadata(displayName, description);
}
}
@@ -1,7 +1,7 @@
package io.github.miniplaceholders.api.placeholder;
import io.github.miniplaceholders.api.types.RelationalAudience;
import io.github.miniplaceholders.api.resolver.RelationalTagResolver;
import io.github.miniplaceholders.api.types.RelationalAudience;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience;
import net.kyori.adventure.pointer.Pointered;
@@ -9,11 +9,15 @@ import net.kyori.adventure.text.minimessage.Context;
import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.TagPattern;
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;
import org.intellij.lang.annotations.Subst;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
import java.util.Objects;
import java.util.function.Predicate;
import static java.util.Objects.requireNonNull;
@@ -24,45 +28,64 @@ import static java.util.Objects.requireNonNull;
* using a type-safe filter and a relational resolver.
*
* @param targetClass the target class, can be null
* @param key the key of the entire placeholder
* @param name the name of this placeholder
* @param resolver the relational resolver,
* in charge of obtaining data based on a relationship of 2 audiences
* @param <A> the audience type, if not specified, will be a generic {@linkplain Audience}
* @see RelationalTagResolver
* @param key the key of the entire placeholder
* @param name the name of this placeholder
* @param resolver the relational resolver,
* in charge of obtaining data based on a relationship of 2 audiences
* @param <A> the audience type, if not specified, will be a generic {@linkplain Audience}
* @apiNote The {@link #key()} method returns the full name of the placeholder,
* for example, if the placeholder is {@code <server_name>},
* the {@linkplain #key()} method will return {@code server_name}.
* @see RelationalTagResolver
*/
@NullMarked
public record RelationalPlaceholder<A extends Audience>(
@Nullable Class<A> targetClass,
@TagPattern String key,
String name,
RelationalTagResolver<A> resolver
@Nullable Class<A> targetClass,
@Nullable Predicate<A> targetFilter,
@TagPattern String key,
@TagPattern String name,
RelationalTagResolver<A> resolver,
@Nullable PlaceholderMetadata placeholderMetadata
) implements Placeholder {
public RelationalPlaceholder(
@Nullable Class<A> targetClass,
@TagPattern String key,
@TagPattern String name,
RelationalTagResolver<A> resolver
) {
this(targetClass, null, key, name, resolver, null);
}
/**
* Static method capable of creating an instance of a relational placeholder.
*
* @param targetClass the target class, can be null
* @param key the key of the entire placeholder
* @param name the name of this placeholder
* @param resolver the relational resolver,
* in charge of obtaining data based on a relationship of 2 audiences
* @param key the key of the entire placeholder
* @param name the name of this placeholder
* @param resolver the relational resolver,
* in charge of obtaining data based on a relationship of 2 audiences
* @param <A> the audience type, if not specified, will be a generic {@linkplain Audience}
* @return a new RelationalPlaceholder
* @param <A> the audience type, if not specified, will be a generic {@linkplain Audience}
* @since 3.0.0
*/
public static <A extends Audience> RelationalPlaceholder<A> relational(
@Nullable Class<A> targetClass,
@TagPattern String key,
String name,
RelationalTagResolver<A> resolver
@Nullable Class<A> targetClass,
@TagPattern String key,
@TagPattern String name,
RelationalTagResolver<A> resolver
) {
return new RelationalPlaceholder<>(targetClass, requireNonNull(key), name, requireNonNull(resolver));
}
public static <A extends Audience> Builder<A> builder(Class<A> targetClazz) {
return new Builder<>(targetClazz);
}
public static Builder<Audience> builder() {
return new Builder<>(Audience.class);
}
@Override
public @Nullable Tag resolve(String name, ArgumentQueue arguments, Context ctx) {
final Pointered targetRaw = ctx.target();
@@ -74,7 +97,7 @@ public record RelationalPlaceholder<A extends Audience>(
}
if (targetClass == null) {
//noinspection unchecked
return this.resolveA(name, (A)a, (A)b, arguments, ctx);
return this.resolveA(name, (A) a, (A) b, arguments, ctx);
}
final @Nullable A audience = forwardingFilter(a);
if (audience == null) {
@@ -84,19 +107,22 @@ public record RelationalPlaceholder<A extends Audience>(
if (relationalAudience == null) {
return null;
}
if (targetFilter != null && (!targetFilter.test(audience) || !targetFilter.test(relationalAudience))) {
return null;
}
return this.resolveA(name, audience, relationalAudience, arguments, ctx);
}
private @Nullable Tag resolveA(
String name,
A audience,
A relationalAudience,
ArgumentQueue arguments,
Context ctx
String name,
A audience,
A relationalAudience,
ArgumentQueue arguments,
Context ctx
) {
return this.has(name)
? resolver.tag(audience, relationalAudience, arguments, ctx)
: null;
? resolver.tag(audience, relationalAudience, arguments, ctx)
: null;
}
@SuppressWarnings("OverrideOnly")
@@ -112,7 +138,7 @@ public record RelationalPlaceholder<A extends Audience>(
}
@Override
public boolean has(@NotNull String name) {
public boolean has(String name) {
return key.equalsIgnoreCase(name);
}
@@ -131,7 +157,62 @@ public record RelationalPlaceholder<A extends Audience>(
@Override
public String toString() {
return "RelationalPlaceholder{" +
"key='" + key + '\'' +
'}';
"key='" + key + '\'' +
'}';
}
@NullUnmarked
public static final class Builder<A extends Audience> {
@NotNull
private final Class<A> targetClass;
@TagPattern
private String name;
@Nullable
private Predicate<A> targetFilter;
private RelationalTagResolver<@NotNull A> resolver;
@Nullable
private PlaceholderMetadata metadata;
Builder(final @NotNull Class<A> targetClass) {
this.targetClass = targetClass;
}
public Builder<A> name(final @NonNull @TagPattern String name) {
this.name = requireNonNull(name, "placeholder name");
return this;
}
public Builder<A> targetFilter(final Predicate<A> targetFilter) {
this.targetFilter = targetFilter;
return this;
}
public Builder<A> resolver(final @NotNull RelationalTagResolver<@NotNull A> resolver) {
this.resolver = requireNonNull(resolver);
return this;
}
public Builder<A> metadata(final PlaceholderMetadata metadata) {
this.metadata = metadata;
return this;
}
public @NonNull RelationalPlaceholder<@NonNull A> build(final @NotNull @Subst("expansion") String expansionPrefix) {
requireNonNull(this.name, "name");
requireNonNull(this.resolver, "resolver");
return new RelationalPlaceholder<>(
targetClass,
targetFilter,
expansionPrefix + name,
name,
resolver,
metadata
);
}
@FunctionalInterface
public interface Provider<A extends Audience> {
void provide(final Builder<A> builderProvider);
}
}
}
@@ -1,7 +1,7 @@
package io.github.miniplaceholders.api.provider;
import io.github.miniplaceholders.api.types.Platform;
import net.kyori.examination.Examinable;
import org.intellij.lang.annotations.Pattern;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
@@ -14,125 +14,203 @@ import static java.util.Objects.requireNonNull;
/**
* This class specifies the conditions that must be met
* in order to load an {@link io.github.miniplaceholders.api.Expansion} from an {@link ExpansionProvider}.
*
* @since 3.0.0
*/
@NullMarked
public sealed interface LoadRequirement {
/**
* It means that there is no requirement to load an expansion.
*
* @return a LoadRequirement that does not perform any validation
* @since 3.0.0
*/
static LoadRequirement none() {
return NoneLoadRequirement.INSTANCE;
}
/**
* Requirement for a complement, whether a plugin or mod, to be present in order for an expansion to be loaded.<br>
* The ability to provide multiple strings is provided because names may vary on some platforms.
* For example, Velocity requires plugin IDs to be lowercase, such as {@code miniplaceholders},
* while Paper can contain uppercase letters, such as {@code MiniPlaceholders}.
*
* @param name the principal name of this complement
* @param aliases the platform aliases of a complement
* @return a load requirement that checks for an available complement
* @since 3.0.0
*/
static LoadRequirement requiredComplement(final String name, final String... aliases) {
requireNonNull(name);
requireNonNull(aliases);
return new AvailableComplementRequirement(name, aliases);
}
/**
* Multiple requirement, in case an expansion needs to depend on a platform
* as well as one or more add-ons that are installed.
*
* @param requirements the load requirements
* @return a load requirement of many load requirements
* @since 3.0.0
*/
static LoadRequirement allOf(LoadRequirement... requirements) {
requireNonNull(requirements);
assert requirements.length != 0;
return new MultiLoadRequirement(requirements);
}
/**
* Requirement that the current execution be on a specific platform.
*
* @param platforms the supported platforms
* @return a load requirement that checks for a supported platform
* @since 3.0.0
*/
static LoadRequirement platform(final Platform... platforms) {
requireNonNull(platforms);
assert platforms.length != 0;
return new PlatformRequirement(platforms);
}
/**
* Requirement that a specific class be present.
*
* @param clazz the specified class.
* @return a load requirement that checks for a present class
* @since 3.1.0
*/
static LoadRequirement requiredClass(
@Pattern("^([a-z_][a-z0-9_]*)(\\.[a-z_][a-z0-9_]*)*(\\.[A-Z][A-Za-z0-9_]*)$") final String clazz
) {
requireNonNull(clazz);
return new ClassRequirement(clazz);
}
/**
* This requirement checks whether any of the conditions presented are satisfied.
*
* @param requirements the requerimients
* @return a requirement that checks whether a condition is valid
* @since 3.1.0
*/
static LoadRequirement anyOf(LoadRequirement... requirements) {
requireNonNull(requirements);
assert requirements.length != 0;
return new AnyOfRequirement(requirements);
}
/**
* NoneLoadRequirement
*
* @since 3.0.0
*/
@ApiStatus.Internal
enum NoneLoadRequirement implements LoadRequirement {
/**
* It means that there is no requirement to load an expansion.
* @return a LoadRequirement that does not perform any validation
* NoneLoadRequirement instance
*/
static LoadRequirement none() {
return NoneLoadRequirement.INSTANCE;
}
INSTANCE
}
/**
* AvailableComplementRequirement
*
* @param name the main complement name
* @param platformAliases the complement aliases
* @since 3.0.0
*/
@ApiStatus.Internal
@NullMarked
record AvailableComplementRequirement(String name, String... platformAliases) implements LoadRequirement {
/**
* Requirement for a complement, whether a plugin or mod, to be present in order for an expansion to be loaded.<br>
* The ability to provide multiple strings is provided because names may vary on some platforms.
* For example, Velocity requires plugin IDs to be lowercase, such as {@code miniplaceholders},
* while Paper can contain uppercase letters, such as {@code MiniPlaceholders}.
* @param name the principal name of this complement
* @param aliases the platform aliases of a complement
* @return a load requirement that checks for an available complement
*/
static LoadRequirement requiredComplement(final String name, String... aliases) {
requireNonNull(name);
return new AvailableComplementRequirement(name, aliases);
}
/**
* Multiple requirement, in case an expansion needs to depend on a platform
* as well as one or more add-ons that are installed.
* Checks if a complement is loaded
*
* @param requirements the load requirements
* @return a load requirement of many load requirements
* @param platformComplementTester the checker predicate
* @return is this expansion should load
* @since 3.0.0
*/
static LoadRequirement allOf(LoadRequirement... requirements) {
requireNonNull(requirements);
assert requirements.length != 0;
return new MultiLoadRequirement(requirements);
}
/**
* Requirement that the current execution be on a specific platform.
* @param platforms the supported platforms
* @return a load requirement that checks for a supported platform
*/
static LoadRequirement platform(Platform... platforms) {
requireNonNull(platforms);
assert platforms.length != 0;
return new PlatformRequirement(platforms);
}
/**
* NoneLoadRequirement
*/
@ApiStatus.Internal
enum NoneLoadRequirement implements LoadRequirement {
/**
* NoneLoadRequirement instance
*/
INSTANCE
}
/**
* AvailableComplementRequirement
* @param name the main complement name
* @param platformAliases the complement aliases
*/
@ApiStatus.Internal
@NullMarked
record AvailableComplementRequirement(String name, String... platformAliases) implements LoadRequirement {
/**
* Checks if a complement is loaded
* @param platformComplementTester the checker predicate
* @return is this expansion should load
*/
public boolean shouldLoad(Predicate<String> platformComplementTester) {
if (platformComplementTester.test(name)) {
return true;
}
for (String alias : platformAliases) {
if (platformComplementTester.test(alias)) {
return true;
}
}
return false;
}
@Override
public String toString() {
return "AvailableComplementRequirement{" +
"complement='" + name + '\'' +
'}';
public boolean shouldLoad(final Predicate<String> platformComplementTester) {
if (platformComplementTester.test(name)) {
return true;
}
for (String alias : platformAliases) {
if (platformComplementTester.test(alias)) {
return true;
}
}
return false;
}
/**
* MultiLoadRequirement
* @param requirements the load requirements
*/
@ApiStatus.Internal
@NullMarked
record MultiLoadRequirement(LoadRequirement... requirements) implements LoadRequirement {
@Override
public String toString() {
return "AvailableComplementRequirement{" +
"complement='" + name + '\'' +
'}';
}
}
/**
* PlatformLoadRequirement
* @param platforms the platforms supported
*/
@ApiStatus.Internal
@NullMarked
record PlatformRequirement(Platform... platforms) implements LoadRequirement, Examinable {
@Override
public String toString() {
final String platforms = Stream.of(this.platforms)
.map(Platform::name)
.collect(Collectors.joining(", "));
return "PlatformRequirement{" +
"platforms=" + platforms +
'}';
}
/**
* MultiLoadRequirement
*
* @param requirements the load requirements
* @since 3.0.0
*/
@ApiStatus.Internal
@NullMarked
record MultiLoadRequirement(LoadRequirement... requirements) implements LoadRequirement {
}
/**
* PlatformLoadRequirement
*
* @param platforms the platforms supported
* @since 3.0.0
*/
@ApiStatus.Internal
@NullMarked
record PlatformRequirement(Platform... platforms) implements LoadRequirement {
@Override
public String toString() {
final String platforms = Stream.of(this.platforms)
.map(Platform::name)
.collect(Collectors.joining(", "));
return "PlatformRequirement{" +
"platforms=" + platforms +
'}';
}
}
/**
* ClassRequirement
*
* @param clazz a specified class
* @since 3.1.0
*/
@NullMarked
@ApiStatus.Internal
record ClassRequirement(String clazz) implements LoadRequirement {
public boolean shouldLoad() {
try {
Class.forName(clazz);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
/**
* AnyOfLoadRequirement
*
* @param requirements the requirements
* @since 3.1.0
*/
@ApiStatus.Internal
@NullMarked
record AnyOfRequirement(LoadRequirement... requirements) implements LoadRequirement {
}
}
@@ -4,26 +4,32 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience;
import org.jspecify.annotations.NullMarked;
import static java.util.Objects.requireNonNull;
/**
* A relationship between 2 Audiences.
*
* @param audience the principal audience
* @param audience the principal audience
* @param relational the secondary audience, with which the relationship will be evaluated
* @param <A> the audiences type
* @param <A> the audiences type
* @since 3.0.0
*/
@NullMarked
public record RelationalAudience<A extends Audience>(A audience, A relational) implements ForwardingAudience.Single {
/**
* Creates a new relation between two audiences.
*
* @param audience the principal audience
* @param relational the secondary audience, with which the relationship will be evaluated
* @return a new relation
* @param <A> the audiences type
* @since 3.0.0
*/
public static <A extends Audience> RelationalAudience<A> from(A audience, A relational) {
return new RelationalAudience<>(audience, relational);
}
public RelationalAudience {
requireNonNull(audience);
requireNonNull(relational);
}
/**
* Creates a new relation between two audiences.
*
* @param audience the principal audience
* @param relational the secondary audience, with which the relationship will be evaluated
* @param <A> the audiences type
* @return a new relation
* @since 3.0.0
*/
public static <A extends Audience> RelationalAudience<A> from(A audience, A relational) {
return new RelationalAudience<>(audience, relational);
}
}
@@ -0,0 +1,32 @@
package io.github.miniplaceholders.test;
import io.github.miniplaceholders.api.Expansion;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.tag.Tag;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ArgumentTest implements MiniTest {
@Test
@DisplayName("Empty Arguments")
void testEmptyArguments() {
final Expansion expansion = Expansion.builder("test")
.globalPlaceholder("testing", (queue, ctx) -> {
int arguments = 0;
while (queue.hasNext()) {
arguments++;
Tag.Argument argument = queue.pop();
assertTrue(argument.value().isBlank());
}
return Tag.selfClosingInserting(Component.text("Arguments: " + arguments));
})
.build();
Component parsed = miniMessage().deserialize("<test_testing::>", expansion.globalPlaceholders());
assertContentEquals(parsed, Component.text("Arguments: 2"));
}
}
@@ -14,13 +14,13 @@ import static org.junit.jupiter.api.Assertions.*;
class ExpansionsTest implements MiniTest {
@Test
@DisplayName("Expansion Equality")
void equalsExpansions(){
void equalsExpansions() {
final Expansion firstExpansion = Expansion.builder("equals")
.audiencePlaceholder("test", Tags.emptyAudienceResolver())
.build();
.audiencePlaceholder("test", Tags.emptyAudienceResolver())
.build();
final Expansion secondExpansion = Expansion.builder("equals")
.audiencePlaceholder("test", Tags.emptyAudienceResolver())
.build();
.audiencePlaceholder("test", Tags.emptyAudienceResolver())
.build();
assertEquals(firstExpansion, secondExpansion);
}
@@ -1,83 +1,22 @@
package io.github.miniplaceholders.test;
import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage;
import static org.junit.jupiter.api.Assertions.*;
import io.github.miniplaceholders.api.types.RelationalAudience;
import io.github.miniplaceholders.test.instances.TestAudience;
import io.github.miniplaceholders.test.instances.TestAudienceHolder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import io.github.miniplaceholders.api.Expansion;
import io.github.miniplaceholders.api.MiniPlaceholders;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.Tag;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class PlaceholderTest implements MiniTest {
@Test
@DisplayName("Audience Placeholder Test")
void audienceExpansionPlaceholdersTest() {
TestAudience player = new TestAudience("4drian3d");
Expansion expansion = Expansion.builder("example")
.audiencePlaceholder("name", (aud, queue, ctx) -> {
TestAudience audience = (TestAudience) aud;
return Tag.selfClosingInserting(Component.text(audience.name()));
}).build();
final Component expected = Component.text("Player Name: 4drian3d");
final Component result = miniMessage().deserialize("Player Name: <example_name>", player, expansion.audiencePlaceholders());
assertContentEquals(expected, result);
}
@Test
@DisplayName("Relational Placeholder Test")
void relationalExpansionPlaceholdersTest() {
Audience p1 = new TestAudience("PlayerOne");
Audience p2 = new TestAudience("PlayerTwo");
Expansion expansion = Expansion.builder("relational")
.relationalPlaceholder(TestAudience.class, "enemy",
(aud, relational, queue, ctx) ->
Tag.selfClosingInserting(this.isEnemy(aud, relational)
? Component.text("Enemy", NamedTextColor.RED)
: Component.text("Neutral", NamedTextColor.GREEN)
))
.build();
assertEquals(
MiniMessage.miniMessage().deserialize("You are <red>Enemy"),
MiniMessage.miniMessage().deserialize("You are <relational_rel_enemy>",
RelationalAudience.from(p1, p2),
expansion.relationalPlaceholders()));
}
@Test
@DisplayName("Global Placeholder Test")
void globalExpansionPlaceholderTest() {
Expansion expansion = Expansion.builder("global")
.globalPlaceholder("players", (queue, ctx) -> Tag.selfClosingInserting(Component.text(1305)))
.globalPlaceholder("servers", (queue, ctx) -> Tag.selfClosingInserting(Component.text(7)))
.build();
final Component expected = Component.text("Online players: 1305 | Servers: 7");
final Component actual = MiniMessage.miniMessage().deserialize("Online players: <global_players> | Servers: <global_servers>",
expansion.globalPlaceholders());
assertContentEquals(expected, actual);
}
@Test
@DisplayName("Registered Placeholders")
void instancePlaceholdersTest() {
Expansion.builder("instance")
.globalPlaceholder("hello", (queue, ctx) -> Tag.selfClosingInserting(Component.text("hello", NamedTextColor.RED)))
.globalPlaceholder("hello", (queue, ctx) ->
Tag.selfClosingInserting(Component.text("hello", NamedTextColor.RED)))
.build()
.register();
@@ -87,65 +26,4 @@ class PlaceholderTest implements MiniTest {
assertContentEquals(expected, actual);
}
@Test
@DisplayName("Filtered Expansion")
void filteredExpansion() {
Expansion expansion = Expansion.builder("filter")
.audiencePlaceholder(TestAudience.class, "name",
(aud, queue, ctx) -> Tag.selfClosingInserting(Component.text(aud.name())))
.build();
Audience player = new TestAudience("TestPlayer04");
Audience emptyAudience = Audience.empty();
String string = "Player Name: <filter_name>";
Component playerExpected = Component.text("Player Name: TestPlayer04");
Component emptyExpected = Component.text("Player Name: <filter_name>");
assertContentEquals(playerExpected, MiniMessage.miniMessage().deserialize(string, player, expansion.audiencePlaceholders()));
assertContentEquals(emptyExpected, MiniMessage.miniMessage().deserialize(string, emptyAudience, expansion.audiencePlaceholders()));
}
@Test
@DisplayName("Empty Arguments")
void testEmptyArguments() {
final Expansion expansion = Expansion.builder("test")
.globalPlaceholder("testing", (queue, ctx) -> {
int arguments = 0;
while (queue.hasNext()) {
arguments++;
Tag.Argument argument = queue.pop();
assertTrue(argument.value().isBlank());
}
return Tag.selfClosingInserting(Component.text("Arguments: " + arguments));
})
.build();
Component parsed = miniMessage().deserialize("<test_testing::>", expansion.globalPlaceholders());
assertContentEquals(parsed, Component.text("Arguments: 2"));
}
@Test
@DisplayName("Forwarding Audience Parsing Test")
void forwardingAudienceTest() {
Expansion expansion = Expansion.builder("test")
.audiencePlaceholder(TestAudience.class, "testing",
(audience, queue, ctx) -> Tag.selfClosingInserting(Component.text("Name: " + audience.name())))
.build();
TestAudience testAudience = new TestAudience("4drian3d");
Audience forward = new TestAudienceHolder(testAudience);
Component parsed = miniMessage().deserialize("<test_testing>.", forward, expansion.audiencePlaceholders());
Component expected = Component.text("Name: 4drian3d.");
assertContentEquals(parsed, expected);
}
private boolean isEnemy(TestAudience a1, TestAudience a2) {
return a1.name().equals("PlayerOne") && a2.name().equals("PlayerTwo");
}
}
@@ -0,0 +1,93 @@
package io.github.miniplaceholders.test.placeholder;
import io.github.miniplaceholders.api.Expansion;
import io.github.miniplaceholders.api.placeholder.PlaceholderMetadata;
import io.github.miniplaceholders.test.MiniTest;
import io.github.miniplaceholders.test.instances.TestAudience;
import io.github.miniplaceholders.test.instances.TestAudienceHolder;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.Tag;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage;
public class AudiencePlaceholderTest implements MiniTest {
@Test
@DisplayName("Audience Placeholder Test")
void audienceExpansionPlaceholdersTest() {
TestAudience player = new TestAudience("4drian3d");
Expansion expansion = Expansion.builder("example")
.audiencePlaceholder("name", (aud, queue, ctx) -> {
TestAudience audience = (TestAudience) aud;
return Tag.selfClosingInserting(Component.text(audience.name()));
}).build();
final Component expected = Component.text("Player Name: 4drian3d");
final Component result = miniMessage().deserialize("Player Name: <example_name>", player, expansion.audiencePlaceholders());
assertContentEquals(expected, result);
}
@Test
@DisplayName("Forwarding Audience Parsing Test")
void forwardingAudienceTest() {
Expansion expansion = Expansion.builder("test")
.audiencePlaceholder(TestAudience.class, "testing",
(audience, queue, ctx) -> Tag.selfClosingInserting(Component.text("Name: " + audience.name())))
.build();
TestAudience testAudience = new TestAudience("4drian3d");
Audience forward = new TestAudienceHolder(testAudience);
Component parsed = miniMessage().deserialize("<test_testing>.", forward, expansion.audiencePlaceholders());
Component expected = Component.text("Name: 4drian3d.");
assertContentEquals(parsed, expected);
}
@Test
@DisplayName("Filtered Expansion")
void filteredExpansion() {
Expansion expansion = Expansion.builder("filter")
.audiencePlaceholder(TestAudience.class, "name",
(aud, queue, ctx) -> Tag.selfClosingInserting(Component.text(aud.name())))
.build();
Audience player = new TestAudience("TestPlayer04");
Audience emptyAudience = Audience.empty();
String string = "Player Name: <filter_name>";
Component playerExpected = Component.text("Player Name: TestPlayer04");
Component emptyExpected = Component.text("Player Name: <filter_name>");
assertContentEquals(playerExpected, MiniMessage.miniMessage().deserialize(string, player, expansion.audiencePlaceholders()));
assertContentEquals(emptyExpected, MiniMessage.miniMessage().deserialize(string, emptyAudience, expansion.audiencePlaceholders()));
}
@Test
@DisplayName("AudiencePlaceholder Builder creation")
void builderAudiencePlaceholderCreation() {
final Expansion expansion = Expansion.builder("builder")
.audiencePlaceholder(
TestAudience.class,
builder -> builder.name("test")
.resolver((audience, queue, ctx) -> {
final String name = audience.name();
return Tag.preProcessParsed(name);
})
.metadata(PlaceholderMetadata.data("Builder Test", "Returns the Audience Name"))
.targetFilter(target -> target.name().length() <= 16)
).build();
final TestAudience audience = new TestAudience("4drian3d");
Component expected = Component.text("My name is: 4drian3d");
Component parsed = miniMessage().deserialize("My name is: <builder_test>", audience, expansion.audiencePlaceholders());
assertContentEquals(expected, parsed);
}
}
@@ -0,0 +1,26 @@
package io.github.miniplaceholders.test.placeholder;
import io.github.miniplaceholders.api.Expansion;
import io.github.miniplaceholders.test.MiniTest;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.Tag;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class GlobalPlaceholderTest implements MiniTest {
@Test
@DisplayName("Global Placeholder Test")
void globalExpansionPlaceholderTest() {
Expansion expansion = Expansion.builder("global")
.globalPlaceholder("players", (queue, ctx) -> Tag.selfClosingInserting(Component.text(1305)))
.globalPlaceholder("servers", (queue, ctx) -> Tag.selfClosingInserting(Component.text(7)))
.build();
final Component expected = Component.text("Online players: 1305 | Servers: 7");
final Component actual = MiniMessage.miniMessage().deserialize("Online players: <global_players> | Servers: <global_servers>",
expansion.globalPlaceholders());
assertContentEquals(expected, actual);
}
}
@@ -0,0 +1,117 @@
package io.github.miniplaceholders.test.placeholder;
import io.github.miniplaceholders.api.Expansion;
import io.github.miniplaceholders.api.placeholder.PlaceholderMetadata;
import io.github.miniplaceholders.api.types.RelationalAudience;
import io.github.miniplaceholders.test.MiniTest;
import io.github.miniplaceholders.test.instances.TestAudience;
import io.github.miniplaceholders.test.instances.TestAudienceHolder;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.Tag;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class RelationalPlaceholderTest implements MiniTest {
@Test
@DisplayName("Relational Placeholder Test")
void relationalExpansionPlaceholdersTest() {
Audience p1 = new TestAudience("PlayerOne");
Audience p2 = new TestAudience("PlayerTwo");
Expansion expansion = Expansion.builder("relational")
.relationalPlaceholder(TestAudience.class, "enemy",
(aud, relational, queue, ctx) ->
Tag.selfClosingInserting(this.isEnemy(aud, relational)
? Component.text("Enemy", NamedTextColor.RED)
: Component.text("Neutral", NamedTextColor.GREEN)
))
.build();
assertEquals(
MiniMessage.miniMessage().deserialize("You are <red>Enemy"),
MiniMessage.miniMessage().deserialize("You are <relational_rel_enemy>",
RelationalAudience.from(p1, p2),
expansion.relationalPlaceholders()));
}
private boolean isEnemy(TestAudience a1, TestAudience a2) {
return a1.name().equals("PlayerOne") && a2.name().equals("PlayerTwo");
}
@Test
@DisplayName("Relational Forwarding Audience Parsing Test")
void relationalForwardingAudienceTest() {
Expansion expansion = Expansion.builder("test")
.relationalPlaceholder(TestAudience.class, "testing",
(audience, another, queue, ctx) ->
Tag.selfClosingInserting(Component.text("Name: " + audience.name() + ", Other Name: " + another.name())))
.build();
TestAudience testAudience = new TestAudience("4drian3d");
TestAudience secondAudience = new TestAudience("PepitoAlcachofa");
Audience forward = new TestAudienceHolder(testAudience);
Audience secondForward = new TestAudienceHolder(secondAudience);
Component parsed = miniMessage().deserialize("<test_rel_testing>.",
RelationalAudience.from(forward, secondForward), expansion.relationalPlaceholders());
Component expected = Component.text("Name: 4drian3d, Other Name: PepitoAlcachofa.");
assertContentEquals(expected, parsed);
}
@Test
@DisplayName("Relational Filtered Expansion")
void relationalFilteredExpansion() {
Expansion expansion = Expansion.builder("filter")
.relationalPlaceholder(TestAudience.class, "name",
(aud, another, queue, ctx) ->
Tag.selfClosingInserting(Component.text(aud.name().replace(another.name(), ""))))
.build();
Audience player = new TestAudience("TestPlayer04");
Audience second = new TestAudience("Player");
Audience emptyAudience = Audience.empty();
String string = "Player Name: <filter_rel_name>";
Component playerExpected = Component.text("Player Name: Test04");
Component emptyExpected = Component.text("Player Name: <filter_rel_name>");
assertContentEquals(playerExpected, MiniMessage.miniMessage().deserialize(string,
RelationalAudience.from(player, second), expansion.relationalPlaceholders()));
assertContentEquals(emptyExpected, MiniMessage.miniMessage().deserialize(string,
RelationalAudience.from(player, emptyAudience), expansion.relationalPlaceholders()));
}
@Test
@DisplayName("RelationalPlaceholder Builder creation")
void builderRelationalPlaceholderCreation() {
final Expansion expansion = Expansion.builder("builder")
.relationalPlaceholder(
TestAudience.class,
builder -> builder.name("test")
.resolver((audience, relational, queue, ctx) -> {
final String name = audience.name();
final String relationalName = relational.name();
return Tag.preProcessParsed(name + " and " + relationalName);
})
.metadata(PlaceholderMetadata.data("Builder Test", "Returns the Audience plus the Relational Audience combined Names"))
.targetFilter(target -> target.name().length() <= 16)
).build();
final TestAudience audience = new TestAudience("4drian3d");
final TestAudience relational = new TestAudience("PepitoAlcachofa");
final RelationalAudience<@NotNull TestAudience> relationalAudience = RelationalAudience.from(audience, relational);
Component expected = Component.text("Our names are: 4drian3d and PepitoAlcachofa");
Component parsed = miniMessage().deserialize("Our names are: <builder_test>", relationalAudience, expansion.relationalPlaceholders());
assertContentEquals(expected, parsed);
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
plugins {
kotlin("jvm") version "2.2.20-RC" apply false
kotlin("jvm") version "2.2.20" apply false
}
tasks {
+2 -4
View File
@@ -9,13 +9,11 @@ dependencies {
implementation(libs.desertwell) {
exclude("org.json")
}
implementation(libs.cloud.core)
implementation(libs.unnamedinject)
implementation(libs.cloud.extras) {
isTransitive = false
}
compileOnly(libs.adventure.api)
compileOnly(libs.adventure.minimessage)
compileOnly(libs.jspecify)
compileOnly(libs.brigadier)
}
sourceSets {
@@ -10,82 +10,126 @@ import io.github.miniplaceholders.common.loader.ExpansionProviderLoader;
import io.github.miniplaceholders.common.loader.FailedToLoadExpansion;
import io.github.miniplaceholders.common.loader.ProviderLoadResult;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import team.unnamed.inject.Injector;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public interface PlaceholdersPlugin {
default void registerPlatformCommand() {
default void registerPlatformCommand() {
}
default void loadProvidedExpansions(Path providersFolderDirectory) throws Exception {
final List<Expansion> loadedExpansions = new ArrayList<>();
final PlatformData platformData = new PlatformData(this.platformServerInstance(), this);
final Injector injector = Injector.create(binder -> binder.bind(PlatformData.class).toInstance(platformData));
for (ExpansionProvider provider : ExpansionProviderLoader.loadProvidersFromFolder(providersFolderDirectory)) {
injector.injectMembers(provider);
ProviderLoadResult loadResult = tryLoad(provider, provider.loadRequirement());
final ExpansionProvider loadedProvider = loadResult.provider();
if (loadedProvider != null) {
final Expansion loadedExpansion = loadedProvider.provideExpansion();
loadedExpansions.add(loadedExpansion);
loadedExpansion.register();
continue;
}
final FailedToLoadExpansion failedToLoad = loadResult.failed();
logError(Component.text("The expansion "
+ provider.getClass() +
" could not be loaded because the requirement " + failedToLoad.requirement() + " could not be resolved",
NamedTextColor.RED));
}
default void loadProvidedExpansions(Path providersFolderDirectory) throws Exception {
final List<Expansion> loadedExpansions = new ArrayList<>();
final PlatformData platformData = new PlatformData(this.platformServerInstance(), this);
final Injector injector = Injector.create(binder -> binder.bind(PlatformData.class).toInstance(platformData));
for (ExpansionProvider provider : ExpansionProviderLoader.loadProvidersFromFolder(providersFolderDirectory)) {
injector.injectMembers(provider);
ProviderLoadResult loadResult = tryLoad(provider, provider.loadRequirement());
final Expansion loadedExpansion = loadResult.expansion();
if (loadedExpansion != null) {
loadedExpansions.add(loadedExpansion);
loadedExpansion.register();
continue;
}
final FailedToLoadExpansion failedToLoad = loadResult.failed();
logError(Component.text("The expansion "
+ provider.getClass() +
" could not be loaded because the requirement " + failedToLoad.requirement() + " could not be resolved",
NamedTextColor.RED));
if (loadedExpansions.isEmpty()) {
logInfo(Component.text("Not found expansions", NamedTextColor.GRAY));
return;
}
final Component expansionsInfo = loadedExpansions.stream()
.map(this::expansionInfo)
.collect(Component.toComponent(Component.text(", ", NamedTextColor.WHITE)));
logInfo(Component.text("Loaded expansions: ", NamedTextColor.GREEN).append(expansionsInfo));
}
private Component expansionInfo(Expansion expansion) {
final TextComponent.Builder builder = Component.text();
builder.append(Component.text(expansion.name(), NamedTextColor.AQUA));
final String author = expansion.author();
final String version = expansion.version();
if (author == null && version == null) {
return builder.build();
}
builder.append(Component.text('[', NamedTextColor.DARK_GRAY));
if (author != null) {
builder.append(
Component.text("Author", NamedTextColor.GRAY),
Component.text(':', NamedTextColor.WHITE),
Component.space(),
Component.text(author, TextColor.color(0xd4ff54))
);
}
if (version != null) {
if (author != null) {
builder.append(Component.text(", ", NamedTextColor.WHITE));
}
builder.append(
Component.text("Version", NamedTextColor.GRAY),
Component.text(':', NamedTextColor.WHITE),
Component.space(),
Component.text(version, TextColor.color(0xd4ff54))
);
}
builder.append(Component.text(']', NamedTextColor.DARK_GRAY));
return builder.build();
}
default ProviderLoadResult tryLoad(final ExpansionProvider provider, final LoadRequirement loadRequirement) {
return switch (loadRequirement) {
case LoadRequirement.PlatformRequirement platformRequirement -> {
for (final Platform supportedPlatform : platformRequirement.platforms()) {
if (supportedPlatform == MiniPlaceholders.platform()) {
yield ProviderLoadResult.ofLoaded(provider);
}
}
if (loadedExpansions.isEmpty()) {
logInfo(Component.text("Not found expansions", NamedTextColor.GRAY));
return;
yield ProviderLoadResult.ofFailed(platformRequirement);
}
case LoadRequirement.AvailableComplementRequirement complementRequirement ->
complementRequirement.shouldLoad(this::platformHasComplementLoaded)
? ProviderLoadResult.ofLoaded(provider)
: ProviderLoadResult.ofFailed(complementRequirement);
case LoadRequirement.MultiLoadRequirement multiLoadRequirement -> {
ProviderLoadResult loadResult;
for (final LoadRequirement requirement : multiLoadRequirement.requirements()) {
if ((loadResult = this.tryLoad(provider, requirement)).failed() != null) {
yield loadResult;
}
}
yield ProviderLoadResult.ofLoaded(provider);
}
case LoadRequirement.ClassRequirement classRequirement -> classRequirement.shouldLoad()
? ProviderLoadResult.ofLoaded(provider) : ProviderLoadResult.ofFailed(classRequirement);
case LoadRequirement.AnyOfRequirement anyOfRequirement -> {
ProviderLoadResult loadResult;
for (final LoadRequirement requirement : anyOfRequirement.requirements()) {
if ((loadResult = this.tryLoad(provider, requirement)).provider() != null) {
yield loadResult;
}
}
yield ProviderLoadResult.ofFailed(anyOfRequirement);
}
case LoadRequirement.NoneLoadRequirement ignored -> ProviderLoadResult.ofLoaded(provider);
};
}
final String expansionsInfo = loadedExpansions.stream()
.map(Expansion::shortToString)
.collect(Collectors.joining(","));
logInfo(Component.text("Loaded expansions: " + expansionsInfo, NamedTextColor.GREEN));
}
boolean platformHasComplementLoaded(String complementName);
default ProviderLoadResult tryLoad(final ExpansionProvider provider, final LoadRequirement loadRequirement) {
return switch (loadRequirement) {
case LoadRequirement.PlatformRequirement platformRequirement -> {
for (final Platform supportedPlatform : platformRequirement.platforms()) {
if (supportedPlatform == MiniPlaceholders.platform()) {
yield ProviderLoadResult.ofLoaded(provider.provideExpansion());
}
}
yield ProviderLoadResult.ofFailed(platformRequirement);
}
case LoadRequirement.AvailableComplementRequirement complementRequirement ->
complementRequirement.shouldLoad(this::platformHasComplementLoaded)
? ProviderLoadResult.ofLoaded(provider.provideExpansion())
: ProviderLoadResult.ofFailed(complementRequirement);
case LoadRequirement.MultiLoadRequirement multiLoadRequirement -> {
ProviderLoadResult loadResult;
for (final LoadRequirement requirement : multiLoadRequirement.requirements()) {
if ((loadResult = this.tryLoad(provider, requirement)).failed() != null) {
yield loadResult;
}
}
yield ProviderLoadResult.ofLoaded(provider.provideExpansion());
}
// NoneRequirement
default -> ProviderLoadResult.ofLoaded(provider.provideExpansion());
};
}
void logError(Component component);
boolean platformHasComplementLoaded(String complementName);
void logInfo(Component component);
void logError(Component component);
void logInfo(Component component);
Object platformServerInstance();
Object platformServerInstance();
}
@@ -0,0 +1,10 @@
package io.github.miniplaceholders.common.command;
import net.kyori.adventure.audience.Audience;
import org.jspecify.annotations.NullMarked;
@NullMarked
@FunctionalInterface
public interface AudienceExtractor<S> {
Audience extract(S source);
}
@@ -0,0 +1,104 @@
package io.github.miniplaceholders.common.command;
import com.google.common.base.Suppliers;
import io.github.miniplaceholders.api.MiniPlaceholders;
import io.github.miniplaceholders.api.types.Platform;
import io.github.miniplaceholders.common.PluginConstants;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.william278.desertwell.about.AboutMenu;
import net.william278.desertwell.util.Version;
import java.util.function.Supplier;
import static net.kyori.adventure.text.Component.newline;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage;
public final class CommandConstants {
// Global
public static final Component TITLE = miniMessage().deserialize("<b><gradient:#4d8bff:#a4ff96>MiniPlaceholders</b>");
public static final Component HEADER = miniMessage().deserialize(
"""
<gradient:aqua:white:aqua><st><b> </st> \
<title> \
<st><b> </st>""", Placeholder.component("title", TITLE));
public static final Component FOOTER = miniMessage().deserialize(
"<gradient:aqua:white:aqua><st><b> ");
// -- /miniplaceholders --
public static final Component INFO = AboutMenu.builder()
.title(TITLE)
.description(text("MiniMessage Component-based Placeholders API"))
.credits("Author", AboutMenu.Credit.of("4drian3d").url("https://github.com/4drian3d"))
.credits("Contributors",
AboutMenu.Credit.of("Sliman4"),
AboutMenu.Credit.of("Stampede2011"),
AboutMenu.Credit.of("powercasgamer"),
AboutMenu.Credit.of("fabianmakila"),
AboutMenu.Credit.of("Willsr71")
)
.buttons(
AboutMenu.Link.of("https://miniplaceholders.github.io/docs/category/user-guide").text("Documentation").icon("⛏"),
AboutMenu.Link.of("https://discord.gg/5NMMzK5mAn").text("Discord").color(TextColor.color(0x7289da)).icon("⭐"),
AboutMenu.Link.of("https://modrinth.com/plugin/miniplaceholders").text("Downloads").color(TextColor.color(0xff496e)).icon("↓")
).version(Version.fromString(PluginConstants.VERSION))
.build()
.toComponent();
// -- /miniplaceholders help --
public static final Supplier<Component> HELP = Suppliers.memoize(() -> {
final Platform platform = MiniPlaceholders.platform();
final String command = platform == Platform.VELOCITY ? "/vminiplaceholders" : "/miniplaceholders";
return text()
.append(newline(), HEADER)
.append(newline(), miniMessage().deserialize(
"<gradient:#9694ff:#5269ff>Commands:</gradient>")
)
.append(newline(), miniMessage().deserialize(
"<gradient:aqua:#94d1ff>" + command + "</gradient> <aqua>help</aqua>")
.clickEvent(ClickEvent.runCommand(command + " help"))
)
.append(newline(), miniMessage().deserialize(
"""
<gradient:aqua:#94d1ff>%s</gradient> \
<aqua>parse</aqua> \
<#8fadff><player | me | --null></#8fadff> \
<#99ffb6><message></#99ffb6>""".formatted(command))
.clickEvent(ClickEvent.suggestCommand(command + " parse "))
)
.append(newline(), miniMessage().deserialize(
"""
<gradient:aqua:#94d1ff>%s</gradient> \
<aqua>parserel</aqua> \
<#8fadff><player | me></#8fadff> \
<#8fadff><relational player | me></#8fadff> \
<#99ffb6><message></#99ffb6>""".formatted(command))
.clickEvent(ClickEvent.suggestCommand(command + " parserel "))
)
.append(newline(), miniMessage().deserialize(
"<gradient:aqua:#94d1ff>" + command + "</gradient> <aqua>expansions</aqua>")
.clickEvent(ClickEvent.runCommand(command + " help"))
)
.append(newline(), FOOTER)
.build();
});
// -- /miniplaceholders expansions --
public static final Component EXPANSIONS_TITLE = miniMessage().deserialize(
"""
<br><gradient:aqua:white:aqua><st><b> </st> \
<title> Expansions <st><b> \
</gradient><br>""", Placeholder.component("title", TITLE)
);
public static final Supplier<Component> NON_INSTALLED_EXPANSIONS = Suppliers.memoize(() -> miniMessage().deserialize(
"""
<aqua>It seems that you don't have any <#a1ff4a>Expansion</#a1ff4a> installed yet,
You can find the <#a1ff4a>Expansion</#a1ff4a> you want here <br>\
<blue><click:open_url:"https://miniplaceholders.github.io/docs/user-guide/Placeholders">\
<hover:show_text:"<gold>Search Expansions">[MiniPlaceholders Expansions]</blue>""")
);
}
@@ -1,9 +1,11 @@
package io.github.miniplaceholders.common.command;
import net.kyori.adventure.audience.Audience;
import org.jetbrains.annotations.NotNull;
import net.kyori.adventure.util.TriState;
import org.jspecify.annotations.NullMarked;
@NullMarked
@FunctionalInterface
public interface PermissionTester<S extends Audience> {
boolean hasPermission(@NotNull S source, @NotNull String permission);
public interface PermissionTester {
TriState permissionValue(Audience audience, String permission);
}
@@ -0,0 +1,9 @@
package io.github.miniplaceholders.common.command;
import net.kyori.adventure.audience.Audience;
import org.jspecify.annotations.NonNull;
@FunctionalInterface
public interface PlaceholderCommandExecutor {
void execute(@NonNull Audience audience);
}
@@ -1,98 +0,0 @@
package io.github.miniplaceholders.common.command;
import io.github.miniplaceholders.common.command.node.ExpansionsNode;
import io.github.miniplaceholders.common.command.node.HelpNode;
import io.github.miniplaceholders.common.command.node.ParseNode;
import io.github.miniplaceholders.common.command.node.RootNode;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.builder.AbstractBuilder;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.minecraft.extras.AudienceProvider;
import org.incendo.cloud.minecraft.extras.MinecraftExceptionHandler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage;
public record PlaceholdersCommand<A extends Audience>(
PlayerSuggestions playersSuggestions,
AudienceConverter audienceConverter,
PermissionTester<A> hasPermission,
String command,
CommandManager<A> commandManager
) {
public static final Component TITLE = miniMessage().deserialize("<gradient:#4d8bff:#a4ff96>MiniPlaceholders</gradient>");
public static final Component HEADER = miniMessage().deserialize(
"<gradient:aqua:white:aqua><st><b> </st> <title> <st><b> </st>", Placeholder.component("title", TITLE));
public static final Component FOOTER = miniMessage().deserialize(
"<gradient:aqua:white:aqua><st><b> ");
public void register() {
//noinspection unchecked
MinecraftExceptionHandler.create(AudienceProvider.nativeAudience())
.defaultHandlers()
.decorator(component -> Component.text().append(TITLE).appendSpace().append(component))
.registerTo((CommandManager<Audience>) commandManager);
// Nodes
Stream.of(new RootNode<>(hasPermission), new HelpNode<A>(), new ParseNode<A>(playersSuggestions, audienceConverter), new ExpansionsNode<A>())
.map(handler -> handler.apply(commandManager.commandBuilder(command)))
.forEach(commandManager::command);
}
public static <A extends Audience> Builder<A> builder() {
return new Builder<>();
}
public final static class Builder<A extends Audience> implements AbstractBuilder<PlaceholdersCommand<A>> {
private PlayerSuggestions playersSuggestions;
private AudienceConverter audienceConverter;
private PermissionTester<A> hasPermission;
private String command;
private CommandManager<A> commandManager;
private Builder() {}
public Builder<A> playerSuggestions(final @NotNull PlayerSuggestions playersSuggestions) {
this.playersSuggestions = playersSuggestions;
return this;
}
public Builder<A> toAudience(final @NotNull AudienceConverter audienceConverter) {
this.audienceConverter = audienceConverter;
return this;
}
public Builder<A> hasPermissionCheck(final @Nullable PermissionTester<A> hasPermission) {
this.hasPermission = hasPermission;
return this;
}
public Builder<A> command(final @NotNull String command) {
this.command = command;
return this;
}
public Builder<A> manager(final @NotNull CommandManager<A> commandManager) {
this.commandManager = commandManager;
return this;
}
@Override
public @NotNull PlaceholdersCommand<A> build() {
return new PlaceholdersCommand<>(
requireNonNull(playersSuggestions),
requireNonNull(audienceConverter),
hasPermission,
requireNonNull(command),
requireNonNull(commandManager)
);
}
}
}
@@ -1,10 +0,0 @@
package io.github.miniplaceholders.common.command;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@FunctionalInterface
public interface PlayerSuggestions {
@NotNull ArrayList<String> suggest();
}
@@ -0,0 +1,10 @@
package io.github.miniplaceholders.common.command;
import org.jspecify.annotations.NonNull;
import java.util.ArrayList;
@FunctionalInterface
public interface PlayersNameProvider {
@NonNull ArrayList<String> provide();
}
@@ -0,0 +1,97 @@
package io.github.miniplaceholders.common.command.brigadier;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.tree.LiteralCommandNode;
import io.github.miniplaceholders.common.command.*;
import io.github.miniplaceholders.common.command.node.*;
import net.kyori.adventure.audience.Audience;
import org.jspecify.annotations.NullMarked;
@NullMarked
public final class BrigadierCommandProvider {
public static <A> LiteralCommandNode<A> provideCommand(
final String command,
final PlayersNameProvider playersSuggestions,
final AudienceConverter audienceConverter,
final AudienceExtractor<A> audienceExtractor,
final PermissionTester permissionChecker
) {
// Inspired by MiniMotd command implementation (MIT licenced)
class WrappingExecutor implements Command<A> {
private final PlaceholderCommandExecutor executor;
private WrappingExecutor(final PlaceholderCommandExecutor executor) {
this.executor = executor;
}
@Override
public int run(final CommandContext<A> commandContext) {
executor.execute(audienceExtractor.extract(commandContext.getSource()));
return Command.SINGLE_SUCCESS;
}
}
// Command Nodes
final RootNode rootNode = new RootNode(permissionChecker);
final ExpansionsNode expansionsNode = new ExpansionsNode(permissionChecker);
final HelpNode helpNode = new HelpNode(permissionChecker);
final ParseNode parseNode = new ParseNode(playersSuggestions, audienceConverter, permissionChecker);
final ParseRelNode parseRelNode = new ParseRelNode(playersSuggestions, audienceConverter, permissionChecker);
return LiteralArgumentBuilder.<A>literal(command)
.requires(src -> rootNode.hasPermission(audienceExtractor.extract(src)))
.executes(new WrappingExecutor(rootNode::execute))
.then(LiteralArgumentBuilder.<A>literal("expansions")
.requires(src -> expansionsNode.hasPermission(audienceExtractor.extract(src)))
.executes(new WrappingExecutor(expansionsNode::showExpansions))
.build()
)
.then(LiteralArgumentBuilder.<A>literal("help")
.executes(new WrappingExecutor(helpNode::execute))
.requires(src -> helpNode.hasPermission(audienceExtractor.extract(src)))
)
.then(LiteralArgumentBuilder.<A>literal("parse")
.requires(src -> parseNode.hasPermission(audienceExtractor.extract(src)))
.then(RequiredArgumentBuilder.<A, String>argument("source", StringArgumentType.word())
.suggests(new BrigadierSuggestionProvider<>(parseNode))
.then(RequiredArgumentBuilder.<A, String>argument("message", StringArgumentType.greedyString())
.executes(ctx -> {
final Audience audience = audienceExtractor.extract(ctx.getSource());
final String source = StringArgumentType.getString(ctx, "source");
final String message = StringArgumentType.getString(ctx, "message");
parseNode.parseString(audience, source, message);
return Command.SINGLE_SUCCESS;
})
)
)
)
.then(LiteralArgumentBuilder.<A>literal("parserel")
.requires(src -> parseRelNode.hasPermission(audienceExtractor.extract(src)))
.then(RequiredArgumentBuilder.<A, String>argument("source", StringArgumentType.word())
.suggests(new BrigadierSuggestionProvider<>(parseRelNode))
.then(RequiredArgumentBuilder.<A, String>argument("relational", StringArgumentType.word())
.suggests(new BrigadierSuggestionProvider<>(parseRelNode))
.then(RequiredArgumentBuilder.<A, String>argument("message", StringArgumentType.greedyString())
.executes(ctx -> {
final Audience audience = audienceExtractor.extract(ctx.getSource());
final String source = StringArgumentType.getString(ctx, "source");
final String relational = StringArgumentType.getString(ctx, "source");
final String message = StringArgumentType.getString(ctx, "message");
parseRelNode.parseString(audience, source, relational, message);
return Command.SINGLE_SUCCESS;
})
)
)
)
)
.build();
}
}
@@ -0,0 +1,19 @@
package io.github.miniplaceholders.common.command.brigadier;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import io.github.miniplaceholders.common.command.node.PlayerCompleterNode;
import org.jspecify.annotations.NullMarked;
import java.util.concurrent.CompletableFuture;
@NullMarked
public record BrigadierSuggestionProvider<A>(PlayerCompleterNode playerSuggestions) implements SuggestionProvider<A> {
@Override
public CompletableFuture<Suggestions> getSuggestions(final CommandContext<A> context, final SuggestionsBuilder builder) {
this.playerSuggestions.providePlayerSuggestions().forEach(builder::suggest);
return builder.buildFuture();
}
}
@@ -2,69 +2,64 @@ package io.github.miniplaceholders.common.command.node;
import io.github.miniplaceholders.api.Expansion;
import io.github.miniplaceholders.api.MiniPlaceholders;
import io.github.miniplaceholders.common.command.PermissionTester;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import org.incendo.cloud.Command;
import org.jspecify.annotations.NullMarked;
import java.util.Collection;
import static io.github.miniplaceholders.common.command.PlaceholdersCommand.FOOTER;
import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage;
import static io.github.miniplaceholders.common.command.CommandConstants.*;
@NullMarked
public record ExpansionsNode(PermissionTester permissionTester) implements Node {
public void showExpansions(final Audience audience) {
final TextComponent.Builder builder = Component.text()
.append(EXPANSIONS_TITLE);
final Collection<Expansion> expansions = MiniPlaceholders.expansionsAvailable();
if (expansions.isEmpty()) {
// In virtually all installations, there will always be some expansion installed,
// so there is no need to cache this component in that case
builder.append(NON_INSTALLED_EXPANSIONS.get());
} else {
builder.append(expansions.stream()
.map(expansion -> {
final TextComponent.Builder expansionMetaBuilder = Component.text();
final String expansionName = expansion.name();
final String expansionAuthor = expansion.author();
final String expansionVersion = expansion.version();
expansionMetaBuilder.append(Component.text(expansionName, NamedTextColor.GRAY));
if (expansionAuthor != null) {
expansionMetaBuilder.appendNewline()
.append(Component.text("Author: ", TextColor.color(0x29fbff)))
.append(Component.text(expansionAuthor, NamedTextColor.GRAY));
}
if (expansionVersion != null) {
expansionMetaBuilder.appendNewline()
.append(Component.text("Version: ", TextColor.color(0xff7b61)))
.append(Component.text(expansionVersion, NamedTextColor.GRAY));
}
return Component.text()
.content(expansion.name())
.hoverEvent(expansionMetaBuilder.build())
.build();
}).collect(Component.toComponent(Component.text(" | ", NamedTextColor.DARK_GRAY))));
}
builder.appendNewline().append(FOOTER);
audience.sendMessage(builder.build());
}
public final class ExpansionsNode<S extends Audience> implements Node<S> {
@Override
public Command.Builder<S> apply(Command.Builder<S> rootBuilder) {
return rootBuilder.literal("expansions")
.permission("miniplaceholders.command.expansions")
.handler(handler -> {
TextComponent.Builder builder = Component.text()
.appendNewline()
.append(miniMessage().deserialize(
"<gradient:aqua:white:aqua><st><b> </st> <gradient:#4d8bff:#a4ff96>MiniPlaceholders</gradient> Expansions <st><b> </gradient>"
))
.appendNewline();
final Collection<Expansion> expansions = MiniPlaceholders.expansionsAvailable();
if (expansions.isEmpty()) {
builder.append(miniMessage().deserialize("""
<aqua>It seems that you don't have any <#a1ff4a>Expansion</#a1ff4a> installed yet,
You can find the <#a1ff4a>Expansion</#a1ff4a> you want here"""))
.appendSpace()
.append(Component.text()
.content("[MiniPlaceholders Expansions]")
.color(NamedTextColor.BLUE)
.clickEvent(ClickEvent.openUrl("https://github.com/MiniPlaceholders/MiniPlaceholders/wiki/Placeholders"))
.hoverEvent(Component.text("Search Expansions", NamedTextColor.GOLD)));
} else {
builder.append(expansions.stream()
.map(expansion -> {
final TextComponent.Builder expansionMetaBuilder = Component.text();
final String expansionName = expansion.name();
final String expansionAuthor = expansion.author();
final String expansionVersion = expansion.version();
public boolean hasPermission(Audience audience) {
return permissionTester.permissionValue(audience, permission()).toBooleanOrElse(false);
}
expansionMetaBuilder.append(Component.text(expansionName, NamedTextColor.GRAY));
if (expansionAuthor != null) {
expansionMetaBuilder.appendNewline()
.append(Component.text("Author: ", TextColor.color(0x29fbff)))
.append(Component.text(expansionAuthor, NamedTextColor.GRAY));
}
if (expansionVersion != null){
expansionMetaBuilder.appendNewline()
.append(Component.text("Version: ", TextColor.color(0xff7b61)))
.append(Component.text(expansionVersion, NamedTextColor.GRAY));
}
return Component.text()
.content(expansion.name())
.hoverEvent(expansionMetaBuilder.build())
.build();
}).collect(Component.toComponent(Component.text(" | "))));
}
builder.appendNewline().append(FOOTER);
handler.sender().sendMessage(builder.build());
});
@Override
public String permission() {
return "miniplaceholders.command.expansions";
}
}
@@ -1,33 +1,24 @@
package io.github.miniplaceholders.common.command.node;
import io.github.miniplaceholders.common.command.PermissionTester;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import org.incendo.cloud.Command;
import org.jspecify.annotations.NullMarked;
import static io.github.miniplaceholders.common.command.PlaceholdersCommand.FOOTER;
import static io.github.miniplaceholders.common.command.PlaceholdersCommand.HEADER;
import static net.kyori.adventure.text.Component.newline;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage;
import static io.github.miniplaceholders.common.command.CommandConstants.HELP;
public final class HelpNode<S extends Audience> implements Node<S> {
private static final Component HELP = text()
.append(newline(), HEADER)
.append(newline(), miniMessage().deserialize(
"<gradient:#9694ff:#5269ff>Commands:</gradient>"))
.append(newline(), miniMessage().deserialize(
"<gradient:aqua:#94d1ff>/miniplaceholders</gradient> <aqua>help</aqua>"))
.append(newline(), miniMessage().deserialize(
"<gradient:aqua:#94d1ff>/miniplaceholders</gradient> <aqua>parse</aqua> <#8fadff><player | me></#8fadff> <#99ffb6><player></#99ffb6>"))
.append(newline(), miniMessage().deserialize(
"<gradient:aqua:#94d1ff>/miniplaceholders</gradient> <aqua>expansions</aqua>"))
.append(newline(), FOOTER)
.build();
@NullMarked
public record HelpNode(PermissionTester permissionChecker) implements Node {
public void execute(final Audience audience) {
audience.sendMessage(HELP.get());
}
@Override
public Command.Builder<S> apply(Command.Builder<S> rootBuilder) {
return rootBuilder.literal("help")
.permission("miniplaceholders.command.help")
.handler(handler -> handler.sender().sendMessage(HELP));
public boolean hasPermission(Audience audience) {
return permissionChecker.permissionValue(audience, permission()).toBooleanOrElse(false);
}
@Override
public String permission() {
return "miniplaceholders.command.help";
}
}
@@ -1,8 +1,11 @@
package io.github.miniplaceholders.common.command.node;
import net.kyori.adventure.audience.Audience;
import org.incendo.cloud.Command;
import org.jspecify.annotations.NullMarked;
public sealed interface Node<S extends Audience> permits ExpansionsNode, HelpNode, ParseNode, RootNode {
Command.Builder<S> apply(Command.Builder<S> rootBuilder);
@NullMarked
public sealed interface Node permits ExpansionsNode, HelpNode, ParseNode, ParseRelNode, RootNode {
boolean hasPermission(Audience audience);
String permission();
}
@@ -2,15 +2,12 @@ package io.github.miniplaceholders.common.command.node;
import io.github.miniplaceholders.api.MiniPlaceholders;
import io.github.miniplaceholders.common.command.AudienceConverter;
import io.github.miniplaceholders.common.command.PlayerSuggestions;
import io.github.miniplaceholders.common.command.PermissionTester;
import io.github.miniplaceholders.common.command.PlayersNameProvider;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import org.incendo.cloud.Command;
import org.incendo.cloud.component.CommandComponent;
import org.incendo.cloud.component.TypedCommandComponent;
import org.incendo.cloud.description.Description;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.suggestion.SuggestionProvider;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.jspecify.annotations.NullMarked;
import java.util.List;
@@ -18,48 +15,45 @@ import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.format.NamedTextColor.RED;
import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage;
public record ParseNode<S extends Audience>(
PlayerSuggestions playerSuggestions,
AudienceConverter audienceConverter
) implements Node<S> {
@NullMarked
public record ParseNode(
PlayersNameProvider playersNameProvider,
AudienceConverter audienceConverter,
PermissionTester permissionChecker
) implements Node, PlayerCompleterNode {
public void parseString(final Audience sender, final String providedSource, final String providedString) {
final Audience source = switch (providedSource) {
case "me" -> sender;
case "--null" -> Audience.empty();
default -> audienceConverter.convert(providedSource);
};
if (source == null) {
sender.sendMessage(text("You must specify a valid player", RED));
return;
}
final TagResolver resolver = source == Audience.empty()
? MiniPlaceholders.globalPlaceholders()
: MiniPlaceholders.audienceGlobalPlaceholders();
final Component parsed = miniMessage().deserialize(providedString, source, resolver);
sender.sendMessage(parsed);
}
@Override
public Command.Builder<S> apply(Command.Builder<S> rootBuilder) {
final TypedCommandComponent<S, String> sourceArgument = CommandComponent.<S, String>ofType(String.class, "source")
.parser(StringParser.stringParser(StringParser.StringMode.SINGLE))
.description(Description.of("The source from which the message will be parsed"))
.suggestionProvider(SuggestionProvider.blockingStrings((ctx, st) -> {
final List<String> suggestions = playerSuggestions.suggest();
suggestions.add("me");
return suggestions;
}))
.build();
final TypedCommandComponent<S, String> stringArgument = CommandComponent.<S, String>ofType(String.class, "message")
.parser(StringParser.greedyStringParser())
.description(Description.of("Message to be parsed"))
.build();
public boolean hasPermission(final Audience audience) {
return permissionChecker.permissionValue(audience, permission()).toBooleanOrElse(false);
}
return rootBuilder.literal("parse")
.argument(sourceArgument)
.argument(stringArgument)
.permission("miniplaceholders.command.parse")
.handler(handler -> {
final String source = handler.get(sourceArgument);
@Override
public String permission() {
return "miniplaceholders.command.parse";
}
final Audience objective = "me".equals(source)
? handler.sender()
: audienceConverter.convert(source);
if (objective == null){
handler.sender().sendMessage(text("You must specify a valid player", RED));
return;
}
final String toParse = handler.get(stringArgument);
final Component parsed = miniMessage().deserialize(
toParse,
objective,
MiniPlaceholders.audienceGlobalPlaceholders()
);
handler.sender().sendMessage(parsed);
});
@Override
public List<String> providePlayerSuggestions() {
final List<String> suggestions = PlayerCompleterNode.super.providePlayerSuggestions();
suggestions.add("--null");
return suggestions;
}
}
@@ -0,0 +1,59 @@
package io.github.miniplaceholders.common.command.node;
import io.github.miniplaceholders.api.MiniPlaceholders;
import io.github.miniplaceholders.api.types.RelationalAudience;
import io.github.miniplaceholders.common.command.AudienceConverter;
import io.github.miniplaceholders.common.command.PermissionTester;
import io.github.miniplaceholders.common.command.PlayersNameProvider;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import org.jspecify.annotations.NullMarked;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.format.NamedTextColor.RED;
import static net.kyori.adventure.text.minimessage.MiniMessage.miniMessage;
@NullMarked
public record ParseRelNode(
PlayersNameProvider playersNameProvider,
AudienceConverter audienceConverter,
PermissionTester permissionChecker
) implements Node, PlayerCompleterNode {
public void parseString(final Audience sender, final String source, final String relational, final String toParse) {
final Audience senderTarget = "me".equals(source)
? sender
: audienceConverter.convert(source);
if (senderTarget == null) {
sender.sendMessage(text("You must specify a valid player", RED));
return;
}
final Audience relationalTarget = "me".equals(relational)
? sender
: audienceConverter.convert(relational);
if (relationalTarget == null) {
sender.sendMessage(text("You must specify a valid relational player", RED));
return;
}
final RelationalAudience<Audience> relationalAudience = RelationalAudience.from(senderTarget, relationalTarget);
final Component parsed = miniMessage().deserialize(
toParse,
relationalAudience,
MiniPlaceholders.relationalGlobalPlaceholders()
);
sender.sendMessage(parsed);
}
@Override
public boolean hasPermission(Audience audience) {
return permissionChecker.permissionValue(audience, permission()).toBooleanOrElse(false);
}
@Override
public String permission() {
return "miniplaceholders.command.parse";
}
}
@@ -0,0 +1,15 @@
package io.github.miniplaceholders.common.command.node;
import io.github.miniplaceholders.common.command.PlayersNameProvider;
import java.util.List;
public interface PlayerCompleterNode {
default List<String> providePlayerSuggestions() {
final List<String> suggestions = playersNameProvider().provide();
suggestions.add("me");
return suggestions;
}
PlayersNameProvider playersNameProvider();
}
@@ -1,52 +1,26 @@
package io.github.miniplaceholders.common.command.node;
import io.github.miniplaceholders.common.PluginConstants;
import io.github.miniplaceholders.common.command.PermissionTester;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.permission.PermissionChecker;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.util.TriState;
import net.william278.desertwell.about.AboutMenu;
import net.william278.desertwell.util.Version;
import org.incendo.cloud.Command;
import org.incendo.cloud.permission.Permission;
import org.incendo.cloud.permission.PermissionResult;
import org.jspecify.annotations.NullMarked;
import static io.github.miniplaceholders.common.command.PlaceholdersCommand.TITLE;
import static net.kyori.adventure.text.Component.text;
import static io.github.miniplaceholders.common.command.CommandConstants.INFO;
public record RootNode<S extends Audience>(PermissionTester<S> hasPermission) implements Node<S> {
private static final Component INFO = AboutMenu.builder()
.title(TITLE)
.description(text("MiniMessage Component-based Placeholders for Paper, Folia, Fabric, Velocity and Sponge platforms"))
.credits("Author", AboutMenu.Credit.of("4drian3d").url("https://github.com/4drian3d"))
.credits("Contributors",
AboutMenu.Credit.of("Sliman4"),
AboutMenu.Credit.of("Stampede2011"),
AboutMenu.Credit.of("powercasgamer")
)
.buttons(
AboutMenu.Link.of("https://github.com/MiniPlaceholders/MiniPlaceholders/wiki/User-Getting-Started").text("Documentation").icon("⛏"),
AboutMenu.Link.of("https://discord.gg/5NMMzK5mAn").text("Discord").color(TextColor.color(0x7289da)).icon("⭐"),
AboutMenu.Link.of("https://modrinth.com/plugin/miniplaceholders").text("Downloads").color(TextColor.color(0xff496e)).icon("↓")
).version(Version.fromString(PluginConstants.VERSION))
.build()
.toComponent();
@NullMarked
public record RootNode(PermissionTester permissionChecker) implements Node {
public void execute(Audience audience) {
audience.sendMessage(INFO);
}
@Override
public Command.Builder<S> apply(Command.Builder<S> rootBuilder) {
return rootBuilder
.permission(src -> {
if (hasPermission != null) {
return PermissionResult.of(hasPermission.hasPermission(src, "miniplaceholders.command"), Permission.permission("miniplaceholders.command"));
}
return PermissionResult.of(src.get(PermissionChecker.POINTER)
.map(checker -> checker.value("miniplaceholders.command"))
.orElse(TriState.FALSE) != TriState.FALSE,
Permission.permission("miniplaceholders.command")
);
})
.handler(handler -> handler.sender().sendMessage(INFO));
public boolean hasPermission(Audience audience) {
return permissionChecker.permissionValue(audience, permission()) != TriState.FALSE;
}
@Override
public String permission() {
return "miniplaceholders.command";
}
}
@@ -1,6 +1,7 @@
package io.github.miniplaceholders.common.loader;
import io.github.miniplaceholders.api.provider.ExpansionProvider;
import org.jspecify.annotations.NullMarked;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -17,6 +18,7 @@ import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
@NullMarked
public final class ExpansionProviderLoader {
public static List<ExpansionProvider> loadProvidersFromFolder(final Path folferPath) throws Exception {
@@ -1,6 +1,7 @@
package io.github.miniplaceholders.common.loader;
import io.github.miniplaceholders.api.provider.LoadRequirement;
import org.jspecify.annotations.NonNull;
public record FailedToLoadExpansion(LoadRequirement requirement) {
public record FailedToLoadExpansion(@NonNull LoadRequirement requirement) {
}
@@ -1,11 +1,11 @@
package io.github.miniplaceholders.common.loader;
import io.github.miniplaceholders.api.Expansion;
import io.github.miniplaceholders.api.provider.ExpansionProvider;
import io.github.miniplaceholders.api.provider.LoadRequirement;
public record ProviderLoadResult(Expansion expansion, FailedToLoadExpansion failed) {
public static ProviderLoadResult ofLoaded(Expansion expansion) {
return new ProviderLoadResult(expansion, null);
public record ProviderLoadResult(ExpansionProvider provider, FailedToLoadExpansion failed) {
public static ProviderLoadResult ofLoaded(ExpansionProvider provider) {
return new ProviderLoadResult(provider, null);
}
public static ProviderLoadResult ofFailed(LoadRequirement requirement) {
-3
View File
@@ -16,9 +16,6 @@ dependencies {
includeDependency(libs.adventure.serializer.legacy)
includeDependency(libs.adventure.minimessage)
includeDependency(libs.luckopermissionsapi)
includeDependency(libs.cloud.fabric)
includeDependency(libs.cloud.core)
includeDependency(libs.cloud.extras)
includeDependency(libs.desertwell)
includeDependency(libs.unnamedinject)
@@ -1,80 +1,86 @@
package io.github.miniplaceholders.fabric;
import com.mojang.brigadier.tree.LiteralCommandNode;
import io.github.miniplaceholders.common.PlaceholdersPlugin;
import io.github.miniplaceholders.common.command.PlaceholdersCommand;
import io.github.miniplaceholders.common.command.brigadier.BrigadierCommandProvider;
import io.github.miniplaceholders.connect.InternalPlatform;
import me.lucko.fabric.api.permissions.v0.Permissions;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.api.DedicatedServerModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import net.kyori.adventure.util.TriState;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.server.MinecraftServer;
import org.incendo.cloud.SenderMapper;
import org.incendo.cloud.execution.ExecutionCoordinator;
import org.incendo.cloud.fabric.FabricServerCommandManager;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public final class FabricMod implements ModInitializer, PlaceholdersPlugin {
private final ComponentLogger componentLogger = ComponentLogger.logger("miniplaceholders");
private MinecraftServer minecraftServer;
public final class FabricMod implements DedicatedServerModInitializer, PlaceholdersPlugin {
private final ComponentLogger componentLogger = ComponentLogger.logger("miniplaceholders");
private MinecraftServer minecraftServer;
@Override
public void onInitialize() {
componentLogger.info("Starting MiniPlaceholders Fabric");
InternalPlatform.platform(InternalPlatform.FABRIC);
@Override
public void onInitializeServer() {
componentLogger.info("Starting MiniPlaceholders Fabric");
InternalPlatform.platform(InternalPlatform.FABRIC);
ServerLifecycleEvents.SERVER_STARTING.register(minecraftServer -> {
this.minecraftServer = minecraftServer;
try {
final Path expansionsFolder = FabricLoader.getInstance().getConfigDir()
.resolve("miniplaceholders").resolve("expansions");
this.loadProvidedExpansions(expansionsFolder);
} catch (Throwable e) {
componentLogger.error("Unable to load expansion providers", e);
}
});
ServerLifecycleEvents.SERVER_STARTING.register(minecraftServer -> {
this.minecraftServer = minecraftServer;
try {
final Path expansionsFolder = FabricLoader.getInstance().getConfigDir()
.resolve("miniplaceholders").resolve("expansions");
this.loadProvidedExpansions(expansionsFolder);
} catch (Throwable e) {
componentLogger.error("Unable to load expansion providers", e);
}
});
this.registerPlatformCommand();
}
this.registerPlatformCommand();
}
@Override
public void registerPlatformCommand() {
final FabricServerCommandManager<CommandSourceStack> commandManager = new FabricServerCommandManager<>(
ExecutionCoordinator.simpleCoordinator(),
SenderMapper.identity()
);
PlaceholdersCommand.<CommandSourceStack>builder()
.hasPermissionCheck((source, permission) -> Permissions.check(source, permission, 4))
.toAudience(string -> this.minecraftServer.getPlayerList().getPlayerByName(string))
.playerSuggestions(() -> new ArrayList<>(List.of(this.minecraftServer.getPlayerNames())))
.manager(commandManager)
.command("miniplaceholders")
.build()
.register();
}
@Override
public void registerPlatformCommand() {
final LiteralCommandNode<CommandSourceStack> node = BrigadierCommandProvider.provideCommand(
"miniplaceholders",
() -> new ArrayList<>(List.of(this.minecraftServer.getPlayerNames())),
string -> this.minecraftServer.getPlayerList().getPlayerByName(string),
aud -> aud,
(audience, permission) -> {
final CommandSourceStack commandSourceStack = (CommandSourceStack) audience;
return switch (Permissions.getPermissionValue(commandSourceStack, permission)) {
case TRUE -> TriState.TRUE;
case FALSE -> TriState.FALSE;
case DEFAULT -> commandSourceStack.hasPermission(3) ? TriState.TRUE : TriState.NOT_SET;
};
}
);
CommandRegistrationCallback.EVENT.register(
(dispatcher, ctx, selection) ->
dispatcher.getRoot().addChild(node)
);
}
@Override
public boolean platformHasComplementLoaded(String complementName) {
return FabricLoader.getInstance().isModLoaded(complementName);
}
@Override
public boolean platformHasComplementLoaded(String complementName) {
return FabricLoader.getInstance().isModLoaded(complementName);
}
@Override
public void logError(Component component) {
componentLogger.error(component);
}
@Override
public void logError(Component component) {
componentLogger.error(component);
}
@Override
public void logInfo(Component component) {
componentLogger.info(component);
}
@Override
public void logInfo(Component component) {
componentLogger.info(component);
}
@Override
public Object platformServerInstance() {
return this.minecraftServer;
}
@Override
public Object platformServerInstance() {
return this.minecraftServer;
}
}
+5 -6
View File
@@ -8,14 +8,14 @@
"4drian3d"
],
"contact": {
"homepage": "https://github.com/MiniPlaceholders/MiniPlaceholders",
"homepage": "https://miniplaceholders.github.io/",
"sources": "https://github.com/MiniPlaceholders/MiniPlaceholders",
"issues": "https://github.com/MiniPlaceholders/MiniPlaceholders/issues"
},
"license": "Apache-2",
"environment": "server",
"entrypoints": {
"main": [
"server": [
"io.github.miniplaceholders.fabric.FabricMod"
]
},
@@ -23,9 +23,8 @@
"fabricloader": ">=0.14.19",
"fabric": "*",
"java": ">=21",
"cloud": "*",
"adventure-platform-fabric": "*",
"minecraft": ">=1.21.6",
"fabric-permissions-api-v0": "*"
"adventure-platform-fabric": "~6.6.0",
"minecraft": ">=1.21.6 <=1.21.8",
"fabric-permissions-api-v0": "~0.4.1"
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
group = io.github.miniplaceholders
version = 3.0.1
version = 3.1.0-SNAPSHOT
description = MiniMessage Component-based Placeholders API
org.gradle.jvmargs=-Xmx2G
+7 -15
View File
@@ -6,20 +6,17 @@ velocity = "3.4.0-SNAPSHOT"
paper = "1.21.8-R0.1-SNAPSHOT"
minecraft = "1.21.8"
runtask = "2.3.1"
runtask = "3.0.0"
blossom = "2.1.0"
shadow = "9.0.2"
shadow = "9.2.2"
jmh = "0.7.3"
dokka = "2.0.0"
cloud = "2.0.0"
cloud-minecraft = "2.0.0-SNAPSHOT"
cloud-minecraft-modded = "2.0.0-SNAPSHOT"
adventure = "4.24.0"
adventureModded = "6.6.0"
kotlin = "2.2.20-RC"
kotlin = "2.2.20"
fabric-api = "0.132.0+1.21.8"
fabric-api = "0.133.14+1.21.9"
fabric-loader = "0.17.2"
desertwell = "2.0.4"
@@ -40,22 +37,17 @@ adventure-platform-fabric = { group = "net.kyori", name = "adventure-platform-fa
adventure-serializer-legacy = { group = "net.kyori", name = "adventure-text-serializer-legacy", version.ref = "adventure" }
adventure-serializer-plain = { group = "net.kyori", name = "adventure-text-serializer-plain", version.ref = "adventure" }
cloud-core = { group = "org.incendo", name = "cloud-core", version.ref = "cloud" }
cloud-velocity = { group = "org.incendo", name = "cloud-velocity", version.ref = "cloud-minecraft" }
cloud-paper = { group = "org.incendo", name = "cloud-paper", version.ref = "cloud-minecraft" }
cloud-fabric = { group = "org.incendo", name = "cloud-fabric", version.ref = "cloud-minecraft-modded" }
cloud-extras = { group = "org.incendo", name = "cloud-minecraft-extras", version.ref = "cloud-minecraft" }
plugin-shadow = { module = "com.gradleup.shadow:shadow-gradle-plugin", version.ref = "shadow" }
plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
plugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
desertwell = { group = "net.william278", name = "DesertWell", version.ref = "desertwell" }
junit-bom = "org.junit:junit-bom:5.13.4"
junit-bom = "org.junit:junit-bom:6.0.0"
jspecify = "org.jspecify:jspecify:1.0.0"
unnamedinject = "team.unnamed:inject:2.0.1"
brigadier = "com.mojang:brigadier:1.0.500"
[plugins]
@@ -64,5 +56,5 @@ runvelocity = { id = "xyz.jpenilla.run-velocity", version.ref = "runtask" }
blossom = { id = "net.kyori.blossom", version.ref = "blossom" }
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
jmh = { id = "me.champeau.jmh", version.ref = "jmh" }
idea-ext = { id = "org.jetbrains.gradle.plugin.idea-ext", version = "1.2" }
idea-ext = { id = "org.jetbrains.gradle.plugin.idea-ext", version = "1.3" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
-3
View File
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
Vendored
+1 -2
View File
@@ -70,11 +70,10 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
-1
View File
@@ -9,7 +9,6 @@ dependencies {
implementation(projects.miniplaceholdersCommon)
implementation(projects.miniplaceholdersApi)
implementation(projects.miniplaceholdersConnect)
implementation(libs.cloud.paper)
}
tasks {
@@ -1,20 +1,20 @@
package io.github.miniplaceholders.paper;
import io.github.miniplaceholders.common.command.PlaceholdersCommand;
import com.mojang.brigadier.tree.LiteralCommandNode;
import io.github.miniplaceholders.common.command.brigadier.BrigadierCommandProvider;
import io.github.miniplaceholders.connect.InternalPlatform;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import net.kyori.adventure.util.TriState;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.SenderMapper;
import org.incendo.cloud.execution.ExecutionCoordinator;
import org.incendo.cloud.paper.PaperCommandManager;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@SuppressWarnings({"unused", "UnstableApiUsage"})
@@ -22,30 +22,26 @@ public final class PaperBootstrap implements PluginBootstrap {
@Override
public void bootstrap(@NotNull BootstrapContext bootstrapContext) {
InternalPlatform.platform(InternalPlatform.PAPER);
PaperCommandManager<SourceWrapper> commandManager = PaperCommandManager
.builder(SenderMapper.create(SourceWrapper::new, SourceWrapper::stack))
.executionCoordinator(ExecutionCoordinator.simpleCoordinator())
.buildBootstrapped(bootstrapContext);
final LiteralCommandNode<CommandSourceStack> command = BrigadierCommandProvider.provideCommand(
"miniplaceholders",
() -> Bukkit.getServer()
.getOnlinePlayers()
.stream()
.map(Player::getName)
.collect(Collectors.toCollection(ArrayList::new)),
string -> Bukkit.getServer().getPlayer(string),
CommandSourceStack::getSender,
(audience, permission) -> {
final CommandSender sender = (CommandSender) audience;
final TriState triState = sender.permissionValue(permission);
return triState == TriState.NOT_SET
? sender.isOp() ? TriState.TRUE : TriState.NOT_SET
: triState;
}
);
bootstrapContext.getLifecycleManager()
.registerEventHandler(LifecycleEvents.COMMANDS,
commands -> commands.registrar().register(bootstrapContext.getPluginMeta(), command, "", List.of()));
PlaceholdersCommand.<SourceWrapper>builder()
.playerSuggestions(() -> Bukkit.getServer().getOnlinePlayers()
.stream()
.map(Player::getName)
.collect(Collectors.toCollection(ArrayList::new)))
.toAudience(st -> Bukkit.getServer().getPlayer(st))
.hasPermissionCheck((aud, p) -> aud.stack.getSender().hasPermission(p))
.manager(commandManager)
.command("miniplaceholders")
.build()
.register();
}
public record SourceWrapper(CommandSourceStack stack) implements ForwardingAudience.Single {
@Override
public @NotNull Audience audience() {
return this.stack.getSender();
}
}
}
+2 -2
View File
@@ -37,10 +37,10 @@ dependencyResolutionManagement {
}
plugins {
id("quiet-fabric-loom") version "1.11.324"
id("quiet-fabric-loom") version "1.11.325"
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
id("com.vanniktech.maven.publish") version "0.34.0" apply false
kotlin("jvm") version "2.2.20-RC" apply false
kotlin("jvm") version "2.2.20" apply false
}
arrayOf(
+3 -3
View File
@@ -8,11 +8,11 @@ plugins {
}
dependencies {
implementation(projects.miniplaceholdersCommon) {
exclude(group = "org.incendo")
}
implementation(projects.miniplaceholdersCommon)
implementation(projects.miniplaceholdersApi)
implementation(projects.miniplaceholdersConnect)
compileOnly(libs.jspecify)
}
sponge {
@@ -3,6 +3,7 @@ package io.github.miniplaceholders.sponge;
import com.google.inject.Inject;
import io.github.miniplaceholders.common.PlaceholdersPlugin;
import io.github.miniplaceholders.connect.InternalPlatform;
import io.github.miniplaceholders.sponge.command.SpongeCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.apache.logging.log4j.Logger;
@@ -12,62 +13,61 @@ import org.spongepowered.api.config.ConfigDir;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.lifecycle.RegisterCommandEvent;
import org.spongepowered.api.event.lifecycle.StartingEngineEvent;
import org.spongepowered.plugin.PluginContainer;
import org.spongepowered.plugin.builtin.jvm.Plugin;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicInteger;
@Plugin("miniplaceholders")
public class SpongePlugin implements PlaceholdersPlugin {
@Inject
private Logger logger;
@Inject
@ConfigDir(sharedRoot = false)
private Path configDir;
@Inject
private Logger logger;
@Inject
@ConfigDir(sharedRoot = false)
private Path configDir;
@Inject
private PluginContainer pluginContainer;
private Server server;
private Server server;
@Listener
public void onServerStart(final StartingEngineEvent<Server> event) {
this.server = event.engine();
logger.info("Starting MiniPlaceholders Sponge");
InternalPlatform.platform(InternalPlatform.SPONGE);
@Listener
public void onServerStart(final StartingEngineEvent<Server> event) {
this.server = event.engine();
InternalPlatform.platform(InternalPlatform.SPONGE);
logger.info("Starting MiniPlaceholders Sponge");
try {
this.loadProvidedExpansions(configDir.resolve("expansions"));
} catch (Throwable e) {
logger.error("Unable to load expansion providers", e);
}
try {
this.loadProvidedExpansions(configDir.resolve("expansions"));
} catch (Throwable e) {
logger.error("Unable to load expansion providers", e);
}
}
private final AtomicInteger registration = new AtomicInteger(0);
@Listener
public void onCommandRegister(final RegisterCommandEvent<Command.Parameterized> event) {
event.register(pluginContainer, SpongeCommand.provideCommand(), "miniplaceholders");
}
@Listener
public void onCommandRegister(final RegisterCommandEvent<Command.Parameterized> event) {
if (registration.getAndIncrement() == 0) {
this.registerPlatformCommand();
}
}
@Override
public boolean platformHasComplementLoaded(String complementName) {
return server.game().pluginManager().plugin(complementName).isPresent();
}
@Override
public boolean platformHasComplementLoaded(String complementName) {
return server.game().pluginManager().plugin(complementName).isPresent();
}
// TODO: Replace this with some implementation of a ComponentLogger
@Override
public void logError(Component component) {
logger.error(PlainTextComponentSerializer.plainText().serialize(component));
}
// TODO: Replace this with some implementation of a ComponentLogger
@Override
public void logError(Component component) {
logger.error(PlainTextComponentSerializer.plainText().serialize(component));
}
@Override
public void logInfo(Component component) {
logger.info(PlainTextComponentSerializer.plainText().serialize(component));
}
@Override
public void logInfo(Component component) {
logger.info(PlainTextComponentSerializer.plainText().serialize(component));
}
@Override
public Object platformServerInstance() {
return this.server;
}
@Override
public Object platformServerInstance() {
return this.server;
}
}
@@ -0,0 +1,107 @@
package io.github.miniplaceholders.sponge.command;
import io.github.miniplaceholders.common.command.*;
import io.github.miniplaceholders.common.command.node.*;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.util.TriState;
import org.jspecify.annotations.NullMarked;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.command.*;
import org.spongepowered.api.command.parameter.CommandContext;
import org.spongepowered.api.command.parameter.Parameter;
import org.spongepowered.api.entity.living.player.User;
import org.spongepowered.api.entity.living.player.server.ServerPlayer;
import org.spongepowered.api.util.Tristate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@NullMarked
public final class SpongeCommand {
public static Command.Parameterized provideCommand() {
final AudienceExtractor<CommandCause> audienceExtractor = CommandCause::audience;
final PermissionTester permissionTester = (audience, permission) -> TriState.NOT_SET; // Not really used here
final PlayersNameProvider playerSuggestions = () -> Sponge.server().onlinePlayers()
.stream()
.map(ServerPlayer::user)
.map(User::name)
.collect(Collectors.toCollection(ArrayList::new));
final AudienceConverter audienceConverter = string -> Sponge.server().player(string).orElse(null);
// Command Nodes
final RootNode rootNode = new RootNode(permissionTester);
final ExpansionsNode expansionsNode = new ExpansionsNode(permissionTester);
final ParseNode parseNode = new ParseNode(playerSuggestions, audienceConverter, permissionTester);
final ParseRelNode parseRelNode = new ParseRelNode(playerSuggestions, audienceConverter, permissionTester);
final HelpNode helpNode = new HelpNode(permissionTester);
final Parameter.Value<String> sourceParameter = Parameter.string().key("source")
.completer((context, currentInput) -> parseNode.providePlayerSuggestions()
.stream()
.map(CommandCompletion::of)
.toList()
).build();
final Parameter.Value<String> relationalSourceParameter = Parameter.string().key("relational")
.completer((context, currentInput) -> parseNode.providePlayerSuggestions()
.stream()
.map(CommandCompletion::of)
.toList()
).build();
final Parameter.Value<String> messageParameter = Parameter.remainingJoinedStrings().key("message").build();
return Command.builder()
.executionRequirements(src -> src.permissionValue(rootNode.permission()) != Tristate.FALSE)
.executor(new WrappingExecutor(rootNode::execute))
.addChildren(Map.of(
List.of("expansions"), Command.builder()
.permission(expansionsNode.permission())
.executor(new WrappingExecutor(expansionsNode::showExpansions))
.build(),
List.of("help"), Command.builder()
.permission(helpNode.permission())
.executor(new WrappingExecutor(helpNode::execute))
.build(),
List.of("parse"), Command.builder()
.permission(parseNode.permission())
.addParameter(sourceParameter)
.addParameter(messageParameter)
.executor(ctx -> {
final Audience executor = audienceExtractor.extract(ctx.cause());
final String source = ctx.requireOne(sourceParameter);
final String message = ctx.requireOne(messageParameter);
parseNode.parseString(executor, source, message);
return CommandResult.success();
})
.build(),
List.of("parserel"), Command.builder()
.permission(parseRelNode.permission())
.addParameter(sourceParameter)
.addParameter(relationalSourceParameter)
.addParameter(messageParameter)
.executor(ctx -> {
final Audience executor = audienceExtractor.extract(ctx.cause());
final String source = ctx.requireOne(sourceParameter);
final String relational = ctx.requireOne(relationalSourceParameter);
final String message = ctx.requireOne(messageParameter);
parseRelNode.parseString(executor, source, relational, message);
return CommandResult.success();
})
.build()
))
.build();
}
private record WrappingExecutor(PlaceholderCommandExecutor executor) implements CommandExecutor {
@Override
public CommandResult execute(CommandContext context) {
executor.execute(context.cause().audience());
return CommandResult.success();
}
}
}
-1
View File
@@ -10,7 +10,6 @@ dependencies {
implementation(projects.miniplaceholdersCommon)
implementation(projects.miniplaceholdersApi)
implementation(projects.miniplaceholdersConnect)
implementation(libs.cloud.velocity)
}
tasks {
@@ -1,9 +1,10 @@
package io.github.miniplaceholders.velocity;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.mojang.brigadier.tree.LiteralCommandNode;
import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.command.CommandMeta;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
@@ -13,97 +14,88 @@ import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import io.github.miniplaceholders.common.PlaceholdersPlugin;
import io.github.miniplaceholders.common.PluginConstants;
import io.github.miniplaceholders.common.command.PlaceholdersCommand;
import io.github.miniplaceholders.common.command.brigadier.BrigadierCommandProvider;
import io.github.miniplaceholders.connect.InternalPlatform;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import org.incendo.cloud.SenderMapper;
import org.incendo.cloud.execution.ExecutionCoordinator;
import org.incendo.cloud.velocity.CloudInjectionModule;
import org.incendo.cloud.velocity.VelocityCommandManager;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.stream.Collectors;
@Plugin(
name = "MiniPlaceholders",
id = "miniplaceholders",
version = PluginConstants.VERSION,
authors = {"MiniPlaceholders Contributors", "4drian3d"},
description = "MiniMessage Component-based Placeholders API"
name = "MiniPlaceholders",
id = "miniplaceholders",
version = PluginConstants.VERSION,
authors = {"MiniPlaceholders Contributors", "4drian3d"},
description = "MiniMessage Component-based Placeholders API"
)
public final class VelocityPlugin implements PlaceholdersPlugin {
@Inject
private Injector injector;
@Inject
@DataDirectory
private Path dataDirectory;
@Inject
private ProxyServer proxyServer;
@Inject
private ComponentLogger componentLogger;
@Inject
@DataDirectory
private Path dataDirectory;
@Inject
private ProxyServer proxyServer;
@Inject
private CommandManager commandManager;
@Inject
private ComponentLogger componentLogger;
@Subscribe(priority = 999)
public void onEarlyProxyInitialize(final ProxyInitializeEvent event) {
componentLogger.info(Component.text("Starting MiniPlaceholders Velocity", NamedTextColor.GREEN));
@Subscribe(priority = 999)
public void onEarlyProxyInitialize(final ProxyInitializeEvent event) {
InternalPlatform.platform(InternalPlatform.VELOCITY);
componentLogger.info(Component.text("Starting MiniPlaceholders Velocity", NamedTextColor.GREEN));
InternalPlatform.platform(InternalPlatform.VELOCITY);
this.registerPlatformCommand();
}
this.registerPlatformCommand();
@Subscribe(priority = -404)
public void onLateProxyInitialize(final ProxyInitializeEvent event) {
try {
this.loadProvidedExpansions(dataDirectory.resolve("expansions"));
} catch (Throwable e) {
componentLogger.error("Unable to load expansion providers", e);
}
}
@Subscribe(priority = -404)
public void onLateProxyInitialize(final ProxyInitializeEvent event) {
try {
this.loadProvidedExpansions(dataDirectory.resolve("expansions"));
} catch (Throwable e) {
componentLogger.error("Unable to load expansion providers", e);
}
}
@Override
public void registerPlatformCommand() {
final LiteralCommandNode<CommandSource> node = BrigadierCommandProvider.provideCommand(
"vminiplaceholders",
() -> proxyServer.getAllPlayers()
.stream()
.map(Player::getUsername)
.collect(Collectors.toCollection(ArrayList::new)),
str -> this.proxyServer.getPlayer(str).orElse(null),
aud -> aud,
(audience, permission) -> ((CommandSource) audience).getPermissionValue(permission)
.toAdventureTriState()
);
final BrigadierCommand command = new BrigadierCommand(node);
final CommandMeta meta = commandManager.metaBuilder(command)
.plugin(this)
.build();
commandManager.register(meta, command);
}
@Override
public void registerPlatformCommand() {
injector = injector.createChildInjector(
new CloudInjectionModule<>(
CommandSource.class,
ExecutionCoordinator.simpleCoordinator(),
SenderMapper.identity()
)
);
final VelocityCommandManager<CommandSource> commandManager = injector.getInstance(
Key.get(new TypeLiteral<>() {
})
);
PlaceholdersCommand.<CommandSource>builder()
.playerSuggestions(() -> proxyServer.getAllPlayers()
.stream()
.map(Player::getUsername)
.collect(Collectors.toCollection(ArrayList::new)))
.toAudience(st -> proxyServer.getPlayer(st).orElse(null))
.command("vminiplaceholders")
.manager(commandManager)
.build().register();
}
@Override
public boolean platformHasComplementLoaded(String complementName) {
return this.proxyServer.getPluginManager().isLoaded(complementName);
}
@Override
public boolean platformHasComplementLoaded(String complementName) {
return this.proxyServer.getPluginManager().isLoaded(complementName);
}
@Override
public void logError(Component component) {
componentLogger.error(component);
}
@Override
public void logError(Component component) {
componentLogger.error(component);
}
@Override
public void logInfo(Component component) {
componentLogger.info(component);
}
@Override
public void logInfo(Component component) {
componentLogger.info(component);
}
@Override
public Object platformServerInstance() {
return this.proxyServer;
}
@Override
public Object platformServerInstance() {
return this.proxyServer;
}
}