Settings rework and encapsulation (#1330)

* Settings rework and encapsulation

* Settings overhaul 3

Move everything in the new system to a new API package

Require rules using the new system to specify descriptions in their translation files

Implement temporary system on top of current translations to allow that to work properly

* Fix getRules fallback

* Final touches to settings refactor

* Mention that set shouldn't throw and remove comment

* Change translation keys to unify with translation PR

* Deprecate printAllRulesToLog to allow using any PrintStream

* Remove restriction of CarpetRules being Comparable

And fix overreaction in a deprecation message

* Bring back user input to rule observers

* Make Validator deprecation warning not spam on use

* Remove unneeded abstract methods in CarpetRule already inherited from CarpetRule

They literally do nothing, don't even force them to be implemented, and may be confusing as to why they're there or as if they are needed to be implemented

* Fix legacy SettingsManager field not being assigned correctly

* Finish fixing conflicts from future merge

* Actually fix remaining conflicts from future merge (missing imports)

* Import the newer rule in SettingsManager

* Refactor Validators

* Don't spam deprecation warnings on custom managers

* Get out the var

* Fix missing warning in old rule observers

* Update src/main/java/carpet/api/settings/Rule.java

* Remove `@ApiStatus.Experimental` annotations

* Fix incorrect `@deprecated` javadoc tags

By removing them given the class is deprecated anyway and else there's too much noise

* Add missing static import

* Remove TypedField and use a regular field

I liked that class tbh, but it's true that it wasn't really needed

* Move `field` assignment out of for removal

* Fix not following contract of `type()` returning boxed value

Previously handled by TypedField

* Fix few incorrect translation keys

* Remove resaving of comments when saving config files

Comments ignoring is still a thing when reading, so `locked` configs for example will keep comments

* Condense rule load messages in one

* Only print loaded settings message when at least one setting was changed

* Fix legacy validator detector

* Tweak and add more context to response to invalid values

* Fix strict validator not actually erroring on invalid input
This commit is contained in:
altrisi authored and GitHub committed 2022-06-27 13:12:15 +02:00
1 parent f5a92ebb8e
commit 0a972d6c56
44 files changed
+2687 -1616

No files matched your search

+19 -6
View File
@@ -1,13 +1,14 @@
package carpet;
import carpet.script.CarpetExpression;
import carpet.settings.SettingsManager;
import carpet.api.settings.SettingsManager;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.RegistryAccess;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import java.util.Collections;
import java.util.Map;
public interface CarpetExtension
@@ -69,7 +70,11 @@ public interface CarpetExtension
registerCommands(dispatcher);
}
/**
* @deprecated Implement {@link #extensionSettingsManager()} instead
*/
@Deprecated(forRemoval = true)
default carpet.settings.SettingsManager customSettingsManager() {return null;}
/**
* Provide your own custom settings manager managed in the same way as base /carpet
@@ -78,7 +83,15 @@ public interface CarpetExtension
* @return Your custom {@link SettingsManager} instance to be managed by Carpet
*
*/
default SettingsManager customSettingsManager() {return null;}
default SettingsManager extensionSettingsManager() {
// Warn extensions overriding the other (deprecated) method, go ahead and override this if you want to provide a custom SettingsManager
SettingsManager deprecatedManager = customSettingsManager();
if (deprecatedManager != null) {
// Extension is providing a manager via the old method (and also hasn't overriden this)
CarpetServer.warnOutdatedManager(this);
}
return customSettingsManager();
}
/**
* Event that gets called when a player logs in
@@ -134,10 +147,10 @@ public interface CarpetExtension
*
* @param lang A {@link String} being the language id selected by the user
* @return A {@link Map<String, String>} containing the string key with it's
* respective translation {@link String} or {@link null} if not available
* respective translation {@link String} or an empty map if not available
*
*/
default Map<String, String> canHasTranslations(String lang) { return null;}
default Map<String, String> canHasTranslations(String lang) { return Collections.emptyMap();}
/**
* Handles each call that creates / parses the scarpet expression.
+20 -5
View File
@@ -1,8 +1,10 @@
package carpet;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import carpet.commands.CounterCommand;
import carpet.commands.DistanceCommand;
@@ -22,7 +24,7 @@ import carpet.helpers.HopperCounter;
import carpet.helpers.TickSpeed;
import carpet.logging.LoggerRegistry;
import carpet.script.CarpetScriptServer;
import carpet.settings.SettingsManager;
import carpet.api.settings.SettingsManager;
import carpet.logging.HUDController;
import carpet.utils.FabricAPIHooks;
import carpet.utils.MobAI;
@@ -43,7 +45,7 @@ public class CarpetServer // static for now - easier to handle all around the co
public static MinecraftServer minecraft_server;
private static CommandDispatcher<CommandSourceStack> currentCommandDispatcher;
public static CarpetScriptServer scriptServer;
public static SettingsManager settingsManager;
public static carpet.settings.SettingsManager settingsManager; // to change type to api type, can't change right now because of binary and source compat
public static final List<CarpetExtension> extensions = new ArrayList<>();
// Separate from onServerLoaded, because a server can be loaded multiple times in singleplayer
@@ -77,7 +79,7 @@ public class CarpetServer // static for now - easier to handle all around the co
// to register before this call in a ModInitializer (declared in fabric.mod.json)
public static void onGameStarted()
{
settingsManager = new SettingsManager(CarpetSettings.carpetVersion, "carpet", "Carpet Mod");
settingsManager = new carpet.settings.SettingsManager(CarpetSettings.carpetVersion, "carpet", "Carpet Mod");
settingsManager.parseSettingsClass(CarpetSettings.class);
extensions.forEach(CarpetExtension::onGameStarted);
FabricAPIHooks.initialize();
@@ -92,7 +94,7 @@ public class CarpetServer // static for now - easier to handle all around the co
settingsManager.attachServer(server);
extensions.forEach(e -> {
SettingsManager sm = e.customSettingsManager();
SettingsManager sm = e.extensionSettingsManager();
if (sm != null) sm.attachServer(server);
e.onServerLoaded(server);
});
@@ -129,7 +131,7 @@ public class CarpetServer // static for now - easier to handle all around the co
}
settingsManager.registerCommand(dispatcher, commandBuildContext);
extensions.forEach(e -> {
SettingsManager sm = e.customSettingsManager();
SettingsManager sm = e.extensionSettingsManager();
if (sm != null) sm.registerCommand(dispatcher, commandBuildContext);
});
TickCommand.register(dispatcher, commandBuildContext);
@@ -204,6 +206,10 @@ public class CarpetServer // static for now - easier to handle all around the co
public static void onServerDoneClosing(MinecraftServer server)
{
settingsManager.detachServer();
extensions.forEach(e -> {
SettingsManager manager = e.extensionSettingsManager();
if (manager != null) manager.detachServer();
});
}
public static void registerExtensionLoggers()
@@ -216,5 +222,14 @@ public class CarpetServer // static for now - easier to handle all around the co
scriptServer.reload(server);
extensions.forEach(e -> e.onReload(server));
}
private static final Set<CarpetExtension> warnedOutdatedManagerProviders = new HashSet<>();
static void warnOutdatedManager(CarpetExtension ext)
{
if (!warnedOutdatedManagerProviders.contains(ext))
CarpetSettings.LOG.warn("""
%s is providing a SettingsManager from an outdated method in CarpetExtension!
This behaviour will not work in later Carpet versions and the manager won't be registered!""".formatted(ext.getClass().getName()));
}
}
+73 -72
View File
@@ -1,11 +1,13 @@
package carpet;
import carpet.api.settings.CarpetRule;
import carpet.api.settings.RuleCategory;
import carpet.api.settings.Validators;
import carpet.api.settings.Validator;
import carpet.script.utils.AppStoreManager;
import carpet.settings.ParsedRule;
import carpet.settings.Rule;
import carpet.settings.SettingsManager;
import carpet.settings.Validator;
import carpet.utils.Translations;
import carpet.utils.CommandHelper;
import carpet.utils.Messenger;
import carpet.utils.SpawnChunks;
import net.minecraft.commands.CommandSourceStack;
@@ -30,19 +32,19 @@ import java.util.Iterator;
import java.util.Locale;
import java.util.Optional;
import static carpet.settings.RuleCategory.BUGFIX;
import static carpet.settings.RuleCategory.COMMAND;
import static carpet.settings.RuleCategory.CREATIVE;
import static carpet.settings.RuleCategory.EXPERIMENTAL;
import static carpet.settings.RuleCategory.FEATURE;
import static carpet.settings.RuleCategory.OPTIMIZATION;
import static carpet.settings.RuleCategory.SURVIVAL;
import static carpet.settings.RuleCategory.TNT;
import static carpet.settings.RuleCategory.DISPENSER;
import static carpet.settings.RuleCategory.SCARPET;
import static carpet.settings.RuleCategory.CLIENT;
import static carpet.api.settings.RuleCategory.BUGFIX;
import static carpet.api.settings.RuleCategory.COMMAND;
import static carpet.api.settings.RuleCategory.CREATIVE;
import static carpet.api.settings.RuleCategory.EXPERIMENTAL;
import static carpet.api.settings.RuleCategory.FEATURE;
import static carpet.api.settings.RuleCategory.OPTIMIZATION;
import static carpet.api.settings.RuleCategory.SURVIVAL;
import static carpet.api.settings.RuleCategory.TNT;
import static carpet.api.settings.RuleCategory.DISPENSER;
import static carpet.api.settings.RuleCategory.SCARPET;
import static carpet.api.settings.RuleCategory.CLIENT;
@SuppressWarnings("CanBeFinal")
@SuppressWarnings({"CanBeFinal", "removal"}) // removal should be removed after migrating rules to the new system
public class CarpetSettings
{
public static final String carpetVersion = "1.4.79+v220607";
@@ -56,29 +58,25 @@ public class CarpetSettings
public static final int vanillaStructureBlockLimit = 48;
private static class LanguageValidator extends Validator<String> {
@Override public String validate(CommandSourceStack source, ParsedRule<String> currentRule, String newValue, String string) {
if (currentRule.get().equals(newValue) || source == null)
{
return newValue;
}
@Override public String validate(CommandSourceStack source, CarpetRule<String> currentRule, String newValue, String string) {
if (!Translations.isValidLanguage(newValue))
{
Messenger.m(source, "r "+newValue+" is not a valid language");
return null;
}
CarpetSettings.language = newValue;
Translations.updateLanguage(source);
Translations.updateLanguage();
return newValue;
}
}
@Rule(
desc = "sets the language for carpet",
desc = "Sets the language for Carpet",
category = FEATURE,
options = {"none", "zh_cn", "zh_tw"},
strict = false,
options = {"en_us", "zh_cn", "zh_tw"},
strict = true, // the current system doesn't handle fallbacks and other, not defined languages would make unreadable mess. Change later
validate = LanguageValidator.class
)
public static String language = "none";
public static String language = "en_us";
/*
These will be turned when events can be added / removed in code
@@ -129,7 +127,7 @@ public class CarpetSettings
*/
private static class CarpetPermissionLevel extends Validator<String> {
@Override public String validate(CommandSourceStack source, ParsedRule<String> currentRule, String newValue, String string) {
@Override public String validate(CommandSourceStack source, CarpetRule<String> currentRule, String newValue, String string) {
if (source == null || source.hasPermission(4))
return newValue;
return null;
@@ -174,14 +172,14 @@ public class CarpetSettings
private static class OneHourMaxDelayLimit extends Validator<Integer> {
@Override public Integer validate(CommandSourceStack source, ParsedRule<Integer> currentRule, Integer newValue, String string) {
@Override public Integer validate(CommandSourceStack source, CarpetRule<Integer> currentRule, Integer newValue, String string) {
return (newValue > 0 && newValue <= 72000) ? newValue : null;
}
@Override
public String description() { return "You must choose a value from 1 to 72000";}
}
@Rule(desc = "Dropping entire stacks works also from on the crafting UI result slot", category = {BUGFIX, SURVIVAL})
@Rule(desc = "Dropping entire stacks works also from on the crafting UI result slot", category = {RuleCategory.BUGFIX, SURVIVAL})
public static boolean ctrlQCraftingFix = false;
@Rule(desc = "Parrots don't get of your shoulders until you receive proper damage", category = {SURVIVAL, FEATURE})
@@ -209,7 +207,7 @@ public class CarpetSettings
public static class StackableShulkerBoxValidator extends Validator<String>
{
@Override
public String validate(CommandSourceStack source, ParsedRule<String> currentRule, String newValue, String string)
public String validate(CommandSourceStack source, CarpetRule<String> currentRule, String newValue, String string)
{
if (newValue.matches("^[0-9]+$")) {
int value = Integer.parseInt(newValue);
@@ -262,8 +260,8 @@ public class CarpetSettings
private static class CheckOptimizedTntEnabledValidator<T> extends Validator<T>
{
@Override
public T validate(CommandSourceStack source, ParsedRule<T> currentRule, T newValue, String string) {
return optimizedTNT || currentRule.defaultValue.equals(newValue) ? newValue : null;
public T validate(CommandSourceStack source, CarpetRule<T> currentRule, T newValue, String string) {
return optimizedTNT || currentRule.defaultValue().equals(newValue) ? newValue : null;
}
@Override
@@ -278,7 +276,7 @@ public class CarpetSettings
private static class TNTRandomRangeValidator extends Validator<Double> {
@Override
public Double validate(CommandSourceStack source, ParsedRule<Double> currentRule, Double newValue, String string) {
public Double validate(CommandSourceStack source, CarpetRule<Double> currentRule, Double newValue, String string) {
return newValue == -1 || newValue >= 0 ? newValue : null;
}
@@ -294,7 +292,7 @@ public class CarpetSettings
private static class TNTAngleValidator extends Validator<Double> {
@Override
public Double validate(CommandSourceStack source, ParsedRule<Double> currentRule, Double newValue, String string) {
public Double validate(CommandSourceStack source, CarpetRule<Double> currentRule, Double newValue, String string) {
return (newValue >= 0 && newValue < Math.PI * 2) || newValue == -1 ? newValue : null;
}
@@ -388,7 +386,7 @@ public class CarpetSettings
private static class ChainStoneSetting extends Validator<String> {
@Override public String validate(CommandSourceStack source, ParsedRule<String> currentRule, String newValue, String string) {
@Override public String validate(CommandSourceStack source, CarpetRule<String> currentRule, String newValue, String string) {
CarpetSettings.doChainStone = !newValue.toLowerCase(Locale.ROOT).equals("false");
CarpetSettings.chainStoneStickToAll = newValue.toLowerCase(Locale.ROOT).equals("stick_to_all");
@@ -485,12 +483,26 @@ public class CarpetSettings
public static String commandScript = "true";
private static class ModulePermissionLevel extends Validator<String> {
@Override public String validate(CommandSourceStack source, ParsedRule<String> currentRule, String newValue, String string) {
int permissionLevel = SettingsManager.getCommandLevel(newValue);
@Override public String validate(CommandSourceStack source, CarpetRule<String> currentRule, String newValue, String string) {
int permissionLevel = switch (newValue) {
case "false":
yield 0;
case "true":
case "ops":
yield 2;
case "0":
case "1":
case "2":
case "3":
case "4":
yield Integer.parseInt(newValue);
default: throw new IllegalArgumentException();
};
if (source != null && !source.hasPermission(permissionLevel))
return null;
CarpetSettings.runPermissionLevel = permissionLevel;
CarpetServer.settingsManager.notifyPlayersCommandsChanged();
if (source != null)
CommandHelper.notifyPlayersCommandsChanged(source.getServer());
return newValue;
}
@Override
@@ -506,7 +518,7 @@ public class CarpetSettings
},
category = {SCARPET},
options = {"ops", "0", "1", "2", "3", "4"},
validate = {Validator._COMMAND_LEVEL_VALIDATOR.class, ModulePermissionLevel.class}
validate = {Validators.CommandLevel.class, ModulePermissionLevel.class}
)
public static String commandScriptACE = "ops";
@@ -585,7 +597,7 @@ public class CarpetSettings
//public static boolean miningGhostBlockFix = false;
private static class PushLimitLimits extends Validator<Integer> {
@Override public Integer validate(CommandSourceStack source, ParsedRule<Integer> currentRule, Integer newValue, String string) {
@Override public Integer validate(CommandSourceStack source, CarpetRule<Integer> currentRule, Integer newValue, String string) {
return (newValue>0 && newValue <= 1024) ? newValue : null;
}
@Override
@@ -610,7 +622,7 @@ public class CarpetSettings
public static int railPowerLimit = 9;
private static class FillLimitLimits extends Validator<Integer> {
@Override public Integer validate(CommandSourceStack source, ParsedRule<Integer> currentRule, Integer newValue, String string) {
@Override public Integer validate(CommandSourceStack source, CarpetRule<Integer> currentRule, Integer newValue, String string) {
return (newValue>0 && newValue <= 20000000) ? newValue : null;
}
@Override
@@ -640,7 +652,7 @@ public class CarpetSettings
options = {"0", "1", "20"},
category = OPTIMIZATION,
strict = false,
validate = Validator.NONNEGATIVE_NUMBER.class
validate = Validators.NonNegativeNumber.class
)
public static int maxEntityCollisions = 0;
@@ -649,7 +661,7 @@ public class CarpetSettings
options = {"0", "12", "20", "40"},
category = CREATIVE,
strict = false,
validate = Validator.NONNEGATIVE_NUMBER.class
validate = Validators.NonNegativeNumber.class
)
public static int pingPlayerListLimit = 12;
/*
@@ -680,9 +692,9 @@ public class CarpetSettings
private static class ViewDistanceValidator extends Validator<Integer>
{
@Override public Integer validate(CommandSourceStack source, ParsedRule<Integer> currentRule, Integer newValue, String string)
@Override public Integer validate(CommandSourceStack source, CarpetRule<Integer> currentRule, Integer newValue, String string)
{
if (currentRule.get().equals(newValue) || source == null)
if (currentRule.value().equals(newValue) || source == null)
{
return newValue;
}
@@ -721,9 +733,9 @@ public class CarpetSettings
private static class SimulationDistanceValidator extends Validator<Integer>
{
@Override public Integer validate(CommandSourceStack source, ParsedRule<Integer> currentRule, Integer newValue, String string)
@Override public Integer validate(CommandSourceStack source, CarpetRule<Integer> currentRule, Integer newValue, String string)
{
if (currentRule.get().equals(newValue) || source == null)
if (currentRule.value().equals(newValue) || source == null)
{
return newValue;
}
@@ -773,14 +785,14 @@ public class CarpetSettings
SpawnChunks.changeSpawnChunks(overworld.getChunkSource(), centerChunk, size);
}
}
@Override public Integer validate(CommandSourceStack source, ParsedRule<Integer> currentRule, Integer newValue, String string) {
@Override public Integer validate(CommandSourceStack source, CarpetRule<Integer> currentRule, Integer newValue, String string) {
if (source == null) return newValue;
if (newValue < 0 || newValue > 32)
{
Messenger.m(source, "r spawn chunk size has to be between 0 and 32");
return null;
}
if (currentRule.get().intValue() == newValue.intValue())
if (currentRule.value().intValue() == newValue.intValue())
{
//must been some startup thing
return newValue;
@@ -805,42 +817,31 @@ public class CarpetSettings
public static int spawnChunksSize = 11;
public static class LightBatchValidator extends Validator<Integer> {
public static void applyLightBatchSizes()
public static void applyLightBatchSizes(int maxBatchSize)
{
Iterator<ServerLevel> iterator = CarpetServer.minecraft_server.getAllLevels().iterator();
while (iterator.hasNext())
{
ServerLevel serverWorld = iterator.next();
serverWorld.getChunkSource().getLightEngine().setTaskPerBatch(lightEngineMaxBatchSize);
serverWorld.getChunkSource().getLightEngine().setTaskPerBatch(maxBatchSize);
}
}
@Override public Integer validate(CommandSourceStack source, ParsedRule<Integer> currentRule, Integer newValue, String string) {
@Override public Integer validate(CommandSourceStack source, CarpetRule<Integer> currentRule, Integer newValue, String string) {
if (source == null) return newValue;
if (newValue < 0)
{
Messenger.m(source, "r light batch size has to be at least 0");
return null;
}
if (currentRule.get().intValue() == newValue.intValue())
if (currentRule.value().intValue() == newValue.intValue())
{
//must been some startup thing
return newValue;
}
if (CarpetServer.minecraft_server == null) return newValue;
// Set the field before we apply.
try
{
currentRule.field.set(null, newValue.intValue());
}
catch (IllegalAccessException e)
{
Messenger.m(source, "r Unable to access setting for "+currentRule.name);
return null;
}
applyLightBatchSizes(); // Apply new settings
applyLightBatchSizes(newValue); // Apply new settings
return newValue;
}
@@ -881,13 +882,13 @@ public class CarpetSettings
)
public static boolean renewableDeepslate = false;
@Rule(desc = "fixes block placement rotation issue when player rotates quickly while placing blocks", category = BUGFIX)
@Rule(desc = "fixes block placement rotation issue when player rotates quickly while placing blocks", category = RuleCategory.BUGFIX)
public static boolean placementRotationFix = false;
@Rule(
desc = "Fixes leads breaking/becoming invisible in unloaded chunks",
extra = "You may still get visibly broken leash links on the client side, but server side the link is still there.",
category = BUGFIX
category = RuleCategory.BUGFIX
)// needs checkfix for 1.15
public static boolean leadFix = false;
@@ -943,7 +944,7 @@ public class CarpetSettings
},
category = {CREATIVE, CLIENT},
strict = false,
validate = Validator.NONNEGATIVE_NUMBER.class
validate = Validators.NonNegativeNumber.class
)
public static double creativeFlySpeed = 1.0;
@@ -959,7 +960,7 @@ public class CarpetSettings
},
category = {CREATIVE, CLIENT},
strict = false,
validate = Validator.PROBABILITY.class
validate = Validators.Probablity.class
)
public static double creativeFlyDrag = 0.09;
@@ -975,7 +976,7 @@ public class CarpetSettings
public static class StructureBlockLimitValidator extends Validator<Integer> {
@Override public Integer validate(CommandSourceStack source, ParsedRule<Integer> currentRule, Integer newValue, String string) {
@Override public Integer validate(CommandSourceStack source, CarpetRule<Integer> currentRule, Integer newValue, String string) {
return (newValue >= vanillaStructureBlockLimit) ? newValue : null;
}
@@ -1002,7 +1003,7 @@ public class CarpetSettings
public static class StructureBlockIgnoredValidator extends Validator<String> {
@Override
public String validate(CommandSourceStack source, ParsedRule<String> currentRule, String newValue, String string) {
public String validate(CommandSourceStack source, CarpetRule<String> currentRule, String newValue, String string) {
Optional<Block> ignoredBlock = Registry.BLOCK.getOptional(ResourceLocation.tryParse(newValue));
if (!ignoredBlock.isPresent()) {
Messenger.m(source, "r Unknown block '" + newValue + "'.");
@@ -1027,7 +1028,7 @@ public class CarpetSettings
options = {"96", "192", "2048"},
category = {CREATIVE, CLIENT},
strict = false,
validate = Validator.NONNEGATIVE_NUMBER.class
validate = Validators.NonNegativeNumber.class
)
public static int structureBlockOutlineDistance = 96;
@@ -1050,7 +1051,7 @@ public class CarpetSettings
private static class UpdateSuppressionBlockModes extends Validator<Integer> {
@Override
public Integer validate(CommandSourceStack source, ParsedRule<Integer> currentRule, Integer newValue, String string) {
public Integer validate(CommandSourceStack source, CarpetRule<Integer> currentRule, Integer newValue, String string) {
return newValue < -1 ? null : newValue;
}
@Override
@@ -0,0 +1,115 @@
package carpet.api.settings;
import java.util.Collection;
import java.util.List;
import org.apache.commons.lang3.ClassUtils;
import carpet.network.ServerNetworkHandler;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.Component;
/**
* <p>A Carpet rule, that can return its required properties and stores a value.</p>
*
* <p>Name and description translations are picked up from the translation system.</p>
*
* @param <T> The value's type
*/
public interface CarpetRule<T> {
/**
* <p>Returns this rule's name</p>
*
* <p>This is also the rule's id, used as its key in {@link SettingsManager}, for changing rules in-game and for creation
* of translation keys for the rule.</p>
*
* <p>This method must always return the same value for the same {@link CarpetRule}.</p>
*/
String name();
/**
* <p>Returns a {@link List} of {@link BaseComponent} with extra information about this rule, that is,
* the lines after the rule's description.</p>
*
* <p>Handling of translation of the result of this method is responsibility of the rule implementation.</p>
*/
List<Component> extraInfo();
/**
* <p>Returns a {@link Collection} of categories this rule is on.</p>
*/
Collection<String> categories();
/**
* <p>Returns a {@link Collection} of suggestions for values that this rule will be able
* to accept as {@link String strings}. The rule must be able to accept all of the suggestions in the returned {@link Collection} as a value,
* though it may have requirements for those to be applicable.</p>
*
* <p>The returned collection must contain the rule's default value.</p>
*/
Collection<String> suggestions();
/**
* <p>Returns the {@link SettingsManager} this rule is in.</p>
*
* <p>This method may be removed or changed in the future, and is not part of the api contract, but it's needed to sync
* the rule with clients and other workings. Overriding this method should be safe.</p>
*/
SettingsManager settingsManager();
/**
* <p>Returns this rule's value</p>
*/
T value();
/**
* <p>Returns whether this rule can be toggled in the client-side when not connected to a
* Carpet server.</p>
*
* <p>In the default implementation, this is the case when {@link #categories()} contains {@link RuleCategory#CLIENT} </p>
*/
boolean canBeToggledClientSide();
/**
* <p>Returns the type of this rule's value.</p>
*
* <p>If this rule's type is primitive, it returns a wrapped version of it (such as the result of running
* {@link ClassUtils#primitiveToWrapper(Class)} on it) </p>
*/
Class<T> type();
/**
* <p>Returns this rule's default value.</p>
*
* <p>This value will never be {@code null}, and will always be a valid value for {@link #set(CommandSourceStack, Object)}.</p>
*/
T defaultValue();
/**
* <p>Sets this rule's value to the provided {@link String}, after first converting the {@link String} into a suitable type.</p>
*
* <p>This methods run any required validation on the value first, and throws {@link InvalidRuleValueException} if the value is not suitable
* for this rule, regardless of whether it was impossible to convert the value to the required type, the rule doesn't accept the
* value, or the rule is immutable.</p>
*
* <p>Implementations of this method must notify their {@link SettingsManager} by calling
* {@link SettingsManager#notifyRuleChanged(CommandSourceStack, CarpetRule, String)}, who is responsible for
* notifying the {@link ServerNetworkHandler} (if the rule isn't restricted from being synchronized with clients)
* and the {@link CarpetEventServer.Event#CARPET_RULE_CHANGES#onCarpetRuleChanges(CarpetRule, CommandSourceStack)} Scarpet event
* in case the value of the rule was changed because of the invocation.</p>
*
* <p>This method must not throw any exception other than the documented {@link InvalidRuleValueException}.</p>
*
* @param source The {@link CommandSourceStack} to notify about the result of this rule change or {@code null} in order to not notify
* @param value The new value for this rule as a {@link String}
* @throws InvalidRuleValueException if the value passed to the method was not valid as a value to this rule, either because of incompatible type,
* because the rule can't accept that value or because there was some requirement missing for that value to be allowed
*/
void set(CommandSourceStack source, String value) throws InvalidRuleValueException;
/**
* <p>This method follows the same contract as {@link #set(CommandSourceStack, String)}, but accepts a value already parsed (though not verified).</p>
* @see #set(CommandSourceStack, String)
*/
void set(CommandSourceStack source, T value) throws InvalidRuleValueException;
}
@@ -0,0 +1,36 @@
package carpet.api.settings;
import carpet.utils.Messenger;
import net.minecraft.commands.CommandSourceStack;
/**
* <p>An {@link Exception} thrown when the value given for a {@link CarpetRule} is invalid.</p>
*
* <p>It can hold a message to be sent to the executing source.</p>
*/
public class InvalidRuleValueException extends Exception {
/**
* <p>Constructs a new {@link InvalidRuleValueException} with a message that will be passed to the executing source</p>
* @param cause The cause of the exception
*/
public InvalidRuleValueException(String cause) {
super(cause);
}
/**
* <p>Constructs a new {@link InvalidRuleValueException} with no detail message, that therefore should not notify the source</p>
*/
public InvalidRuleValueException() {
super();
}
/**
* <p>Notifies the given source with the exception's message if it exists, does nothing if it doesn't exist or it is {@code null}</p>
* @param source The source to notify
*/
public void notifySource(String ruleName, CommandSourceStack source) {
if (getMessage() != null)
Messenger.m(source, "r Couldn't set value for rule " + ruleName + ": "+ getMessage());
}
}
@@ -0,0 +1,82 @@
package carpet.api.settings;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Any field in this class annotated with this class is interpreted as a carpet rule.
* The field must be static and have a type of one of:
* - boolean
* - int
* - double
* - String
* - long
* - float
* - a subclass of Enum
* The default value of the rule will be the initial value of the field.
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Rule
{
/**
* <p>An array of categories the rule is in, as {@link String strings}.</p>
*
* <p>Those must have a corresponding translation key. Categories provided in Carpet's {@link RuleCategory} already
* have translation key.</p>
*/
String[] categories();
/**
* Options to select in menu.
* Inferred for booleans and enums. Otherwise, must be present.
*/
String[] options() default {};
/**
* if a rule is not strict - can take any value, otherwise it needs to match
* any of the options
* For enums, its always strict, same for booleans - no need to set that for them.
*/
boolean strict() default true;
/**
* If specified, the rule will automatically enable or disable
* a builtin Scarpet Rule App with this name.
*/
String appSource() default "";
/**
* The class of the validator checked right before the rule is changed, using the returned value as the new value to set, or cancel the change if null is returned.
*/
@SuppressWarnings("rawtypes")
Class<? extends Validator>[] validators() default {};
/**
* <p>A class or list of classes implementing {@link Condition} that will have their {@link Condition#shouldRegister()} method
* executed while the rule is being parsed in {@link SettingsManager#parseSettingsClass(Class)}, that will cause the rule to be skipped
* if it evaluates to false</p>
*
* @see Condition
*/
Class<? extends Condition>[] conditions() default {};
/**
* <p>Represents a condition that must be met for a rule to be registered in a {@link SettingsManager} via
* {@link SettingsManager#parseSettingsClass(Class)}</p>
*
* @see Rule#conditions()
* @see #shouldRegister()
*/
interface Condition {
/**
* <p>Returns whether the rule that has had this {@link Condition} added should register.</p>
* @return {@code true} to register the rule, {@code false} otherwise
*/
boolean shouldRegister();
}
}
@@ -0,0 +1,29 @@
package carpet.api.settings;
/**
* <p>Multiple categories for rules that Carpet uses and can be used to group other rules under the same
* categories.</p>
*
* <p>These are not the only categories that can be used, you can create your own ones for your rules
* and they will be added to the SettingsManager.</p>
*
*/
public class RuleCategory {
public static final String BUGFIX = "bugfix";
public static final String SURVIVAL = "survival";
public static final String CREATIVE = "creative";
public static final String EXPERIMENTAL = "experimental";
public static final String OPTIMIZATION = "optimization";
public static final String FEATURE = "feature";
public static final String COMMAND = "command";
public static final String TNT = "tnt";
public static final String DISPENSER = "dispenser";
public static final String SCARPET = "scarpet";
/**
* Rules with this {@link RuleCategory} will have a client-side
* counterpart, so they can be set independently without the server
* running Carpet
*/
public static final String CLIENT = "client";
}
@@ -0,0 +1,107 @@
package carpet.api.settings;
import java.util.Locale;
import carpet.utils.TranslationKeys;
import carpet.utils.Translations;
import net.minecraft.commands.CommandSourceStack;
/**
* <p>A helper class for operating with {@link CarpetRule} instances and values.</p>
*
* <p>If a method is visible and has javadocs it's probably API</p>
*/
public final class RuleHelper {
private RuleHelper() {}
/**
* <p>Gets a {@code boolean} value for a given {@link CarpetRule}.</p>
*
* <p>The current implementation is as follows:</p>
* <ul>
* <li>If the rule's type is a {@code boolean}, it will return the value directly.</li>
* <li>If the rule's type is a {@link Number}, it will return {@code true} if the value is greater than zero.</li>
* <li>In any other case, it will return {@code false}.</li>
* </ul>
* @param rule The rule to get the {@code boolean} value from
* @return A {@code boolean} representation of this rule's value
*/
public static boolean getBooleanValue(CarpetRule<?> rule) {
if (rule.type() == Boolean.class) return (boolean) rule.value();
if (Number.class.isAssignableFrom(rule.type())) return ((Number) rule.value()).doubleValue() > 0;
return false;
}
/**
* <p>Converts a rule's value into its similar {@link String} representation.</p>
*
* <p>If the value is an {@link Enum}, this method returns the name of the enum constant lowercased, else
* it returns the result of running {@link #toString()} on the value.</p>
* @param value A rule's value
* @return A {@link String} representation of the given value
*/
public static String toRuleString(Object value) {
if (value instanceof Enum) return ((Enum<?>) value).name().toLowerCase(Locale.ROOT);
return value.toString();
}
/**
* <p>Checks whether the given {@code CarpetRule rule} is in its default value</p>
*
* @param rule The rule to check
* @return {@code true} if the rule's default value equals its current value
*/
public static boolean isInDefaultValue(CarpetRule<?> rule) {
return rule.defaultValue().equals(rule.value());
}
/**
* <p>Resets the given {@link CarpetRule rule} to its default value, and notifies the given source, if provided.</p>
* @param rule The {@link CarpetRule} to reset to its default value
* @param source A {@link ServerCommandSource} to notify about this change, or {@code null}
*
* @param <T> The type of the {@link CarpetRule}
*/
public static <T> void resetToDefault(CarpetRule<T> rule, CommandSourceStack source) {
try {
rule.set(source, rule.defaultValue());
} catch (InvalidRuleValueException e) {
throw new IllegalStateException("Rule couldn't be set to default value!", e);
}
}
// Translations
// The methods from this point are not stable API yet and may change or be removed in binary incompatible ways later
/**
* @param rule The {@link CarpetRule} to get the translated name of
* @return A {@link String} being the translated {@link CarpetRule#name() name} of the given rule, the current language.
*
* @apiNote This method isn't stable API yet and may change or be removed in binary incompatible ways in later Carpet versions
*/
public static String translatedName(CarpetRule<?> rule) {
String key = String.format(TranslationKeys.RULE_NAME_PATTERN, rule.settingsManager().identifier(), rule.name());
return Translations.hasTranslation(key) ? Translations.tr(key) + String.format(" (%s)", rule.name()): rule.name();
}
/**
* @param rule The {@link CarpetRule} to get the translated description of
* @return A {@link String} being the translated description of this rule, in Carpet's configured language.
*
* @apiNote This method isn't stable API yet and may change or be removed in binary incompatible ways in later Carpet versions
*/
public static String translatedDescription(CarpetRule<?> rule) {
return Translations.tr(String.format(TranslationKeys.RULE_DESC_PATTERN, rule.settingsManager().identifier(), rule.name()));
}
/**
* @param manager A settings manager identifier
* @param category A category identifier
* @return The translated name of the category
*
* @apiNote This method isn't stable API yet and may change or be removed in binary incompatible ways in later Carpet versions
*/
public static String translatedCategory(String manager, String category) {
return Translations.tr(TranslationKeys.CATEGORY_PATTERN.formatted(manager, category), category);
}
}
@@ -0,0 +1,790 @@
package carpet.api.settings;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.common.collect.Sets;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import carpet.CarpetExtension;
import carpet.CarpetServer;
import carpet.CarpetSettings;
import carpet.api.settings.Rule.Condition;
import carpet.network.ServerNetworkHandler;
import carpet.settings.ParsedRule;
import carpet.utils.CommandHelper;
import carpet.utils.Messenger;
import carpet.utils.TranslationKeys;
import carpet.utils.Translations;
import net.fabricmc.api.EnvType;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.storage.LevelResource;
import static carpet.script.CarpetEventServer.Event.CARPET_RULE_CHANGES;
import static carpet.utils.Translations.tr;
import static java.util.Comparator.comparing;
import static net.minecraft.commands.Commands.argument;
import static net.minecraft.commands.Commands.literal;
import static net.minecraft.commands.SharedSuggestionProvider.suggest;
/**
* <p>A {@link SettingsManager} is a class that manages {@link CarpetRule carpet rules} in a {@link MinecraftServer},
* including a command to manage it, and allowing other code to hook into rule changes by using {@link RuleObserver rule observers}.</p>
*
* <p>You can create your own {@link SettingsManager} if you want to have your own command that will be handled the same way as Carpet's
* by using {@link #SettingsManager(String, String, String)} and returning it in your {@link CarpetExtension}'s
* {@link CarpetExtension#extensionSettingsManager()} method.</p>
*/
@SuppressWarnings({"deprecation", "removal"}) // remove after removing old system
public class SettingsManager {
private final Map<String, CarpetRule<?>> rules = new HashMap<>();
private final String version;
private final String identifier;
private final String fancyName;
private boolean locked;
private MinecraftServer server;
private final List<RuleObserver> observers = new ArrayList<>();
private static final List<RuleObserver> staticObservers = new ArrayList<>();
static record ConfigReadResult(Map<String, String> ruleMap, boolean locked) {}
/**
* <p>Defines a class that can be notified about a {@link CarpetRule} changing.</p>
*
* @see #ruleChanged(CommandSourceStack, CarpetRule, String)
* @see SettingsManager#registerRuleObserver(RuleObserver)
* @see SettingsManager#registerGlobalRuleObserver(RuleObserver)
*/
@FunctionalInterface
public static interface RuleObserver {
/**
* <p>Notifies this {@link RuleObserver} about the change of a {@link CarpetRule}.</p>
*
* <p>When this is called, the {@link CarpetRule} value has already been set.</p>
*
* @param source The {@link CommandSourceStack} that likely originated this change, and should be the notified source for further
* messages. Can be {@code null} if there was none and the operation shouldn't send feedback.
* @param changedRule The {@link CarpetRule} that changed. Use {@link CarpetRule#value() changedRule.value()} to get the rule's value,
* and pass it to {@link RuleHelper#toRuleString(Object)} to get the {@link String} version of it
* @param userInput The {@link String} that the user entered when changing the rule, or a best-effort representation of it in case that is
* is not available at the time (such as loading from disk or a rule being changed programmatically). Note that this value
* may not represent the same string as converting the current value to a {@link String} via {@link RuleHelper#toRuleString(Object)},
* given the rule implementation may have adapted the input into a different value, for example with the use of a {@link Validator}
*/
void ruleChanged(CommandSourceStack source, CarpetRule<?> changedRule, String userInput);
}
/**
* Creates a new {@link SettingsManager} with the given version, identifier and fancy name
*
* @param version A {@link String} with the mod's version
* @param identifier A {@link String} with the mod's id, will be the command name
* @param fancyName A {@link String} being the mod's fancy name.
*/
public SettingsManager(String version, String identifier, String fancyName)
{
this.version = version;
this.identifier = identifier;
this.fancyName = fancyName;
}
/**
* <p>Registers a {@link RuleObserver} to changes in rules from
* this {@link SettingsManager} instance.</p>
*
* @see SettingsManager#registerGlobalRuleObserver(RuleObserver)
*
* @param observer A {@link RuleObserver} that will be called with
* the used {@link CommandSourceStack} and the changed
* {@link CarpetRule}.
*/
public void registerRuleObserver(RuleObserver observer)
{
observers.add(observer);
}
/**
* Registers a {@link RuleObserver} to changes in rules from
* <b>any</b> {@link SettingsManager} instance (unless their implementation disallows it).
* @see SettingsManager#registerRuleObserver(RuleObserver)
*
* @param observer A {@link RuleObserver} that will be called with
* the used {@link CommandSourceStack}, and the changed
* {@link CarpetRule}.
*/
public static void registerGlobalRuleObserver(RuleObserver observer)
{
staticObservers.add(observer);
}
/**
* @return A {@link String} being this {@link SettingsManager}'s
* identifier, which is also the command name
*/
public String identifier() {
return identifier;
}
/**
* <p>Returns whether this {@link SettingsManager} is locked, and any rules in it should therefore not be
* toggleable and its management command should not be available.</p>
* @return {@code true} if this {@link SettingsManager} is locked
*/
public boolean locked() {
return locked;
}
/**
* Adds all annotated fields with the {@link Rule} annotation
* to this {@link SettingsManager} in order to handle them.
* @param settingsClass The class that will be analyzed
*/
public void parseSettingsClass(Class<?> settingsClass)
{
// In the current translation system languages are not loaded this early. Ensure they are loaded
Translations.updateLanguage();
boolean warned = settingsClass == CarpetSettings.class; // don't warn for ourselves
nextRule: for (Field field : settingsClass.getDeclaredFields())
{
Class<? extends Rule.Condition>[] conditions;
Rule newAnnotation = field.getAnnotation(Rule.class);
carpet.settings.Rule oldAnnotation = field.getAnnotation(carpet.settings.Rule.class);
if (newAnnotation != null) {
conditions = newAnnotation.conditions();
} else if (oldAnnotation != null) {
conditions = oldAnnotation.condition();
if (!warned) {
CarpetSettings.LOG.warn("""
Registering outdated rules for settings class '%s'!
This won't be supported in the future and rules won't be registered!""".formatted(settingsClass.getName()));
warned = true;
}
} else {
continue;
}
for (Class<? extends Rule.Condition> condition : conditions) { //Should this be moved to ParsedRule.of?
try
{
Constructor<? extends Condition> constr = condition.getDeclaredConstructor();
constr.setAccessible(true);
if (!(constr.newInstance()).shouldRegister())
continue nextRule;
}
catch (ReflectiveOperationException e)
{
throw new IllegalArgumentException(e);
}
}
CarpetRule<?> parsed = ParsedRule.of(field, this);
rules.put(parsed.name(), parsed);
}
}
/**
* @return A String {@link Iterable} with all categories
* that the rules in this {@link SettingsManager} have.
* @implNote This method doesn't cache the result, so each call loops through all rules and finds all present categories
*/
public Iterable<String> getCategories()
{
return getCarpetRules().stream().map(CarpetRule::categories).<String>mapMulti(Collection::forEach).collect(Collectors.toSet());
}
/**
* <p>Gets a registered rule in this {@link SettingsManager}.</p>
*
* @param name The name of the rule to get
* @return A {@link CarpetRule} with the provided name or {@code null} if none in this {@link SettingsManager} matches
*/
public CarpetRule<?> getCarpetRule(String name)
{
return rules.get(name);
}
/**
* @return An unmodifiable {@link Collection} of the registered rules in this {@link SettingsManager}.
*/
public Collection<CarpetRule<?>> getCarpetRules()
{
return Collections.unmodifiableCollection(rules.values());
}
/**
* <p>Adds a {@link CarpetRule} to this {@link SettingsManager}.</p>
*
* <p>Useful when having different {@link CarpetRule} implementations instead of a class of {@code static},
* annotated fields.</p>
*
* @param rule The {@link CarpetRule} to add
* @throws UnsupportedOperationException If a rule with that name is already present in this {@link SettingsManager}
*/
public void addCarpetRule(CarpetRule<?> rule) {
if (rules.containsKey(rule.name()))
throw new UnsupportedOperationException(fancyName + " settings manager already contains a rule with that name!");
rules.put(rule.name(), rule);
}
public void notifyRuleChanged(CommandSourceStack source, CarpetRule<?> rule, String userInput)
{
observers.forEach(observer -> observer.ruleChanged(source, rule, userInput));
staticObservers.forEach(observer -> observer.ruleChanged(source, rule, userInput));
ServerNetworkHandler.updateRuleWithConnectedClients(rule);
switchScarpetRuleIfNeeded(source, rule); //TODO move into rule
if (CARPET_RULE_CHANGES.isNeeded()) CARPET_RULE_CHANGES.onCarpetRuleChanges(rule, source);
}
/**
* Attaches a {@link MinecraftServer} to this {@link SettingsManager}.<br>
* This is handled automatically by Carpet and calling it manually is not supported.
*
* @param server The {@link MinecraftServer} instance to be attached
*/
public void attachServer(MinecraftServer server)
{
this.server = server;
loadConfigurationFromConf();
}
/**
* Detaches the {@link MinecraftServer} of this {@link SettingsManager} and
* resets its {@link CarpetRule}s to their default values.<br>
* This is handled automatically by Carpet and calling it manually is not supported.
*/
public void detachServer()
{
for (CarpetRule<?> rule : rules.values()) RuleHelper.resetToDefault(rule, null);
server = null;
}
/**
* <p>Initializes Scarpet rules in this {@link SettingsManager}, if any.</p>
* <p>This is handled automatically by Carpet and calling it is not supported.</p>
*/
public void initializeScarpetRules() { //TODO try remove
for (CarpetRule<?> rule : rules.values())
{
if (rule instanceof ParsedRule<?> pr && !pr.scarpetApp.isEmpty()) {
switchScarpetRuleIfNeeded(server.createCommandSourceStack(), pr);
}
}
}
/**
* Calling this method is not supported.
*/
public void inspectClientsideCommand(CommandSourceStack source, String string)
{
if (string.startsWith("/" + identifier + " "))
{
String[] res = string.split("\\s+", 3);
if (res.length == 3)
{
String rule = res[1];
String strOption = res[2];
if (rules.containsKey(rule) && rules.get(rule).canBeToggledClientSide())
{
try {
rules.get(rule).set(source, strOption);
} catch (InvalidRuleValueException e) {
e.notifySource(rule, source);
}
}
}
}
}
private void switchScarpetRuleIfNeeded(CommandSourceStack source, CarpetRule<?> carpetRule) //TODO remove. This should be handled by the rule
{
if (carpetRule instanceof ParsedRule<?> rule && !rule.scarpetApp.isEmpty())
{
if (RuleHelper.getBooleanValue(rule) || (rule.type() == String.class && !rule.value().equals("false")))
{
CarpetServer.scriptServer.addScriptHost(source, rule.scarpetApp, s -> CommandHelper.canUseCommand(s, rule.value()), false, false, true, null);
} else {
CarpetServer.scriptServer.removeScriptHost(source, rule.scarpetApp, false, true);
}
}
}
private Path getFile()
{
return server.getWorldPath(LevelResource.ROOT).resolve(identifier + ".conf");
}
private Collection<CarpetRule<?>> getRulesSorted()
{
return rules.values().stream().sorted(comparing(CarpetRule::name)).toList();
}
/**
* Disables all {@link CarpetRule}s with the {@link RuleCategory#COMMAND} category,
* called when the {@link SettingsManager} is {@link #locked}.
*/
private void disableBooleanCommands()
{
for (CarpetRule<?> rule : rules.values())
{
if (!rule.categories().contains(RuleCategory.COMMAND))
continue;
try {
if (rule.suggestions().contains("false"))
rule.set(server.createCommandSourceStack(), "false");
else
CarpetSettings.LOG.warn("Couldn't disable command rule "+ rule.name() + ": it doesn't suggest false as a valid option");
} catch (InvalidRuleValueException e) {
throw new IllegalStateException(e); // contract of CarpetRule.suggestions()
}
}
}
private void writeSettingsToConf(ConfigReadResult data)
{
if (locked)
return;
try (BufferedWriter fw = Files.newBufferedWriter(getFile()))
{
for (String key: data.ruleMap().keySet())
{
fw.write(key + " " + data.ruleMap().get(key));
fw.newLine();
}
}
catch (IOException e)
{
CarpetSettings.LOG.error("[CM]: failed write "+identifier+".conf config file", e);
}
}
private Collection<CarpetRule<?>> findStartupOverrides()
{
Set<String> defaults = readSettingsFromConf(getFile()).ruleMap().keySet();
return rules.values().stream().filter(r -> defaults.contains(r.name())).
sorted(comparing(CarpetRule::name)).toList();
}
private Collection<CarpetRule<?>> getNonDefault()
{
return rules.values().stream().filter(Predicate.not(RuleHelper::isInDefaultValue)).sorted(comparing(CarpetRule::name)).toList();
}
private void loadConfigurationFromConf()
{
for (CarpetRule<?> rule : rules.values()) RuleHelper.resetToDefault(rule, server.createCommandSourceStack());
ConfigReadResult conf = readSettingsFromConf(getFile());
locked = false;
if (conf.locked())
{
CarpetSettings.LOG.info("[CM]: "+fancyName+" features are locked by the administrator");
disableBooleanCommands();
}
int loadedCount = 0;
for (String key: conf.ruleMap().keySet())
{
try
{
rules.get(key).set(server.createCommandSourceStack(), conf.ruleMap().get(key));
loadedCount++;
}
catch (InvalidRuleValueException exc)
{
CarpetSettings.LOG.error("[CM Error]: Failed to load setting: " + key, exc);
}
}
if (loadedCount > 0)
CarpetSettings.LOG.info("[CM] Loaded " + loadedCount + " settings from " + identifier + ".conf");
locked = conf.locked();
}
private ConfigReadResult readSettingsFromConf(Path path)
{
try (BufferedReader reader = Files.newBufferedReader(path))
{
String line = "";
boolean confLocked = false;
Map<String, String> result = new HashMap<String, String>();
while ((line = reader.readLine()) != null)
{
line = line.replaceAll("[\\r\\n]", "");
if ("locked".equalsIgnoreCase(line))
{
confLocked = true;
}
String[] fields = line.split("\\s+",2);
if (fields.length > 1)
{
if (result.isEmpty() && fields[0].startsWith("#") || fields[1].startsWith("#"))
{
continue;
}
if (!rules.containsKey(fields[0]))
{
CarpetSettings.LOG.error("[CM]: "+fancyName+" Setting " + fields[0] + " is not a valid rule - ignoring...");
continue;
}
result.put(fields[0], fields[1]);
}
}
return new ConfigReadResult(result, confLocked);
}
catch (NoSuchFileException e)
{
if (path.equals(getFile()) && FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT)
{
Path defaultsPath = FabricLoader.getInstance().getConfigDir().resolve("carpet/default_"+identifier+".conf");
try {
if (Files.notExists(defaultsPath))
{
Files.createDirectories(defaultsPath.getParent());
Files.createFile(defaultsPath);
try (BufferedWriter fw = Files.newBufferedWriter(defaultsPath))
{
fw.write("# This is " + fancyName + "'s default configuration file");
fw.newLine();
fw.write("# Settings specified here will be used when a world doesn't have a config file, but they will be completely "
+ "ignored once the world has one.");
fw.newLine();
}
}
return readSettingsFromConf(defaultsPath);
} catch (IOException e2) {
CarpetSettings.LOG.error("Exception when loading fallback default config: ", e2);
}
}
return new ConfigReadResult(new HashMap<>(), false);
}
catch (IOException e)
{
CarpetSettings.LOG.error("Exception while loading Carpet rules from config", e);
return new ConfigReadResult(new HashMap<>(), false);
}
}
private Collection<CarpetRule<?>> getRulesMatching(String search) {
String lcSearch = search.toLowerCase(Locale.ROOT);
return rules.values().stream().filter(rule ->
{
if (rule.name().toLowerCase(Locale.ROOT).contains(lcSearch)) return true; // substring match, case insensitive
for (String c : rule.categories()) if (c.equals(search)) return true; // category exactly, case sensitive
return Sets.newHashSet(RuleHelper.translatedDescription(rule).toLowerCase(Locale.ROOT).split("\\W+")).contains(lcSearch); // contains full term in description, but case insensitive
}).sorted(comparing(CarpetRule::name)).toList();
}
/**
* A method to pretty print in markdown (useful for Github wiki/readme) the current
* registered rules for a category to the log. Contains the name, description,
* categories, type, defaults, whether or not they are strict, their suggested
* values, etc.
*
* @param ps A {@link PrintStream} to dump the rules to, such as {@link System#out}
* @param category A {@link String} being the category to print, {@link null} to print
* all registered rules.
* @return actually nothing, the int is just there for brigadier
*
* @apiNote While extensions are expected to be able to call this method, binary compatibility isn't
* guaranteed, but this shouldn't be an issue given this is only intended to be ran for doc
* generation (where version is controlled) and it's not expected to be referenced in production anyway
*/
public int dumpAllRulesToStream(PrintStream ps, String category)
{
ps.println("# "+fancyName+" Settings");
for (CarpetRule<?> rule : new TreeMap<>(rules).values())
{
if (category != null && !rule.categories().contains(category))
continue;
ps.println("## " + rule.name());
ps.println(RuleHelper.translatedDescription(rule)+" ");
for (Component extra : rule.extraInfo())
ps.println(extra.getString() + " ");
ps.println("* Type: `" + rule.type().getSimpleName() + "` ");
ps.println("* Default value: `" + RuleHelper.toRuleString(rule.defaultValue()) + "` ");
String options = rule.suggestions().stream().map(s -> "`" + s + "`").collect(Collectors.joining(", "));
if (!options.isEmpty()) ps.println((rule instanceof ParsedRule<?> pr && pr.isStrict?"* Required":"* Suggested")+" options: " + options + " ");
ps.println("* Categories: " + rule.categories().stream().map(s -> "`" + s.toUpperCase(Locale.ROOT) + "`").collect(Collectors.joining(", ")) + " ");
if (rule instanceof ParsedRule<?>)
{
boolean preamble = false;
for (Validator<?> validator : ((ParsedRule<?>) rule).realValidators)
{
if (validator.description() != null)
{
if (!preamble)
{
ps.println("* Additional notes: ");
preamble = true;
}
ps.println(" * "+validator.description()+" ");
}
}
}
ps.println(" ");
}
return 1;
}
private CarpetRule<?> contextRule(CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException
{
String ruleName = StringArgumentType.getString(ctx, "rule");
CarpetRule<?> rule = getCarpetRule(ruleName);
if (rule == null)
throw new SimpleCommandExceptionType(Messenger.c("rb "+ tr(TranslationKeys.UNKNOWN_RULE) + ": "+ruleName)).create();
return rule;
}
static CompletableFuture<Suggestions> suggestMatchingContains(Stream<String> stream, SuggestionsBuilder suggestionsBuilder) {
List<String> regularSuggestionList = new ArrayList<>();
List<String> smartSuggestionList = new ArrayList<>();
String query = suggestionsBuilder.getRemaining().toLowerCase(Locale.ROOT);
stream.forEach((listItem) -> {
// Regex camelCase Search
var words = Arrays.stream(listItem.split("(?<!^)(?=[A-Z])")).map(s -> s.toLowerCase(Locale.ROOT)).collect(Collectors.toList());
var prefixes = new ArrayList<String>(words.size());
for (int i = 0; i < words.size(); i++)
prefixes.add(String.join("", words.subList(i, words.size())));
if (prefixes.stream().anyMatch(s -> s.startsWith(query))) {
smartSuggestionList.add(listItem);
}
// Regular prefix matching, reference: CommandSource.suggestMatching
if (SharedSuggestionProvider.matchesSubStr(query, listItem.toLowerCase(Locale.ROOT))) {
regularSuggestionList.add(listItem);
}
});
var filteredSuggestionList = regularSuggestionList.isEmpty() ? smartSuggestionList : regularSuggestionList;
Objects.requireNonNull(suggestionsBuilder);
filteredSuggestionList.forEach(suggestionsBuilder::suggest);
return suggestionsBuilder.buildFuture();
}
/**
* Registers the settings command for this {@link SettingsManager}.<br>
* It is handled automatically by Carpet.
* @param dispatcher The current {@link CommandDispatcher}
* @param commandBuildContext The current {@link CommandBuildContext}
*/
public void registerCommand(final CommandDispatcher<CommandSourceStack> dispatcher, final CommandBuildContext commandBuildContext)
{
if (dispatcher.getRoot().getChildren().stream().anyMatch(node -> node.getName().equalsIgnoreCase(identifier)))
{
CarpetSettings.LOG.error("Failed to add settings command for " + identifier + ". It is masking previous command.");
return;
}
LiteralArgumentBuilder<CommandSourceStack> literalargumentbuilder = literal(identifier).requires((player) ->
CommandHelper.canUseCommand(player, CarpetSettings.carpetCommandPermissionLevel) && !locked());
literalargumentbuilder.executes((context)-> listAllSettings(context.getSource())).
then(literal("list").
executes( (c) -> listSettings(c.getSource(), String.format(tr(TranslationKeys.ALL_MOD_SETTINGS), fancyName),
getRulesSorted())).
then(literal("defaults").
executes( (c)-> listSettings(c.getSource(),
String.format(tr(TranslationKeys.CURRENT_FROM_FILE_HEADER), fancyName, (identifier+".conf")),
findStartupOverrides()))).
then(argument("tag",StringArgumentType.word()).
suggests( (c, b)->suggest(getCategories(), b)).
executes( (c) -> listSettings(c.getSource(),
String.format(tr(TranslationKeys.MOD_SETTINGS_MATCHING), fancyName, RuleHelper.translatedCategory(identifier(),StringArgumentType.getString(c, "tag"))),
getRulesMatching(StringArgumentType.getString(c, "tag")))))).
then(literal("removeDefault").
requires(s -> !locked()).
then(argument("rule", StringArgumentType.word()).
suggests( (c, b) -> suggestMatchingContains(getRulesSorted().stream().map(CarpetRule::name), b)).
executes((c) -> removeDefault(c.getSource(), contextRule(c))))).
then(literal("setDefault").
requires(s -> !locked()).
then(argument("rule", StringArgumentType.word()).
suggests( (c, b) -> suggestMatchingContains(getRulesSorted().stream().map(CarpetRule::name), b)).
then(argument("value", StringArgumentType.greedyString()).
suggests((c, b)-> suggest(contextRule(c).suggestions(), b)).
executes((c) -> setDefault(c.getSource(), contextRule(c), StringArgumentType.getString(c, "value")))))).
then(argument("rule", StringArgumentType.word()).
suggests( (c, b) -> suggestMatchingContains(getRulesSorted().stream().map(CarpetRule::name), b)).
requires(s -> !locked() ).
executes( (c) -> displayRuleMenu(c.getSource(), contextRule(c))).
then(argument("value", StringArgumentType.greedyString()).
suggests((c, b)-> suggest(contextRule(c).suggestions(),b)).
executes((c) -> setRule(c.getSource(), contextRule(c), StringArgumentType.getString(c, "value")))));
dispatcher.register(literalargumentbuilder);
}
private int displayRuleMenu(CommandSourceStack source, CarpetRule<?> rule) //TODO check if there's dupe code around options buttons
{
String displayName = RuleHelper.translatedName(rule);
Messenger.m(source, "");
Messenger.m(source, "wb "+ displayName ,"!/"+identifier+" "+rule.name(),"^g refresh");
Messenger.m(source, "w "+ RuleHelper.translatedDescription(rule));
rule.extraInfo().forEach(s -> Messenger.m(source, s));
List<Component> tags = new ArrayList<>();
tags.add(Messenger.c("w "+ tr(TranslationKeys.TAGS)+": "));
for (String t: rule.categories())
{
String translated = RuleHelper.translatedCategory(identifier(), t);
tags.add(Messenger.c("c ["+ translated +"]", "^g "+ String.format(tr(TranslationKeys.LIST_ALL_CATEGORY), translated),"!/"+identifier+" list "+t));
tags.add(Messenger.c("w , "));
}
tags.remove(tags.size() - 1);
Messenger.m(source, tags.toArray(new Object[0]));
Messenger.m(source, "w "+ tr(TranslationKeys.CURRENT_VALUE)+": ", String.format("%s %s (%s value)", RuleHelper.getBooleanValue(rule) ? "lb" : "nb", RuleHelper.toRuleString(rule.value()), RuleHelper.isInDefaultValue(rule) ? "default" : "modified"));
List<Component> options = new ArrayList<>();
options.add(Messenger.c("w Options: ", "y [ "));
for (String o: rule.suggestions())
{
options.add(makeSetRuleButton(rule, o, false));
options.add(Messenger.c("w "));
}
options.remove(options.size()-1);
options.add(Messenger.c("y ]"));
Messenger.m(source, options.toArray(new Object[0]));
return 1;
}
private int setRule(CommandSourceStack source, CarpetRule<?> rule, String newValue)
{
try {
rule.set(source, newValue);
Messenger.m(source, "w "+rule.toString()+", ", "c ["+ tr(TranslationKeys.CHANGE_PERMANENTLY)+"?]",
"^w "+String.format(tr(TranslationKeys.CHANGE_PERMANENTLY_HOVER), identifier+".conf"),
"?/"+identifier+" setDefault "+rule.name()+" "+ RuleHelper.toRuleString(rule.value()));
} catch (InvalidRuleValueException e) {
e.notifySource(rule.name(), source);
}
return 1;
}
// stores different defaults in the file
private int setDefault(CommandSourceStack source, CarpetRule<?> rule, String stringValue)
{
if (locked()) return 0;
if (!rules.containsKey(rule.name())) return 0;
ConfigReadResult conf = readSettingsFromConf(getFile());
conf.ruleMap().put(rule.name(), stringValue);
writeSettingsToConf(conf); // this may feels weird, but if conf
// is locked, it will never reach this point.
try {
rule.set(source, stringValue);
Messenger.m(source ,"gi "+String.format(tr(TranslationKeys.DEFAULT_SET), RuleHelper.translatedName(rule), stringValue));
} catch (InvalidRuleValueException e) {
e.notifySource(rule.name(), source);
}
return 1;
}
// removes overrides of the default values in the file
private int removeDefault(CommandSourceStack source, CarpetRule<?> rule)
{
if (locked) return 0;
if (!rules.containsKey(rule.name())) return 0;
ConfigReadResult conf = readSettingsFromConf(getFile());
conf.ruleMap().remove(rule.name());
writeSettingsToConf(conf);
RuleHelper.resetToDefault(rules.get(rule.name()), source);
Messenger.m(source ,"gi "+String.format(tr(TranslationKeys.DEFAULT_REMOVED), RuleHelper.translatedName(rule)));
return 1;
}
private Component displayInteractiveSetting(CarpetRule<?> rule)
{
String displayName = RuleHelper.translatedName(rule);
List<Object> args = new ArrayList<>();
args.add("w - "+ displayName +" ");
args.add("!/"+identifier+" "+rule.name());
args.add("^y "+RuleHelper.translatedDescription(rule));
for (String option: rule.suggestions())
{
args.add(makeSetRuleButton(rule, option, true));
args.add("w ");
}
if (!rule.suggestions().contains(RuleHelper.toRuleString(rule.value())))
{
args.add(makeSetRuleButton(rule, RuleHelper.toRuleString(rule.value()), true));
args.add("w ");
}
args.remove(args.size()-1);
return Messenger.c(args.toArray(new Object[0]));
}
private Component makeSetRuleButton(CarpetRule<?> rule, String option, boolean brackets)
{
String style = RuleHelper.isInDefaultValue(rule)?"g":(option.equalsIgnoreCase(RuleHelper.toRuleString(rule.defaultValue()))?"e":"y");
if (option.equalsIgnoreCase(RuleHelper.toRuleString(rule.value())))
{
style = style + "u";
if (option.equalsIgnoreCase(RuleHelper.toRuleString(rule.defaultValue())))
style = style + "b";
}
String component = style + (brackets ? " [" : " ") + option + (brackets ? "]" : "");
if (option.equalsIgnoreCase(RuleHelper.toRuleString(rule.value())))
return Messenger.c(component);
return Messenger.c(component, "^g "+ tr(TranslationKeys.SWITCH_TO).formatted(option + (option.equals(RuleHelper.toRuleString(rule.defaultValue()))?" (default)":"")), "?/"+identifier+" " + rule.name() + " " + option);
}
private int listSettings(CommandSourceStack source, String title, Collection<CarpetRule<?>> settings_list)
{
Messenger.m(source,String.format("wb %s:",title));
settings_list.forEach(e -> Messenger.m(source, displayInteractiveSetting(e)));
return settings_list.size();
}
private int listAllSettings(CommandSourceStack source)
{
int count = listSettings(source, String.format(tr(TranslationKeys.CURRENT_SETTINGS_HEADER), fancyName), getNonDefault());
if (version != null)
Messenger.m(source, "g "+fancyName+" "+ tr(TranslationKeys.VERSION) + ": " + version);
List<String> tags = new ArrayList<>();
tags.add("w " + tr(TranslationKeys.BROWSE_CATEGORIES) + ":\n");
for (String t : getCategories())
{
String translated = RuleHelper.translatedCategory(identifier(), t);
String translatedPlus = !translated.equals(t) ? "%s (%s)".formatted(translated, t) : t;
tags.add("c [" + translated +"]");
tags.add("^g " + String.format(tr(TranslationKeys.LIST_ALL_CATEGORY), translatedPlus));
tags.add("!/"+identifier+" list " + t);
tags.add("w ");
}
tags.remove(tags.size() - 1);
Messenger.m(source, tags.toArray(new Object[0]));
return count;
}
}
@@ -0,0 +1,59 @@
package carpet.api.settings;
import carpet.utils.Messenger;
import net.minecraft.commands.CommandSourceStack;
/**
* <p>A {@link Validator} is a class that is able to validate the values given to a {@link CarpetRule}, cancelling rule
* modification if the value is not valid or even changing the value to a different one if needed.</p>
*
* <p>Validators are used in the default implementation, {@link ParsedRule}, as the way of validating most input (other than validating
* it can actually be introduced in the rule), but can (and may) also be used in other {@link CarpetRule} implementations, though those are not
* required to.</p>
*
* @see #validate(CommandSourceStack, CarpetRule, Object, String)
*
* @param <T> The type of the rule's value
*/
public abstract class Validator<T>
{
/**
* <p>Validates whether the value passed to the given {@link CarpetRule} is valid as a new value for it.</p>
*
* <p>Validators can also change the value that the rule is going to be set to by returning a value different to the
* one that has been passed to them</p>
*
* <p>This method must not throw any exceptions.</p>
*
* @param source The {@link CommandSourceStack} that originated this change, and should be further notified
* about it. May be {@code null} during rule synchronization.
* @param changingRule The {@link CarpetRule} that is being changed
* @param newValue The new value that is being set to the rule
* @param userInput The value that is being given to this rule by the user as a {@link String}, or a best-effort representation
* of the value as a {@link String}. This value may not correspond to the result of {@link RuleHelper#toRuleString(Object)}
* if other validators have modified the value, it's just a representation of the user's input.
* @return The new value to set the rule to instead, can return the {@code newValue} if the given value is correct.
* Returns {@code null} if the given value is not correct.
*/
public abstract T validate(CommandSourceStack source, CarpetRule<T> changingRule, T newValue, String userInput);
/**
* @return A description of this {@link Validator}. It is used in the default {@link #notifyFailure(CommandSourceStack, CarpetRule, String)}
* implementation and to add extra information in {@link SettingsManager#printAllRulesToLog(String)}
*/
public String description() {return null;}
/**
* <p>Called after failing validation of the {@link CarpetRule} in order to notify the causing {@link CommandSourceStack} about the
* failure.</p>
*
* @param source The {@link CommandSourceStack} that originated this change. It will never be {@code null}
* @param currentRule The {@link CarpetRule} that failed verification
* @param providedValue The {@link String} that was provided to the changing rule
*/
public void notifyFailure(CommandSourceStack source, CarpetRule<T> currentRule, String providedValue)
{
Messenger.m(source, "r Wrong value for " + currentRule.name() + ": " + providedValue);
if (description() != null)
Messenger.m(source, "r " + description());
}
}
@@ -0,0 +1,67 @@
package carpet.api.settings;
import java.util.List;
import carpet.utils.CommandHelper;
import net.minecraft.commands.CommandSourceStack;
/**
* <p>A collection of standard {@link Validator validators} you can use in your rules.</p>
*
* @see Rule
* @see Rule#validators()
*
*/
public final class Validators {
private Validators() {};
/**
* <p>A {@link Validator} that checks whether the given {@link String} value was a valid command level as Carpet allows them,
* so either a number from 0 to 4, or one of the keywords {@code true}, {@code false} or {@code ops} </p>
*
* <p>While there is no public API method for checking whether a source can execute a command,
* {@link CommandHelper#canUseCommand(CommandSourceStack, Object)} is not expected to change anytime soon.</p>
*
*/
public static class CommandLevel extends Validator<String> {
@Deprecated(forRemoval = true) // internal use only, will be made pckg private when phasing out old api
public static final List<String> OPTIONS = List.of("true", "false", "ops", "0", "1", "2", "3", "4");
@Override
public String validate(CommandSourceStack source, CarpetRule<String> currentRule, String newValue, String userString) {
if (!OPTIONS.contains(newValue))
{
return null;
}
return newValue;
}
@Override public String description() { return "Can be limited to 'ops' only, true/false for everyone/no one, or a custom permission level";}
}
/**
* <p>A {@link Validator} that checks whether the entered number is equal or greater than {@code 0}.</p>
*/
public static class NonNegativeNumber<T extends Number> extends Validator<T>
{
@Override
public T validate(CommandSourceStack source, CarpetRule<T> currentRule, T newValue, String string)
{
return newValue.doubleValue() >= 0 ? newValue : null;
}
@Override
public String description() { return "Must be a positive number or 0";}
}
/**
* <p>A {@link Validator} that checks whether the entered number is between 0 and 1, inclusive.</p>
*/
public static class Probablity<T extends Number> extends Validator<T>
{
@Override
public T validate(CommandSourceStack source, CarpetRule<T> currentRule, T newValue, String string)
{
return (newValue.doubleValue() >= 0 && newValue.doubleValue() <= 1 )? newValue : null;
}
@Override
public String description() { return "Must be between 0 and 1";}
}
}
@@ -1,7 +1,7 @@
package carpet.commands;
import carpet.CarpetSettings;
import carpet.settings.SettingsManager;
import carpet.utils.CommandHelper;
import carpet.utils.DistanceCalculator;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
@@ -17,7 +17,7 @@ public class DistanceCommand
public static void register(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext commandBuildContext)
{
LiteralArgumentBuilder<CommandSourceStack> command = literal("distance").
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandDistance)).
requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandDistance)).
then(literal("from").
executes( (c) -> DistanceCalculator.setStart(c.getSource(), c.getSource().getPosition())).
then(argument("from", Vec3Argument.vec3()).
@@ -1,7 +1,7 @@
package carpet.commands;
import carpet.CarpetSettings;
import carpet.settings.SettingsManager;
import carpet.utils.CommandHelper;
import carpet.utils.Messenger;
import com.google.common.collect.Lists;
import com.mojang.brigadier.Command;
@@ -40,7 +40,7 @@ public class DrawCommand
public static void register(CommandDispatcher<CommandSourceStack> dispatcher, final CommandBuildContext context)
{
LiteralArgumentBuilder<CommandSourceStack> command = literal("draw").
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandDraw)).
requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandDraw)).
then(literal("sphere").
then(argument("center", BlockPosArgument.blockPos()).
then(argument("radius", IntegerArgumentType.integer(1)).
@@ -1,8 +1,8 @@
package carpet.commands;
import carpet.CarpetSettings;
import carpet.settings.SettingsManager;
import carpet.utils.BlockInfo;
import carpet.utils.CommandHelper;
import carpet.utils.Messenger;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
@@ -26,7 +26,7 @@ public class InfoCommand
public static void register(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext commandBuildContext)
{
LiteralArgumentBuilder<CommandSourceStack> command = literal("info").
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandInfo)).
requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandInfo)).
then(literal("block").
then(argument("block position", BlockPosArgument.blockPos()).
executes( (c) -> infoBlock(
@@ -3,7 +3,7 @@ package carpet.commands;
import carpet.CarpetSettings;
import carpet.logging.Logger;
import carpet.logging.LoggerRegistry;
import carpet.settings.SettingsManager;
import carpet.utils.CommandHelper;
import carpet.utils.Messenger;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
@@ -29,7 +29,7 @@ public class LogCommand
public static void register(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext commandBuildContext)
{
LiteralArgumentBuilder<CommandSourceStack> literalargumentbuilder = Commands.literal("log").
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandLog)).
requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandLog)).
executes((context) -> listLogs(context.getSource())).
then(Commands.literal("clear").
executes( (c) -> unsubFromAll(c.getSource(), c.getSource().getTextName())).
@@ -1,7 +1,7 @@
package carpet.commands;
import carpet.CarpetSettings;
import carpet.settings.SettingsManager;
import carpet.utils.CommandHelper;
import carpet.utils.MobAI;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
@@ -20,7 +20,7 @@ public class MobAICommand
public static void register(CommandDispatcher<CommandSourceStack> dispatcher, final CommandBuildContext commandBuildContext)
{
LiteralArgumentBuilder<CommandSourceStack> command = literal("track").
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandTrackAI)).
requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandTrackAI)).
then(argument("entity type", EntitySummonArgument.id()).
suggests( (c, b) -> suggest(MobAI.availbleTypes(), b)).
@@ -1,7 +1,7 @@
package carpet.commands;
import carpet.CarpetSettings;
import carpet.settings.SettingsManager;
import carpet.utils.CommandHelper;
import carpet.utils.Messenger;
import carpet.utils.PerimeterDiagnostics;
import com.mojang.brigadier.CommandDispatcher;
@@ -25,7 +25,7 @@ public class PerimeterInfoCommand
public static void register(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext commandBuildContext)
{
LiteralArgumentBuilder<CommandSourceStack> command = literal("perimeterinfo").
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandPerimeterInfo)).
requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandPerimeterInfo)).
executes( (c) -> perimeterDiagnose(
c.getSource(),
new BlockPos(c.getSource().getPosition()),
@@ -4,7 +4,7 @@ import carpet.helpers.EntityPlayerActionPack;
import carpet.CarpetSettings;
import carpet.fakes.ServerPlayerEntityInterface;
import carpet.patches.EntityPlayerMPFake;
import carpet.settings.SettingsManager;
import carpet.utils.CommandHelper;
import carpet.utils.Messenger;
import com.mojang.authlib.GameProfile;
import com.mojang.brigadier.Command;
@@ -53,7 +53,7 @@ public class PlayerCommand
.map(GameType::getName)
.toArray(String[]::new);
LiteralArgumentBuilder<CommandSourceStack> literalargumentbuilder = literal("player")
.requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandPlayer))
.requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandPlayer))
.then(argument("player", StringArgumentType.word())
.suggests( (c, b) -> suggest(getPlayers(c.getSource()), b))
.then(literal("stop").executes(PlayerCommand::stop))
@@ -1,7 +1,8 @@
package carpet.commands;
import carpet.CarpetSettings;
import carpet.settings.SettingsManager;
import carpet.utils.CommandHelper;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.commands.CommandBuildContext;
@@ -19,7 +20,7 @@ public class ProfileCommand
public static void register(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext commandBuildContext)
{
LiteralArgumentBuilder<CommandSourceStack> literalargumentbuilder = literal("profile").
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandProfile)).
requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandProfile)).
executes( (c) -> healthReport(c.getSource(), 100)).
then(literal("health").
executes( (c) -> healthReport(c.getSource(), 100)).
@@ -13,8 +13,8 @@ import carpet.script.Tokenizer;
import carpet.script.exception.CarpetExpressionException;
import carpet.script.value.FunctionValue;
import carpet.script.value.Value;
import carpet.settings.SettingsManager;
import carpet.utils.CarpetProfiler;
import carpet.utils.CommandHelper;
import carpet.utils.Messenger;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
@@ -140,7 +140,7 @@ public class ScriptCommand
LiteralArgumentBuilder<CommandSourceStack> u = literal("resume").
executes( (cc) -> { CarpetServer.scriptServer.stopAll = false; return 1;});
LiteralArgumentBuilder<CommandSourceStack> l = literal("run").
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE)).
requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandScriptACE)).
then(argument("expr", StringArgumentType.greedyString()).suggests(ScriptCommand::suggestCode).
executes((cc) -> compute(
cc,
@@ -266,7 +266,7 @@ public class ScriptCommand
BlockPredicateArgument.getBlockPredicate(cc, "filter"),
"outline"
)))))))));
LiteralArgumentBuilder<CommandSourceStack> a = literal("load").requires( (player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE) ).
LiteralArgumentBuilder<CommandSourceStack> a = literal("load").requires( (player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandScriptACE) ).
then(argument("app", StringArgumentType.word()).
suggests( (cc, bb) -> suggest(CarpetServer.scriptServer.listAvailableModules(true),bb)).
executes((cc) ->
@@ -283,7 +283,7 @@ public class ScriptCommand
)
)
);
LiteralArgumentBuilder<CommandSourceStack> f = literal("unload").requires( (player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE) ).
LiteralArgumentBuilder<CommandSourceStack> f = literal("unload").requires( (player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandScriptACE) ).
then(argument("app", StringArgumentType.word()).
suggests( (cc, bb) -> suggest(CarpetServer.scriptServer.unloadableModules,bb)).
executes((cc) ->
@@ -292,7 +292,7 @@ public class ScriptCommand
return success?1:0;
}));
LiteralArgumentBuilder<CommandSourceStack> q = literal("event").requires( (player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE) ).
LiteralArgumentBuilder<CommandSourceStack> q = literal("event").requires( (player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandScriptACE) ).
executes( (cc) -> listEvents(cc.getSource())).
then(literal("add_to").
then(argument("event", StringArgumentType.word()).
@@ -327,11 +327,11 @@ public class ScriptCommand
StringArgumentType.getString(cc, "call")
)?1:0))));
LiteralArgumentBuilder<CommandSourceStack> d = literal("download").requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE)).
LiteralArgumentBuilder<CommandSourceStack> d = literal("download").requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandScriptACE)).
then(argument("path", StringArgumentType.greedyString()).
suggests(ScriptCommand::suggestDownloadableApps).
executes(cc-> AppStoreManager.downloadScript(cc.getSource(), StringArgumentType.getString(cc,"path"))));
LiteralArgumentBuilder<CommandSourceStack> r = literal("remove").requires( (player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE) ).
LiteralArgumentBuilder<CommandSourceStack> r = literal("remove").requires( (player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandScriptACE) ).
then(argument("app", StringArgumentType.word()).
suggests( (cc, bb) -> suggest(CarpetServer.scriptServer.unloadableModules,bb)).
executes((cc) ->
@@ -341,10 +341,10 @@ public class ScriptCommand
}));
dispatcher.register(literal("script").
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScript)).
requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandScript)).
then(b).then(u).then(o).then(l).then(s).then(c).then(h).then(i).then(e).then(t).then(a).then(f).then(q).then(d).then(r));
dispatcher.register(literal("script").
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScript)).
requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandScript)).
then(literal("in").
then(argument("app", StringArgumentType.word()).
suggests( (cc, bb) -> suggest(CarpetServer.scriptServer.modules.keySet(), bb)).
@@ -4,7 +4,7 @@ import carpet.CarpetSettings;
import carpet.fakes.SpawnGroupInterface;
import carpet.helpers.HopperCounter;
import carpet.helpers.TickSpeed;
import carpet.settings.SettingsManager;
import carpet.utils.CommandHelper;
import carpet.utils.Messenger;
import carpet.utils.SpawnReporter;
import com.mojang.brigadier.CommandDispatcher;
@@ -37,13 +37,12 @@ import static net.minecraft.commands.Commands.argument;
import static net.minecraft.commands.Commands.literal;
import static net.minecraft.commands.SharedSuggestionProvider.suggest;
public class SpawnCommand
{
public static void register(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext commandBuildContext)
{
LiteralArgumentBuilder<CommandSourceStack> literalargumentbuilder = literal("spawn").
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandSpawn));
requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandSpawn));
literalargumentbuilder.
then(literal("list").
@@ -17,9 +17,9 @@ public class TestCommand
{
dispatcher.register(literal("testcarpet").
then(literal("dump").
executes((c) -> CarpetServer.settingsManager.printAllRulesToLog(null)).
executes((c) -> CarpetServer.settingsManager.dumpAllRulesToStream(System.out, null)).
then(argument("category", word()).
executes( (c) -> CarpetServer.settingsManager.printAllRulesToLog(getString(c, "category"))))).
executes( (c) -> CarpetServer.settingsManager.dumpAllRulesToStream(System.out, getString(c, "category"))))).
then(argument("first",word()).
executes( (c)-> test(c, getString(c, "first")+" 1"))).
then(argument("second", word()).
@@ -3,8 +3,8 @@ package carpet.commands;
import carpet.CarpetSettings;
import carpet.helpers.TickSpeed;
import carpet.network.ServerNetworkHandler;
import carpet.settings.SettingsManager;
import carpet.utils.CarpetProfiler;
import carpet.utils.CommandHelper;
import carpet.utils.Messenger;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
@@ -29,7 +29,7 @@ public class TickCommand
public static void register(CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext commandBuildContext)
{
LiteralArgumentBuilder<CommandSourceStack> literalargumentbuilder = literal("tick").
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandTick)).
requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandTick)).
then(literal("rate").
executes((c) -> queryTps(c.getSource())).
then(argument("rate", floatArg(0.1F, 500.0F)).
@@ -2,7 +2,7 @@ package carpet.mixins;
import carpet.CarpetServer;
import carpet.network.CarpetClient;
import carpet.settings.SettingsManager;
import carpet.api.settings.SettingsManager;
import net.minecraft.client.player.LocalPlayer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@@ -25,7 +25,7 @@ public class LocalPlayer_clientCommandMixin
LocalPlayer playerSource = (LocalPlayer)(Object) this;
CarpetServer.settingsManager.inspectClientsideCommand(playerSource.createCommandSourceStack(), string);
CarpetServer.extensions.forEach(e -> {
SettingsManager sm = e.customSettingsManager();
SettingsManager sm = e.extensionSettingsManager();
if (sm != null) sm.inspectClientsideCommand(playerSource.createCommandSourceStack(), string);
});
}
@@ -62,6 +62,6 @@ public abstract class MinecraftServer_coreMixin
if (CarpetSettings.spawnChunksSize != 11)
CarpetSettings.ChangeSpawnChunksValidator.changeSpawnSize(CarpetSettings.spawnChunksSize);
CarpetSettings.LightBatchValidator.applyLightBatchSizes();
CarpetSettings.LightBatchValidator.applyLightBatchSizes(CarpetSettings.lightEngineMaxBatchSize);
}
}
@@ -3,9 +3,10 @@ package carpet.network;
import carpet.CarpetServer;
import carpet.CarpetExtension;
import carpet.CarpetSettings;
import carpet.api.settings.CarpetRule;
import carpet.api.settings.InvalidRuleValueException;
import carpet.helpers.TickSpeed;
import carpet.settings.ParsedRule;
import carpet.settings.SettingsManager;
import carpet.api.settings.SettingsManager;
import io.netty.buffer.Unpooled;
import java.util.HashMap;
import java.util.Map;
@@ -42,8 +43,8 @@ public class ClientNetworkHandler
else
{
for (CarpetExtension extension: CarpetServer.extensions) {
SettingsManager eManager = extension.customSettingsManager();
if (eManager != null && managerName.equals(eManager.getIdentifier()))
SettingsManager eManager = extension.extensionSettingsManager();
if (eManager != null && managerName.equals(eManager.identifier()))
{
manager = eManager;
break;
@@ -56,11 +57,11 @@ public class ClientNetworkHandler
manager = CarpetServer.settingsManager;
ruleName = ruleKey;
}
ParsedRule<?> rule = (manager != null) ? manager.getRule(ruleName) : null;
CarpetRule<?> rule = (manager != null) ? manager.getCarpetRule(ruleName) : null;
if (rule != null)
{
String value = ruleNBT.getString("Value");
try { rule.set(null, value); } catch (Exception ignored) { }
try { rule.set(null, value); } catch (InvalidRuleValueException ignored) { }
}
}
});
@@ -2,10 +2,11 @@ package carpet.network;
import carpet.CarpetServer;
import carpet.CarpetSettings;
import carpet.api.settings.CarpetRule;
import carpet.api.settings.RuleHelper;
import carpet.helpers.TickSpeed;
import carpet.script.utils.SnoopyCommandSource;
import carpet.settings.ParsedRule;
import carpet.settings.SettingsManager;
import carpet.api.settings.SettingsManager;
import io.netty.buffer.Unpooled;
import java.util.ArrayList;
import java.util.HashMap;
@@ -75,11 +76,11 @@ public class ServerNetworkHandler
else
CarpetSettings.LOG.warn("Player "+playerEntity.getName().getString()+" joined with another carpet version: "+clientVersion);
DataBuilder data = DataBuilder.create().withTickRate().withFrozenState().withTickPlayerActiveTimeout(); // .withSuperHotState()
CarpetServer.settingsManager.getRules().forEach(data::withRule);
CarpetServer.settingsManager.getCarpetRules().forEach(data::withRule);
CarpetServer.extensions.forEach(e -> {
SettingsManager eManager = e.customSettingsManager();
SettingsManager eManager = e.extensionSettingsManager();
if (eManager != null) {
eManager.getRules().forEach(data::withRule);
eManager.getCarpetRules().forEach(data::withRule);
}
});
playerEntity.connection.send(new ClientboundCustomPayloadPacket(CarpetClient.CARPET_CHANNEL, data.build() ));
@@ -130,7 +131,7 @@ public class ServerNetworkHandler
}
}
public static void updateRuleWithConnectedClients(ParsedRule<?> rule)
public static void updateRuleWithConnectedClients(CarpetRule<?> rule)
{
if (CarpetSettings.superSecretSetting) return;
for (ServerPlayer player : remoteCarpetPlayers.keySet())
@@ -275,7 +276,7 @@ public class ServerNetworkHandler
tag.putInt("TickPlayerActiveTimeout", TickSpeed.player_active_timeout);
return this;
}
private DataBuilder withRule(ParsedRule<?> rule)
private DataBuilder withRule(CarpetRule<?> rule)
{
CompoundTag rules = (CompoundTag) tag.get("Rules");
if (rules == null)
@@ -283,13 +284,13 @@ public class ServerNetworkHandler
rules = new CompoundTag();
tag.put("Rules", rules);
}
String identifier = rule.settingsManager.getIdentifier();
String key = rule.name;
String identifier = rule.settingsManager().identifier();
String key = rule.name();
while (rules.contains(key)) { key = key+"2";}
CompoundTag ruleNBT = new CompoundTag();
ruleNBT.putString("Value", rule.getAsString());
ruleNBT.putString("Manager",identifier);
ruleNBT.putString("Rule",rule.name);
ruleNBT.putString("Value", RuleHelper.toRuleString(rule.value()));
ruleNBT.putString("Manager", identifier);
ruleNBT.putString("Rule", rule.name());
rules.put(key, ruleNBT);
return this;
}
@@ -2,6 +2,8 @@ package carpet.script;
import carpet.CarpetServer;
import carpet.CarpetSettings;
import carpet.api.settings.CarpetRule;
import carpet.api.settings.RuleHelper;
import carpet.helpers.TickSpeed;
import carpet.script.exception.IntegrityException;
import carpet.script.exception.InternalExpressionException;
@@ -17,7 +19,6 @@ import carpet.script.value.NumericValue;
import carpet.script.value.StringValue;
import carpet.script.value.Value;
import carpet.script.value.ValueConversions;
import carpet.settings.ParsedRule;
import carpet.utils.CarpetProfiler;
import carpet.utils.Messenger;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
@@ -917,9 +918,9 @@ public class CarpetEventServer
public static final Event CARPET_RULE_CHANGES = new Event("carpet_rule_changes", 2, true)
{
@Override
public void onCarpetRuleChanges(ParsedRule<?> rule, CommandSourceStack source)
public void onCarpetRuleChanges(CarpetRule<?> rule, CommandSourceStack source)
{
String identifier = rule.settingsManager.getIdentifier();
String identifier = rule.settingsManager().identifier();
final String namespace;
if (!identifier.equals("carpet"))
{
@@ -927,8 +928,8 @@ public class CarpetEventServer
} else { namespace = "";}
handler.call(
() -> Arrays.asList(
new StringValue(namespace+rule.name),
new StringValue(rule.getAsString())
new StringValue(namespace+rule.name()),
new StringValue(RuleHelper.toRuleString(rule.value()))
), () -> source
);
}
@@ -1123,7 +1124,7 @@ public class CarpetEventServer
public void onExplosion(ServerLevel world, Entity e, Supplier<LivingEntity> attacker, double x, double y, double z, float power, boolean createFire, List<BlockPos> affectedBlocks, List<Entity> affectedEntities, Explosion.BlockInteraction type) { }
public void onWorldEvent(ServerLevel world, BlockPos pos) { }
public void onWorldEventFlag(ServerLevel world, BlockPos pos, int flag) { }
public void onCarpetRuleChanges(ParsedRule<?> rule, CommandSourceStack source) { }
public void onCarpetRuleChanges(CarpetRule<?> rule, CommandSourceStack source) { }
public void onCustomPlayerEvent(ServerPlayer player, Object ... args)
{
if (handler.reqArgs != (args.length+1))
@@ -21,6 +21,7 @@ import carpet.script.language.Threading;
import carpet.script.utils.AppStoreManager;
import carpet.script.value.FunctionValue;
import carpet.utils.CarpetProfiler;
import carpet.utils.CommandHelper;
import carpet.utils.Messenger;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.tree.CommandNode;
@@ -140,8 +141,8 @@ public class CarpetScriptServer extends ScriptServer
{
CarpetServer.settingsManager.initializeScarpetRules();
CarpetServer.extensions.forEach(e -> {
if (e.customSettingsManager() != null) {
e.customSettingsManager().initializeScarpetRules();
if (e.extensionSettingsManager() != null) {
e.extensionSettingsManager().initializeScarpetRules();
}
});
if (CarpetSettings.scriptsAutoload)
@@ -305,7 +306,7 @@ public class CarpetScriptServer extends ScriptServer
}
else if (isCommandAdded)
{
CarpetServer.settingsManager.notifyPlayersCommandsChanged();
CommandHelper.notifyPlayersCommandsChanged(server);
if (!isRuleApp) Messenger.m(source, "gi "+name+" app "+action+" with /"+name+" command");
}
else
@@ -352,7 +353,7 @@ public class CarpetScriptServer extends ScriptServer
modules.get(name).onClose();
modules.remove(name);
if (!isRuleApp) unloadableModules.remove(name);
CarpetServer.settingsManager.notifyPlayersCommandsChanged();
CommandHelper.notifyPlayersCommandsChanged(server);
if (notifySource) Messenger.m(source, "gi Removed "+name+" app");
return true;
}
@@ -36,6 +36,8 @@ import carpet.script.value.StringValue;
import carpet.script.value.Value;
import carpet.script.value.ValueConversions;
import carpet.utils.BlockInfo;
import carpet.utils.CommandHelper;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongSet;
@@ -1497,7 +1499,7 @@ public class WorldAccess {
boolean success = WorldTools.createWorld(cc.s.getServer(), worldKey, seed);
if (!success) return Value.FALSE;
CarpetServer.settingsManager.notifyPlayersCommandsChanged();
CommandHelper.notifyPlayersCommandsChanged(cc.s.getServer());
return Value.TRUE;
});
*/
@@ -1,14 +1,14 @@
package carpet.script.utils;
import carpet.CarpetServer;
import carpet.api.settings.CarpetRule;
import carpet.api.settings.Validator;
import carpet.script.CarpetScriptHost;
import carpet.script.CarpetScriptServer;
import carpet.script.exception.InternalExpressionException;
import carpet.script.value.MapValue;
import carpet.script.value.StringValue;
import carpet.script.value.Value;
import carpet.settings.ParsedRule;
import carpet.settings.Validator;
import carpet.utils.Messenger;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@@ -57,7 +57,7 @@ public class AppStoreManager
public static class ScarpetAppStoreValidator extends Validator<String>
{
@Override public String validate(CommandSourceStack source, ParsedRule<String> currentRule, String newValue, String string)
@Override public String validate(CommandSourceStack source, CarpetRule<String> currentRule, String newValue, String stringInput)
{
APP_STORE_ROOT = StoreNode.folder(null, "");
if (newValue.equalsIgnoreCase("none"))
@@ -2,6 +2,8 @@ package carpet.script.utils;
import carpet.CarpetServer;
import carpet.CarpetSettings;
import carpet.api.settings.CarpetRule;
import carpet.api.settings.RuleHelper;
import carpet.script.CarpetContext;
import carpet.script.CarpetScriptHost;
import carpet.script.value.BooleanValue;
@@ -12,8 +14,7 @@ import carpet.script.value.NumericValue;
import carpet.script.value.StringValue;
import carpet.script.value.Value;
import carpet.script.value.ValueConversions;
import carpet.settings.ParsedRule;
import carpet.settings.SettingsManager;
import carpet.api.settings.SettingsManager;
import com.sun.management.OperatingSystemMXBean;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
@@ -172,18 +173,18 @@ public class SystemInfo {
return new NumericValue(osBean.getProcessCpuLoad());
});
put("world_carpet_rules", c -> {
Collection<ParsedRule<?>> rules = CarpetServer.settingsManager.getRules();
Collection<CarpetRule<?>> rules = CarpetServer.settingsManager.getCarpetRules();
MapValue carpetRules = new MapValue(Collections.emptyList());
rules.forEach(rule -> {
carpetRules.put(new StringValue(rule.name), new StringValue(rule.getAsString()));
carpetRules.put(new StringValue(rule.name()), new StringValue(RuleHelper.toRuleString(rule.value())));
});
CarpetServer.extensions.forEach(e -> {
SettingsManager manager = e.customSettingsManager();
SettingsManager manager = e.extensionSettingsManager();
if (manager == null) return;
Collection<ParsedRule<?>> extensionRules = manager.getRules();
Collection<CarpetRule<?>> extensionRules = manager.getCarpetRules();
extensionRules.forEach(rule -> {
carpetRules.put(new StringValue(manager.getIdentifier()+":"+rule.name), new StringValue(rule.getAsString()));
carpetRules.put(new StringValue(manager.identifier()+":"+rule.name()), new StringValue(RuleHelper.toRuleString(rule.value())));
});
});
return carpetRules;
+16 -1
View File
@@ -1,5 +1,20 @@
package carpet.settings;
public interface Condition {
import carpet.CarpetSettings;
/**
* @deprecated Use {@link carpet.api.settings.Rule.Condition} instead
*
*/
@Deprecated(forRemoval = true)
public interface Condition extends carpet.api.settings.Rule.Condition {
boolean isTrue();
@Override
default boolean shouldRegister() {
CarpetSettings.LOG.warn("""
Extension is referencing outdated Condition class! Class that caused this was: %s
This won't be supported in later Carpet versions and will crash the game!""".formatted(getClass().getName()));
return isTrue();
}
}
+381 -183
View File
@@ -1,91 +1,231 @@
package carpet.settings;
import carpet.utils.Translations;
import carpet.CarpetSettings;
import carpet.api.settings.CarpetRule;
import carpet.api.settings.InvalidRuleValueException;
import carpet.api.settings.RuleHelper;
import carpet.api.settings.SettingsManager;
import carpet.api.settings.Validators;
import carpet.utils.Messenger;
import carpet.utils.TranslationKeys;
import carpet.utils.Translations;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.Component;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import net.minecraft.commands.CommandSourceStack;
import java.util.stream.Stream;
import org.apache.commons.lang3.ClassUtils;
import static carpet.utils.Translations.tr;
/**
* A parsed Carpet rule, with its field, name, value, and other useful stuff.
* A Carpet rule parsed from a field, with its name, value, and other useful stuff.
*
* It is generated from the fields with the {@link Rule} annotation
* It is used for the fields with the {@link Rule} annotation
* when being parsed by {@link SettingsManager#parseSettingsClass(Class)}.
*
* @param <T> The field's type
* @param <T> The field's (and rule's) type
* @deprecated Use the type {@link CarpetRule} instead
*/
public final class ParsedRule<T> implements Comparable<ParsedRule> {
@Deprecated(forRemoval = true) // to move to api.settings package and visibility to package private
public final class ParsedRule<T> implements CarpetRule<T>, Comparable<ParsedRule<?>> {
private static final Map<Class<?>, FromStringConverter<?>> CONVERTER_MAP = Map.ofEntries(
Map.entry(String.class, str -> str),
Map.entry(Boolean.class, str -> {
return switch (str) {
case "true" -> true;
case "false" -> false;
default -> throw new InvalidRuleValueException("Invalid boolean value");
};
}),
numericalConverter(Integer.class, Integer::parseInt),
numericalConverter(Double.class, Double::parseDouble),
numericalConverter(Long.class, Long::parseLong),
numericalConverter(Float.class, Float::parseFloat)
);
/**
* @deprecated No replacement for this, since a {@link CarpetRule} may not always use a {@link Field}.
* Use {@link #value()} to access the rule's value
*/
@Deprecated(forRemoval = true) // to private
public final Field field;
/**
* @deprecated Use {@link CarpetRule#name()} instead
*/
@Deprecated(forRemoval = true) // to private
public final String name;
/**
* @deprecated Use {@link RuleHelper#translatedDescription(CarpetRule)}, or get it from the translation system
*/
@Deprecated(forRemoval = true) // to remove
public final String description;
public final String scarpetApp;
/**
* @deprecated Use {@link CarpetRule#extraInfo()} instead
*/
@Deprecated(forRemoval = true) // to remove
public final List<String> extraInfo;
/**
* @deprecated Use {@link CarpetRule#categories()} instead
*/
@Deprecated(forRemoval = true) // to private
public final List<String> categories;
/**
* @deprecated Use {@link CarpetRule#suggestions()} instead
*/
@Deprecated(forRemoval = true) // to private (and rename?)
public final List<String> options;
/**
* @deprecated No replacement for this
*/
@Deprecated(forRemoval = true) // to pckg private (for printRulesToLog, or get a different way)
public boolean isStrict;
/**
* @deprecated Use {@link CarpetRule#canBeToggledClientSide()} instead
*/
@Deprecated(forRemoval = true) // to private (and maybe rename?)
public boolean isClient;
/**
* @deprecated Use {@link CarpetRule#type()} instead
*/
@Deprecated(forRemoval = true) // to private (or remove and delegate to typedfield?)
public final Class<T> type;
public final List<Validator<T>> validators;
/**
* @deprecated Use {@link CarpetRule#defaultValue()} instead
*/
@Deprecated(forRemoval = true) // to private
public final T defaultValue;
/**
* @deprecated Use {@link CarpetRule#settingsManager()} instead.
* This field may be {@code null} if the settings manager isn't an instance of the old type
*/
@Deprecated(forRemoval = true) // to remove in favour of realSettingsManager
public final carpet.settings.SettingsManager settingsManager;
/**
* @deprecated No replacement for this. A Carpet rule may not use {@link Validator}
*/
@Deprecated(forRemoval = true) // to remove (in favour of realValidators)
public final List<Validator<T>> validators;
/**
* @deprecated Use {@link CarpetRule#defaultValue()} and pass it to {@link RuleHelper#toRuleString(Object)} instead
*/
@Deprecated(forRemoval = true) // to remove
public final String defaultAsString;
public final SettingsManager settingsManager;
/**
* @deprecated No replacement for this, Scarpet Rules should be managed by the rule implementation
*/
@Deprecated(forRemoval = true) // to private/subclass
public final String scarpetApp;
private final FromStringConverter<T> converter;
private final SettingsManager realSettingsManager; // to rename to settingsManager
/**
* If you reference this field I'll steal your kneecaps
*/
@Deprecated(forRemoval = true)
public final List<carpet.api.settings.Validator<T>> realValidators; // to rename to validators and to package private for printRulesToLog
private final boolean isLegacy; // to remove, only used for fallbacks
@FunctionalInterface
interface FromStringConverter<T> {
T convert(String value) throws InvalidRuleValueException;
}
record RuleAnnotation(boolean isLegacy, String name, String desc, String[] extra, String[] category, String[] options, boolean strict, String appSource, Class<? extends carpet.api.settings.Validator>[] validators) {
}
ParsedRule(Field field, Rule rule, SettingsManager settingsManager)
/**
* If you call this method I'll steal your kneecaps
*/
@Deprecated(forRemoval = true)
public static <T> ParsedRule<T> of(Field field, SettingsManager settingsManager) {
RuleAnnotation rule;
if (settingsManager instanceof carpet.settings.SettingsManager && field.isAnnotationPresent(Rule.class)) { // Legacy path
Rule a = field.getAnnotation(Rule.class);
rule = new RuleAnnotation(true, a.name(), a.desc(), a.extra(), a.category(), a.options(), a.strict(), a.appSource(), a.validate());
} else if (field.isAnnotationPresent(carpet.api.settings.Rule.class)) {
carpet.api.settings.Rule a = field.getAnnotation(carpet.api.settings.Rule.class);
rule = new RuleAnnotation(false, null, null, null, a.categories(), a.options(), a.strict(), a.appSource(), a.validators());
} else {
// Don't allow to use old rule types in custom AND migrated settings manager
throw new IllegalArgumentException("Old rule annotation is only supported in legacy SettngsManager!");
}
return new ParsedRule<>(field, rule, settingsManager);
}
private ParsedRule(Field field, RuleAnnotation rule, SettingsManager settingsManager)
{
this.isLegacy = rule.isLegacy();
this.name = !isLegacy || rule.name().isEmpty() ? field.getName() : rule.name();
this.field = field;
this.name = rule.name().isEmpty() ? field.getName() : rule.name();
this.type = (Class<T>) field.getType();
this.description = rule.desc();
@SuppressWarnings("unchecked") // We are "defining" T here
Class<T> type = (Class<T>)ClassUtils.primitiveToWrapper(field.getType());
this.type = type;
this.isStrict = rule.strict();
this.extraInfo = List.of(rule.extra());
this.categories = List.of(rule.category());
this.scarpetApp = rule.appSource();
this.settingsManager = settingsManager;
this.validators = new ArrayList<>();
for (Class v : rule.validate())
this.validators.add((Validator<T>) callConstructor(v));
this.realSettingsManager = settingsManager;
if (!(settingsManager instanceof carpet.settings.SettingsManager)) {
// this is awkward... but people using a custom, new (extends only new api) manager should not be using this anyway but the interface method
this.settingsManager = null;
} else {
this.settingsManager = (carpet.settings.SettingsManager) settingsManager;
}
this.realValidators = Stream.of(rule.validators()).map(this::instantiateValidator).collect(Collectors.toList());
this.defaultValue = value();
FromStringConverter<T> converter0 = null;
if (categories.contains(RuleCategory.COMMAND))
{
this.validators.add(callConstructor(Validator._COMMAND.class));
this.realValidators.add(new Validator._COMMAND<T>());
if (this.type == String.class)
{
this.isStrict = false;
this.validators.add((Validator<T>) callConstructor(Validator._COMMAND_LEVEL_VALIDATOR.class));
this.realValidators.add(instantiateValidator(Validators.CommandLevel.class));
}
}
if (!scarpetApp.isEmpty())
{
this.validators.add((Validator<T>) callConstructor(Validator._SCARPET.class));
}
this.isClient = categories.contains(RuleCategory.CLIENT);
if (this.isClient)
{
this.validators.add(callConstructor(Validator._CLIENT.class));
this.realValidators.add(new Validator._CLIENT<>());
}
this.defaultValue = get();
this.defaultAsString = convertToString(this.defaultValue);
if (!scarpetApp.isEmpty())
{
this.realValidators.add(new Validator.ScarpetValidator<>());
}
if (rule.options().length > 0)
{
this.options = List.of(rule.options());
}
else if (this.type == boolean.class){
this.options = List.of("true","false");
else if (this.type == Boolean.class) {
this.options = List.of("true", "false");
}
else if(this.type == String.class && categories.contains(RuleCategory.COMMAND))
else if (this.type == String.class && categories.contains(RuleCategory.COMMAND))
{
this.options = List.of("true", "false", "ops");
this.options = Validators.CommandLevel.OPTIONS;
}
else if (this.type.isEnum())
{
this.options = Arrays.stream(this.type.getEnumConstants()).map(e -> ((Enum) e).name().toLowerCase(Locale.ROOT)).collect(Collectors.toUnmodifiableList());
this.options = Arrays.stream(this.type.getEnumConstants()).map(e -> ((Enum<?>) e).name().toLowerCase(Locale.ROOT)).collect(Collectors.toUnmodifiableList());
converter0 = str -> {
try {
@SuppressWarnings({"unchecked", "rawtypes"}) // Raw necessary because of signature. Unchecked because compiler doesn't know T extends Enum
T ret = (T)Enum.valueOf((Class<? extends Enum>) type, str.toUpperCase(Locale.ROOT));
return ret;
} catch (IllegalArgumentException e) {
throw new InvalidRuleValueException("Valid values for this rule are: " + this.options);
}
};
}
else
{
@@ -93,157 +233,83 @@ public final class ParsedRule<T> implements Comparable<ParsedRule> {
}
if (isStrict && !this.options.isEmpty())
{
if (this.type == boolean.class || this.type == int.class || this.type == double.class || this.type == float.class)
{
this.validators.add(callConstructor(Validator._STRICT_IGNORECASE.class));
}
else
{
this.validators.add(callConstructor(Validator._STRICT.class));
}
this.realValidators.add(0, new Validator.StrictValidator<>()); // at 0 prevents validators with side effects from running when invalid
}
if (converter0 == null) {
@SuppressWarnings("unchecked")
FromStringConverter<T> converterFromMap = (FromStringConverter<T>)CONVERTER_MAP.get(type);
if (converterFromMap == null) throw new UnsupportedOperationException("Unsupported type for ParsedRule" + type);
converter0 = converterFromMap;
}
this.converter = converter0;
// Language "constants"
String nameKey = TranslationKeys.RULE_NAME_PATTERN.formatted(settingsManager().identifier(), name());
String descKey = TranslationKeys.RULE_DESC_PATTERN.formatted(settingsManager().identifier(), name());
String extraPrefix = TranslationKeys.RULE_EXTRA_PREFIX_PATTERN.formatted(settingsManager().identifier(), name());
// to remove
this.description = isLegacy ? rule.desc() : Objects.requireNonNull(Translations.trOrNull(descKey), "No language key provided for " + descKey);
this.extraInfo = isLegacy ? List.of(rule.extra()) : getTranslationArray(extraPrefix);
this.defaultAsString = RuleHelper.toRuleString(this.defaultValue);
this.validators = realValidators.stream().filter(Validator.class::isInstance).map(v -> (Validator<T>) v).toList();
if (!isLegacy && !validators.isEmpty()) throw new IllegalArgumentException("Can't use legacy validators with new rules!");
// Language fallbacks - Also asserts the strings will be available in non-english languages, given current system has no fallback
if (isLegacy && !rule.name().isEmpty()) Translations.registerFallbackTranslation(nameKey, name);
Translations.registerFallbackTranslation(descKey, description);
Iterator<String> infoIterator = extraInfo.iterator();
for (int i = 0; infoIterator.hasNext(); i++) {
Translations.registerFallbackTranslation(extraPrefix + i, infoIterator.next());
}
}
private <T> T callConstructor(Class<T> cls)
@SuppressWarnings({"unchecked", "rawtypes"}) // Needed because of the annotation
private carpet.api.settings.Validator<T> instantiateValidator(Class<? extends carpet.api.settings.Validator> cls)
{
try
{
Constructor<T> constr = cls.getDeclaredConstructor();
Constructor<? extends carpet.api.settings.Validator> constr = cls.getDeclaredConstructor();
constr.setAccessible(true);
return constr.newInstance();
}
catch (ReflectiveOperationException e)
{
throw new RuntimeException(e);
throw new IllegalArgumentException(e);
}
}
public ParsedRule<T> set(CommandSourceStack source, String value)
@Override
public void set(CommandSourceStack source, String value) throws InvalidRuleValueException
{
if (settingsManager != null && settingsManager.locked)
return null;
if (type == String.class)
{
return set(source, (T) value, value);
}
else if (type == boolean.class)
{
return set(source, (T) (Object) Boolean.parseBoolean(value), value);
}
else if (type == int.class)
{
return set(source, (T) (Object) Integer.parseInt(value), value);
}
else if (type == double.class)
{
return set(source, (T) (Object) Double.parseDouble(value), value);
}
else if (type.isEnum())
{
String ucValue = value.toUpperCase(Locale.ROOT);
return set(source, (T) (Object) Enum.valueOf((Class<? extends Enum>) type, ucValue), value);
}
else
{
Messenger.m(source, "r Unknown type " + type.getSimpleName());
return null;
}
set(source, converter.convert(value), value);
}
ParsedRule<T> set(CommandSourceStack source, T value, String stringValue)
private void set(CommandSourceStack source, T value, String userInput) throws InvalidRuleValueException
{
try
for (carpet.api.settings.Validator<T> validator : this.realValidators)
{
for (Validator<T> validator : this.validators)
{
value = validator.validate(source, this, value, stringValue);
if (value == null)
{
if (source != null)
{
validator.notifyFailure(source, this, stringValue);
if (validator.description() != null)
Messenger.m(source, "r " + validator.description());
}
return null;
}
value = validator.validate(source, this, value, userInput); // should this recalculate the string? Another validator may have changed value
if (value == null) {
if (source != null) validator.notifyFailure(source, this, userInput);
throw new InvalidRuleValueException();
}
if (!value.equals(get()) || source == null)
{
}
if (!value.equals(value()) || source == null)
{
try {
this.field.set(null, value);
if (source != null) settingsManager.notifyRuleChanged(source, this, stringValue);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Couldn't access field for rule: " + name, e);
}
if (source != null) settingsManager().notifyRuleChanged(source, this, userInput);
}
catch (IllegalAccessException e)
{
Messenger.m(source, "r Unable to access setting for "+name);
return null;
}
return this;
}
/**
* @return The value of this {@link ParsedRule}, in its type
*/
public T get()
{
try
{
return (T) this.field.get(null);
}
catch (IllegalAccessException e)
{
throw new IllegalStateException(e);
}
}
/**
* @return The value of this {@link ParsedRule}, as a {@link String}
*/
public String getAsString()
{
return convertToString(get());
}
/**
* @return The value of this {@link ParsedRule}, converted to a {@link boolean}.
* It will only return {@link true} if it's a true {@link boolean} or
* a number greater than zero.
*/
public boolean getBoolValue()
{
if (type == boolean.class) return (Boolean) get();
if (ClassUtils.primitiveToWrapper(type).isAssignableFrom(Number.class)) return ((Number) get()).doubleValue() > 0;
return false;
}
/**
* @return Wether or not this {@link ParsedRule} is in its default value
*/
public boolean isDefault()
{
return defaultValue.equals(get());
}
/**
* Resets this rule to its default value
*/
public void resetToDefault(CommandSourceStack source)
{
set(source, defaultValue, defaultAsString);
}
private static String convertToString(Object value)
{
if (value instanceof Enum) return ((Enum) value).name().toLowerCase(Locale.ROOT);
return value.toString();
}
@Override
public boolean equals(Object obj)
{
return obj.getClass() == ParsedRule.class && ((ParsedRule) obj).name.equals(this.name);
return obj instanceof ParsedRule && ((ParsedRule<?>) obj).name.equals(this.name);
}
@Override
@@ -253,55 +319,187 @@ public final class ParsedRule<T> implements Comparable<ParsedRule> {
}
@Override
public int compareTo(ParsedRule o)
public String toString()
{
return this.name.compareTo(o.name);
return this.name + ": " + RuleHelper.toRuleString(value());
}
@Override
public String toString()
{
return this.name + ": " + getAsString();
public String name() {
return name;
}
private String translationKey()
{
return String.format("rule.%s.name", name);
@Override
public List<Component> extraInfo() {
return getTranslationArray(TranslationKeys.RULE_EXTRA_PREFIX_PATTERN.formatted(settingsManager().identifier(), name()))
.stream()
.map(str -> Messenger.c("g " + str))
.toList();
}
private List<String> getTranslationArray(String prefix) {
List<String> ret = new ArrayList<>();
for (int i = 0; Translations.hasTranslation(prefix + i); i++) {
ret.add(Translations.tr(prefix + i));
}
return ret;
}
@Override
public Collection<String> categories() {
return categories;
}
@Override
public Collection<String> suggestions() {
return options;
}
@Override
public SettingsManager settingsManager() {
return realSettingsManager;
}
@Override
@SuppressWarnings("unchecked") // T comes from the field
public T value() {
try {
return (T) field.get(null);
} catch (IllegalAccessException e) {
// Can't happen at regular runtime because we'd have thrown it on construction
throw new IllegalArgumentException("Couldn't access field for rule: " + name, e);
}
}
@Override
public boolean canBeToggledClientSide() {
return isClient;
}
@Override
public Class<T> type() {
return type;
}
@Override
public T defaultValue() {
return defaultValue;
}
@Override
public void set(CommandSourceStack source, T value) throws InvalidRuleValueException {
set(source, value, RuleHelper.toRuleString(value));
}
private static <T> Map.Entry<Class<T>, FromStringConverter<T>> numericalConverter(Class<T> outputClass, Function<String, T> converter) {
return Map.entry(outputClass, str -> {
try {
return converter.apply(str);
} catch (NumberFormatException e) {
throw new InvalidRuleValueException("Invalid number for rule");
}
});
}
//TO REMOVE
/**
* @deprecated Use {@link CarpetRule#value()} instead
*/
@Deprecated(forRemoval = true)
public T get()
{
return value();
}
/**
* @deprecated Use {@link RuleHelper#toRuleString(Object) RuleHelper.convertToRuleString(rule.value())}
*/
@Deprecated(forRemoval = true)
public String getAsString()
{
return RuleHelper.toRuleString(value());
}
/**
* @return The value of this {@link ParsedRule}, converted to a {@link boolean}.
* It will only return {@link true} if it's a true {@link boolean} or
* a number greater than zero.
* @deprecated Use {@link RuleHelper#getBooleanValue(CarpetRule)}
*/
@Deprecated(forRemoval = true)
public boolean getBoolValue()
{
return RuleHelper.getBooleanValue(this);
}
/**
* @deprecated Use {@link RuleHelper#isInDefaultValue(CarpetRule)}
*/
@Deprecated(forRemoval = true)
public boolean isDefault()
{
return RuleHelper.isInDefaultValue(this);
}
/**
* @deprecated Use {@link RuleHelper#resetToDefault(CarpetRule, CommandSourceStack)}
*/
@Deprecated(forRemoval = true)
public void resetToDefault(CommandSourceStack source)
{
RuleHelper.resetToDefault(this, source);
}
/**
* @deprecated Forcing {@link Comparable} isn't a thing on {@link CarpetRule}s. Instead, pass a comparator by name to your
* sort methods, you can get one by calling {@code Comparator.comparing(CarpetRule::name)}
*/
@Override
@Deprecated(forRemoval = true)
public int compareTo(ParsedRule<?> o)
{
if (!warnedComparable) {
warnedComparable = true;
CarpetSettings.LOG.warn("""
Extension is relying on carpet rules to be comparable! This is not true for all carpet rules anymore, \
and will crash the game in future versions or if an extension adds non-comparable rules!
Fixing it is as simple as passing Comparator.comparing(CarpetRule::name) to the sorting method!""",
new Throwable("Location:").fillInStackTrace());
}
return this.name.compareTo(o.name);
}
private static boolean warnedComparable = false;
/**
* @return A {@link String} being the translated {@link ParsedRule#name} of this rule,
* in Carpet's configured language.
* @deprecated Use {@link RuleHelper#translatedName(CarpetRule)} instead
*/
public String translatedName(){
String key = translationKey();
return Translations.hasTranslation(key) ? tr(key) + String.format(" (%s)", name): name;
@Deprecated(forRemoval = true)
public String translatedName() {
return RuleHelper.translatedName(this);
}
/**
* @return A {@link String} being the translated {@link ParsedRule#description} of this rule,
* @return A {@link String} being the translated {@link ParsedRule#description description} of this rule,
* in Carpet's configured language.
* @deprecated Use {@link RuleHelper#translatedDescription(CarpetRule)} instead
*/
@Deprecated(forRemoval = true)
public String translatedDescription()
{
return tr(String.format("rule.%s.desc", (name)), description);
return RuleHelper.translatedDescription(this);
}
/**
* @return A {@link String} being the translated {@link ParsedRule#extraInfo} of this
* {@link ParsedRule}, in Carpet's configured language.
* @return A {@link String} being the translated {@link ParsedRule#extraInfo extraInfo} of this
* {@link ParsedRule}, in Carpet's configured language.
* @deprecated Use {@link CarpetRule#extraInfo()} instead
*/
@Deprecated(forRemoval = true)
public List<String> translatedExtras()
{
if (!Translations.hasTranslations()) return extraInfo;
String keyBase = String.format("rule.%s.extra.", name);
List<String> extras = new ArrayList<>();
int i = 0;
while (Translations.hasTranslation(keyBase+i))
{
extras.add(Translations.tr(keyBase+i));
i++;
}
return (extras.isEmpty()) ? extraInfo : extras;
return extraInfo().stream().map(Component::getString).toList();
}
}
+8 -5
View File
@@ -14,7 +14,10 @@ import java.lang.annotation.Target;
* - String
* - a subclass of Enum
* The default value of the rule will be the initial value of the field.
*
* @deprecated Use {@link carpet.api.settings.Rule} instead
*/
@Deprecated(forRemoval = true)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Rule
@@ -55,18 +58,18 @@ public @interface Rule
/**
* If specified, the rule will automatically enable or disable
* a builtin Scarpet Rule App with this name.
* Consider telling the rule name so users can edit globals
* (in case there are relevant globals to edit ofc)
*/
String appSource() default "";
/**
* The class of the validator checked when the rule is changed.
*/
Class<? extends Validator>[] validate() default {};
@SuppressWarnings("rawtypes")
Class<? extends carpet.api.settings.Validator>[] validate() default {};
/**
* The class of the condition checked when the rule is parsed.
* The class of the condition checked when the rule is parsed, before being added
* to the Settings Manager.
*/
Class<? extends Condition>[] condition() default {};
Class<? extends carpet.api.settings.Rule.Condition>[] condition() default {};
}
+48 -11
View File
@@ -1,22 +1,59 @@
package carpet.settings;
/**
* @deprecated Use {@link carpet.api.settings.RuleCategory} instead. Should be as simple as changing to that in the imports
*
*/
@Deprecated(forRemoval = true)
public class RuleCategory
{
public static final String BUGFIX = "bugfix";
public static final String SURVIVAL = "survival";
public static final String CREATIVE = "creative";
public static final String EXPERIMENTAL = "experimental";
public static final String OPTIMIZATION = "optimization";
public static final String FEATURE = "feature";
public static final String COMMAND = "command";
public static final String TNT = "tnt";
public static final String DISPENSER = "dispenser";
public static final String SCARPET = "scarpet";
/**
* Use {@link carpet.api.settings.RuleCategory#BUGFIX} instead
*/
public static final String BUGFIX = carpet.api.settings.RuleCategory.BUGFIX;
/**
* Use {@link carpet.api.settings.RuleCategory#SURVIVAL} instead
*/
public static final String SURVIVAL = carpet.api.settings.RuleCategory.SURVIVAL;
/**
* Use {@link carpet.api.settings.RuleCategory#CREATIVE} instead
*/
public static final String CREATIVE = carpet.api.settings.RuleCategory.CREATIVE;
/**
* Use {@link carpet.api.settings.RuleCategory#EXPERIMENTAL} instead
*/
public static final String EXPERIMENTAL = carpet.api.settings.RuleCategory.EXPERIMENTAL;
/**
* Use {@link carpet.api.settings.RuleCategory#OPTIMIZATION} instead
*/
public static final String OPTIMIZATION = carpet.api.settings.RuleCategory.OPTIMIZATION;
/**
* Use {@link carpet.api.settings.RuleCategory#FEATURE} instead
*/
public static final String FEATURE = carpet.api.settings.RuleCategory.FEATURE;
/**
* Use {@link carpet.api.settings.RuleCategory#COMMAND} instead
*/
public static final String COMMAND = carpet.api.settings.RuleCategory.COMMAND;
/**
* Use {@link carpet.api.settings.RuleCategory#TNT} instead
*/
public static final String TNT = carpet.api.settings.RuleCategory.TNT;
/**
* Use {@link carpet.api.settings.RuleCategory#DISPENSER} instead
*/
public static final String DISPENSER = carpet.api.settings.RuleCategory.DISPENSER;
/**
* Use {@link carpet.api.settings.RuleCategory#SCARPET} instead
*/
public static final String SCARPET = carpet.api.settings.RuleCategory.SCARPET;
/**
* Rules with this {@link RuleCategory} will have a client-side
* counterpart, so they can be set independently without the server
* being Carpet's
* @deprecated Use {@link carpet.api.settings.RuleCategory#CLIENT} instead
*/
public static final String CLIENT = "client";
@Deprecated(forRemoval = true)
public static final String CLIENT = carpet.api.settings.RuleCategory.CLIENT;
}
@@ -2,84 +2,29 @@ package carpet.settings;
import carpet.CarpetServer;
import carpet.CarpetSettings;
import carpet.network.ServerNetworkHandler;
import carpet.utils.Translations;
import carpet.utils.Messenger;
import com.google.common.collect.Sets;
import carpet.api.settings.CarpetRule;
import carpet.utils.CommandHelper;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.fabricmc.api.EnvType;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.core.RegistryAccess;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.TickTask;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.storage.LevelResource;
import org.apache.logging.log4j.util.TriConsumer;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static carpet.utils.Translations.tr;
import static carpet.script.CarpetEventServer.Event.CARPET_RULE_CHANGES;
import static net.minecraft.commands.SharedSuggestionProvider.suggest;
import static net.minecraft.commands.Commands.argument;
import static net.minecraft.commands.Commands.literal;
/**
* Manages and parses Carpet rules with their own command.
* @see #SettingsManager(String, String, String)
*
* @deprecated Use {@link carpet.api.settings.SettingsManager} instead
*/
public class SettingsManager
@Deprecated(forRemoval = true)
public class SettingsManager extends carpet.api.settings.SettingsManager
{
private Map<String, ParsedRule<?>> rules = new HashMap<>();
/**
* Whether or not this {@link SettingsManager} is locked. That is specified
* by writing "locked" at the beginning of the settings file. May not
* correctly react to changes.
* @deprecated Use {@link #locked()} instead
*/
@Deprecated(forRemoval = true) //to private (or protected?)
public boolean locked;
private final String version;
private final String identifier;
private final String fancyName;
private MinecraftServer server;
private List<TriConsumer<CommandSourceStack, ParsedRule<?>, String>> observers = new ArrayList<>();
private static List<TriConsumer<CommandSourceStack, ParsedRule<?>, String>> staticObservers = new ArrayList<>();
static record ConfigReadResult(Map<String, String> ruleMap, boolean locked) {}
/**
* Creates a new {@link SettingsManager} without a fancy name.
@@ -87,101 +32,56 @@ public class SettingsManager
*
* @param version A {@link String} with the mod's version
* @param identifier A {@link String} with the mod's id, will be the command name
* @deprecated This type is deprecated, use {@link carpet.api.settings.SettingsManager#SettingsManager(String, String, String)} instead
*/
@Deprecated(forRemoval = true)
public SettingsManager(String version, String identifier)
{
this.version = version;
this.identifier = identifier;
this.fancyName = identifier;
this(version, identifier, identifier);
}
/**
* Creates a new {@link SettingsManager} with a fancy name
* @see #SettingsManager(String, String)
*
* @param version A {@link String} with the mod's version
* @param identifier A {@link String} with the mod's id, will be the command name
* @param fancyName A {@link String} being the mod's fancy name.
*
* @deprecated This type is deprecated, use {@link carpet.api.settings.SettingsManager#SettingsManager(String, String, String)} instead
*/
@Deprecated(forRemoval = true)
public SettingsManager(String version, String identifier, String fancyName)
{
this.version = version;
this.identifier = identifier;
this.fancyName = fancyName;
super(version, identifier, fancyName);
}
/**
* @return A {@link String} being this {@link SettingsManager}'s
* identifier, which is also the command name
*/
public String getIdentifier() {
return identifier;
}
/**
* Attaches a {@link MinecraftServer} to this {@link SettingsManager}.<br>
* This is handled automatically by Carpet
* @param server The {@link MinecraftServer} instance to be attached
*/
public void attachServer(MinecraftServer server)
{
this.server = server;
loadConfigurationFromConf();
}
/**
* Detaches the {@link MinecraftServer} of this {@link SettingsManager} and
* resets its {@link ParsedRule}s to their default values.<br>
* This is handled automatically by Carpet
*/
public void detachServer()
{
for (ParsedRule<?> rule : rules.values()) rule.resetToDefault(null);
server = null;
}
/**
* Adds all annotated fields with the {@link Rule} annotation
* to this {@link SettingsManager} in order to handle them.
* @param settingsClass The class that will be analyzed
*/
public void parseSettingsClass(Class settingsClass)
{
rule: for (Field f : settingsClass.getDeclaredFields())
{
Rule rule = f.getAnnotation(Rule.class);
if (rule == null) continue;
ParsedRule<?> parsed = new ParsedRule<>(f, rule, this);
for (Class<? extends Condition> condition : rule.condition()) {
try
{
Constructor<?> constr = condition.getDeclaredConstructor();
constr.setAccessible(true);
if (!((Condition) constr.newInstance()).isTrue())
continue rule;
}
catch (ReflectiveOperationException e)
{
throw new RuntimeException(e);
}
}
rules.put(parsed.name, parsed);
}
}
/**
* Adds a custom rule observer to changes in rules from
* <b>this specific</b> {@link SettingsManager} instance.
* @see SettingsManager#addGlobalRuleObserver(TriConsumer)
* <p>Adds a custom rule observer to changes in rules from
* <b>this specific</b> {@link SettingsManager} instance.</p>
*
* <p>This <b>outdated</b> method may not be able to listen to all {@link CarpetRule} implementations</p>
*
* @see SettingsManager#registerGlobalRuleObserver(RuleObserver)
*
* @param observer A {@link TriConsumer} that will be called with
* the used {@link CommandSourceStack}, the changed
* {@link ParsedRule} and a {@link String} being the
* value that the user typed.
* @deprecated Use {@link SettingsManager#registerRuleObserver(RuleObserver)} instead.
*/
@Deprecated(forRemoval = true) //to remove
public void addRuleObserver(TriConsumer<CommandSourceStack, ParsedRule<?>, String> observer)
{
observers.add(observer);
registerRuleObserver((source, rule, stringValue) -> {
if (rule instanceof ParsedRule<?> pr)
observer.accept(source, pr, stringValue);
else
CarpetSettings.LOG.warn("Failed to notify observer '" + observer.getClass().getName() + "' about rule change");
});
CarpetSettings.LOG.warn("""
Extension added outdated rule observer, this is deprecated and will crash in later carpet versions \
(way before the rest of the old settings api because of relying on log4j)!
The observer class name is '%s'""".formatted(observer.getClass().getName()));
}
/**
@@ -193,187 +93,87 @@ public class SettingsManager
* the used {@link CommandSourceStack}, the changed
* {@link ParsedRule} and a {@link String} being the
* value that the user typed.
* @deprecated Use {@link SettingsManager#registerRuleObserver(RuleObserver)} instead, given this one can't deal with {@link CarpetRule}
*/
@Deprecated(forRemoval = true) // to remove. This isn't really used anywhere
public static void addGlobalRuleObserver(TriConsumer<CommandSourceStack, ParsedRule<?>, String> observer)
{
staticObservers.add(observer);
registerGlobalRuleObserver((source, rule, stringValue) -> {
if (rule instanceof ParsedRule<?> pr)
observer.accept(source, pr, stringValue);
else
CarpetSettings.LOG.warn("Failed to notify observer '" + observer.getClass().getName() + "' about rule change");
});
CarpetSettings.LOG.warn("""
Extension added outdated rule observer, this is deprecated and will crash in later carpet versions \
(way before the rest of the old settings api because of relying on log4j)!
The observer class name is '%s'""".formatted(observer.getClass().getName()));
}
void notifyRuleChanged(CommandSourceStack source, ParsedRule<?> rule, String userTypedValue)
{
observers.forEach(observer -> observer.accept(source, rule, userTypedValue));
staticObservers.forEach(observer -> observer.accept(source, rule, userTypedValue));
ServerNetworkHandler.updateRuleWithConnectedClients(rule);
switchScarpetRuleIfNeeded(source, rule);
if (CARPET_RULE_CHANGES.isNeeded()) CARPET_RULE_CHANGES.onCarpetRuleChanges(rule, source);
}
private void switchScarpetRuleIfNeeded(CommandSourceStack source, ParsedRule<?> rule)
{
if (!rule.scarpetApp.isEmpty())
{
if (rule.getBoolValue() || (rule.type == String.class && !rule.get().equals("false")))
{
CarpetServer.scriptServer.addScriptHost(source, rule.scarpetApp, s -> canUseCommand(s, rule.get()), false, false, true, null);
} else {
CarpetServer.scriptServer.removeScriptHost(source, rule.scarpetApp, false, true);
}
}
}
/**
* Initializes Scarpet rules in this {@link SettingsManager}, if any.<br>
* This is handled automatically by Carpet.
* @deprecated Use {@link #identifier()} instead
*/
public void initializeScarpetRules() {
for (ParsedRule<?> rule : rules.values())
{
if (!rule.scarpetApp.isEmpty()) {
switchScarpetRuleIfNeeded(server.createCommandSourceStack(), rule);
}
}
@Deprecated(forRemoval = true)
public String getIdentifier() {
return identifier();
}
/**
* @return An {@link Iterable} list of all categories
* from rules in this {@link SettingsManager}
*/
public Iterable<String> getCategories()
{
Set<String> categories = new HashSet<>();
getRules().stream().map(r -> r.categories).forEach(categories::addAll);
return categories;
}
/**
* Gets a registered rule in this {@link SettingsManager} by its name.
* <p>Gets a registered {@link ParsedRule} in this {@link SettingsManager} by its name.</p>
* @param name The rule name
* @return The {@link ParsedRule} with that name
* @deprecated Use {@link #getCarpetRule(String)} instead. This method is not able to return rules not implemented by {@link ParsedRule}
*/
@Deprecated(forRemoval = true)
public ParsedRule<?> getRule(String name)
{
return rules.get(name);
return getCarpetRule(name) instanceof ParsedRule<?> pr ? pr : null;
}
/**
* @return A {@link Collection} of the registered rules in this
* {@link SettingsManager}.
* @return A {@link Collection} of the registered {@link ParsedRule}s in this {@link SettingsManager}.
* @deprecated Use {@link #getCarpetRules()} instead. This method won't be able to return rules not implemented by {@link ParsedRule}
*/
@Deprecated(forRemoval = true)
public Collection<ParsedRule<?>> getRules()
{
return rules.values().stream().sorted().collect(Collectors.toList());
return List.of(getCarpetRules().stream().filter(ParsedRule.class::isInstance).map(ParsedRule.class::cast).toArray(ParsedRule[]::new));
}
/**
* @return A {@link Collection} of {@link ParsedRule}s that have defaults
* for this world different than the rule's default value.
* @deprecated Use {@link #dumpAllRulesToStream(java.io.PrintStream, String)} instead
*/
public Collection<ParsedRule<?>> findStartupOverrides()
{
Set<String> defaults = readSettingsFromConf(getFile()).ruleMap().keySet();
return rules.values().stream().filter(r -> defaults.contains(r.name)).
sorted().collect(Collectors.toList());
}
/**
* @return A collection of {@link ParsedRule} that are not in
* their default value
*/
public Collection<ParsedRule<?>> getNonDefault()
{
return rules.values().stream().filter(r -> !r.isDefault()).sorted().collect(Collectors.toList());
}
private Path getFile()
{
return server.getWorldPath(LevelResource.ROOT).resolve(identifier+".conf");
}
/**
* Disables all {@link ParsedRule} with the {@link RuleCategory#COMMAND} category,
* called when the {@link SettingsManager} is {@link #locked}.
*/
public void disableBooleanCommands()
{
for (ParsedRule<?> rule : rules.values())
{
if (!rule.categories.contains(RuleCategory.COMMAND))
continue;
if (rule.type == boolean.class)
((ParsedRule<Boolean>) rule).set(server.createCommandSourceStack(), false, "false");
if (rule.type == String.class && rule.options.contains("false"))
((ParsedRule<String>) rule).set(server.createCommandSourceStack(), "false", "false");
}
}
private void writeSettingsToConf(Map<String, String> values)
{
if (locked)
return;
try (BufferedWriter fw = Files.newBufferedWriter(getFile()))
{
for (String key: values.keySet())
{
fw.write(key+" "+values.get(key));
fw.newLine();
}
}
catch (IOException e)
{
CarpetSettings.LOG.error("[CM]: failed write "+identifier+".conf config file", e);
}
///todo is it really needed? resendCommandTree();
@Deprecated(forRemoval = true)
public int printAllRulesToLog(String category) {
return dumpAllRulesToStream(System.out, category);
}
/**
* Notifies all players that the commands changed by resending the command tree.
* @deprecated While there's not an API replacement for this (at least yet),
* you can use {@link CommandHelper#notifyPlayersCommandsChanged(MinecraftServer)} instead
*/
@Deprecated(forRemoval = true)
public void notifyPlayersCommandsChanged()
{
if (server == null || server.getPlayerList() == null)
{
return;
}
server.tell(new TickTask(this.server.getTickCount(), () ->
{
try {
for (ServerPlayer entityplayermp : server.getPlayerList().getPlayers()) {
server.getCommands().sendCommands(entityplayermp);
}
}
catch (NullPointerException ignored) {}
}));
CommandHelper.notifyPlayersCommandsChanged(CarpetServer.minecraft_server);
}
/**
* Returns whether the the {@link CommandSourceStack} can execute
* Returns whether the {@link CommandSourceStack} can execute
* a command given the required permission level, according to
* Carpet's standard for permissions.
* @param source The origin {@link CommandSourceStack}
* @param commandLevel A {@link String} being the permission level (either 0-4, a
* {@link boolean} value or "ops".
* @param commandLevel The permission level
* @return Whether or not the {@link CommandSourceStack} meets the required level
*
* @deprecated While there's not an API replacement for this (at least yet),
* you can use {@link CommandHelper#canUseCommand(CommandSourceStack, Object)} instead
*/
@Deprecated(forRemoval = true)
public static boolean canUseCommand(CommandSourceStack source, Object commandLevel)
{
if (commandLevel instanceof Boolean) return (Boolean) commandLevel;
String commandLevelString = commandLevel.toString();
switch (commandLevelString)
{
case "true": return true;
case "false": return false;
case "ops": return source.hasPermission(2); // typical for other cheaty commands
case "0":
case "1":
case "2":
case "3":
case "4":
return source.hasPermission(Integer.parseInt(commandLevelString));
}
return false;
return CommandHelper.canUseCommand(source, commandLevel);
}
/**
@@ -381,7 +181,13 @@ public class SettingsManager
* Carpet's standard for permissions (either 0-4, a {@link boolean},
* or "ops".
* @return An {@link int} with the translated Vanilla permission level
*
* @deprecated While there's not an API replacement for this (at least yet),
* you can use the similar {@link CommandHelper#canUseCommand(CommandSourceStack, Object)} instead
* @apiNote Note that this method returns {@code 2} for {@code false} and {@code 0} for {@code true}.
* You probably want to use {@link #canUseCommand(CommandSourceStack, Object)}e
*/
@Deprecated(forRemoval = true)
public static int getCommandLevel(String commandLevel)
{
switch (commandLevel)
@@ -398,183 +204,6 @@ public class SettingsManager
}
return 0;
}
private void loadConfigurationFromConf()
{
for (ParsedRule<?> rule : rules.values()) rule.resetToDefault(server.createCommandSourceStack());
ConfigReadResult conf = readSettingsFromConf(getFile());
locked = false;
if (conf.locked())
{
CarpetSettings.LOG.info("[CM]: "+fancyName+" features are locked by the administrator");
disableBooleanCommands();
}
for (String key: conf.ruleMap().keySet())
{
try
{
if (rules.get(key).set(server.createCommandSourceStack(), conf.ruleMap().get(key)) != null)
CarpetSettings.LOG.info("[CM]: loaded setting " + key + " as " + conf.ruleMap().get(key) + " from " + identifier + ".conf");
}
catch (Exception exc)
{
CarpetSettings.LOG.error("[CM Error]: Failed to load setting: "+key+", "+exc);
}
}
locked = conf.locked();
}
private ConfigReadResult readSettingsFromConf(Path path)
{
try (BufferedReader reader = Files.newBufferedReader(path))
{
String line = "";
boolean confLocked = false;
Map<String,String> result = new HashMap<String, String>();
while ((line = reader.readLine()) != null)
{
line = line.replaceAll("[\\r\\n]", "");
if ("locked".equalsIgnoreCase(line))
{
confLocked = true;
}
String[] fields = line.split("\\s+",2);
if (fields.length > 1)
{
if (!rules.containsKey(fields[0]))
{
CarpetSettings.LOG.error("[CM]: "+fancyName+" Setting " + fields[0] + " is not a valid - ignoring...");
continue;
}
ParsedRule rule = rules.get(fields[0]);
if (!(rule.options.contains(fields[1])) && rule.isStrict)
{
CarpetSettings.LOG.error("[CM]: The value of " + fields[1] + " for " + fields[0] + "("+fancyName+") is not valid - ignoring...");
continue;
}
result.put(fields[0],fields[1]);
}
}
return new ConfigReadResult(result, confLocked);
}
catch (NoSuchFileException e)
{
if (path.equals(getFile()) && FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT)
{
Path defaultsPath = FabricLoader.getInstance().getConfigDir().resolve("carpet/default_"+identifier+".conf");
try {
if (Files.notExists(defaultsPath))
{
Files.createDirectories(defaultsPath.getParent());
Files.createFile(defaultsPath);
}
return readSettingsFromConf(defaultsPath);
} catch (IOException e2) {
CarpetSettings.LOG.error("Exception when loading fallback default config: ", e2);
}
}
return new ConfigReadResult(new HashMap<>(), false);
}
catch (IOException e)
{
CarpetSettings.LOG.error("Exception while loading Carpet rules from config", e);
return new ConfigReadResult(new HashMap<>(), false);
}
}
private Collection<ParsedRule<?>> getRulesMatching(String search) {
String lcSearch = search.toLowerCase(Locale.ROOT);
return rules.values().stream().filter(rule ->
{
if (rule.name.toLowerCase(Locale.ROOT).contains(lcSearch)) return true; // substring match, case insensitive
for (String c : rule.categories) if (c.equals(search)) return true; // category exactly, case sensitive
return Sets.newHashSet(rule.description.toLowerCase(Locale.ROOT).split("\\W+")).contains(lcSearch); // contains full term in description, but case insensitive
}).sorted().collect(Collectors.toUnmodifiableList());
}
/**
* A method to pretty print in markdown (useful for Github wiki/readme) the current
* registered rules for a category to the log. Contains the name, description,
* categories, type, defaults, wether or not they are strict, their suggested
* values, and the descriptions of their validators.
* @param category A {@link String} being the category to print, {@link null} to print
* all registered rules.
* @return {@link int} 1 if things didn't fail, and all the info to System.out
*/
public int printAllRulesToLog(String category)
{
PrintStream ps = System.out;
ps.println("# "+fancyName+" Settings");
for (Map.Entry<String, ParsedRule<?>> e : new TreeMap<>(rules).entrySet())
{
ParsedRule<?> rule = e.getValue();
if (category != null && !rule.categories.contains(category))
continue;
ps.println("## " + rule.name);
ps.println(rule.description+" ");
for (String extra : rule.extraInfo)
ps.println(extra + " ");
ps.println("* Type: `" + rule.type.getSimpleName() + "` ");
ps.println("* Default value: `" + rule.defaultAsString + "` ");
String optionString = rule.options.stream().map(s -> "`" + s + "`").collect(Collectors.joining(", "));
if (!optionString.isEmpty()) ps.println((rule.isStrict?"* Required":"* Suggested")+" options: " + optionString + " ");
ps.println("* Categories: " + rule.categories.stream().map(s -> "`" + s.toUpperCase(Locale.ROOT) + "`").collect(Collectors.joining(", ")) + " ");
boolean preamble = false;
for (Validator<?> validator : rule.validators)
{
if(validator.description() != null)
{
if (!preamble)
{
ps.println("* Additional notes: ");
preamble = true;
}
ps.println(" * "+validator.description()+" ");
}
}
ps.println(" ");
}
return 1;
}
private ParsedRule<?> contextRule(CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException
{
String ruleName = StringArgumentType.getString(ctx, "rule");
ParsedRule<?> rule = getRule(ruleName);
if (rule == null)
throw new SimpleCommandExceptionType(Messenger.c("rb "+ tr("ui.unknown_rule","Unknown rule")+": "+ruleName)).create();
return rule;
}
static CompletableFuture<Suggestions> suggestMatchingContains(Stream<String> stream, SuggestionsBuilder suggestionsBuilder) {
List<String> regularSuggestionList = new ArrayList<>();
List<String> smartSuggestionList = new ArrayList<>();
String query = suggestionsBuilder.getRemaining().toLowerCase(Locale.ROOT);
stream.forEach((listItem) -> {
// Regex camelCase Search
var words = Arrays.stream(listItem.split("(?<!^)(?=[A-Z])")).map(s -> s.toLowerCase(Locale.ROOT)).collect(Collectors.toList());
var prefixes = new ArrayList<String>(words.size());
for (int i = 0; i < words.size(); i++)
prefixes.add(String.join("",words.subList(i, words.size())));
if (prefixes.stream().anyMatch(s -> s.startsWith(query))) {
smartSuggestionList.add(listItem);
}
// Regular prefix matching, reference: CommandSource.suggestMatching
if (SharedSuggestionProvider.matchesSubStr(query, listItem.toLowerCase(Locale.ROOT))) {
regularSuggestionList.add(listItem);
}
});
var filteredSuggestionList = regularSuggestionList.isEmpty() ? smartSuggestionList : regularSuggestionList;
Objects.requireNonNull(suggestionsBuilder);
filteredSuggestionList.forEach(suggestionsBuilder::suggest);
return suggestionsBuilder.buildFuture();
}
@Deprecated(forRemoval = true)
public void registerCommand(CommandDispatcher<CommandSourceStack> dispatcher)
{
@@ -582,216 +211,4 @@ public class SettingsManager
context.missingTagAccessPolicy(CommandBuildContext.MissingTagAccessPolicy.RETURN_EMPTY);
registerCommand(dispatcher, context);
}
/**
* Registers the settings command for this {@link SettingsManager}.<br>
* It is handled automatically by Carpet.
* @param dispatcher The current {@link CommandDispatcher}
* @param commandBuildContext The current {@link CommandBuildContext}
*/
public void registerCommand(final CommandDispatcher<CommandSourceStack> dispatcher, final CommandBuildContext commandBuildContext)
{
if (dispatcher.getRoot().getChildren().stream().anyMatch(node -> node.getName().equalsIgnoreCase(identifier)))
{
CarpetSettings.LOG.error("Failed to add settings command for " + identifier + ". It is masking previous command.");
return;
}
LiteralArgumentBuilder<CommandSourceStack> literalargumentbuilder = literal(identifier).requires((player) ->
canUseCommand(player, CarpetSettings.carpetCommandPermissionLevel) && !locked);
literalargumentbuilder.executes((context)-> listAllSettings(context.getSource())).
then(literal("list").
executes( (c) -> listSettings(c.getSource(), String.format(tr("ui.all_%(mod)s_settings","All %s Settings"), fancyName),
getRules())).
then(literal("defaults").
executes( (c)-> listSettings(c.getSource(),
String.format(tr("ui.current_%(mod)s_startup_settings_from_%(conf)s","Current %s Startup Settings from %s"), fancyName, (identifier+".conf")),
findStartupOverrides()))).
then(argument("tag",StringArgumentType.word()).
suggests( (c, b)->suggest(getCategories(), b)).
executes( (c) -> listSettings(c.getSource(),
String.format(tr("ui.%(mod)s_settings_matching_'%(query)s'","%s Settings matching \"%s\""), fancyName, tr("category." + StringArgumentType.getString(c, "tag"),StringArgumentType.getString(c, "tag"))),
getRulesMatching(StringArgumentType.getString(c, "tag")))))).
then(literal("removeDefault").
requires(s -> !locked).
then(argument("rule", StringArgumentType.word()).
suggests( (c, b) -> suggestMatchingContains(getRules().stream().map(r -> r.name), b)).
executes((c) -> removeDefault(c.getSource(), contextRule(c))))).
then(literal("setDefault").
requires(s -> !locked).
then(argument("rule", StringArgumentType.word()).
suggests( (c, b) -> suggestMatchingContains(getRules().stream().map(r -> r.name), b)).
then(argument("value", StringArgumentType.greedyString()).
suggests((c, b)-> suggest(contextRule(c).options, b)).
executes((c) -> setDefault(c.getSource(), contextRule(c), StringArgumentType.getString(c, "value")))))).
then(argument("rule", StringArgumentType.word()).
suggests( (c, b) -> suggestMatchingContains(getRules().stream().map(r -> r.name), b)).
requires(s -> !locked ).
executes( (c) -> displayRuleMenu(c.getSource(), contextRule(c))).
then(argument("value", StringArgumentType.greedyString()).
suggests((c, b)-> suggest(contextRule(c).options,b)).
executes((c) -> setRule(c.getSource(), contextRule(c), StringArgumentType.getString(c, "value")))));
dispatcher.register(literalargumentbuilder);
}
private int displayRuleMenu(CommandSourceStack source, ParsedRule<?> rule)
{
Player player;
String displayName = rule.translatedName();
Messenger.m(source, "");
Messenger.m(source, "wb "+ displayName ,"!/"+identifier+" "+rule.name,"^g refresh");
Messenger.m(source, "w "+ rule.translatedDescription());
rule.translatedExtras().forEach(s -> Messenger.m(source, "g "+s));
List<Component> tags = new ArrayList<>();
tags.add(Messenger.c("w "+ tr("ui.tags", "Tags")+": "));
for (String t: rule.categories)
{
String translated = tr("category." + t, t);
tags.add(Messenger.c("c ["+ translated +"]", "^g "+ String.format(tr("list_all_%s_settings","list all %s settings"), translated),"!/"+identifier+" list "+t));
tags.add(Messenger.c("w , "));
}
tags.remove(tags.size()-1);
Messenger.m(source, tags.toArray(new Object[0]));
Messenger.m(source, "w "+ tr("ui.current_value", "Current value")+": ",String.format("%s %s (%s value)",rule.getBoolValue()?"lb":"nb", rule.getAsString(),rule.isDefault()?"default":"modified"));
List<Component> options = new ArrayList<>();
options.add(Messenger.c("w Options: ", "y [ "));
for (String o: rule.options)
{
options.add(makeSetRuleButton(rule, o, false));
options.add(Messenger.c("w "));
}
options.remove(options.size()-1);
options.add(Messenger.c("y ]"));
Messenger.m(source, options.toArray(new Object[0]));
return 1;
}
private int setRule(CommandSourceStack source, ParsedRule<?> rule, String newValue)
{
if (rule.set(source, newValue) != null)
Messenger.m(source, "w "+rule.toString()+", ", "c ["+ tr("ui.change_permanently","change permanently")+"?]",
"^w "+String.format(tr("ui.click_to_keep_the_settings_in_%(conf)s_to_save_across_restarts","Click to keep the settings in %s to save across restarts"), identifier+".conf"),
"?/"+identifier+" setDefault "+rule.name+" "+rule.getAsString());
return 1;
}
// stores different defaults in the file
private int setDefault(CommandSourceStack source, ParsedRule<?> rule, String stringValue)
{
if (locked) return 0;
if (!rules.containsKey(rule.name)) return 0;
ConfigReadResult conf = readSettingsFromConf(getFile());
conf.ruleMap().put(rule.name, stringValue);
writeSettingsToConf(conf.ruleMap()); // this may feels weird, but if conf
// is locked, it will never reach this point.
rule.set(source,stringValue);
Messenger.m(source ,"gi "+String.format(tr("ui.rule_%(rule)s_will_now_default_to_%(value)s","Rule %s will now default to %s"), rule.translatedName(), stringValue));
return 1;
}
// removes overrides of the default values in the file
private int removeDefault(CommandSourceStack source, ParsedRule<?> rule)
{
if (locked) return 0;
if (!rules.containsKey(rule.name)) return 0;
ConfigReadResult conf = readSettingsFromConf(getFile());
conf.ruleMap().remove(rule.name);
writeSettingsToConf(conf.ruleMap());
rules.get(rule.name).resetToDefault(source);
Messenger.m(source ,"gi "+String.format(tr("ui.rule_%(rule)s_not_set_restart","Rule %s will now not be set on restart"), rule.translatedName()));
return 1;
}
private Component displayInteractiveSetting(ParsedRule<?> rule)
{
String displayName = rule.translatedName();
List<Object> args = new ArrayList<>();
args.add("w - "+ displayName +" ");
args.add("!/"+identifier+" "+rule.name);
args.add("^y "+rule.translatedDescription());
for (String option: rule.options)
{
args.add(makeSetRuleButton(rule, option, true));
args.add("w ");
}
if (!rule.options.contains(rule.getAsString()))
{
args.add(makeSetRuleButton(rule, rule.getAsString(), true));
args.add("w ");
}
args.remove(args.size()-1);
return Messenger.c(args.toArray(new Object[0]));
}
private Component makeSetRuleButton(ParsedRule<?> rule, String option, boolean brackets)
{
String style = rule.isDefault()?"g":(option.equalsIgnoreCase(rule.defaultAsString)?"e":"y");
if (option.equalsIgnoreCase(rule.getAsString()))
{
style = style + "u";
if (option.equalsIgnoreCase(rule.defaultAsString))
style = style + "b";
}
String baseText = style + (brackets ? " [" : " ") + option + (brackets ? "]" : "");
if (locked)
return Messenger.c(baseText, "^g "+fancyName+" " + tr("ui.settings_are_locked","settings are locked"));
if (option.equalsIgnoreCase(rule.getAsString()))
return Messenger.c(baseText);
return Messenger.c(baseText, "^g "+ tr("ui.switch_to","Switch to") +" " + option+(option.equalsIgnoreCase(rule.defaultAsString)?" (default)":""), "?/"+identifier+" " + rule.name + " " + option);
}
private int listSettings(CommandSourceStack source, String title, Collection<ParsedRule<?>> settings_list)
{
Messenger.m(source,String.format("wb %s:",title));
settings_list.forEach(e -> Messenger.m(source,displayInteractiveSetting(e)));
return settings_list.size();
}
private int listAllSettings(CommandSourceStack source)
{
int count = listSettings(source, String.format(tr("ui.current_%(mod)s_settings","Current %s Settings"), fancyName), getNonDefault());
if (version != null)
Messenger.m(source, "g "+fancyName+" "+ tr("ui.version", "version") + ": "+ version);
List<Object> tags = new ArrayList<>();
tags.add("w " + tr("ui.browse_categories", "Browse Categories") + ":\n");
for (String t : getCategories())
{
String catKey = "category." + t;
String translated = tr(catKey, t);
String translatedPlus = Translations.hasTranslation(catKey) ? String.format("%s (%s)",tr(catKey, t), t) : t;
tags.add("c [" + translated +"]");
tags.add("^g " + String.format(tr("ui.list_all_%(cat)s_settings","list all %s settings"), translatedPlus));
tags.add("!/"+identifier+" list " + t);
tags.add("w ");
}
tags.remove(tags.size() - 1);
Messenger.m(source, tags.toArray(new Object[0]));
return count;
}
public void inspectClientsideCommand(CommandSourceStack source, String string)
{
if (string.startsWith("/"+identifier+" "))
{
String[] res = string.split("\\s+", 3);
if (res.length == 3)
{
String setting = res[1];
String strOption = res[2];
if (rules.containsKey(setting) && rules.get(setting).isClient)
{
rules.get(setting).set(source, strOption);
}
}
}
}
}
+63 -86
View File
@@ -1,42 +1,82 @@
package carpet.settings;
import carpet.CarpetServer;
import carpet.CarpetSettings;
import carpet.api.settings.CarpetRule;
import carpet.api.settings.Validators;
import carpet.utils.CommandHelper;
import carpet.utils.Messenger;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import net.minecraft.commands.CommandSourceStack;
public abstract class Validator<T>
/**
* @deprecated Use {@link carpet.api.settings.Validator} instead
*/
@Deprecated(forRemoval = true)
public abstract class Validator<T> extends carpet.api.settings.Validator<T>
{
{
// Print deprecation warning once while instantiating the class
CarpetSettings.LOG.warn("""
Validator '%s' is implementing the old Validator class! This class is deprecated and will be removed \
and crash in later Carpet versions!""".formatted(getClass().getName()));
}
/**
* Validate the new value of a rule
* @return true if valid, false if new rule invalid.
* @return a value if the given one was valid or could be cleanly adapted, null if new value is invalid.
*/
public abstract T validate(CommandSourceStack source, ParsedRule<T> currentRule, T newValue, String string);
public String description() {return null;}
public void notifyFailure(CommandSourceStack source, ParsedRule<T> currentRule, String providedValue)
{
Messenger.m(source, "r Wrong value for " + currentRule.name + ": " + providedValue);
@Override
public final T validate(CommandSourceStack source, CarpetRule<T> changingRule, T newValue, String stringInput) {
// Compatibility code
if (!(changingRule instanceof ParsedRule<T> parsedRule))
// Throwing here is not an issue because Carpet's current implementation only calls validators with ParsedRule.
// This would be thrown if a different implementation tries to use it, and then it's their issue in multiple ways
throw new IllegalArgumentException("Passed a non-ParsedRule to a validator using the outdated method!");
return validate(source, parsedRule, newValue, stringInput);
}
/**
* @deprecated Implement {@link #validate(CommandSourceStack, CarpetRule, Object, String)} instead! It will get abstract soon!
*/
@Deprecated(forRemoval = true)
public abstract T validate(CommandSourceStack source, ParsedRule<T> currentRule, T newValue, String string);
public static class _COMMAND<T> extends Validator<T>
/**
* @deprecated Use {@link Validators.CommandLevel} instead
*/
@Deprecated(forRemoval = true) // to remove
public static class _COMMAND_LEVEL_VALIDATOR extends Validators.CommandLevel {}
/**
* @deprecated Use {@link Validators.NonNegativeNumber} instead
*/
@Deprecated(forRemoval = true) // to remove
public static class NONNEGATIVE_NUMBER<T extends Number> extends Validators.NonNegativeNumber<T> {}
/**
* @deprecated Use {@link Validators.Probablity} instead
*/
@Deprecated(forRemoval = true) // to remove
public static class PROBABILITY <T extends Number> extends Validators.Probablity<T> {}
// The ones below are part of the implementation of ParsedRule or printRulesToLog, so they need to be close to it to stay hidden
// They will need to be moved when moving ParsedRule
static class _COMMAND<T> extends carpet.api.settings.Validator<T>
{
@Override
public T validate(CommandSourceStack source, ParsedRule<T> currentRule, T newValue, String string)
public T validate(CommandSourceStack source, CarpetRule<T> currentRule, T newValue, String string)
{
if (CarpetServer.settingsManager != null && source != null)
CarpetServer.settingsManager.notifyPlayersCommandsChanged();
if (source != null)
CommandHelper.notifyPlayersCommandsChanged(source.getServer());
return newValue;
}
@Override
public String description() { return "It has an accompanying command";}
}
public static class _CLIENT<T> extends Validator<T>
// maybe remove this one and make printRulesToLog check for canBeToggledClientSide instead
static class _CLIENT<T> extends carpet.api.settings.Validator<T>
{
@Override
public T validate(CommandSourceStack source, ParsedRule<T> currentRule, T newValue, String string)
public T validate(CommandSourceStack source, CarpetRule<T> currentRule, T newValue, String string)
{
return newValue;
}
@@ -45,25 +85,10 @@ public abstract class Validator<T>
"In these situations (on vanilla servers) it will only affect the executing player, so each player needs to type it" +
" separately for the desired effect";}
}
public static class _COMMAND_LEVEL_VALIDATOR extends Validator<String> {
private static List<String> OPTIONS = List.of("true", "false", "ops", "0", "1", "2", "3", "4");
@Override public String validate(CommandSourceStack source, ParsedRule<String> currentRule, String newValue, String userString) {
if (!OPTIONS.contains(userString.toLowerCase(Locale.ROOT)))
{
Messenger.m(source, "r Valid options for command type rules is 'true' or 'false'");
Messenger.m(source, "r Optionally you can choose 'ops' to only allow operators");
Messenger.m(source, "r or provide a custom required permission level");
return null;
}
return userString.toLowerCase(Locale.ROOT);
}
@Override public String description() { return "Can be limited to 'ops' only, or a custom permission level";}
}
public static class _SCARPET<T> extends Validator<T> {
static class ScarpetValidator<T> extends carpet.api.settings.Validator<T> { //TODO remove? The additional info isn't that useful tbh
@Override
public T validate(CommandSourceStack source, ParsedRule<T> currentRule, T newValue, String string)
public T validate(CommandSourceStack source, CarpetRule<T> currentRule, T newValue, String string)
{
return newValue;
}
@@ -72,65 +97,17 @@ public abstract class Validator<T>
}
}
public static class WIP<T> extends Validator<T>
static class StrictValidator<T> extends carpet.api.settings.Validator<T>
{
@Override
public T validate(CommandSourceStack source, ParsedRule<T> currentRule, T newValue, String string)
public T validate(CommandSourceStack source, CarpetRule<T> currentRule, T newValue, String string)
{
Messenger.m(source, "r "+currentRule.name+" is missing a few bits - we are still working on it.");
return newValue;
}
@Override
public String description() { return "A few bits still needs implementing - we are working on it";}
}
public static class _STRICT<T> extends Validator<T>
{
@Override
public T validate(CommandSourceStack source, ParsedRule<T> currentRule, T newValue, String string)
{
if (!currentRule.options.contains(string))
if (!currentRule.suggestions().contains(string))
{
Messenger.m(source, "r Valid options: " + currentRule.options.toString());
Messenger.m(source, "r Valid options: " + currentRule.suggestions().toString());
return null;
}
return newValue;
}
}
public static class _STRICT_IGNORECASE<T> extends Validator<T>
{
@Override
public T validate(CommandSourceStack source, ParsedRule<T> currentRule, T newValue, String string)
{
if (!currentRule.options.stream().map(s->s.toLowerCase(Locale.ROOT)).collect(Collectors.toSet())
.contains(string.toLowerCase(Locale.ROOT)))
{
Messenger.m(source, "r Valid options (case insensitive): " + currentRule.options.toString());
return null;
}
return newValue;
}
}
public static class NONNEGATIVE_NUMBER <T extends Number> extends Validator<T>
{
@Override
public T validate(CommandSourceStack source, ParsedRule<T> currentRule, T newValue, String string)
{
return newValue.doubleValue() >= 0 ? newValue : null;
}
@Override
public String description() { return "Must be a positive number";}
}
public static class PROBABILITY <T extends Number> extends Validator<T>
{
@Override
public T validate(CommandSourceStack source, ParsedRule<T> currentRule, T newValue, String string)
{
return (newValue.doubleValue() >= 0 && newValue.doubleValue() <= 1 )? newValue : null;
}
@Override
public String description() { return "Must be between 0 and 1";}
}
}
@@ -0,0 +1,57 @@
package carpet.utils;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.TickTask;
import net.minecraft.server.level.ServerPlayer;
/**
* A few helpful methods to work with settings and commands.
*
* This is not any kind of API, but it's unlikely to change
*
*/
public final class CommandHelper {
private CommandHelper() {}
/**
* Notifies all players that the commands changed by resending the command tree.
*/
public static void notifyPlayersCommandsChanged(MinecraftServer server)
{
if (server == null || server.getPlayerList() == null)
{
return;
}
server.tell(new TickTask(server.getTickCount(), () ->
{
try {
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
server.getCommands().sendCommands(player);
}
}
catch (NullPointerException ignored) {}
}));
}
/**
* Whether the given source has enough permission level to run a command that requires the given commandLevel
*/
public static boolean canUseCommand(CommandSourceStack source, Object commandLevel)
{
if (commandLevel instanceof Boolean) return (Boolean) commandLevel;
String commandLevelString = commandLevel.toString();
switch (commandLevelString)
{
case "true": return true;
case "false": return false;
case "ops": return source.hasPermission(2); // typical for other cheaty commands
case "0":
case "1":
case "2":
case "3":
case "4":
return source.hasPermission(Integer.parseInt(commandLevelString));
}
return false;
}
}
@@ -0,0 +1,31 @@
package carpet.utils;
/**
* This is not public API!
*/
public final class TranslationKeys {
public static final String BASE_RULE_NAMESPACE = "%s.rule.";
public static final String BASE_RULE_PATTERN = BASE_RULE_NAMESPACE + "%s."; // [settingsManager].rule.[name]
public static final String RULE_NAME_PATTERN = BASE_RULE_PATTERN + "name";
public static final String RULE_DESC_PATTERN = BASE_RULE_PATTERN + "desc";
public static final String RULE_EXTRA_PREFIX_PATTERN = BASE_RULE_PATTERN + "extra.";
public static final String CATEGORY_PATTERN = "%s.category.%s"; //[settingsManager].category.[name]
// Settings command
private static final String SETTINGS_BASE = "carpet.settings.command.";
public static final String BROWSE_CATEGORIES = SETTINGS_BASE + "browse_categories";
public static final String VERSION = SETTINGS_BASE + "version";
public static final String LIST_ALL_CATEGORY = SETTINGS_BASE + "list_all_category";
public static final String CURRENT_SETTINGS_HEADER = SETTINGS_BASE + "current_settings_header";
public static final String SWITCH_TO = SETTINGS_BASE + "switch_to";
public static final String UNKNOWN_RULE = SETTINGS_BASE + "unknown_rule";
public static final String CURRENT_FROM_FILE_HEADER = SETTINGS_BASE + "current_from_file_header";
public static final String MOD_SETTINGS_MATCHING = SETTINGS_BASE + "mod_settings_matching";
public static final String ALL_MOD_SETTINGS = SETTINGS_BASE + "all_mod_settings";
public static final String TAGS = SETTINGS_BASE + "tags";
public static final String CHANGE_PERMANENTLY = SETTINGS_BASE + "change_permanently";
public static final String CHANGE_PERMANENTLY_HOVER = SETTINGS_BASE + "change_permanently_tooltip";
public static final String DEFAULT_SET = SETTINGS_BASE + "default_set";
public static final String DEFAULT_REMOVED = SETTINGS_BASE + "default_removed";
public static final String CURRENT_VALUE = SETTINGS_BASE + "current_value";
}
+66 -30
View File
@@ -10,33 +10,38 @@ import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import net.minecraft.commands.CommandSourceStack;
public class Translations
{
private static Map<String, String> translationMap;
private static Map<String, String> translationMap = Collections.emptyMap();
public static String tr(String key)
{
return translationMap == null ? key : translationMap.getOrDefault(key, key);
return translationMap.getOrDefault(key, key);
}
public static String trOrNull(String key)
{
return translationMap.get(key);
}
public static String tr(String key, String str)
{
return translationMap == null ? str : translationMap.getOrDefault(key, str);
return translationMap.getOrDefault(key, str);
}
public static boolean hasTranslations()
{
return translationMap != null;
return !translationMap.isEmpty();
}
public static boolean hasTranslation(String key)
{
return translationMap != null && translationMap.containsKey(key);
return translationMap.containsKey(key);
}
public static Map<String, String> getTranslationFromResourcePath(String path)
@@ -45,44 +50,59 @@ public class Translations
try
{
dataJSON = IOUtils.toString(
Objects.requireNonNull(Translations.class.getClassLoader().getResourceAsStream(String.format("assets/carpet/lang/%s.json", CarpetSettings.language))),
Objects.requireNonNull(Translations.class.getClassLoader().getResourceAsStream(path)),
StandardCharsets.UTF_8);
} catch (NullPointerException | IOException e) {
return null;
return Map.of();
}
Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
Gson gson = new GsonBuilder().setLenient().create();
return gson.fromJson(dataJSON, new TypeToken<Map<String, String>>() {}.getType());
}
public static void updateLanguage(CommandSourceStack source)
public static void updateLanguage()
{
if (CarpetSettings.language.equalsIgnoreCase("none"))
{
translationMap = null;
return;
}
Map<String, String> translations = new HashMap<>();
Map<String, String> trans = getTranslationFromResourcePath(String.format("assets/carpet/lang/%s.json", CarpetSettings.language));
if (trans != null) trans.forEach(translations::put);
translations.putAll(getTranslationFromResourcePath(String.format("assets/carpet/lang/%s.json", CarpetSettings.language)));
for (CarpetExtension ext : CarpetServer.extensions)
{
Map<String, String> extMappings = ext.canHasTranslations(CarpetSettings.language);
if (extMappings != null)
{
extMappings.forEach((key, value) ->
{
if (!translations.containsKey(key)) translations.put(key, value);
});
if (extMappings == null) continue; // would be nice to get rid of this, but too many extensions return null where they don't know they do
boolean warned = false;
for (var entry : extMappings.entrySet()) {
var key = entry.getKey();
// Migrate the old format
if (!key.startsWith("carpet.")) {
if (key.startsWith("rule.")) {
// default to carpet's settings manager. Custom managers are really uncommon and the known ones don't provide translations anyway
key = TranslationKeys.BASE_RULE_NAMESPACE.formatted("carpet") + key.substring(5);
} else if (key.startsWith("category.")) {
key = TranslationKeys.CATEGORY_PATTERN.formatted("carpet", key.substring(9));
}
if (!warned && key != entry.getKey()) {
CarpetSettings.LOG.warn("""
Found outdated translation keys in extension '%s'!
These won't be supported in a later Carpet version!
Carpet will now try to map them to the correct keys in a best-effort basis""".formatted(ext.getClass().getName()));
warned = true;
}
}
translations.putIfAbsent(key, entry.getValue());
}
}
translations.entrySet().removeIf(e -> e.getKey().startsWith("//"));
if (translations.isEmpty())
{
translationMap = null;
return;
}
translations.keySet().removeIf(e -> {
if (e.startsWith("//")) {
CarpetSettings.LOG.warn("""
Found translation key starting with // while preparing translations!
Doing this is deprecated and may cause issues in later versions! Consider settings GSON to "lenient" mode and
using regular comments instead!
Translation key is '%s'""".formatted(e));
return true;
} else
return false;
});
// Remove after deprecated settings api is removed
addFallbacksTo(translations);
translationMap = translations;
}
@@ -91,4 +111,20 @@ public class Translations
// will put some validations for availble languages at some point
return true;
}
// fallbacks for old rules that don't define rule descriptions or stuff in language files yet
// to be removed when old settings system is removed and translation refactor is finished
private static final Map<String, String> FALLBACKS = new HashMap<>();
/**
* @deprecated if you compile against this method I'll steal your kneecaps
*/
@Deprecated(forRemoval = true)
public static void registerFallbackTranslation(String key, String description) {
FALLBACKS.put(key, description);
}
private static void addFallbacksTo(Map<String, String> translationMap) {
FALLBACKS.forEach(translationMap::putIfAbsent);
}
}