diff --git a/src/main/java/com/minelittlepony/unicopia/Race.java b/src/main/java/com/minelittlepony/unicopia/Race.java index ac63cfff..72ae4d08 100644 --- a/src/main/java/com/minelittlepony/unicopia/Race.java +++ b/src/main/java/com/minelittlepony/unicopia/Race.java @@ -19,6 +19,7 @@ import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; import net.minecraft.command.argument.RegistryKeyArgumentType; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; import net.minecraft.util.Identifier; import net.minecraft.registry.Registry; @@ -135,18 +136,19 @@ public record Race (Supplier compositeSupplier, Availability availabi public boolean isPermitted(@Nullable PlayerEntity sender) { Set whitelist = Unicopia.getConfig().speciesWhiteList.get(); - return isUnset() + return this == HUMAN + || isUnset() || whitelist.isEmpty() || whitelist.contains(getId().toString()); } public Race validate(PlayerEntity sender) { if (!isPermitted(sender)) { - if (this == EARTH) { - return HUMAN; + Race alternative = this == EARTH ? HUMAN : EARTH.validate(sender); + if (alternative != this && sender instanceof ServerPlayerEntity spe) { + spe.sendMessageToClient(Text.translatable("respawn.reason.illegal_race", getDisplayName()), false); } - - return EARTH.validate(sender); + return alternative; } return this; diff --git a/src/main/java/com/minelittlepony/unicopia/client/FlowingText.java b/src/main/java/com/minelittlepony/unicopia/client/TextHelper.java similarity index 59% rename from src/main/java/com/minelittlepony/unicopia/client/FlowingText.java rename to src/main/java/com/minelittlepony/unicopia/client/TextHelper.java index 1d820649..52d57c43 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/FlowingText.java +++ b/src/main/java/com/minelittlepony/unicopia/client/TextHelper.java @@ -1,12 +1,14 @@ package com.minelittlepony.unicopia.client; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import net.minecraft.client.MinecraftClient; import net.minecraft.text.*; -public interface FlowingText { +public interface TextHelper { static Stream wrap(Text text, int maxWidth) { return MinecraftClient.getInstance().textRenderer.getTextHandler().wrapLines(text, maxWidth, Style.EMPTY).stream().map(line -> { MutableText compiled = Text.literal(""); @@ -17,4 +19,11 @@ public interface FlowingText { return compiled; }); } + + static Text join(Text delimiter, Iterable elements) { + MutableText initial = Text.empty(); + return StreamSupport.stream(elements.spliterator(), false).collect(Collectors.reducing(initial, (a, b) -> { + return a == initial ? b : a.append(delimiter).append(b); + })); + } } diff --git a/src/main/java/com/minelittlepony/unicopia/client/gui/DismissSpellScreen.java b/src/main/java/com/minelittlepony/unicopia/client/gui/DismissSpellScreen.java index ca759456..57ea2a25 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/gui/DismissSpellScreen.java +++ b/src/main/java/com/minelittlepony/unicopia/client/gui/DismissSpellScreen.java @@ -9,7 +9,7 @@ import com.minelittlepony.common.client.gui.GameGui; import com.minelittlepony.unicopia.USounds; import com.minelittlepony.unicopia.ability.magic.SpellPredicate; import com.minelittlepony.unicopia.ability.magic.spell.*; -import com.minelittlepony.unicopia.client.FlowingText; +import com.minelittlepony.unicopia.client.TextHelper; import com.minelittlepony.unicopia.client.render.model.SphereModel; import com.minelittlepony.unicopia.entity.player.Pony; import com.minelittlepony.unicopia.item.UItems; @@ -195,7 +195,7 @@ public class DismissSpellScreen extends GameGui { tooltip.add(ScreenTexts.EMPTY); tooltip.add(Text.translatable("gui.unicopia.dispell_screen.affinity", actualSpell.getAffinity().name()).formatted(actualSpell.getAffinity().getColor())); tooltip.add(ScreenTexts.EMPTY); - tooltip.addAll(FlowingText.wrap(Text.translatable(actualSpell.getType().getTranslationKey() + ".lore").formatted(actualSpell.getAffinity().getColor()), 180).toList()); + tooltip.addAll(TextHelper.wrap(Text.translatable(actualSpell.getType().getTranslationKey() + ".lore").formatted(actualSpell.getAffinity().getColor()), 180).toList()); if (spell instanceof TimedSpell timed) { tooltip.add(ScreenTexts.EMPTY); tooltip.add(Text.translatable("gui.unicopia.dispell_screen.time_left", StringHelper.formatTicks(timed.getTimer().getTicksRemaining()))); diff --git a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookScreen.java b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookScreen.java index 0dc0a71e..20a1dfb7 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookScreen.java +++ b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookScreen.java @@ -14,7 +14,7 @@ import com.minelittlepony.unicopia.Debug; import com.minelittlepony.unicopia.USounds; import com.minelittlepony.unicopia.Unicopia; import com.minelittlepony.unicopia.ability.magic.spell.effect.CustomisedSpellType; -import com.minelittlepony.unicopia.client.FlowingText; +import com.minelittlepony.unicopia.client.TextHelper; import com.minelittlepony.unicopia.client.gui.*; import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.*; import com.minelittlepony.unicopia.compat.trinkets.TrinketSlotBackSprites; @@ -219,7 +219,7 @@ public class SpellbookScreen extends HandledScreen imple List tooltip = new ArrayList<>(); tooltip.add(spell.type().getName()); - tooltip.addAll(FlowingText.wrap(Text.translatable(spell.type().getTranslationKey() + ".lore").formatted(spell.type().getAffinity().getColor()), 180).toList()); + tooltip.addAll(TextHelper.wrap(Text.translatable(spell.type().getTranslationKey() + ".lore").formatted(spell.type().getAffinity().getColor()), 180).toList()); context.drawTooltip(textRenderer, tooltip, x, y); diff --git a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookTraitDexPageContent.java b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookTraitDexPageContent.java index e8f83db6..c5f7f347 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookTraitDexPageContent.java +++ b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookTraitDexPageContent.java @@ -9,7 +9,7 @@ import com.minelittlepony.common.client.gui.element.Label; import com.minelittlepony.common.client.gui.sprite.TextureSprite; import com.minelittlepony.unicopia.USounds; import com.minelittlepony.unicopia.ability.magic.spell.trait.*; -import com.minelittlepony.unicopia.client.FlowingText; +import com.minelittlepony.unicopia.client.TextHelper; import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.Chapter; import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookScreen.ImageButton; import com.minelittlepony.unicopia.container.SpellbookState; @@ -191,7 +191,7 @@ public class SpellbookTraitDexPageContent implements SpellbookChapterList.Conten .setTextureSize(16, 16) .setSize(16, 16) .setTexture(trait.getSprite())); - getStyle().setTooltip(Tooltip.of(FlowingText.wrap(trait.getTooltip(), 200).toList())); + getStyle().setTooltip(Tooltip.of(TextHelper.wrap(trait.getTooltip(), 200).toList())); onClick(sender -> Pony.of(MinecraftClient.getInstance().player).getDiscoveries().markRead(trait)); } diff --git a/src/main/java/com/minelittlepony/unicopia/command/RacelistCommand.java b/src/main/java/com/minelittlepony/unicopia/command/RacelistCommand.java index 333da868..30a19dd0 100644 --- a/src/main/java/com/minelittlepony/unicopia/command/RacelistCommand.java +++ b/src/main/java/com/minelittlepony/unicopia/command/RacelistCommand.java @@ -1,13 +1,16 @@ package com.minelittlepony.unicopia.command; +import java.util.HashSet; +import java.util.Set; import java.util.function.Function; - import com.minelittlepony.unicopia.*; +import com.minelittlepony.unicopia.client.TextHelper; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; @@ -15,8 +18,41 @@ class RacelistCommand { static LiteralArgumentBuilder create() { return CommandManager.literal("racelist").requires(s -> s.hasPermissionLevel(3)) + .then(CommandManager.literal("show") + .executes(context -> { + context.getSource().sendFeedback(() -> { + Set whitelist = Unicopia.getConfig().speciesWhiteList.get(); + if (whitelist.isEmpty()) { + return Text.translatable("commands.racelist.inactive"); + } + Set allowed = new HashSet<>(); + Set unallowed = new HashSet<>(); + Race.REGISTRY.forEach(race -> { + (race.isPermitted(null) ? allowed : unallowed).add(Text.translatable("commands.racelist.get.list_item", + race.getDisplayName(), + Text.literal(race.getId().toString()).formatted(Formatting.GRAY) + )); + }); + + return Text.translatable("commands.racelist.get.allowed", allowed.size()).formatted(Formatting.YELLOW) + .append("\n").append(TextHelper.join(Text.literal("\n"), allowed)) + .append("\n") + .append(Text.translatable("commands.racelist.get.not_allowed", unallowed.size()).formatted(Formatting.YELLOW)) + .append("\n").append(TextHelper.join(Text.literal("\n"), unallowed)); + }, false); + return 0; + }) + ) + .then(CommandManager.literal("reset") + .executes(context -> { + Unicopia.getConfig().speciesWhiteList.get().clear(); + Unicopia.getConfig().save(); + context.getSource().sendFeedback(() -> Text.translatable("commands.racelist.clear.success").formatted(Formatting.GREEN), false); + return 0; + }) + ) .then(CommandManager.literal("allow") - .then(CommandManager.argument("race", Race.argument()) + .then(CommandManager.argument("race", Race.argument()).suggests(UCommandSuggestion.ALL_RACE_SUGGESTIONS) .executes(context -> toggle(context.getSource(), context.getSource().getPlayer(), Race.fromArgument(context, "race"), "allowed", race -> { if (race.isUnset()) { @@ -31,7 +67,7 @@ class RacelistCommand { })) )) .then(CommandManager.literal("disallow") - .then(CommandManager.argument("race", Race.argument()) + .then(CommandManager.argument("race", Race.argument()).suggests(UCommandSuggestion.ALL_RACE_SUGGESTIONS) .executes(context -> toggle(context.getSource(), context.getSource().getPlayer(), Race.fromArgument(context, "race"), "disallowed", race -> { boolean result = Unicopia.getConfig().speciesWhiteList.get().remove(race.getId().toString()); diff --git a/src/main/java/com/minelittlepony/unicopia/command/SpeciesCommand.java b/src/main/java/com/minelittlepony/unicopia/command/SpeciesCommand.java index 1363ee96..e1e9e999 100644 --- a/src/main/java/com/minelittlepony/unicopia/command/SpeciesCommand.java +++ b/src/main/java/com/minelittlepony/unicopia/command/SpeciesCommand.java @@ -6,7 +6,6 @@ import com.minelittlepony.unicopia.entity.player.Pony; import com.minelittlepony.unicopia.network.Channel; import com.minelittlepony.unicopia.network.MsgTribeSelect; import com.mojang.brigadier.builder.LiteralArgumentBuilder; - import net.minecraft.command.argument.EntityArgumentType; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.server.command.CommandManager; @@ -38,13 +37,13 @@ class SpeciesCommand { .executes(context -> get(context.getSource(), EntityArgumentType.getPlayer(context, "target"), false)) )) .then(CommandManager.literal("set") - .then(CommandManager.argument("race", Race.argument()) + .then(CommandManager.argument("race", Race.argument()).suggests(UCommandSuggestion.ALL_RACE_SUGGESTIONS) .executes(context -> set(context.getSource(), context.getSource().getPlayer(), Race.fromArgument(context, "race"), true)) .then(CommandManager.argument("target", EntityArgumentType.player()) .executes(context -> set(context.getSource(), EntityArgumentType.getPlayer(context, "target"), Race.fromArgument(context, "race"), false))) )) .then(CommandManager.literal("describe") - .then(CommandManager.argument("race", Race.argument()) + .then(CommandManager.argument("race", Race.argument()).suggests(UCommandSuggestion.ALL_RACE_SUGGESTIONS) .executes(context -> describe(context.getSource().getPlayer(), Race.fromArgument(context, "race"))) )) .then(CommandManager.literal("list") @@ -60,7 +59,7 @@ class SpeciesCommand { pony.setDirty(); if (race.isUnset()) { - Channel.SERVER_SELECT_TRIBE.sendToPlayer(new MsgTribeSelect(Race.allPermitted(player), "gui.unicopia.tribe_selection.respawn"), (ServerPlayerEntity)player); + Channel.SERVER_SELECT_TRIBE.sendToPlayer(new MsgTribeSelect(Race.allPermitted(player), "gui.unicopia.tribe_selection.welcome"), (ServerPlayerEntity)player); } if (player == source.getPlayer()) { diff --git a/src/main/java/com/minelittlepony/unicopia/command/UCommandSuggestion.java b/src/main/java/com/minelittlepony/unicopia/command/UCommandSuggestion.java new file mode 100644 index 00000000..39d8f858 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/command/UCommandSuggestion.java @@ -0,0 +1,62 @@ +package com.minelittlepony.unicopia.command; + +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; +import org.jetbrains.annotations.Nullable; + +import com.minelittlepony.unicopia.Race; +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 net.minecraft.command.CommandSource; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.util.Identifier; + +public class UCommandSuggestion { + public static final SuggestionProvider ALL_RACE_SUGGESTIONS = suggestFromRegistry(Race.REGISTRY_KEY); + public static final SuggestionProvider ALLOWED_RACE_SUGGESTIONS = suggestFromRegistry(Race.REGISTRY_KEY, (context, race) -> race.isPermitted(context.getSource().getPlayer())); + + public static SuggestionProvider suggestFromRegistry(RegistryKey> registryKey, @Nullable BiPredicate, T> filter) { + return (context, builder) -> { + Registry registry = context.getSource().getRegistryManager().get(registryKey); + return suggestIdentifiers( + filter == null ? registry : registry.stream().filter(v -> filter.test(context, v))::iterator, + registry::getId, + builder, registryKey.getValue().getNamespace()); + }; + } + + public static SuggestionProvider suggestFromRegistry(RegistryKey> registryKey) { + return suggestFromRegistry(registryKey, null); + } + + public static CompletableFuture suggestIdentifiers(Iterable candidates, Function idFunc, SuggestionsBuilder builder, String defaultNamespace) { + forEachMatching(candidates, builder.getRemaining().toLowerCase(Locale.ROOT), idFunc, id -> builder.suggest(idFunc.apply(id).toString()), defaultNamespace); + return builder.buildFuture(); + } + + public static void forEachMatching(Iterable candidates, String input, Function idFunc, Consumer consumer, String defaultNamespace) { + final boolean hasNamespaceDelimiter = input.indexOf(58) > -1; + for (T object : candidates) { + final Identifier id = idFunc.apply(object); + if (hasNamespaceDelimiter) { + if (CommandSource.shouldSuggest(input, id.toString())) { + consumer.accept(object); + } + } else { + if (CommandSource.shouldSuggest(input, id.getNamespace()) + || (id.getNamespace().equals(defaultNamespace) && CommandSource.shouldSuggest(input, id.getPath())) + ) { + consumer.accept(object); + } + } + } + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/command/WorldTribeCommand.java b/src/main/java/com/minelittlepony/unicopia/command/WorldTribeCommand.java index e09b9af1..116068f7 100644 --- a/src/main/java/com/minelittlepony/unicopia/command/WorldTribeCommand.java +++ b/src/main/java/com/minelittlepony/unicopia/command/WorldTribeCommand.java @@ -14,7 +14,7 @@ class WorldTribeCommand { return CommandManager.literal("worldtribe").requires(s -> s.hasPermissionLevel(3)) .then(CommandManager.literal("get").executes(context -> get(context.getSource()))) .then(CommandManager.literal("set") - .then(CommandManager.argument("race", Race.argument()) + .then(CommandManager.argument("race", Race.argument()).suggests(UCommandSuggestion.ALLOWED_RACE_SUGGESTIONS) .executes(context -> set(context.getSource(), Race.fromArgument(context, "race"))))); } diff --git a/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java b/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java index 181fd53d..072cee98 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java @@ -231,7 +231,7 @@ public class Pony extends Living implements Copyable, Update public void setSpecies(Race race) { race = race.validate(entity); Race current = getSpecies(); - entity.getDataTracker().set(RACE, Race.REGISTRY.getId(race.validate(entity)).toString()); + entity.getDataTracker().set(RACE, race.getId().toString()); if (race != current) { clearSuppressedRace(); } @@ -244,7 +244,7 @@ public class Pony extends Living implements Copyable, Update } public void setSuppressedRace(Race race) { - entity.getDataTracker().set(SUPPRESSED_RACE, Race.REGISTRY.getId(race.validate(entity)).toString()); + entity.getDataTracker().set(SUPPRESSED_RACE, race.validate(entity).getId().toString()); } public void clearSuppressedRace() { diff --git a/src/main/java/com/minelittlepony/unicopia/item/GemstoneItem.java b/src/main/java/com/minelittlepony/unicopia/item/GemstoneItem.java index 62160c86..dcc35a05 100644 --- a/src/main/java/com/minelittlepony/unicopia/item/GemstoneItem.java +++ b/src/main/java/com/minelittlepony/unicopia/item/GemstoneItem.java @@ -10,7 +10,7 @@ import com.minelittlepony.unicopia.Affinity; import com.minelittlepony.unicopia.Unicopia; import com.minelittlepony.unicopia.ability.magic.spell.effect.CustomisedSpellType; import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType; -import com.minelittlepony.unicopia.client.FlowingText; +import com.minelittlepony.unicopia.client.TextHelper; import com.minelittlepony.unicopia.entity.player.PlayerCharmTracker; import com.minelittlepony.unicopia.entity.player.Pony; import com.minelittlepony.unicopia.item.group.MultiItem; @@ -90,7 +90,7 @@ public class GemstoneItem extends Item implements MultiItem, EnchantableItem { line = line.formatted(Formatting.OBFUSCATED); } - lines.addAll(FlowingText.wrap(line, 180).toList()); + lines.addAll(TextHelper.wrap(line, 180).toList()); } } diff --git a/src/main/java/com/minelittlepony/unicopia/network/MsgRequestSpeciesChange.java b/src/main/java/com/minelittlepony/unicopia/network/MsgRequestSpeciesChange.java index a0af9093..0fe99689 100644 --- a/src/main/java/com/minelittlepony/unicopia/network/MsgRequestSpeciesChange.java +++ b/src/main/java/com/minelittlepony/unicopia/network/MsgRequestSpeciesChange.java @@ -36,7 +36,11 @@ public record MsgRequestSpeciesChange ( Pony player = Pony.of(sender); if (force || player.getSpecies().isUnset()) { - player.setSpecies(newRace.isPermitted(sender) ? newRace : UnicopiaWorldProperties.forWorld((ServerWorld)player.asWorld()).getDefaultRace()); + boolean permitted = newRace.isPermitted(sender); + player.setSpecies(permitted ? newRace : UnicopiaWorldProperties.forWorld((ServerWorld)player.asWorld()).getDefaultRace()); + if (!permitted) { + sender.sendMessageToClient(Text.translatable("respawn.reason.illegal_race", newRace.getDisplayName()), false); + } if (force) { if (sender.getWorld().getGameRules().getBoolean(UGameRules.ANNOUNCE_TRIBE_JOINS)) { diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json index c55ccb5b..ff4debfb 100644 --- a/src/main/resources/assets/unicopia/lang/en_us.json +++ b/src/main/resources/assets/unicopia/lang/en_us.json @@ -699,6 +699,7 @@ "gui.unicopia.page_num": "%d of %d", "respawn.reason.joined_new_tribe": "%1$s was reborn as a %2$s", + "respawn.reason.illegal_race": "The %s race is not permitted by your server's configuration.", "gui.unicopia.tribe_selection.respawn": "You have died.", "gui.unicopia.tribe_selection.respawn.journey": "But the end is not all, for at the end of every end is another beginning.", @@ -1327,7 +1328,12 @@ "commands.racelist.illegal": "The default race %s cannot be used with this command.", "commands.racelist.allowed": "Added %1$s to the whitelist.", - "commands.racelist.allowed.failed": "%1$s is already whitelisted.", + "commands.racelist.get.allowed": "Allowed (%s):", + "commands.racelist.get.not_allowed": "Not Allowed (%s):", + "commands.racelist.get.list_item": "- %s (%s)", + "commands.racelist.clear.success": "Disabled Whitelist", + "commands.racelist.allowed.failed": "%1$s is already allowed.", + "commands.racelist.inactive": "The allowlist is not active. Add races with /unicopia racelist allow to configure it.", "commands.racelist.disallowed": "Removed %1$s from the whitelist.", "commands.racelist.disallowed.failed": "%1$s is not on the whitelist.", @@ -1335,7 +1341,6 @@ "commands.worldtribe.success.get": "Default race for all new players is currently set to: %s", "commands.worldtribe.success.set": "Set default race for new players is now set to: %s", - "commands.disguise.usage": "/disguise [nbt]", "commands.disguise.notfound": "The entity id '%s' does not exist.", "commands.disguise.removed": "Your disguise has been removed.", "commands.disguise.removed.self": "Removed own disguise.",