diff --git a/src/main/java/carpet/CarpetExtension.java b/src/main/java/carpet/CarpetExtension.java index a49f38ab..1918a166 100644 --- a/src/main/java/carpet/CarpetExtension.java +++ b/src/main/java/carpet/CarpetExtension.java @@ -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} 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 canHasTranslations(String lang) { return null;} + default Map canHasTranslations(String lang) { return Collections.emptyMap();} /** * Handles each call that creates / parses the scarpet expression. diff --git a/src/main/java/carpet/CarpetServer.java b/src/main/java/carpet/CarpetServer.java index 9bbf056a..34051be4 100644 --- a/src/main/java/carpet/CarpetServer.java +++ b/src/main/java/carpet/CarpetServer.java @@ -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 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 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 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())); + } } diff --git a/src/main/java/carpet/CarpetSettings.java b/src/main/java/carpet/CarpetSettings.java index 03114194..a2972d7f 100644 --- a/src/main/java/carpet/CarpetSettings.java +++ b/src/main/java/carpet/CarpetSettings.java @@ -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 { - @Override public String validate(CommandSourceStack source, ParsedRule currentRule, String newValue, String string) { - if (currentRule.get().equals(newValue) || source == null) - { - return newValue; - } + @Override public String validate(CommandSourceStack source, CarpetRule 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 { - @Override public String validate(CommandSourceStack source, ParsedRule currentRule, String newValue, String string) { + @Override public String validate(CommandSourceStack source, CarpetRule 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 { - @Override public Integer validate(CommandSourceStack source, ParsedRule currentRule, Integer newValue, String string) { + @Override public Integer validate(CommandSourceStack source, CarpetRule 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 { @Override - public String validate(CommandSourceStack source, ParsedRule currentRule, String newValue, String string) + public String validate(CommandSourceStack source, CarpetRule 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 extends Validator { @Override - public T validate(CommandSourceStack source, ParsedRule currentRule, T newValue, String string) { - return optimizedTNT || currentRule.defaultValue.equals(newValue) ? newValue : null; + public T validate(CommandSourceStack source, CarpetRule 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 { @Override - public Double validate(CommandSourceStack source, ParsedRule currentRule, Double newValue, String string) { + public Double validate(CommandSourceStack source, CarpetRule 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 { @Override - public Double validate(CommandSourceStack source, ParsedRule currentRule, Double newValue, String string) { + public Double validate(CommandSourceStack source, CarpetRule 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 { - @Override public String validate(CommandSourceStack source, ParsedRule currentRule, String newValue, String string) { + @Override public String validate(CommandSourceStack source, CarpetRule 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 { - @Override public String validate(CommandSourceStack source, ParsedRule currentRule, String newValue, String string) { - int permissionLevel = SettingsManager.getCommandLevel(newValue); + @Override public String validate(CommandSourceStack source, CarpetRule 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 { - @Override public Integer validate(CommandSourceStack source, ParsedRule currentRule, Integer newValue, String string) { + @Override public Integer validate(CommandSourceStack source, CarpetRule 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 { - @Override public Integer validate(CommandSourceStack source, ParsedRule currentRule, Integer newValue, String string) { + @Override public Integer validate(CommandSourceStack source, CarpetRule 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 { - @Override public Integer validate(CommandSourceStack source, ParsedRule currentRule, Integer newValue, String string) + @Override public Integer validate(CommandSourceStack source, CarpetRule 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 { - @Override public Integer validate(CommandSourceStack source, ParsedRule currentRule, Integer newValue, String string) + @Override public Integer validate(CommandSourceStack source, CarpetRule 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 currentRule, Integer newValue, String string) { + @Override public Integer validate(CommandSourceStack source, CarpetRule 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 { - public static void applyLightBatchSizes() + public static void applyLightBatchSizes(int maxBatchSize) { Iterator 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 currentRule, Integer newValue, String string) { + @Override public Integer validate(CommandSourceStack source, CarpetRule 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 { - @Override public Integer validate(CommandSourceStack source, ParsedRule currentRule, Integer newValue, String string) { + @Override public Integer validate(CommandSourceStack source, CarpetRule currentRule, Integer newValue, String string) { return (newValue >= vanillaStructureBlockLimit) ? newValue : null; } @@ -1002,7 +1003,7 @@ public class CarpetSettings public static class StructureBlockIgnoredValidator extends Validator { @Override - public String validate(CommandSourceStack source, ParsedRule currentRule, String newValue, String string) { + public String validate(CommandSourceStack source, CarpetRule currentRule, String newValue, String string) { Optional 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 { @Override - public Integer validate(CommandSourceStack source, ParsedRule currentRule, Integer newValue, String string) { + public Integer validate(CommandSourceStack source, CarpetRule currentRule, Integer newValue, String string) { return newValue < -1 ? null : newValue; } @Override diff --git a/src/main/java/carpet/api/settings/CarpetRule.java b/src/main/java/carpet/api/settings/CarpetRule.java new file mode 100644 index 00000000..90454b47 --- /dev/null +++ b/src/main/java/carpet/api/settings/CarpetRule.java @@ -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; + +/** + *

A Carpet rule, that can return its required properties and stores a value.

+ * + *

Name and description translations are picked up from the translation system.

+ * + * @param The value's type + */ +public interface CarpetRule { + /** + *

Returns this rule's name

+ * + *

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.

+ * + *

This method must always return the same value for the same {@link CarpetRule}.

+ */ + String name(); + + /** + *

Returns a {@link List} of {@link BaseComponent} with extra information about this rule, that is, + * the lines after the rule's description.

+ * + *

Handling of translation of the result of this method is responsibility of the rule implementation.

+ */ + List extraInfo(); + + /** + *

Returns a {@link Collection} of categories this rule is on.

+ */ + Collection categories(); + + /** + *

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.

+ * + *

The returned collection must contain the rule's default value.

+ */ + Collection suggestions(); + + /** + *

Returns the {@link SettingsManager} this rule is in.

+ * + *

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.

+ */ + SettingsManager settingsManager(); + + /** + *

Returns this rule's value

+ */ + T value(); + + /** + *

Returns whether this rule can be toggled in the client-side when not connected to a + * Carpet server.

+ * + *

In the default implementation, this is the case when {@link #categories()} contains {@link RuleCategory#CLIENT}

+ */ + boolean canBeToggledClientSide(); + + /** + *

Returns the type of this rule's value.

+ * + *

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)

+ */ + Class type(); + + /** + *

Returns this rule's default value.

+ * + *

This value will never be {@code null}, and will always be a valid value for {@link #set(CommandSourceStack, Object)}.

+ */ + T defaultValue(); + + /** + *

Sets this rule's value to the provided {@link String}, after first converting the {@link String} into a suitable type.

+ * + *

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.

+ * + *

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.

+ * + *

This method must not throw any exception other than the documented {@link InvalidRuleValueException}.

+ * + * @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; + + /** + *

This method follows the same contract as {@link #set(CommandSourceStack, String)}, but accepts a value already parsed (though not verified).

+ * @see #set(CommandSourceStack, String) + */ + void set(CommandSourceStack source, T value) throws InvalidRuleValueException; +} diff --git a/src/main/java/carpet/api/settings/InvalidRuleValueException.java b/src/main/java/carpet/api/settings/InvalidRuleValueException.java new file mode 100644 index 00000000..82e04b30 --- /dev/null +++ b/src/main/java/carpet/api/settings/InvalidRuleValueException.java @@ -0,0 +1,36 @@ +package carpet.api.settings; + +import carpet.utils.Messenger; +import net.minecraft.commands.CommandSourceStack; + +/** + *

An {@link Exception} thrown when the value given for a {@link CarpetRule} is invalid.

+ * + *

It can hold a message to be sent to the executing source.

+ */ +public class InvalidRuleValueException extends Exception { + + /** + *

Constructs a new {@link InvalidRuleValueException} with a message that will be passed to the executing source

+ * @param cause The cause of the exception + */ + public InvalidRuleValueException(String cause) { + super(cause); + } + + /** + *

Constructs a new {@link InvalidRuleValueException} with no detail message, that therefore should not notify the source

+ */ + public InvalidRuleValueException() { + super(); + } + + /** + *

Notifies the given source with the exception's message if it exists, does nothing if it doesn't exist or it is {@code null}

+ * @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()); + } +} diff --git a/src/main/java/carpet/api/settings/Rule.java b/src/main/java/carpet/api/settings/Rule.java new file mode 100644 index 00000000..f8991407 --- /dev/null +++ b/src/main/java/carpet/api/settings/Rule.java @@ -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 +{ + /** + *

An array of categories the rule is in, as {@link String strings}.

+ * + *

Those must have a corresponding translation key. Categories provided in Carpet's {@link RuleCategory} already + * have translation key.

+ */ + 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[] validators() default {}; + + /** + *

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

+ * + * @see Condition + */ + Class[] conditions() default {}; + + /** + *

Represents a condition that must be met for a rule to be registered in a {@link SettingsManager} via + * {@link SettingsManager#parseSettingsClass(Class)}

+ * + * @see Rule#conditions() + * @see #shouldRegister() + */ + interface Condition { + /** + *

Returns whether the rule that has had this {@link Condition} added should register.

+ * @return {@code true} to register the rule, {@code false} otherwise + */ + boolean shouldRegister(); + } +} diff --git a/src/main/java/carpet/api/settings/RuleCategory.java b/src/main/java/carpet/api/settings/RuleCategory.java new file mode 100644 index 00000000..0f785cad --- /dev/null +++ b/src/main/java/carpet/api/settings/RuleCategory.java @@ -0,0 +1,29 @@ +package carpet.api.settings; + +/** + *

Multiple categories for rules that Carpet uses and can be used to group other rules under the same + * categories.

+ * + *

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.

+ * + */ +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"; + +} diff --git a/src/main/java/carpet/api/settings/RuleHelper.java b/src/main/java/carpet/api/settings/RuleHelper.java new file mode 100644 index 00000000..8966df35 --- /dev/null +++ b/src/main/java/carpet/api/settings/RuleHelper.java @@ -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; + +/** + *

A helper class for operating with {@link CarpetRule} instances and values.

+ * + *

If a method is visible and has javadocs it's probably API

+ */ +public final class RuleHelper { + private RuleHelper() {} + + /** + *

Gets a {@code boolean} value for a given {@link CarpetRule}.

+ * + *

The current implementation is as follows:

+ *
    + *
  • If the rule's type is a {@code boolean}, it will return the value directly.
  • + *
  • If the rule's type is a {@link Number}, it will return {@code true} if the value is greater than zero.
  • + *
  • In any other case, it will return {@code false}.
  • + *
+ * @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; + } + + /** + *

Converts a rule's value into its similar {@link String} representation.

+ * + *

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.

+ * @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(); + } + + /** + *

Checks whether the given {@code CarpetRule rule} is in its default value

+ * + * @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()); + } + + /** + *

Resets the given {@link CarpetRule rule} to its default value, and notifies the given source, if provided.

+ * @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 The type of the {@link CarpetRule} + */ + public static void resetToDefault(CarpetRule 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); + } +} diff --git a/src/main/java/carpet/api/settings/SettingsManager.java b/src/main/java/carpet/api/settings/SettingsManager.java new file mode 100644 index 00000000..c79189b6 --- /dev/null +++ b/src/main/java/carpet/api/settings/SettingsManager.java @@ -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; + +/** + *

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}.

+ * + *

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.

+ */ +@SuppressWarnings({"deprecation", "removal"}) // remove after removing old system +public class SettingsManager { + private final Map> rules = new HashMap<>(); + private final String version; + private final String identifier; + private final String fancyName; + private boolean locked; + private MinecraftServer server; + private final List observers = new ArrayList<>(); + private static final List staticObservers = new ArrayList<>(); + static record ConfigReadResult(Map ruleMap, boolean locked) {} + + /** + *

Defines a class that can be notified about a {@link CarpetRule} changing.

+ * + * @see #ruleChanged(CommandSourceStack, CarpetRule, String) + * @see SettingsManager#registerRuleObserver(RuleObserver) + * @see SettingsManager#registerGlobalRuleObserver(RuleObserver) + */ + @FunctionalInterface + public static interface RuleObserver { + /** + *

Notifies this {@link RuleObserver} about the change of a {@link CarpetRule}.

+ * + *

When this is called, the {@link CarpetRule} value has already been set.

+ * + * @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; + } + + /** + *

Registers a {@link RuleObserver} to changes in rules from + * this {@link SettingsManager} instance.

+ * + * @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 + * any {@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; + } + + /** + *

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.

+ * @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[] 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 condition : conditions) { //Should this be moved to ParsedRule.of? + try + { + Constructor 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 getCategories() + { + return getCarpetRules().stream().map(CarpetRule::categories).mapMulti(Collection::forEach).collect(Collectors.toSet()); + } + + /** + *

Gets a registered rule in this {@link SettingsManager}.

+ * + * @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> getCarpetRules() + { + return Collections.unmodifiableCollection(rules.values()); + } + + /** + *

Adds a {@link CarpetRule} to this {@link SettingsManager}.

+ * + *

Useful when having different {@link CarpetRule} implementations instead of a class of {@code static}, + * annotated fields.

+ * + * @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}.
+ * 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.
+ * 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; + } + + /** + *

Initializes Scarpet rules in this {@link SettingsManager}, if any.

+ *

This is handled automatically by Carpet and calling it is not supported.

+ */ + 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> 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> findStartupOverrides() + { + Set defaults = readSettingsFromConf(getFile()).ruleMap().keySet(); + return rules.values().stream().filter(r -> defaults.contains(r.name())). + sorted(comparing(CarpetRule::name)).toList(); + } + + private Collection> 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 result = new HashMap(); + 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> 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 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 suggestMatchingContains(Stream stream, SuggestionsBuilder suggestionsBuilder) { + List regularSuggestionList = new ArrayList<>(); + List smartSuggestionList = new ArrayList<>(); + String query = suggestionsBuilder.getRemaining().toLowerCase(Locale.ROOT); + stream.forEach((listItem) -> { + // Regex camelCase Search + var words = Arrays.stream(listItem.split("(? s.toLowerCase(Locale.ROOT)).collect(Collectors.toList()); + var prefixes = new ArrayList(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}.
+ * It is handled automatically by Carpet. + * @param dispatcher The current {@link CommandDispatcher} + * @param commandBuildContext The current {@link CommandBuildContext} + */ + public void registerCommand(final CommandDispatcher 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 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 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 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 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> 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 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; + } +} diff --git a/src/main/java/carpet/api/settings/Validator.java b/src/main/java/carpet/api/settings/Validator.java new file mode 100644 index 00000000..62c680a4 --- /dev/null +++ b/src/main/java/carpet/api/settings/Validator.java @@ -0,0 +1,59 @@ +package carpet.api.settings; + +import carpet.utils.Messenger; +import net.minecraft.commands.CommandSourceStack; + +/** + *

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.

+ * + *

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.

+ * + * @see #validate(CommandSourceStack, CarpetRule, Object, String) + * + * @param The type of the rule's value + */ +public abstract class Validator +{ + /** + *

Validates whether the value passed to the given {@link CarpetRule} is valid as a new value for it.

+ * + *

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

+ * + *

This method must not throw any exceptions.

+ * + * @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 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;} + /** + *

Called after failing validation of the {@link CarpetRule} in order to notify the causing {@link CommandSourceStack} about the + * failure.

+ * + * @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 currentRule, String providedValue) + { + Messenger.m(source, "r Wrong value for " + currentRule.name() + ": " + providedValue); + if (description() != null) + Messenger.m(source, "r " + description()); + } +} diff --git a/src/main/java/carpet/api/settings/Validators.java b/src/main/java/carpet/api/settings/Validators.java new file mode 100644 index 00000000..67cf099e --- /dev/null +++ b/src/main/java/carpet/api/settings/Validators.java @@ -0,0 +1,67 @@ +package carpet.api.settings; + +import java.util.List; + +import carpet.utils.CommandHelper; +import net.minecraft.commands.CommandSourceStack; + +/** + *

A collection of standard {@link Validator validators} you can use in your rules.

+ * + * @see Rule + * @see Rule#validators() + * + */ +public final class Validators { + private Validators() {}; + + /** + *

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}

+ * + *

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.

+ * + */ + public static class CommandLevel extends Validator { + @Deprecated(forRemoval = true) // internal use only, will be made pckg private when phasing out old api + public static final List OPTIONS = List.of("true", "false", "ops", "0", "1", "2", "3", "4"); + @Override + public String validate(CommandSourceStack source, CarpetRule 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";} + } + + /** + *

A {@link Validator} that checks whether the entered number is equal or greater than {@code 0}.

+ */ + public static class NonNegativeNumber extends Validator + { + @Override + public T validate(CommandSourceStack source, CarpetRule currentRule, T newValue, String string) + { + return newValue.doubleValue() >= 0 ? newValue : null; + } + @Override + public String description() { return "Must be a positive number or 0";} + } + + /** + *

A {@link Validator} that checks whether the entered number is between 0 and 1, inclusive.

+ */ + public static class Probablity extends Validator + { + @Override + public T validate(CommandSourceStack source, CarpetRule 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";} + } +} diff --git a/src/main/java/carpet/commands/DistanceCommand.java b/src/main/java/carpet/commands/DistanceCommand.java index 9f9193fa..fe5c2e1b 100644 --- a/src/main/java/carpet/commands/DistanceCommand.java +++ b/src/main/java/carpet/commands/DistanceCommand.java @@ -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 dispatcher, CommandBuildContext commandBuildContext) { LiteralArgumentBuilder 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()). diff --git a/src/main/java/carpet/commands/DrawCommand.java b/src/main/java/carpet/commands/DrawCommand.java index 1715067b..df2e544c 100644 --- a/src/main/java/carpet/commands/DrawCommand.java +++ b/src/main/java/carpet/commands/DrawCommand.java @@ -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 dispatcher, final CommandBuildContext context) { LiteralArgumentBuilder 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)). diff --git a/src/main/java/carpet/commands/InfoCommand.java b/src/main/java/carpet/commands/InfoCommand.java index ccf0fe44..c3ab1c20 100644 --- a/src/main/java/carpet/commands/InfoCommand.java +++ b/src/main/java/carpet/commands/InfoCommand.java @@ -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 dispatcher, CommandBuildContext commandBuildContext) { LiteralArgumentBuilder 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( diff --git a/src/main/java/carpet/commands/LogCommand.java b/src/main/java/carpet/commands/LogCommand.java index 6e86f2ef..bd9cd4ea 100644 --- a/src/main/java/carpet/commands/LogCommand.java +++ b/src/main/java/carpet/commands/LogCommand.java @@ -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 dispatcher, CommandBuildContext commandBuildContext) { LiteralArgumentBuilder 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())). diff --git a/src/main/java/carpet/commands/MobAICommand.java b/src/main/java/carpet/commands/MobAICommand.java index ec4e1b41..6ace00d7 100644 --- a/src/main/java/carpet/commands/MobAICommand.java +++ b/src/main/java/carpet/commands/MobAICommand.java @@ -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 dispatcher, final CommandBuildContext commandBuildContext) { LiteralArgumentBuilder 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)). diff --git a/src/main/java/carpet/commands/PerimeterInfoCommand.java b/src/main/java/carpet/commands/PerimeterInfoCommand.java index 525eeb18..41da36fb 100644 --- a/src/main/java/carpet/commands/PerimeterInfoCommand.java +++ b/src/main/java/carpet/commands/PerimeterInfoCommand.java @@ -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 dispatcher, CommandBuildContext commandBuildContext) { LiteralArgumentBuilder 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()), diff --git a/src/main/java/carpet/commands/PlayerCommand.java b/src/main/java/carpet/commands/PlayerCommand.java index 823cc5a7..c93eafed 100644 --- a/src/main/java/carpet/commands/PlayerCommand.java +++ b/src/main/java/carpet/commands/PlayerCommand.java @@ -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 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)) diff --git a/src/main/java/carpet/commands/ProfileCommand.java b/src/main/java/carpet/commands/ProfileCommand.java index 4f3097d7..afe78a02 100644 --- a/src/main/java/carpet/commands/ProfileCommand.java +++ b/src/main/java/carpet/commands/ProfileCommand.java @@ -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 dispatcher, CommandBuildContext commandBuildContext) { LiteralArgumentBuilder 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)). diff --git a/src/main/java/carpet/commands/ScriptCommand.java b/src/main/java/carpet/commands/ScriptCommand.java index 00e027d2..d6a3c260 100644 --- a/src/main/java/carpet/commands/ScriptCommand.java +++ b/src/main/java/carpet/commands/ScriptCommand.java @@ -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 u = literal("resume"). executes( (cc) -> { CarpetServer.scriptServer.stopAll = false; return 1;}); LiteralArgumentBuilder 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 a = literal("load").requires( (player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE) ). + LiteralArgumentBuilder 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 f = literal("unload").requires( (player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE) ). + LiteralArgumentBuilder 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 q = literal("event").requires( (player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE) ). + LiteralArgumentBuilder 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 d = literal("download").requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE)). + LiteralArgumentBuilder 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 r = literal("remove").requires( (player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE) ). + LiteralArgumentBuilder 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)). diff --git a/src/main/java/carpet/commands/SpawnCommand.java b/src/main/java/carpet/commands/SpawnCommand.java index df8b6bc4..07cd3b5c 100644 --- a/src/main/java/carpet/commands/SpawnCommand.java +++ b/src/main/java/carpet/commands/SpawnCommand.java @@ -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 dispatcher, CommandBuildContext commandBuildContext) { LiteralArgumentBuilder literalargumentbuilder = literal("spawn"). - requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandSpawn)); + requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandSpawn)); literalargumentbuilder. then(literal("list"). diff --git a/src/main/java/carpet/commands/TestCommand.java b/src/main/java/carpet/commands/TestCommand.java index 723773b1..316006c1 100644 --- a/src/main/java/carpet/commands/TestCommand.java +++ b/src/main/java/carpet/commands/TestCommand.java @@ -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()). diff --git a/src/main/java/carpet/commands/TickCommand.java b/src/main/java/carpet/commands/TickCommand.java index c4b3f551..415973b1 100644 --- a/src/main/java/carpet/commands/TickCommand.java +++ b/src/main/java/carpet/commands/TickCommand.java @@ -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 dispatcher, CommandBuildContext commandBuildContext) { LiteralArgumentBuilder 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)). diff --git a/src/main/java/carpet/mixins/LocalPlayer_clientCommandMixin.java b/src/main/java/carpet/mixins/LocalPlayer_clientCommandMixin.java index 2c39cfeb..16d7fa43 100644 --- a/src/main/java/carpet/mixins/LocalPlayer_clientCommandMixin.java +++ b/src/main/java/carpet/mixins/LocalPlayer_clientCommandMixin.java @@ -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); }); } diff --git a/src/main/java/carpet/mixins/MinecraftServer_coreMixin.java b/src/main/java/carpet/mixins/MinecraftServer_coreMixin.java index 1f5dcc26..6baa8f36 100644 --- a/src/main/java/carpet/mixins/MinecraftServer_coreMixin.java +++ b/src/main/java/carpet/mixins/MinecraftServer_coreMixin.java @@ -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); } } diff --git a/src/main/java/carpet/network/ClientNetworkHandler.java b/src/main/java/carpet/network/ClientNetworkHandler.java index 56f4a9c8..1b753bbb 100644 --- a/src/main/java/carpet/network/ClientNetworkHandler.java +++ b/src/main/java/carpet/network/ClientNetworkHandler.java @@ -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) { } } } }); diff --git a/src/main/java/carpet/network/ServerNetworkHandler.java b/src/main/java/carpet/network/ServerNetworkHandler.java index df38fcaf..f78d8b82 100644 --- a/src/main/java/carpet/network/ServerNetworkHandler.java +++ b/src/main/java/carpet/network/ServerNetworkHandler.java @@ -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; } diff --git a/src/main/java/carpet/script/CarpetEventServer.java b/src/main/java/carpet/script/CarpetEventServer.java index 8a470d1e..e25cf70e 100644 --- a/src/main/java/carpet/script/CarpetEventServer.java +++ b/src/main/java/carpet/script/CarpetEventServer.java @@ -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 attacker, double x, double y, double z, float power, boolean createFire, List affectedBlocks, List 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)) diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java index 11b0518f..ef18ec8f 100644 --- a/src/main/java/carpet/script/CarpetScriptServer.java +++ b/src/main/java/carpet/script/CarpetScriptServer.java @@ -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; } diff --git a/src/main/java/carpet/script/api/WorldAccess.java b/src/main/java/carpet/script/api/WorldAccess.java index e233f5bf..f5f6128a 100644 --- a/src/main/java/carpet/script/api/WorldAccess.java +++ b/src/main/java/carpet/script/api/WorldAccess.java @@ -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; }); */ diff --git a/src/main/java/carpet/script/utils/AppStoreManager.java b/src/main/java/carpet/script/utils/AppStoreManager.java index b256bec0..bb714687 100644 --- a/src/main/java/carpet/script/utils/AppStoreManager.java +++ b/src/main/java/carpet/script/utils/AppStoreManager.java @@ -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 { - @Override public String validate(CommandSourceStack source, ParsedRule currentRule, String newValue, String string) + @Override public String validate(CommandSourceStack source, CarpetRule currentRule, String newValue, String stringInput) { APP_STORE_ROOT = StoreNode.folder(null, ""); if (newValue.equalsIgnoreCase("none")) diff --git a/src/main/java/carpet/script/utils/SystemInfo.java b/src/main/java/carpet/script/utils/SystemInfo.java index 09095d42..e4cefa73 100644 --- a/src/main/java/carpet/script/utils/SystemInfo.java +++ b/src/main/java/carpet/script/utils/SystemInfo.java @@ -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> rules = CarpetServer.settingsManager.getRules(); + Collection> 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> extensionRules = manager.getRules(); + Collection> 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; diff --git a/src/main/java/carpet/settings/Condition.java b/src/main/java/carpet/settings/Condition.java index 198389fe..8f480b82 100644 --- a/src/main/java/carpet/settings/Condition.java +++ b/src/main/java/carpet/settings/Condition.java @@ -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(); + } } diff --git a/src/main/java/carpet/settings/ParsedRule.java b/src/main/java/carpet/settings/ParsedRule.java index 98660f72..4717e9a0 100644 --- a/src/main/java/carpet/settings/ParsedRule.java +++ b/src/main/java/carpet/settings/ParsedRule.java @@ -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 The field's type + * @param The field's (and rule's) type + * @deprecated Use the type {@link CarpetRule} instead */ -public final class ParsedRule implements Comparable { +@Deprecated(forRemoval = true) // to move to api.settings package and visibility to package private +public final class ParsedRule implements CarpetRule, Comparable> { + private static final Map, 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 extraInfo; + /** + * @deprecated Use {@link CarpetRule#categories()} instead + */ + @Deprecated(forRemoval = true) // to private public final List categories; + /** + * @deprecated Use {@link CarpetRule#suggestions()} instead + */ + @Deprecated(forRemoval = true) // to private (and rename?) public final List 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 type; - public final List> 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> 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 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> 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 convert(String value) throws InvalidRuleValueException; + } + + record RuleAnnotation(boolean isLegacy, String name, String desc, String[] extra, String[] category, String[] options, boolean strict, String appSource, Class[] validators) { + } - ParsedRule(Field field, Rule rule, SettingsManager settingsManager) + /** + * If you call this method I'll steal your kneecaps + */ + @Deprecated(forRemoval = true) + public static ParsedRule 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) field.getType(); - this.description = rule.desc(); + @SuppressWarnings("unchecked") // We are "defining" T here + Class type = (Class)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) 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 converter0 = null; + if (categories.contains(RuleCategory.COMMAND)) { - this.validators.add(callConstructor(Validator._COMMAND.class)); + this.realValidators.add(new Validator._COMMAND()); if (this.type == String.class) { - this.isStrict = false; - this.validators.add((Validator) callConstructor(Validator._COMMAND_LEVEL_VALIDATOR.class)); + this.realValidators.add(instantiateValidator(Validators.CommandLevel.class)); } } - if (!scarpetApp.isEmpty()) - { - this.validators.add((Validator) 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) 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 implements Comparable { } 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 converterFromMap = (FromStringConverter)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) 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 infoIterator = extraInfo.iterator(); + for (int i = 0; infoIterator.hasNext(); i++) { + Translations.registerFallbackTranslation(extraPrefix + i, infoIterator.next()); } } - private T callConstructor(Class cls) + @SuppressWarnings({"unchecked", "rawtypes"}) // Needed because of the annotation + private carpet.api.settings.Validator instantiateValidator(Class cls) { try { - Constructor constr = cls.getDeclaredConstructor(); + Constructor constr = cls.getDeclaredConstructor(); constr.setAccessible(true); return constr.newInstance(); } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); + throw new IllegalArgumentException(e); } } - public ParsedRule 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) type, ucValue), value); - } - else - { - Messenger.m(source, "r Unknown type " + type.getSimpleName()); - return null; - } + set(source, converter.convert(value), value); } - ParsedRule set(CommandSourceStack source, T value, String stringValue) + private void set(CommandSourceStack source, T value, String userInput) throws InvalidRuleValueException { - try + for (carpet.api.settings.Validator validator : this.realValidators) { - for (Validator 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 implements Comparable { } @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 extraInfo() { + return getTranslationArray(TranslationKeys.RULE_EXTRA_PREFIX_PATTERN.formatted(settingsManager().identifier(), name())) + .stream() + .map(str -> Messenger.c("g " + str)) + .toList(); } + + private List getTranslationArray(String prefix) { + List ret = new ArrayList<>(); + for (int i = 0; Translations.hasTranslation(prefix + i); i++) { + ret.add(Translations.tr(prefix + i)); + } + return ret; + } + + @Override + public Collection categories() { + return categories; + } + + @Override + public Collection 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 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 Map.Entry, FromStringConverter> numericalConverter(Class outputClass, Function 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 translatedExtras() { - if (!Translations.hasTranslations()) return extraInfo; - String keyBase = String.format("rule.%s.extra.", name); - List 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(); } } diff --git a/src/main/java/carpet/settings/Rule.java b/src/main/java/carpet/settings/Rule.java index 1e638645..40c9887b 100644 --- a/src/main/java/carpet/settings/Rule.java +++ b/src/main/java/carpet/settings/Rule.java @@ -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[] validate() default {}; + @SuppressWarnings("rawtypes") + Class[] 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[] condition() default {}; + Class[] condition() default {}; } diff --git a/src/main/java/carpet/settings/RuleCategory.java b/src/main/java/carpet/settings/RuleCategory.java index 9f05bac6..8c5612ee 100644 --- a/src/main/java/carpet/settings/RuleCategory.java +++ b/src/main/java/carpet/settings/RuleCategory.java @@ -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; } diff --git a/src/main/java/carpet/settings/SettingsManager.java b/src/main/java/carpet/settings/SettingsManager.java index cf53d20e..b18eb5bc 100644 --- a/src/main/java/carpet/settings/SettingsManager.java +++ b/src/main/java/carpet/settings/SettingsManager.java @@ -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> 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, String>> observers = new ArrayList<>(); - private static List, String>> staticObservers = new ArrayList<>(); - static record ConfigReadResult(Map 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}.
- * 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.
- * 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 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 - * this specific {@link SettingsManager} instance. - * @see SettingsManager#addGlobalRuleObserver(TriConsumer) + *

Adds a custom rule observer to changes in rules from + * this specific {@link SettingsManager} instance.

+ * + *

This outdated method may not be able to listen to all {@link CarpetRule} implementations

+ * + * @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, 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, 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.
- * 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 getCategories() - { - Set 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. + *

Gets a registered {@link ParsedRule} in this {@link SettingsManager} by its name.

* @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> 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> findStartupOverrides() - { - Set 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> 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) rule).set(server.createCommandSourceStack(), false, "false"); - if (rule.type == String.class && rule.options.contains("false")) - ((ParsedRule) rule).set(server.createCommandSourceStack(), "false", "false"); - } - } - - - private void writeSettingsToConf(Map 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 result = new HashMap(); - 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> 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> 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 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 suggestMatchingContains(Stream stream, SuggestionsBuilder suggestionsBuilder) { - List regularSuggestionList = new ArrayList<>(); - List smartSuggestionList = new ArrayList<>(); - String query = suggestionsBuilder.getRemaining().toLowerCase(Locale.ROOT); - stream.forEach((listItem) -> { - // Regex camelCase Search - var words = Arrays.stream(listItem.split("(? s.toLowerCase(Locale.ROOT)).collect(Collectors.toList()); - var prefixes = new ArrayList(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 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}.
- * It is handled automatically by Carpet. - * @param dispatcher The current {@link CommandDispatcher} - * @param commandBuildContext The current {@link CommandBuildContext} - */ - public void registerCommand(final CommandDispatcher 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 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 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 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 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> 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 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); - } - } - } - } } diff --git a/src/main/java/carpet/settings/Validator.java b/src/main/java/carpet/settings/Validator.java index 4112891b..4e191141 100644 --- a/src/main/java/carpet/settings/Validator.java +++ b/src/main/java/carpet/settings/Validator.java @@ -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 +/** + * @deprecated Use {@link carpet.api.settings.Validator} instead + */ +@Deprecated(forRemoval = true) +public abstract class Validator extends carpet.api.settings.Validator { + { + // 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 currentRule, T newValue, String string); - public String description() {return null;} - public void notifyFailure(CommandSourceStack source, ParsedRule currentRule, String providedValue) - { - Messenger.m(source, "r Wrong value for " + currentRule.name + ": " + providedValue); + @Override + public final T validate(CommandSourceStack source, CarpetRule changingRule, T newValue, String stringInput) { + // Compatibility code + if (!(changingRule instanceof ParsedRule 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 currentRule, T newValue, String string); - public static class _COMMAND extends Validator + /** + * @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 extends Validators.NonNegativeNumber {} + + /** + * @deprecated Use {@link Validators.Probablity} instead + */ + @Deprecated(forRemoval = true) // to remove + public static class PROBABILITY extends Validators.Probablity {} + + // 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 extends carpet.api.settings.Validator { @Override - public T validate(CommandSourceStack source, ParsedRule currentRule, T newValue, String string) + public T validate(CommandSourceStack source, CarpetRule 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 extends Validator + // maybe remove this one and make printRulesToLog check for canBeToggledClientSide instead + static class _CLIENT extends carpet.api.settings.Validator { @Override - public T validate(CommandSourceStack source, ParsedRule currentRule, T newValue, String string) + public T validate(CommandSourceStack source, CarpetRule currentRule, T newValue, String string) { return newValue; } @@ -45,25 +85,10 @@ public abstract class Validator "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 { - private static List OPTIONS = List.of("true", "false", "ops", "0", "1", "2", "3", "4"); - @Override public String validate(CommandSourceStack source, ParsedRule 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 extends Validator { + static class ScarpetValidator extends carpet.api.settings.Validator { //TODO remove? The additional info isn't that useful tbh @Override - public T validate(CommandSourceStack source, ParsedRule currentRule, T newValue, String string) + public T validate(CommandSourceStack source, CarpetRule currentRule, T newValue, String string) { return newValue; } @@ -72,65 +97,17 @@ public abstract class Validator } } - public static class WIP extends Validator + static class StrictValidator extends carpet.api.settings.Validator { @Override - public T validate(CommandSourceStack source, ParsedRule currentRule, T newValue, String string) + public T validate(CommandSourceStack source, CarpetRule 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 extends Validator - { - @Override - public T validate(CommandSourceStack source, ParsedRule 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 extends Validator - { - @Override - public T validate(CommandSourceStack source, ParsedRule 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 extends Validator - { - @Override - public T validate(CommandSourceStack source, ParsedRule 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 extends Validator - { - @Override - public T validate(CommandSourceStack source, ParsedRule 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";} - } } diff --git a/src/main/java/carpet/utils/CommandHelper.java b/src/main/java/carpet/utils/CommandHelper.java new file mode 100644 index 00000000..237ddf08 --- /dev/null +++ b/src/main/java/carpet/utils/CommandHelper.java @@ -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; + } +} diff --git a/src/main/java/carpet/utils/TranslationKeys.java b/src/main/java/carpet/utils/TranslationKeys.java new file mode 100644 index 00000000..b1b94665 --- /dev/null +++ b/src/main/java/carpet/utils/TranslationKeys.java @@ -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"; +} diff --git a/src/main/java/carpet/utils/Translations.java b/src/main/java/carpet/utils/Translations.java index b1ac8b62..5b854bfc 100644 --- a/src/main/java/carpet/utils/Translations.java +++ b/src/main/java/carpet/utils/Translations.java @@ -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 translationMap; + private static Map 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 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>() {}.getType()); } - - public static void updateLanguage(CommandSourceStack source) + public static void updateLanguage() { - if (CarpetSettings.language.equalsIgnoreCase("none")) - { - translationMap = null; - return; - } Map translations = new HashMap<>(); - Map 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 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 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 translationMap) { + FALLBACKS.forEach(translationMap::putIfAbsent); + } } diff --git a/src/main/resources/assets/carpet/lang/en_us.json b/src/main/resources/assets/carpet/lang/en_us.json index 100547f6..0fcbc571 100644 --- a/src/main/resources/assets/carpet/lang/en_us.json +++ b/src/main/resources/assets/carpet/lang/en_us.json @@ -1,54 +1,36 @@ -// This file isn't necessary at all, because the language module would use the strings coded in the source if language "none" is selected. -// So en_us is the default language and probably no change will be made after loading this file. -// But it could be a good example for writing the language file for other languages. - { - "// Rules:": "", - "rule.portalSuffocationFix.name": "", - "// the key is genetated as:" : "String.format('rule.%s.name', rule.name)", + // TODO Rules + + // Categories: + "carpet.category.bugfix": "bugfix", + "carpet.category.survival": "survival", + "carpet.category.creative": "creative", + "carpet.category.experimental": "experimental", + "carpet.category.optimization": "optimization", + "carpet.category.feature": "feature", + "carpet.category.command": "command", + "carpet.category.tnt": "TNT", + "carpet.category.dispenser": "dispenser", + "carpet.category.scarpet": "scarpet", + "carpet.category.client": "client", - "rule.portalSuffocationFix.desc": "", - "// the key is genetated as:" : "String.format('rule.%s.desc', rule.name)", + // Settings command UI elements - - "rule.portalSuffocationFix.extra.0": "", - "rule.portalSuffocationFix.extra.1": "", - "// the key is genetated as:" : "String.format('rule.%s.extra.%i', rule.name, index)", - - - "// Categories:": "", - - "category.bugfix": "bugfix", - "category.survival": "survival", - "category.creative": "creative", - "category.experimental": "experimental", - "category.optimization": "optimization", - "category.feature": "feature", - "category.command": "command", - "category.tnt": "TNT", - "category.dispenser": "dispenser", - "category.scarpet": "scarpet", - "category.client": "client", - - "// Settings command UI elements": "", - - "ui.browse_categories": "Browse categories", - "ui.list_all_%(cat)s_settings": "list all %s settings", - "ui.version": "Version", - "ui.current_%(mod)s_settings": "Current %s settings", - "ui.switch_to": "Switch to", - "ui.settings_are_locked": "settings are locked", - "ui.is_set_to": "is set to", - "ui.unknown_rule": "Unknown rule", - "ui.current_%(mod)s_startup_settings_from_%(conf)s": "Current %s Startup Settings from %s", - "ui.%(mod)s_settings_matching_'%(query)s'": "%s settings matching \"%s\" ", - "ui.all_%(mod)s_settings": "All %s settings", - "ui.tags": "Tags", - "ui.change_permanently": "Change permanently", - "ui.click_to_keep_the_settings_in_%(conf)s_to_save_across_restarts": "click to keep the settings in %s to save across restarts", - "ui.rule_%(rule)s_will_now_default_to_%(value)s": "Rule %s will now default to %s", - "ui.rule_%(rule)s_not_set_restart": "Rule %s will now not be set on restart", - "ui.current_value": "current value" + "carpet.settings.command.browse_categories": "Browse categories", + "carpet.settings.command.version": "version", + "carpet.settings.command.list_all_category": "list all %s settings", + "carpet.settings.command.current_settings_header": "Current %s settings", + "carpet.settings.command.switch_to": "Switch to %s", + "carpet.settings.command.unknown_rule": "Unknown rule", + "carpet.settings.command.current_from_file_header": "Current %s Startup Settings from %s", + "carpet.settings.command.mod_settings_matching": "%s settings matching \"%s\" ", + "carpet.settings.command.all_mod_settings": "All %s settings", + "carpet.settings.command.tags": "Tags", + "carpet.settings.command.change_permanently": "Change permanently", + "carpet.settings.command.change_permanently_tooltip": "click to keep the settings in %s to save across restarts", + "carpet.settings.command.default_set": "Rule %s will now default to %s", + "carpet.settings.command.default_removed": "Rule %s will no longer be set on restart", + "carpet.settings.command.current_value": "current value" } diff --git a/src/main/resources/assets/carpet/lang/zh_cn.json b/src/main/resources/assets/carpet/lang/zh_cn.json index 7746790a..fc455ecb 100644 --- a/src/main/resources/assets/carpet/lang/zh_cn.json +++ b/src/main/resources/assets/carpet/lang/zh_cn.json @@ -1,275 +1,273 @@ { - "rule.antiCheatDisabled.name": "禁用反作弊移动监测", - "rule.antiCheatDisabled.desc": "防止玩家因为“本服务器未启用飞行”,“移动过快”等原因被踢出", + "carpet.rule.antiCheatDisabled.name": "禁用反作弊移动监测", + "carpet.rule.antiCheatDisabled.desc": "防止玩家因为“本服务器未启用飞行”,“移动过快”等原因被踢出", - "rule.carpets.name": "多功能地毯", - "rule.carpets.desc": "通过放置地毯方块快捷执行部分功能", + "carpet.rule.carpets.name": "多功能地毯", + "carpet.rule.carpets.desc": "通过放置地毯方块快捷执行部分功能", - "rule.combineXPOrbs.name": "经验球合并", - "rule.combineXPOrbs.desc": "会将多个小经验球合并为更大的经验球", + "carpet.rule.combineXPOrbs.name": "经验球合并", + "carpet.rule.combineXPOrbs.desc": "会将多个小经验球合并为更大的经验球", - "rule.commandDistance.name": "距离测量", - "rule.commandDistance.desc": "启用/distance命令以测量游戏中两点间距离", - "rule.commandDistance.extra.0": "可以通过放置棕色地毯的方式使用本功能,前提是“多功能地毯”已启用", + "carpet.rule.commandDistance.name": "距离测量", + "carpet.rule.commandDistance.desc": "启用/distance命令以测量游戏中两点间距离", + "carpet.rule.commandDistance.extra.0": "可以通过放置棕色地毯的方式使用本功能,前提是“多功能地毯”已启用", - "rule.commandDraw.name": "画图", - "rule.commandDraw.desc": "启用/draw命令以允许绘制一些简单图形", + "carpet.rule.commandDraw.name": "画图", + "carpet.rule.commandDraw.desc": "启用/draw命令以允许绘制一些简单图形", - "rule.commandInfo.name": "获取方块数据", - "rule.commandInfo.desc": "启用/info命令以获取方块数据", - "rule.commandInfo.extra.0": "可以通过放置灰色地毯的方式使用本功能,前提是“多功能地毯”已启用", + "carpet.rule.commandInfo.name": "获取方块数据", + "carpet.rule.commandInfo.desc": "启用/info命令以获取方块数据", + "carpet.rule.commandInfo.extra.0": "可以通过放置灰色地毯的方式使用本功能,前提是“多功能地毯”已启用", - "rule.commandLog.name": "游戏数据监视器", - "rule.commandLog.desc": "启用/log命令以在聊天栏或Tab栏监视游戏部分数据", + "carpet.rule.commandLog.name": "游戏数据监视器", + "carpet.rule.commandLog.desc": "启用/log命令以在聊天栏或Tab栏监视游戏部分数据", - "rule.commandPerimeterInfo.name": "刷怪区域搜寻器", - "rule.commandPerimeterInfo.desc": "启用/perimeterinfo命令以搜寻周围的可刷怪区域", + "carpet.rule.commandPerimeterInfo.name": "刷怪区域搜寻器", + "carpet.rule.commandPerimeterInfo.desc": "启用/perimeterinfo命令以搜寻周围的可刷怪区域", - "rule.commandPlayer.name": "玩家控制", - "rule.commandPlayer.desc": "启用/player命令以控制/召唤玩家", + "carpet.rule.commandPlayer.name": "玩家控制", + "carpet.rule.commandPlayer.desc": "启用/player命令以控制/召唤玩家", - "rule.commandScript.name": "Carpet脚本控制器", - "rule.commandScript.desc": "启用/script相关命令", - "rule.commandScript.extra.0": "一个Scarpet语言的游戏内API", + "carpet.rule.commandScript.name": "Carpet脚本控制器", + "carpet.rule.commandScript.desc": "启用/script相关命令", + "carpet.rule.commandScript.extra.0": "一个Scarpet语言的游戏内API", - "rule.commandSpawn.name": "刷怪追踪器", - "rule.commandSpawn.desc": "启用/spawn命令以追踪怪物生成", + "carpet.rule.commandSpawn.name": "刷怪追踪器", + "carpet.rule.commandSpawn.desc": "启用/spawn命令以追踪怪物生成", - "rule.commandTick.name": "游戏时钟控制器", - "rule.commandTick.desc": "启用/tick命令以控制游戏时钟", + "carpet.rule.commandTick.name": "游戏时钟控制器", + "carpet.rule.commandTick.desc": "启用/tick命令以控制游戏时钟", - "rule.commandTrackAI.name": "AI追踪器", - "rule.commandTrackAI.desc": "启用/track命令以追踪生物的AI", + "carpet.rule.commandTrackAI.name": "AI追踪器", + "carpet.rule.commandTrackAI.desc": "启用/track命令以追踪生物的AI", - "rule.ctrlQCraftingFix.name": "Ctrl+Q合成修复", - "rule.ctrlQCraftingFix.desc": "在合成时允许使用Ctrl+Q快捷键扔出结果格内所有物品", + "carpet.rule.ctrlQCraftingFix.name": "Ctrl+Q合成修复", + "carpet.rule.ctrlQCraftingFix.desc": "在合成时允许使用Ctrl+Q快捷键扔出结果格内所有物品", - "rule.customMOTD.name": "MOTD更改", - "rule.customMOTD.desc": "设置一个不同的MOTD信息", - "rule.customMOTD.extra.0": "使用“_”时将发送默认MOTD(从server.properties中读取)", + "carpet.rule.customMOTD.name": "MOTD更改", + "carpet.rule.customMOTD.desc": "设置一个不同的MOTD信息", + "carpet.rule.customMOTD.extra.0": "使用“_”时将发送默认MOTD(从server.properties中读取)", - "rule.defaultLoggers.name": "默认游戏监视器", - "rule.defaultLoggers.desc": "为所有新进入玩家设置部分监视器默认开启", - "rule.defaultLoggers.extra.0": "比如设置“tps,mobcaps”作为默认监视器", + "carpet.rule.defaultLoggers.name": "默认游戏监视器", + "carpet.rule.defaultLoggers.desc": "为所有新进入玩家设置部分监视器默认开启", + "carpet.rule.defaultLoggers.extra.0": "比如设置“tps,mobcaps”作为默认监视器", - "rule.desertShrubs.name": "树苗在沙漠干枯", - "rule.desertShrubs.desc": "树苗将在附近无水源的沙漠群系枯萎", + "carpet.rule.desertShrubs.name": "树苗在沙漠干枯", + "carpet.rule.desertShrubs.desc": "树苗将在附近无水源的沙漠群系枯萎", - "rule.disableSpawnChunks.name": "允许出生点区块卸载", - "rule.disableSpawnChunks.desc": "允许出生点区块卸载", + "carpet.rule.disableSpawnChunks.name": "允许出生点区块卸载", + "carpet.rule.disableSpawnChunks.desc": "允许出生点区块卸载", - "rule.explosionNoBlockDamage.name": "防爆", - "rule.explosionNoBlockDamage.desc": "任何形式的爆炸都不会摧毁方块", + "carpet.rule.explosionNoBlockDamage.name": "防爆", + "carpet.rule.explosionNoBlockDamage.desc": "任何形式的爆炸都不会摧毁方块", - "rule.extremeBehaviours.name": "随机极端情况", - "rule.extremeBehaviours.desc": "大幅度提升极端情况出现概率,仅用于测试", - "rule.extremeBehaviours.extra.0": "发射器和投掷器将使用最大随机刻", + "carpet.rule.extremeBehaviours.name": "随机极端情况", + "carpet.rule.extremeBehaviours.desc": "大幅度提升极端情况出现概率,仅用于测试", + "carpet.rule.extremeBehaviours.extra.0": "发射器和投掷器将使用最大随机刻", - "rule.fastRedstoneDust.name": "红石粉卡顿优化", - "rule.fastRedstoneDust.desc": "红石粉卡顿优化 作者:Theosib", + "carpet.rule.fastRedstoneDust.name": "红石粉卡顿优化", + "carpet.rule.fastRedstoneDust.desc": "红石粉卡顿优化 作者:Theosib", - "rule.fillLimit.name": "fill/clone上限更改", - "rule.fillLimit.desc": "将fill/clone上限更改为自己需要的值", - "rule.fillLimit.extra.0": "你必须在1-20M之间取一个值", + "carpet.rule.fillLimit.name": "fill/clone上限更改", + "carpet.rule.fillLimit.desc": "将fill/clone上限更改为自己需要的值", + "carpet.rule.fillLimit.extra.0": "你必须在1-20M之间取一个值", - "rule.fillUpdates.name": "fill更新开关", - "rule.fillUpdates.desc": "fill/clone/setblock命令以及结构方块执行时是否产生更新", - "rule.fillUpdates.extra.0": "设置为true有更新,设置为false时为无更新", + "carpet.rule.fillUpdates.name": "fill更新开关", + "carpet.rule.fillUpdates.desc": "fill/clone/setblock命令以及结构方块执行时是否产生更新", + "carpet.rule.fillUpdates.extra.0": "设置为true有更新,设置为false时为无更新", - "rule.flatWorldStructureSpawning.name": "结构在超平坦世界生成", - "rule.flatWorldStructureSpawning.desc": "允许结构在超平坦世界生成", + "carpet.rule.flatWorldStructureSpawning.name": "结构在超平坦世界生成", + "carpet.rule.flatWorldStructureSpawning.desc": "允许结构在超平坦世界生成", - "rule.flippinCactus.name": "仙人掌扳手", - "rule.flippinCactus.desc": "允许使用仙人掌调整方块朝向,并且不会产生更新", + "carpet.rule.flippinCactus.name": "仙人掌扳手", + "carpet.rule.flippinCactus.desc": "允许使用仙人掌调整方块朝向,并且不会产生更新", - "rule.hardcodeTNTangle.name": "硬编码TNT角度", - "rule.hardcodeTNTangle.desc": "把TNT水平随机角度设为固定值,可用于测试机器", - "rule.hardcodeTNTangle.extra.0": "默认为-1,必须在0与360之间,或者-1", + "carpet.rule.hardcodeTNTangle.name": "硬编码TNT角度", + "carpet.rule.hardcodeTNTangle.desc": "把TNT水平随机角度设为固定值,可用于测试机器", + "carpet.rule.hardcodeTNTangle.extra.0": "默认为-1,必须在0与360之间,或者-1", - "rule.hopperCounters.name": "漏斗计数器", - "rule.hopperCounters.desc": "漏斗指向的羊毛会计算经过的物品", - "rule.hopperCounters.extra.0": "在羊毛上放置红色或绿色地毯,使用/counter命令可启用", - "rule.hopperCounters.extra.1": "使用 /counter <颜色> reset重置计数器,使用/counter <颜色>查询", - "rule.hopperCounters.extra.2": "在生存模式,在同色羊毛上放绿地毯可以查询,放红地毯重置", - "rule.hopperCounters.extra.3": "数据是全局性在玩家间共享的,并且有16颜色可用", - "rule.hopperCounters.extra.4": "物品会被销毁,每个漏斗每刻销毁一组物品", + "carpet.rule.hopperCounters.name": "漏斗计数器", + "carpet.rule.hopperCounters.desc": "漏斗指向的羊毛会计算经过的物品", + "carpet.rule.hopperCounters.extra.0": "在羊毛上放置红色或绿色地毯,使用/counter命令可启用", + "carpet.rule.hopperCounters.extra.1": "使用 /counter <颜色> reset重置计数器,使用/counter <颜色>查询", + "carpet.rule.hopperCounters.extra.2": "在生存模式,在同色羊毛上放绿地毯可以查询,放红地毯重置", + "carpet.rule.hopperCounters.extra.3": "数据是全局性在玩家间共享的,并且有16颜色可用", + "carpet.rule.hopperCounters.extra.4": "物品会被销毁,每个漏斗每刻销毁一组物品", - "rule.huskSpawningInTemples.name": "尸壳在神殿里生成", - "rule.huskSpawningInTemples.desc": "只有尸壳会在沙漠神殿生成", + "carpet.rule.huskSpawningInTemples.name": "尸壳在神殿里生成", + "carpet.rule.huskSpawningInTemples.desc": "只有尸壳会在沙漠神殿生成", - "rule.lagFreeSpawning.name": "无卡顿生成", - "rule.lagFreeSpawning.desc": "生物生成需要更少的CPU和内存", + "carpet.rule.lagFreeSpawning.name": "无卡顿生成", + "carpet.rule.lagFreeSpawning.desc": "生物生成需要更少的CPU和内存", - "rule.leadFix.name": "栓绳修复", - "rule.leadFix.desc": "修复栓绳在未加载区块中,变得不可见的问题", + "carpet.rule.leadFix.name": "栓绳修复", + "carpet.rule.leadFix.desc": "修复栓绳在未加载区块中,变得不可见的问题", - "rule.maxEntityCollisions.name": "最大实体挤压数", - "rule.maxEntityCollisions.desc": "设定最大实体挤压限制,0表示无限大", + "carpet.rule.maxEntityCollisions.name": "最大实体挤压数", + "carpet.rule.maxEntityCollisions.desc": "设定最大实体挤压限制,0表示无限大", - "rule.mergeTNT.name": "合并TNT", - "rule.mergeTNT.desc": "合并静止的点燃的TNT实体", + "carpet.rule.mergeTNT.name": "合并TNT", + "carpet.rule.mergeTNT.desc": "合并静止的点燃的TNT实体", - "rule.missingTools.name": "工具缺失修复", - "rule.missingTools.desc": "活塞,玻璃和海绵可以用合适的工具更快地破坏", + "carpet.rule.missingTools.name": "工具缺失修复", + "carpet.rule.missingTools.desc": "活塞,玻璃和海绵可以用合适的工具更快地破坏", - "rule.movableBlockEntities.name": "容器移动", - "rule.movableBlockEntities.desc": "活塞可推动方块实体,比如箱子,漏斗", + "carpet.rule.movableBlockEntities.name": "容器移动", + "carpet.rule.movableBlockEntities.desc": "活塞可推动方块实体,比如箱子,漏斗", - "rule.onePlayerSleeping.name": "单个玩家睡觉", - "rule.onePlayerSleeping.desc": "服务器里一个玩家睡觉即可跳过夜晚", + "carpet.rule.onePlayerSleeping.name": "单个玩家睡觉", + "carpet.rule.onePlayerSleeping.desc": "服务器里一个玩家睡觉即可跳过夜晚", - "rule.optimizedTNT.name": "TNT优化", - "rule.optimizedTNT.desc": "TNT在相同的地点或在流体里,爆炸造成更小的延迟", + "carpet.rule.optimizedTNT.name": "TNT优化", + "carpet.rule.optimizedTNT.desc": "TNT在相同的地点或在流体里,爆炸造成更小的延迟", - "rule.persistentParrots.name": "鹦鹉停留", - "rule.persistentParrots.desc": "鹦鹉不会离开你的肩膀,除非你受到伤害", + "carpet.rule.persistentParrots.name": "鹦鹉停留", + "carpet.rule.persistentParrots.desc": "鹦鹉不会离开你的肩膀,除非你受到伤害", - "rule.placementRotationFix.name": "放置旋转修复", - "rule.placementRotationFix.desc": "修复了玩家放置方块时快速转身造成的问题", + "carpet.rule.placementRotationFix.name": "放置旋转修复", + "carpet.rule.placementRotationFix.desc": "修复了玩家放置方块时快速转身造成的问题", - "rule.pushLimit.name": "自定义推动上限", - "rule.pushLimit.desc": "自定义活塞推动上限", - "rule.pushLimit.extra.0": "你必须在1-1024中选取一个值", + "carpet.rule.pushLimit.name": "自定义推动上限", + "carpet.rule.pushLimit.desc": "自定义活塞推动上限", + "carpet.rule.pushLimit.extra.0": "你必须在1-1024中选取一个值", - "rule.quasiConnectivity.name": "半连接激活开关", - "rule.quasiConnectivity.desc": "活塞,发射器和投掷器在他们上方方块激活时是否响应", - "rule.quasiConnectivity.extra.0": "设置为true为开启,设置为false为禁用", + "carpet.rule.quasiConnectivity.name": "半连接激活开关", + "carpet.rule.quasiConnectivity.desc": "活塞,发射器和投掷器在他们上方方块激活时是否响应", + "carpet.rule.quasiConnectivity.extra.0": "设置为true为开启,设置为false为禁用", - "rule.railPowerLimit.name": "自定义动力铁轨激活距离", - "rule.railPowerLimit.desc": "动力铁轨激活上限可自定义", - "rule.railPowerLimit.extra.0": "你必须在1-1024中选取一个值", + "carpet.rule.railPowerLimit.name": "自定义动力铁轨激活距离", + "carpet.rule.railPowerLimit.desc": "动力铁轨激活上限可自定义", + "carpet.rule.railPowerLimit.extra.0": "你必须在1-1024中选取一个值", - "rule.renewableCoral.name": "珊瑚可再生", - "rule.renewableCoral.desc": "使用骨粉可催熟珊瑚以生成珊瑚结构", + "carpet.rule.renewableCoral.name": "珊瑚可再生", + "carpet.rule.renewableCoral.desc": "使用骨粉可催熟珊瑚以生成珊瑚结构", - "rule.renewableSponges.name": "海绵可再生", - "rule.renewableSponges.desc": "守卫者将在被雷击后会变为远古守卫者", + "carpet.rule.renewableSponges.name": "海绵可再生", + "carpet.rule.renewableSponges.desc": "守卫者将在被雷击后会变为远古守卫者", - "rule.rotatorBlock.name": "仙人掌扳手发射器版", - "rule.rotatorBlock.desc": "发射器中的仙人掌可以旋转方块(尽可能以逆时针)", + "carpet.rule.rotatorBlock.name": "仙人掌扳手发射器版", + "carpet.rule.rotatorBlock.desc": "发射器中的仙人掌可以旋转方块(尽可能以逆时针)", - "rule.scriptsAutoload.name": "Scarpet脚本自动加载", - "rule.scriptsAutoload.desc": "在服务器/世界加载的时候将会自动从世界文件中加载Scarpet脚本文件", - "rule.scriptsAutoload.extra.0": "/script命令必须启用", + "carpet.rule.scriptsAutoload.name": "Scarpet脚本自动加载", + "carpet.rule.scriptsAutoload.desc": "在服务器/世界加载的时候将会自动从世界文件中加载Scarpet脚本文件", + "carpet.rule.scriptsAutoload.extra.0": "/script命令必须启用", - "rule.shulkerSpawningInEndCities.name": "潜影贝重生", - "rule.shulkerSpawningInEndCities.desc": "潜影贝将会在末地城中重生", + "carpet.rule.shulkerSpawningInEndCities.name": "潜影贝重生", + "carpet.rule.shulkerSpawningInEndCities.desc": "潜影贝将会在末地城中重生", - "rule.silverFishDropGravel.name": "蠹虫钻出方块掉落沙砾", - "rule.silverFishDropGravel.desc": "蠹虫钻出方块时会掉落沙砾", + "carpet.rule.silverFishDropGravel.name": "蠹虫钻出方块掉落沙砾", + "carpet.rule.silverFishDropGravel.desc": "蠹虫钻出方块时会掉落沙砾", - "rule.smoothClientAnimations.name": "平滑客户端动画", - "rule.smoothClientAnimations.desc": "低TPS设置时可以使客户端动画更加平滑", - "rule.smoothClientAnimations.extra.0": "仅在单人游戏中生效,并且会降低客户端运行速度", + "carpet.rule.smoothClientAnimations.name": "平滑客户端动画", + "carpet.rule.smoothClientAnimations.desc": "低TPS设置时可以使客户端动画更加平滑", + "carpet.rule.smoothClientAnimations.extra.0": "仅在单人游戏中生效,并且会降低客户端运行速度", - "rule.stackableShulkerBoxes.name": "潜影盒堆叠", - "rule.stackableShulkerBoxes.desc": "空潜影盒仍在地上时将自动堆叠,直至堆叠为1组", - "rule.stackableShulkerBoxes.extra.0": "在物品GUI中,请使用Shift+单击以移动整组潜影盒", + "carpet.rule.stackableShulkerBoxes.name": "潜影盒堆叠", + "carpet.rule.stackableShulkerBoxes.desc": "空潜影盒仍在地上时将自动堆叠,直至堆叠为1组", + "carpet.rule.stackableShulkerBoxes.extra.0": "在物品GUI中,请使用Shift+单击以移动整组潜影盒", - "rule.summonNaturalLightning.name": "召唤闪电自然化", - "rule.summonNaturalLightning.desc": "召唤的闪电将拥有所有自然生成闪电拥有的特性", + "carpet.rule.summonNaturalLightning.name": "召唤闪电自然化", + "carpet.rule.summonNaturalLightning.desc": "召唤的闪电将拥有所有自然生成闪电拥有的特性", - "rule.tntDoNotUpdate.name": "TNT放置时不产生更新", - "rule.tntDoNotUpdate.desc": "TNT在放置时不会产生任何方块更新(不会激活)", + "carpet.rule.tntDoNotUpdate.name": "TNT放置时不产生更新", + "carpet.rule.tntDoNotUpdate.desc": "TNT在放置时不会产生任何方块更新(不会激活)", - "rule.tntPrimerMomentumRemoved.name": "移除TNT点燃时随机动量", - "rule.tntPrimerMomentumRemoved.desc": "TNT点燃时的随机动量将被移除", + "carpet.rule.tntPrimerMomentumRemoved.name": "移除TNT点燃时随机动量", + "carpet.rule.tntPrimerMomentumRemoved.desc": "TNT点燃时的随机动量将被移除", - "rule.tntRandomRange.name": "TNT爆炸范围设置", - "rule.tntRandomRange.desc": "设置TNT随机爆炸范围为一个固定的值,设为-1以禁用", - "rule.tntRandomRange.extra.0": "必须启用TNT优化功能", - "rule.tntRandomRange.extra.1": "不能为负值,除了-1", + "carpet.rule.tntRandomRange.name": "TNT爆炸范围设置", + "carpet.rule.tntRandomRange.desc": "设置TNT随机爆炸范围为一个固定的值,设为-1以禁用", + "carpet.rule.tntRandomRange.extra.0": "必须启用TNT优化功能", + "carpet.rule.tntRandomRange.extra.1": "不能为负值,除了-1", - "rule.unloadedEntityFix.name": "未加载实体修复", - "rule.unloadedEntityFix.desc": "进入或者被推入未加载区块的实体不会消失", + "carpet.rule.unloadedEntityFix.name": "未加载实体修复", + "carpet.rule.unloadedEntityFix.desc": "进入或者被推入未加载区块的实体不会消失", - "rule.viewDistance.name": "自定义视距", - "rule.viewDistance.desc": "改变服务器视距,设为0以使用默认值", - "rule.viewDistance.extra.0": "你必须在0(使用服务器默认设置)-32取一个整数", + "carpet.rule.viewDistance.name": "自定义视距", + "carpet.rule.viewDistance.desc": "改变服务器视距,设为0以使用默认值", + "carpet.rule.viewDistance.extra.0": "你必须在0(使用服务器默认设置)-32取一个整数", - "rule.simulationDistance.name": "自定义模拟视距", - "rule.simulationDistance.desc": "改变服务器模拟视距,设为0以使用默认值", - "rule.simulationDistance.extra.0": "你必须在0(使用服务器默认设置)-32取一个整数", + "carpet.rule.simulationDistance.name": "自定义模拟视距", + "carpet.rule.simulationDistance.desc": "改变服务器模拟视距,设为0以使用默认值", + "carpet.rule.simulationDistance.extra.0": "你必须在0(使用服务器默认设置)-32取一个整数", - "rule.xpNoCooldown.name": "经验吸收无冷却", - "rule.xpNoCooldown.desc": "玩家吸收经验球时不会有冷却时间", + "carpet.rule.xpNoCooldown.name": "经验吸收无冷却", + "carpet.rule.xpNoCooldown.desc": "玩家吸收经验球时不会有冷却时间", - "rule.portalSuffocationFix.name": "传送门窒息修复", - "rule.portalSuffocationFix.desc": "使下界传送门正确地传送实体", - "rule.portalSuffocationFix.extra.0": "传送的实体将不再卡在黑曜石里面", + "carpet.rule.portalSuffocationFix.name": "传送门窒息修复", + "carpet.rule.portalSuffocationFix.desc": "使下界传送门正确地传送实体", + "carpet.rule.portalSuffocationFix.extra.0": "传送的实体将不再卡在黑曜石里面", - "rule.portalCreativeDelay.name": "创造玩家不被地狱门传送", - "rule.portalCreativeDelay.desc": "创造模式的玩家将不会立即被地狱门传送", + "carpet.rule.portalCreativeDelay.name": "创造玩家不被地狱门传送", + "carpet.rule.portalCreativeDelay.desc": "创造模式的玩家将不会立即被地狱门传送", - "rule.commandProfile.name": "游戏性能监视器", - "rule.commandProfile.desc": "启用/profile命令以监视游戏性能", - "rule.commandProfile.extra.0": "/tick命令功能的部分增强版", + "carpet.rule.commandProfile.name": "游戏性能监视器", + "carpet.rule.commandProfile.desc": "启用/profile命令以监视游戏性能", + "carpet.rule.commandProfile.extra.0": "/tick命令功能的部分增强版", - "rule.commandScriptACE.name": "地毯脚本使用权限", - "rule.commandScriptACE.desc": "限制无权限的玩家执行地毯脚本", - "rule.commandScriptACE.extra.0": "无此权限的玩家无法执行/script run指令或者加载脚本", + "carpet.rule.commandScriptACE.name": "地毯脚本使用权限", + "carpet.rule.commandScriptACE.desc": "限制无权限的玩家执行地毯脚本", + "carpet.rule.commandScriptACE.extra.0": "无此权限的玩家无法执行/script run指令或者加载脚本", - "rule.creativeFlyDrag.name": "创造飞行阻力系数", - "rule.creativeFlyDrag.desc": "允许使用本功能更改创造模式下飞行时的空气阻力系数", - "rule.creativeFlyDrag.extra.0": "阻力增加会降低您的飞行速度,\n所以需要相应地调整速度,\n使用1.0阻力系数的时候,使用11的速度系数似乎与原版速度相匹配。\n本功能仅在客户端也安装Carpet Mod(并且版本>=服务器安装版本)的情况下生效", + "carpet.rule.creativeFlyDrag.name": "创造飞行阻力系数", + "carpet.rule.creativeFlyDrag.desc": "允许使用本功能更改创造模式下飞行时的空气阻力系数", + "carpet.rule.creativeFlyDrag.extra.0": "阻力增加会降低您的飞行速度,\n所以需要相应地调整速度,\n使用1.0阻力系数的时候,使用11的速度系数似乎与原版速度相匹配。\n本功能仅在客户端也安装Carpet Mod(并且版本>=服务器安装版本)的情况下生效", - "rule.creativeFlySpeed.name": "创造飞行速度系数", - "rule.creativeFlySpeed.desc": "允许使用本功能更改创造模式下飞行速度系数", - "rule.creativeFlySpeed.extra.0": "本功能仅在客户端也安装Carpet Mod(并且版本>=服务器安装版本)的情况下生效", + "carpet.rule.creativeFlySpeed.name": "创造飞行速度系数", + "carpet.rule.creativeFlySpeed.desc": "允许使用本功能更改创造模式下飞行速度系数", + "carpet.rule.creativeFlySpeed.extra.0": "本功能仅在客户端也安装Carpet Mod(并且版本>=服务器安装版本)的情况下生效", - "rule.creativeNoClip.name": "创造玩家无碰撞检测", - "rule.creativeNoClip.desc": "创造玩家可以穿过方块", - "rule.creativeNoClip.extra.0": "这个选项需要服务端和客户端同时支持才生效。\n仅在服务端设置是无效的,而仅在客户端设置的情况下\n你可以利用一些和活板门有关的技巧来穿墙", + "carpet.rule.creativeNoClip.name": "创造玩家无碰撞检测", + "carpet.rule.creativeNoClip.desc": "创造玩家可以穿过方块", + "carpet.rule.creativeNoClip.extra.0": "这个选项需要服务端和客户端同时支持才生效。\n仅在服务端设置是无效的,而仅在客户端设置的情况下\n你可以利用一些和活板门有关的技巧来穿墙", - "rule.fogOff.name": "关闭雾气", - "rule.fogOff.desc": "启用本功能以关闭地狱与末地的雾气", - "rule.fogOff.extra.0": "提高了可见度,但看起来很奇怪", + "carpet.rule.fogOff.name": "关闭雾气", + "carpet.rule.fogOff.desc": "启用本功能以关闭地狱与末地的雾气", + "carpet.rule.fogOff.extra.0": "提高了可见度,但看起来很奇怪", - "rule.forceloadLimit.name": "永久加载区块上限更改", - "rule.forceloadLimit.desc": "允许使用本功能更改/forceload命令标记的永久加载区块上限", + "carpet.rule.forceloadLimit.name": "永久加载区块上限更改", + "carpet.rule.forceloadLimit.desc": "允许使用本功能更改/forceload命令标记的永久加载区块上限", - "rule.interactionUpdates.name": "放置方块产生更新", - "rule.interactionUpdates.desc": "放置方块是否产生更新的开关,设为false以避免产生更新", + "carpet.rule.interactionUpdates.name": "放置方块产生更新", + "carpet.rule.interactionUpdates.desc": "放置方块是否产生更新的开关,设为false以避免产生更新", - "rule.language.name": "语言", - "rule.language.desc": "为地毯Mod设置所需语言", + "carpet.rule.language.name": "语言", + "carpet.rule.language.desc": "为地毯Mod设置所需语言", - "rule.portalSurvivalDelay.name": "生存模式地狱门传送延迟", - "rule.portalSurvivalDelay.desc": "设置生存模式下地狱门传送的延迟(单位:tick)", + "carpet.rule.portalSurvivalDelay.name": "生存模式地狱门传送延迟", + "carpet.rule.portalSurvivalDelay.desc": "设置生存模式下地狱门传送的延迟(单位:tick)", - "rule.spawnChunksSize.name": "出生点永久加载区块半径", - "rule.spawnChunksSize.desc": "更改出生点永久加载区块半径", - "rule.spawnChunksSize.extra.0": "定义新半径\n设为0以禁用出生点区块加载", + "carpet.rule.spawnChunksSize.name": "出生点永久加载区块半径", + "carpet.rule.spawnChunksSize.desc": "更改出生点永久加载区块半径", + "carpet.rule.spawnChunksSize.extra.0": "定义新半径\n设为0以禁用出生点区块加载", - "category.bugfix": "漏洞修复", - "category.survival": "生存", - "category.creative": "创造", - "category.experimental": "试验性", - "category.optimization": "优化", - "category.feature": "特性", - "category.command": "指令", - "category.tnt": "TNT", - "category.dispenser": "发射器", - "category.scarpet": "Scarpet脚本语言", - "category.client": "客户端", + "carpet.category.bugfix": "漏洞修复", + "carpet.category.survival": "生存", + "carpet.category.creative": "创造", + "carpet.category.experimental": "试验性", + "carpet.category.optimization": "优化", + "carpet.category.feature": "特性", + "carpet.category.command": "指令", + "carpet.category.tnt": "TNT", + "carpet.category.dispenser": "发射器", + "carpet.category.scarpet": "Scarpet脚本语言", + "carpet.category.client": "客户端", - "ui.browse_categories": "浏览分类", - "ui.list_all_%(cat)s_settings": "列出所有 %s 选项", - "ui.version": "版本", - "ui.current_%(mod)s_settings": "目前启用的 %s 选项", - "ui.switch_to": "切换至", - "ui.settings_are_locked": "选项被锁定", - "ui.is_set_to": "被设置为", - "ui.unknown_rule": "未知的规则选项", - "ui.current_%(mod)s_startup_settings_from_%(conf)s": "目前启用的%s选项,从%s中读取得", - "ui.%(mod)s_settings_matching_'%(query)s'": "%s中符合“%s”的选项:", - "ui.all_%(mod)s_settings": "所有的%s选项", - "ui.tags": "分类", - "ui.change_permanently": "永久更改", - "ui.click_to_keep_the_settings_in_%(conf)s_to_save_across_restarts": "点击此按钮以使此选项保存于%s,重启后依旧生效", - "ui.rule_%(rule)s_will_now_default_to_%(value)s": "规则“%s”将会被默认设置为%s", - "ui.rule_%(rule)s_not_set_restart": "规则“%s”将在重启后恢复默认值", - "ui.current_value": "当前值" + "carpet.settings.command.browse_categories": "浏览分类", + "carpet.settings.command.version": "版本", + "carpet.settings.command.list_all_category": "列出所有 %s 选项", + "carpet.settings.command.current_settings_header": "目前启用的 %s 选项", + "carpet.settings.command.switch_to": "切换至%s", + "carpet.settings.command.unknown_rule": "未知的规则选项", + "carpet.settings.command.current_from_file_header": "目前启用的%s选项,从%s中读取得", + "carpet.settings.command.mod_settings_matching": "%s中符合“%s”的选项:", + "carpet.settings.command.all_mod_settings": "所有的%s选项", + "carpet.settings.command.tags": "分类", + "carpet.settings.command.change_permanently": "永久更改", + "carpet.settings.command.change_permanently_tooltip": "点击此按钮以使此选项保存于%s,重启后依旧生效", + "carpet.settings.command.default_set": "规则“%s”将会被默认设置为%s", + "carpet.settings.command.default_removed": "规则“%s”将在重启后恢复默认值", + "carpet.settings.command.current_value": "当前值" } diff --git a/src/main/resources/assets/carpet/lang/zh_tw.json b/src/main/resources/assets/carpet/lang/zh_tw.json index 95993dc3..240b7b59 100644 --- a/src/main/resources/assets/carpet/lang/zh_tw.json +++ b/src/main/resources/assets/carpet/lang/zh_tw.json @@ -1,322 +1,321 @@ { - "rule.antiCheatDisabled.name": "關閉伺服器作弊檢測", - "rule.antiCheatDisabled.desc": "防止玩家因為“本伺服器不允許飛行”,“移動速度過快”等原因被請出伺服器", + "carpet.rule.antiCheatDisabled.name": "關閉伺服器作弊檢測", + "carpet.rule.antiCheatDisabled.desc": "防止玩家因為“本伺服器不允許飛行”,“移動速度過快”等原因被請出伺服器", - "rule.carpets.name": "放置地毯顯示進階數據", - "rule.carpets.desc": "通過把地毯放置在地上取得更進階信息", + "carpet.rule.carpets.name": "放置地毯顯示進階數據", + "carpet.rule.carpets.desc": "通過把地毯放置在地上取得更進階信息", - "rule.combineXPOrbs.name": "合併小經驗球", - "rule.combineXPOrbs.desc": "會將大量的小經驗球合併成等效的大經驗球", + "carpet.rule.combineXPOrbs.name": "合併小經驗球", + "carpet.rule.combineXPOrbs.desc": "會將大量的小經驗球合併成等效的大經驗球", - "rule.commandCameramode.name": "旁觀者模式", - "rule.commandCameramode.desc": "允許玩家使用/c切換成旁觀者模式,利用/s切換回生存模式", - "rule.commandCameramode.extra.0": "/c及/s不需要op權限也可使用", + "carpet.rule.commandCameramode.name": "旁觀者模式", + "carpet.rule.commandCameramode.desc": "允許玩家使用/c切換成旁觀者模式,利用/s切換回生存模式", + "carpet.rule.commandCameramode.extra.0": "/c及/s不需要op權限也可使用", - "rule.commandDistance.name": "距離測定", - "rule.commandDistance.desc": "使用/distance指令測定兩點的距離", - "rule.commandDistance.extra.0": "可以使用放置棕色地毯來使用此功能,前提是“放置地毯顯示進階數據”已啟用", + "carpet.rule.commandDistance.name": "距離測定", + "carpet.rule.commandDistance.desc": "使用/distance指令測定兩點的距離", + "carpet.rule.commandDistance.extra.0": "可以使用放置棕色地毯來使用此功能,前提是“放置地毯顯示進階數據”已啟用", - "rule.commandDraw.name": "繪製圖形", - "rule.commandDraw.desc": "使用/draw指令以允許繪製一些簡易圖形", + "carpet.rule.commandDraw.name": "繪製圖形", + "carpet.rule.commandDraw.desc": "使用/draw指令以允許繪製一些簡易圖形", - "rule.commandInfo.name": "獲取方塊進階資訊", - "rule.commandInfo.desc": "允許使用/info指令以獲得方塊進階資訊", - "rule.commandInfo.extra.0": "可以使用放置灰色地毯來使用此功能,前提是“放置地毯顯示進階數據”已啟用", + "carpet.rule.commandInfo.name": "獲取方塊進階資訊", + "carpet.rule.commandInfo.desc": "允許使用/info指令以獲得方塊進階資訊", + "carpet.rule.commandInfo.extra.0": "可以使用放置灰色地毯來使用此功能,前提是“放置地毯顯示進階數據”已啟用", - "rule.commandLog.name": "遊戲數據追蹤器", - "rule.commandLog.desc": "允許使用/log指令使在Tab欄位追蹤遊戲部分數據", + "carpet.rule.commandLog.name": "遊戲數據追蹤器", + "carpet.rule.commandLog.desc": "允許使用/log指令使在Tab欄位追蹤遊戲部分數據", - "rule.commandPerimeterInfo.name": "刷怪區域追蹤器", - "rule.commandPerimeterInfo.desc": "允許使用/perimeterinfo指令以探查周圍可生怪區域", + "carpet.rule.commandPerimeterInfo.name": "刷怪區域追蹤器", + "carpet.rule.commandPerimeterInfo.desc": "允許使用/perimeterinfo指令以探查周圍可生怪區域", - "rule.commandPlayer.name": "機器人控制", - "rule.commandPlayer.desc": "允許使用/player指令以控制/召喚已存在的玩家id作為機器人", + "carpet.rule.commandPlayer.name": "機器人控制", + "carpet.rule.commandPlayer.desc": "允許使用/player指令以控制/召喚已存在的玩家id作為機器人", - "rule.commandScript.name": "地毯腳本控制器", - "rule.commandScript.desc": "允許使用/script相關指令", - "rule.commandScript.extra.0": "一個Scarpet程式語言的遊戲內API", + "carpet.rule.commandScript.name": "地毯腳本控制器", + "carpet.rule.commandScript.desc": "允許使用/script相關指令", + "carpet.rule.commandScript.extra.0": "一個Scarpet程式語言的遊戲內API", - "rule.commandSpawn.name": "刷怪追蹤器", - "rule.commandSpawn.desc": "允許使用/spawn指令用於怪物生成追蹤", + "carpet.rule.commandSpawn.name": "刷怪追蹤器", + "carpet.rule.commandSpawn.desc": "允許使用/spawn指令用於怪物生成追蹤", - "rule.commandTick.name": "遊戲時鐘控制器", - "rule.commandTick.desc": "允許使用/tick指令控制遊戲內的伺服器端運行速度", + "carpet.rule.commandTick.name": "遊戲時鐘控制器", + "carpet.rule.commandTick.desc": "允許使用/tick指令控制遊戲內的伺服器端運行速度", - "rule.commandTrackAI.name": "AI追蹤器", - "rule.commandTrackAI.desc": "允許使用/track指令以追蹤生物的AI", + "carpet.rule.commandTrackAI.name": "AI追蹤器", + "carpet.rule.commandTrackAI.desc": "允許使用/track指令以追蹤生物的AI", - "rule.ctrlQCraftingFix.name": "ctrlQ合成修復", - "rule.ctrlQCraftingFix.desc": "在合成時允許使用ctrl+Q快捷鍵扔出結果格內所有物品", + "carpet.rule.ctrlQCraftingFix.name": "ctrlQ合成修復", + "carpet.rule.ctrlQCraftingFix.desc": "在合成時允許使用ctrl+Q快捷鍵扔出結果格內所有物品", - "rule.customMOTD.name": "MOTD更改", - "rule.customMOTD.desc": "設置一個不同的MOTD信息", - "rule.customMOTD.extra.0": "設置成'_'時將採用默認的MOTD(從server.properties中讀取的)", + "carpet.rule.customMOTD.name": "MOTD更改", + "carpet.rule.customMOTD.desc": "設置一個不同的MOTD信息", + "carpet.rule.customMOTD.extra.0": "設置成'_'時將採用默認的MOTD(從server.properties中讀取的)", - "rule.defaultLoggers.name": "默認開啟的遊戲數據追蹤器", - "rule.defaultLoggers.desc": "為所有新加入的玩家設置部分遊戲數據追蹤器默認為開啟狀態", - "rule.defaultLoggers.extra.0": "比如設置'tps,mobcaps'最為默認遊戲數據追蹤", + "carpet.rule.defaultLoggers.name": "默認開啟的遊戲數據追蹤器", + "carpet.rule.defaultLoggers.desc": "為所有新加入的玩家設置部分遊戲數據追蹤器默認為開啟狀態", + "carpet.rule.defaultLoggers.extra.0": "比如設置'tps,mobcaps'最為默認遊戲數據追蹤", - "rule.desertShrubs.name": "樹苗在沙漠中乾枯", - "rule.desertShrubs.desc": "樹苗將在附近無水源狀態的沙漠生態群系變成枯灌木", + "carpet.rule.desertShrubs.name": "樹苗在沙漠中乾枯", + "carpet.rule.desertShrubs.desc": "樹苗將在附近無水源狀態的沙漠生態群系變成枯灌木", - "rule.disableSpawnChunks.name": "允許出生點區塊被卸載", - "rule.disableSpawnChunks.desc": "允許出生點區塊被卸載", + "carpet.rule.disableSpawnChunks.name": "允許出生點區塊被卸載", + "carpet.rule.disableSpawnChunks.desc": "允許出生點區塊被卸載", - "rule.explosionNoBlockDamage.name": "爆炸不破壞方塊", - "rule.explosionNoBlockDamage.desc": "任何形式的爆炸都無法摧毀方塊", + "carpet.rule.explosionNoBlockDamage.name": "爆炸不破壞方塊", + "carpet.rule.explosionNoBlockDamage.desc": "任何形式的爆炸都無法摧毀方塊", - "rule.extremeBehaviours.name": "隨機參數極端化", - "rule.extremeBehaviours.desc": "大幅度提升極端情況出現的概率,僅用於測試", - "rule.extremeBehaviours.extra.0": "發射器和投擲器將使用最大隨機刻", + "carpet.rule.extremeBehaviours.name": "隨機參數極端化", + "carpet.rule.extremeBehaviours.desc": "大幅度提升極端情況出現的概率,僅用於測試", + "carpet.rule.extremeBehaviours.extra.0": "發射器和投擲器將使用最大隨機刻", - "rule.fastRedstoneDust.name": "紅石粉卡頓優化", - "rule.fastRedstoneDust.desc": "紅石粉卡頓優化 作者 Theosib", + "carpet.rule.fastRedstoneDust.name": "紅石粉卡頓優化", + "carpet.rule.fastRedstoneDust.desc": "紅石粉卡頓優化 作者 Theosib", - "rule.fillLimit.name": "fill/clone上限更改", - "rule.fillLimit.desc": "將fill/clone上限更改為自己設定的值", - "rule.fillLimit.extra.0": "你必須在1-2千萬之間取一個整數值", + "carpet.rule.fillLimit.name": "fill/clone上限更改", + "carpet.rule.fillLimit.desc": "將fill/clone上限更改為自己設定的值", + "carpet.rule.fillLimit.extra.0": "你必須在1-2千萬之間取一個整數值", - "rule.fillUpdates.name": "fill更新設置", - "rule.fillUpdates.desc": "fill/clone/setblock指令以及結構方塊執行時是否產生更新", - "rule.fillUpdates.extra.0": "設置為true時有更新 設置為false時為無更新", + "carpet.rule.fillUpdates.name": "fill更新設置", + "carpet.rule.fillUpdates.desc": "fill/clone/setblock指令以及結構方塊執行時是否產生更新", + "carpet.rule.fillUpdates.extra.0": "設置為true時有更新 設置為false時為無更新", - "rule.flatWorldStructureSpawning.name": "特定建築怪在超平坦世界生成", - "rule.flatWorldStructureSpawning.desc": "允許特定建築的怪物在超平坦世界生成", + "carpet.rule.flatWorldStructureSpawning.name": "特定建築怪在超平坦世界生成", + "carpet.rule.flatWorldStructureSpawning.desc": "允許特定建築的怪物在超平坦世界生成", - "rule.flippinCactus.name": "仙人掌扳手", - "rule.flippinCactus.desc": "允許使用仙人掌調整方塊朝向,並且不會造成更新", + "carpet.rule.flippinCactus.name": "仙人掌扳手", + "carpet.rule.flippinCactus.desc": "允許使用仙人掌調整方塊朝向,並且不會造成更新", - "rule.hardcodeTNTangle.name": "強制TNT角度", - "rule.hardcodeTNTangle.desc": "把TNT水平隨機角度設定為固定值,可用於測試機器", - "rule.hardcodeTNTangle.extra.0": "默認為-1,必須在0與360之間,或者-1", + "carpet.rule.hardcodeTNTangle.name": "強制TNT角度", + "carpet.rule.hardcodeTNTangle.desc": "把TNT水平隨機角度設定為固定值,可用於測試機器", + "carpet.rule.hardcodeTNTangle.extra.0": "默認為-1,必須在0與360之間,或者-1", - "rule.hopperCounters.name": "漏斗計數器", - "rule.hopperCounters.desc": "漏斗指向的羊毛方塊會計算吸收的物品並清除", - "rule.hopperCounters.extra.0": "在羊毛方塊上放置紅色或綠色地毯,使用/counter指令可啟用", - "rule.hopperCounters.extra.1": "使用 /counter reset重置計數器,使用/counter 查詢資訊", - "rule.hopperCounters.extra.2": "在生存模式,在同色羊毛上放置綠色地毯可以查詢,放紅色地毯重置", - "rule.hopperCounters.extra.3": "數據是全伺服器玩家共享的,並且有16色可以使用", - "rule.hopperCounters.extra.4": "物品會被銷毀,每個漏斗每tick銷毀一組物品", + "carpet.rule.hopperCounters.name": "漏斗計數器", + "carpet.rule.hopperCounters.desc": "漏斗指向的羊毛方塊會計算吸收的物品並清除", + "carpet.rule.hopperCounters.extra.0": "在羊毛方塊上放置紅色或綠色地毯,使用/counter指令可啟用", + "carpet.rule.hopperCounters.extra.1": "使用 /counter reset重置計數器,使用/counter 查詢資訊", + "carpet.rule.hopperCounters.extra.2": "在生存模式,在同色羊毛上放置綠色地毯可以查詢,放紅色地毯重置", + "carpet.rule.hopperCounters.extra.3": "數據是全伺服器玩家共享的,並且有16色可以使用", + "carpet.rule.hopperCounters.extra.4": "物品會被銷毀,每個漏斗每tick銷毀一組物品", - "rule.huskSpawningInTemples.name": "屍殼在沙漠神殿裡生成", - "rule.huskSpawningInTemples.desc": "只有屍殼會在沙漠神殿生成", + "carpet.rule.huskSpawningInTemples.name": "屍殼在沙漠神殿裡生成", + "carpet.rule.huskSpawningInTemples.desc": "只有屍殼會在沙漠神殿生成", - "rule.lagFreeSpawning.name": "延遲自然生成", - "rule.lagFreeSpawning.desc": "生物生成需要更少的CPU和RAM", + "carpet.rule.lagFreeSpawning.name": "延遲自然生成", + "carpet.rule.lagFreeSpawning.desc": "生物生成需要更少的CPU和RAM", - "rule.leadFix.name": "栓繩修復", - "rule.leadFix.desc": "修復栓繩在未加載區塊中,變得不可見的問題", + "carpet.rule.leadFix.name": "栓繩修復", + "carpet.rule.leadFix.desc": "修復栓繩在未加載區塊中,變得不可見的問題", - "rule.maxEntityCollisions.name": "最大實體擠壓數量", - "rule.maxEntityCollisions.desc": "設定最大實體擠壓限制,0表示無限大", + "carpet.rule.maxEntityCollisions.name": "最大實體擠壓數量", + "carpet.rule.maxEntityCollisions.desc": "設定最大實體擠壓限制,0表示無限大", - "rule.mergeTNT.name": "合併TNT", - "rule.mergeTNT.desc": "合併靜止的點燃的tnt實體", + "carpet.rule.mergeTNT.name": "合併TNT", + "carpet.rule.mergeTNT.desc": "合併靜止的點燃的tnt實體", - "rule.missingTools.name": "工具缺失修復", - "rule.missingTools.desc": "活塞,玻璃和海綿可以使用合適的工具更快地破壞", + "carpet.rule.missingTools.name": "工具缺失修復", + "carpet.rule.missingTools.desc": "活塞,玻璃和海綿可以使用合適的工具更快地破壞", - "rule.movableBlockEntities.name": "容器方塊移動", - "rule.movableBlockEntities.desc": "活塞可以推動具有容器標籤的方塊,比如箱子,漏斗", + "carpet.rule.movableBlockEntities.name": "容器方塊移動", + "carpet.rule.movableBlockEntities.desc": "活塞可以推動具有容器標籤的方塊,比如箱子,漏斗", - "rule.onePlayerSleeping.name": "單個玩家睡覺跳過夜晚", - "rule.onePlayerSleeping.desc": "只要伺服器中其中一人睡覺即可跳過全伺服器夜晚", + "carpet.rule.onePlayerSleeping.name": "單個玩家睡覺跳過夜晚", + "carpet.rule.onePlayerSleeping.desc": "只要伺服器中其中一人睡覺即可跳過全伺服器夜晚", - "rule.optimizedTNT.name": "TNT優化", - "rule.optimizedTNT.desc": "tnt在相同位置或者在流體裡,爆炸將造成更低延遲", + "carpet.rule.optimizedTNT.name": "TNT優化", + "carpet.rule.optimizedTNT.desc": "tnt在相同位置或者在流體裡,爆炸將造成更低延遲", - "rule.persistentParrots.name": "鸚鵡停留", - "rule.persistentParrots.desc": "鸚鵡不會離開你的肩膀,除非你受到傷害", + "carpet.rule.persistentParrots.name": "鸚鵡停留", + "carpet.rule.persistentParrots.desc": "鸚鵡不會離開你的肩膀,除非你受到傷害", - "rule.placementRotationFix.name": "放置旋轉問題修復", - "rule.placementRotationFix.desc": "修復玩家放置方塊時快速轉身造成的問題", + "carpet.rule.placementRotationFix.name": "放置旋轉問題修復", + "carpet.rule.placementRotationFix.desc": "修復玩家放置方塊時快速轉身造成的問題", - "rule.pushLimit.name": "自訂義推動上限", - "rule.pushLimit.desc": "自訂義活塞推動上限", - "rule.pushLimit.extra.0": "你必須在1-1024中選取一個值,預設是12", + "carpet.rule.pushLimit.name": "自訂義推動上限", + "carpet.rule.pushLimit.desc": "自訂義活塞推動上限", + "carpet.rule.pushLimit.extra.0": "你必須在1-1024中選取一個值,預設是12", - "rule.quasiConnectivity.name": "半連接激活開關", - "rule.quasiConnectivity.desc": "活塞,發射器和投擲器在他們上方方塊激活時是否響應", - "rule.quasiConnectivity.extra.0": "設置為true為開啟 設置為false為禁用", + "carpet.rule.quasiConnectivity.name": "半連接激活開關", + "carpet.rule.quasiConnectivity.desc": "活塞,發射器和投擲器在他們上方方塊激活時是否響應", + "carpet.rule.quasiConnectivity.extra.0": "設置為true為開啟 設置為false為禁用", - "rule.railPowerLimit.name": "自訂義動力鐵軌激活距離", - "rule.railPowerLimit.desc": "動力鐵軌激活距離能在此更改", - "rule.railPowerLimit.extra.0": "你必須在1-1024中選取一個數值", + "carpet.rule.railPowerLimit.name": "自訂義動力鐵軌激活距離", + "carpet.rule.railPowerLimit.desc": "動力鐵軌激活距離能在此更改", + "carpet.rule.railPowerLimit.extra.0": "你必須在1-1024中選取一個數值", - "rule.renewableCoral.name": "可再生珊瑚", - "rule.renewableCoral.desc": "使用骨粉可催熟珊瑚以長成珊瑚方塊結構", + "carpet.rule.renewableCoral.name": "可再生珊瑚", + "carpet.rule.renewableCoral.desc": "使用骨粉可催熟珊瑚以長成珊瑚方塊結構", - "rule.renewableSponges.name": "可再生海綿", - "rule.renewableSponges.desc": "深海守衛將在被雷擊後變成遠古深海守衛", + "carpet.rule.renewableSponges.name": "可再生海綿", + "carpet.rule.renewableSponges.desc": "深海守衛將在被雷擊後變成遠古深海守衛", - "rule.rotatorBlock.name": "仙人掌扳手發射器版", - "rule.rotatorBlock.desc": "發射器中的仙人掌可以旋轉方塊(盡可能以逆時針)", + "carpet.rule.rotatorBlock.name": "仙人掌扳手發射器版", + "carpet.rule.rotatorBlock.desc": "發射器中的仙人掌可以旋轉方塊(盡可能以逆時針)", - "rule.scriptsAutoload.name": "Scarpet腳本自動加載", - "rule.scriptsAutoload.desc": "在伺服器/世界加載的時候將會自動從世界文件中加載Scarpet腳本文件", - "rule.scriptsAutoload.extra.0": "/script指令必須為啟用狀態", + "carpet.rule.scriptsAutoload.name": "Scarpet腳本自動加載", + "carpet.rule.scriptsAutoload.desc": "在伺服器/世界加載的時候將會自動從世界文件中加載Scarpet腳本文件", + "carpet.rule.scriptsAutoload.extra.0": "/script指令必須為啟用狀態", - "rule.shulkerSpawningInEndCities.name": "可重生界伏蚌", - "rule.shulkerSpawningInEndCities.desc": "界伏蚌將會在終界城市中重生", + "carpet.rule.shulkerSpawningInEndCities.name": "可重生界伏蚌", + "carpet.rule.shulkerSpawningInEndCities.desc": "界伏蚌將會在終界城市中重生", - "rule.silverFishDropGravel.name": "蠹魚鑽出方塊掉落礫石", - "rule.silverFishDropGravel.desc": "蠹魚在鑽出方塊時會掉落礫石", + "carpet.rule.silverFishDropGravel.name": "蠹魚鑽出方塊掉落礫石", + "carpet.rule.silverFishDropGravel.desc": "蠹魚在鑽出方塊時會掉落礫石", - "rule.smoothClientAnimations.name": "更平暢玩家端動畫", - "rule.smoothClientAnimations.desc": "降低TPS設置時可以使玩家端動畫更加平暢", - "rule.smoothClientAnimations.extra.0": "僅在單人模式中生效,並且會降低玩家端運行速度", + "carpet.rule.smoothClientAnimations.name": "更平暢玩家端動畫", + "carpet.rule.smoothClientAnimations.desc": "降低TPS設置時可以使玩家端動畫更加平暢", + "carpet.rule.smoothClientAnimations.extra.0": "僅在單人模式中生效,並且會降低玩家端運行速度", - "rule.stackableShulkerBoxes.name": "可堆疊界伏盒", - "rule.stackableShulkerBoxes.desc": "空的界伏盒扔在地上時將會自動堆疊,直到堆疊為1組", - "rule.stackableShulkerBoxes.extra.0": "在物品GUI中,請使用shift鍵+單擊以移動整組界伏盒", + "carpet.rule.stackableShulkerBoxes.name": "可堆疊界伏盒", + "carpet.rule.stackableShulkerBoxes.desc": "空的界伏盒扔在地上時將會自動堆疊,直到堆疊為1組", + "carpet.rule.stackableShulkerBoxes.extra.0": "在物品GUI中,請使用shift鍵+單擊以移動整組界伏盒", - "rule.summonNaturalLightning.name": "召喚閃電自然化", - "rule.summonNaturalLightning.desc": "召喚的閃電將具有自然生成時的特性", + "carpet.rule.summonNaturalLightning.name": "召喚閃電自然化", + "carpet.rule.summonNaturalLightning.desc": "召喚的閃電將具有自然生成時的特性", - "rule.tntDoNotUpdate.name": "TNT放置時不會被更新", - "rule.tntDoNotUpdate.desc": "TNT在放置時不會產生任何方塊更新(不會激活)", + "carpet.rule.tntDoNotUpdate.name": "TNT放置時不會被更新", + "carpet.rule.tntDoNotUpdate.desc": "TNT在放置時不會產生任何方塊更新(不會激活)", - "rule.tntPrimerMomentumRemoved.name": "移除TNT點燃時隨機動量", - "rule.tntPrimerMomentumRemoved.desc": "TNT點燃時的隨機動量將被移除", + "carpet.rule.tntPrimerMomentumRemoved.name": "移除TNT點燃時隨機動量", + "carpet.rule.tntPrimerMomentumRemoved.desc": "TNT點燃時的隨機動量將被移除", - "rule.tntRandomRange.name": "TNT爆炸範圍設置", - "rule.tntRandomRange.desc": "設置TNT隨機爆炸範圍為一個固定的值,設為-1以禁用", - "rule.tntRandomRange.extra.0": "必須啟用optimizedTNT功能", - "rule.tntRandomRange.extra.1": "不能設置為負數,除了-1", + "carpet.rule.tntRandomRange.name": "TNT爆炸範圍設置", + "carpet.rule.tntRandomRange.desc": "設置TNT隨機爆炸範圍為一個固定的值,設為-1以禁用", + "carpet.rule.tntRandomRange.extra.0": "必須啟用optimizedTNT功能", + "carpet.rule.tntRandomRange.extra.1": "不能設置為負數,除了-1", - "rule.unloadedEntityFix.name": "未加載實體修復", - "rule.unloadedEntityFix.desc": "進入或者被推入未加載區塊的實體不會消失", + "carpet.rule.unloadedEntityFix.name": "未加載實體修復", + "carpet.rule.unloadedEntityFix.desc": "進入或者被推入未加載區塊的實體不會消失", - "rule.viewDistance.name": "自訂義視野", - "rule.viewDistance.desc": "改變伺服器端視野距離,設置為0以使用默認值", - "rule.viewDistance.extra.0": "你必須在0(使用伺服器默認設置)-32取一個整數", + "carpet.rule.viewDistance.name": "自訂義視野", + "carpet.rule.viewDistance.desc": "改變伺服器端視野距離,設置為0以使用默認值", + "carpet.rule.viewDistance.extra.0": "你必須在0(使用伺服器默認設置)-32取一個整數", - "rule.xpNoCooldown.name": "經驗球吸收無冷卻", - "rule.xpNoCooldown.desc": "玩家吸收經驗球不會有冷卻時間", + "carpet.rule.xpNoCooldown.name": "經驗球吸收無冷卻", + "carpet.rule.xpNoCooldown.desc": "玩家吸收經驗球不會有冷卻時間", - "rule.portalSuffocationFix.name": "傳送門窒息修復", - "rule.portalSuffocationFix.desc": "使地獄傳送門正確地傳送實體", - "rule.portalSuffocationFix.extra.0": "傳送的實體將不再卡在黑曜石裡", + "carpet.rule.portalSuffocationFix.name": "傳送門窒息修復", + "carpet.rule.portalSuffocationFix.desc": "使地獄傳送門正確地傳送實體", + "carpet.rule.portalSuffocationFix.extra.0": "傳送的實體將不再卡在黑曜石裡", - "rule.portalCreativeDelay.name": "創造模式玩家地獄門傳送延遲", - "rule.portalCreativeDelay.desc": "創造模式下使用地獄傳送門將同生存時產生延遲", + "carpet.rule.portalCreativeDelay.name": "創造模式玩家地獄門傳送延遲", + "carpet.rule.portalCreativeDelay.desc": "創造模式下使用地獄傳送門將同生存時產生延遲", - "rule.commandProfile.name": "遊戲性能監視器", - "rule.commandProfile.desc": "啟用/profile命令以監視遊戲性能", - "rule.commandProfile.extra.0": "/tick命令功能的部分增強版", + "carpet.rule.commandProfile.name": "遊戲性能監視器", + "carpet.rule.commandProfile.desc": "啟用/profile命令以監視遊戲性能", + "carpet.rule.commandProfile.extra.0": "/tick命令功能的部分增強版", - "rule.commandScriptACE.name": "地毯腳本使用權限", - "rule.commandScriptACE.desc": "限制無權限的玩家執行地毯腳本", - "rule.commandScriptACE.extra.0": "無此權限的玩家無法執行/script run指令或者加載腳本", + "carpet.rule.commandScriptACE.name": "地毯腳本使用權限", + "carpet.rule.commandScriptACE.desc": "限制無權限的玩家執行地毯腳本", + "carpet.rule.commandScriptACE.extra.0": "無此權限的玩家無法執行/script run指令或者加載腳本", - "rule.creativeFlyDrag.name": "創造飛行阻力系數", - "rule.creativeFlyDrag.desc": "允許使用本功能更改創造模式下飛行時的空氣阻力系數", - "rule.creativeFlyDrag.extra.0": "阻力增加會降低您的飛行速度,\n所以需要相應地調整速度,\n使用 1.0 阻力系數的時候,使用 11 的速度系數似乎與原版速度相匹配.\n本功能僅在游戏端也安裝地毯MOD(版本>=伺服器安裝版本)的情況下生效", + "carpet.rule.creativeFlyDrag.name": "創造飛行阻力系數", + "carpet.rule.creativeFlyDrag.desc": "允許使用本功能更改創造模式下飛行時的空氣阻力系數", + "carpet.rule.creativeFlyDrag.extra.0": "阻力增加會降低您的飛行速度,\n所以需要相應地調整速度,\n使用 1.0 阻力系數的時候,使用 11 的速度系數似乎與原版速度相匹配.\n本功能僅在游戏端也安裝地毯MOD(版本>=伺服器安裝版本)的情況下生效", - "rule.creativeFlySpeed.name": "創造飛行速度系數", - "rule.creativeFlySpeed.desc": "允許使用本功能更改創造模式下飛行速度系數", - "rule.creativeFlySpeed.extra.0": "本功能僅在游戏端也安裝地毯MOD(版本>=服務器安裝版本)的情況下生效", + "carpet.rule.creativeFlySpeed.name": "創造飛行速度系數", + "carpet.rule.creativeFlySpeed.desc": "允許使用本功能更改創造模式下飛行速度系數", + "carpet.rule.creativeFlySpeed.extra.0": "本功能僅在游戏端也安裝地毯MOD(版本>=服務器安裝版本)的情況下生效", - "rule.creativeNoClip.name": "創造玩家無碰撞檢測", - "rule.creativeNoClip.desc": "創造玩家可以穿過方塊", - "rule.creativeNoClip.extra.0": "這個選項需要伺服器和游戏端同時支持才生效.\n僅在伺服器設置是無效的,而僅在游戏端設置的情況下\n妳可以利用壹些和活板門有關的技巧來穿墻", + "carpet.rule.creativeNoClip.name": "創造玩家無碰撞檢測", + "carpet.rule.creativeNoClip.desc": "創造玩家可以穿過方塊", + "carpet.rule.creativeNoClip.extra.0": "這個選項需要伺服器和游戏端同時支持才生效.\n僅在伺服器設置是無效的,而僅在游戏端設置的情況下\n妳可以利用壹些和活板門有關的技巧來穿墻", - "rule.fogOff.name": "關閉霧氣", - "rule.fogOff.desc": "啟用本功能以關閉地獄與終界的霧氣", - "rule.fogOff.extra.0": "提高了可見度,但看起來很奇怪", + "carpet.rule.fogOff.name": "關閉霧氣", + "carpet.rule.fogOff.desc": "啟用本功能以關閉地獄與終界的霧氣", + "carpet.rule.fogOff.extra.0": "提高了可見度,但看起來很奇怪", - "rule.forceloadLimit.name": "永加載區塊上限更改", - "rule.forceloadLimit.desc": "允許使用本功能更改/forceload命令標記的永久加載區塊上限", + "carpet.rule.forceloadLimit.name": "永加載區塊上限更改", + "carpet.rule.forceloadLimit.desc": "允許使用本功能更改/forceload命令標記的永久加載區塊上限", - "rule.interactionUpdates.name": "放置方塊產生更新", - "rule.interactionUpdates.desc": "放置方塊是否產生更新的開關,設為false以避免產生更新", + "carpet.rule.interactionUpdates.name": "放置方塊產生更新", + "carpet.rule.interactionUpdates.desc": "放置方塊是否產生更新的開關,設為false以避免產生更新", - "rule.language.name": "語言", - "rule.language.desc": "為地毯Mod設置所需語言", + "carpet.rule.language.name": "語言", + "carpet.rule.language.desc": "為地毯Mod設置所需語言", - "rule.portalSurvivalDelay.name": "生存模式地獄門傳送延遲", - "rule.portalSurvivalDelay.desc": "設置生存模式下地獄門傳送的延遲(單位:tick)", + "carpet.rule.portalSurvivalDelay.name": "生存模式地獄門傳送延遲", + "carpet.rule.portalSurvivalDelay.desc": "設置生存模式下地獄門傳送的延遲(單位:tick)", - "rule.spawnChunksSize.name": "出生點永加載區塊半徑", - "rule.spawnChunksSize.desc": "更改出生點永加載區塊半徑", - "rule.spawnChunksSize.extra.0": "定義新半徑\n設為0以禁用出生點區塊加載", + "carpet.rule.spawnChunksSize.name": "出生點永加載區塊半徑", + "carpet.rule.spawnChunksSize.desc": "更改出生點永加載區塊半徑", + "carpet.rule.spawnChunksSize.extra.0": "定義新半徑\n設為0以禁用出生點區塊加載", - "rule.chainStone.name": "強化鎖鏈", - "rule.chainStone.desc": "長條鎖鏈在末端間將會相互連接", - "rule.chainStone.extra.0": "並且也會與直接連接的方塊連接\n當啟用 stick_to_all : 將無視視覺上連接狀態,使全部連接", + "carpet.rule.chainStone.name": "強化鎖鏈", + "carpet.rule.chainStone.desc": "長條鎖鏈在末端間將會相互連接", + "carpet.rule.chainStone.extra.0": "並且也會與直接連接的方塊連接\n當啟用 stick_to_all : 將無視視覺上連接狀態,使全部連接", - "rule.piglinsSpawningInBastions.name": "豬布林堡壘生成", - "rule.piglinsSpawningInBastions.desc": "豬布林將在堡壘遺跡生成", - "rule.piglinsSpawningInBastions.extra.0": "包括 : 豬布林,蠻兵,及一些豬布獸", + "carpet.rule.piglinsSpawningInBastions.name": "豬布林堡壘生成", + "carpet.rule.piglinsSpawningInBastions.desc": "豬布林將在堡壘遺跡生成", + "carpet.rule.piglinsSpawningInBastions.extra.0": "包括 : 豬布林,蠻兵,及一些豬布獸", - "rule.renewableBlackstone.name": "可再生黑石", - "rule.renewableBlackstone.desc": "地獄玄武岩生成機,但在下方沒有靈魂沙", - "rule.renewableBlackstone.extra.0": "..將會轉換成黑石", + "carpet.rule.renewableBlackstone.name": "可再生黑石", + "carpet.rule.renewableBlackstone.desc": "地獄玄武岩生成機,但在下方沒有靈魂沙", + "carpet.rule.renewableBlackstone.extra.0": "..將會轉換成黑石", - "rule.lightEngineMaxBatchSize.name": "光照引擎最大批次大小", - "rule.lightEngineMaxBatchSize.desc": "更改光照引擎最大批次大小", - "rule.lightEngineMaxBatchSize.extra.0": "允許更高光照抑制容限", - "rule.lightEngineMaxBatchSize.extra.1": "設置成5-遊戲預設容限", + "carpet.rule.lightEngineMaxBatchSize.name": "光照引擎最大批次大小", + "carpet.rule.lightEngineMaxBatchSize.desc": "更改光照引擎最大批次大小", + "carpet.rule.lightEngineMaxBatchSize.extra.0": "允許更高光照抑制容限", + "carpet.rule.lightEngineMaxBatchSize.extra.1": "設置成5-遊戲預設容限", - "rule.cleanLogs.name": "清除數據追蹤器", - "rule.cleanLogs.desc": "從數據追蹤器中刪除令人討厭的信息", - "rule.cleanLogs.extra.0": "在'達到最大聲音對象池247'時將不再顯示", - "rule.cleanLogs.extra.1": "在一般農場與裝置是正常顯示", + "carpet.rule.cleanLogs.name": "清除數據追蹤器", + "carpet.rule.cleanLogs.desc": "從數據追蹤器中刪除令人討厭的信息", + "carpet.rule.cleanLogs.extra.0": "在'達到最大聲音對象池247'時將不再顯示", + "carpet.rule.cleanLogs.extra.1": "在一般農場與裝置是正常顯示", - "rule.structureBlockOutlineDistance.name": "結構方塊外框顯示距離", - "rule.structureBlockOutlineDistance.desc": "自訂義結構方塊外框顯示距離", - "rule.structureBlockOutlineDistance.extra.0": "需要客戶端支持才能正常工作", + "carpet.rule.structureBlockOutlineDistance.name": "結構方塊外框顯示距離", + "carpet.rule.structureBlockOutlineDistance.desc": "自訂義結構方塊外框顯示距離", + "carpet.rule.structureBlockOutlineDistance.extra.0": "需要客戶端支持才能正常工作", - "rule.superSecretSetting.name": "最高機密設置", + "carpet.rule.superSecretSetting.name": "最高機密設置", - "rule.lightningKillsDropsFix.name": "閃電破壞掉落物修正", - "rule.lightningKillsDropsFix.desc": "閃電會破壞自閃電擊殺實體的掉落物", - "rule.lightningKillsDropsFix.extra.0": "設置成true來防止掉落物被閃電破壞", - "rule.lightningKillsDropsFix.extra.1": "修正[MC-206922](https://bugs.mojang.com/browse/MC-206922)", + "carpet.rule.lightningKillsDropsFix.name": "閃電破壞掉落物修正", + "carpet.rule.lightningKillsDropsFix.desc": "閃電會破壞自閃電擊殺實體的掉落物", + "carpet.rule.lightningKillsDropsFix.extra.0": "設置成true來防止掉落物被閃電破壞", + "carpet.rule.lightningKillsDropsFix.extra.1": "修正[MC-206922](https://bugs.mojang.com/browse/MC-206922)", - "rule.pingPlayerListLimit.name": "玩家ping列表數量限制", - "rule.pingPlayerListLimit.desc": "自訂義伺服器ping列表(多人遊戲菜單)玩家列表樣本限制", + "carpet.rule.pingPlayerListLimit.name": "玩家ping列表數量限制", + "carpet.rule.pingPlayerListLimit.desc": "自訂義伺服器ping列表(多人遊戲菜單)玩家列表樣本限制", - "rule.structureBlockIgnored.name": "結構方塊變更無視方塊", - "rule.structureBlockIgnored.desc": "變更被結構方塊無視的方塊", + "carpet.rule.structureBlockIgnored.name": "結構方塊變更無視方塊", + "carpet.rule.structureBlockIgnored.desc": "變更被結構方塊無視的方塊", - "rule.structureBlockLimit.name": "結構方塊上限更改", - "rule.structureBlockLimit.desc": "自訂義結構方塊每個方位距離上限", - "rule.structureBlockLimit.extra.0": "警告 : 必須是永久的才能正確加載", - "rule.structureBlockLimit.extra.1": "建議設置'結構方塊變更無視方塊'成空氣(air)", - "rule.structureBlockLimit.extra.2": "當儲存大量結構時", - "rule.structureBlockLimit.extra.3": "需要客戶端玩家編輯結構方塊", - "rule.structureBlockLimit.extra.4": "當渲染很長的結構時,\n'結構方塊外框顯示距離'將會是必要開啟的", + "carpet.rule.structureBlockLimit.name": "結構方塊上限更改", + "carpet.rule.structureBlockLimit.desc": "自訂義結構方塊每個方位距離上限", + "carpet.rule.structureBlockLimit.extra.0": "警告 : 必須是永久的才能正確加載", + "carpet.rule.structureBlockLimit.extra.1": "建議設置'結構方塊變更無視方塊'成空氣(air)", + "carpet.rule.structureBlockLimit.extra.2": "當儲存大量結構時", + "carpet.rule.structureBlockLimit.extra.3": "需要客戶端玩家編輯結構方塊", + "carpet.rule.structureBlockLimit.extra.4": "當渲染很長的結構時,\n'結構方塊外框顯示距離'將會是必要開啟的", - "category.bugfix": "BUG修復", - "category.survival": "生存用", - "category.creative": "創造用", - "category.experimental": "實驗用", - "category.optimization": "優化", - "category.feature": "特性", - "category.command": "指令", - "category.tnt": "TNT", - "category.dispenser": "發射器", - "category.scarpet": "scarpet腳本程式語言", - "category.client": "玩家端", + "carpet.category.bugfix": "BUG修復", + "carpet.category.survival": "生存用", + "carpet.category.creative": "創造用", + "carpet.category.experimental": "實驗用", + "carpet.category.optimization": "優化", + "carpet.category.feature": "特性", + "carpet.category.command": "指令", + "carpet.category.tnt": "TNT", + "carpet.category.dispenser": "發射器", + "carpet.category.scarpet": "scarpet腳本程式語言", + "carpet.category.client": "玩家端", - "ui.browse_categories": "瀏覽分類", - "ui.list_all_%(cat)s_settings": "列出所有 %s 選項", - "ui.version": "版本", - "ui.current_%(mod)s_settings": "目前啟用的 %s 選項", - "ui.switch_to": "切換至", - "ui.settings_are_locked": "選項被鎖定", - "ui.is_set_to": "被設置為", - "ui.unknown_rule": "未知的規則選項", - "ui.current_%(mod)s_startup_settings_from_%(conf)s": "目前啟用的%s選項,從%s中讀取得", - "ui.%(mod)s_settings_matching_'%(query)s'": "%s中符合\"%s\"的選項:", - "ui.all_%(mod)s_settings": "所有的%s選項", - "ui.tags": "分類", - "ui.change_permanently": "設為預設", - "ui.click_to_keep_the_settings_in_%(conf)s_to_save_across_restarts": "點擊此按鈕以使此選項保存於%s,重啟後依舊生效", - "ui.rule_%(rule)s_will_now_default_to_%(value)s": "規則%s將會被默認設置為%s", - "ui.current_value": "當前的值" + "carpet.settings.command.browse_categories": "瀏覽分類", + "carpet.settings.command.version": "版本", + "carpet.settings.command.list_all_category": "列出所有 %s 選項", + "carpet.settings.command.current_settings_header": "目前啟用的 %s 選項", + "carpet.settings.command.switch_to": "切換至%s", + "carpet.settings.command.unknown_rule": "未知的規則選項", + "carpet.settings.command.current_from_file_header": "目前啟用的%s選項,從%s中讀取得", + "carpet.settings.command.mod_settings_matching": "%s中符合\"%s\"的選項:", + "carpet.settings.command.all_mod_settings": "所有的%s選項", + "carpet.settings.command.tags": "分類", + "carpet.settings.command.change_permanently": "設為預設", + "carpet.settings.command.change_permanently_tooltip": "點擊此按鈕以使此選項保存於%s,重啟後依舊生效", + "carpet.settings.command.default_set": "規則%s將會被默認設置為%s", + "carpet.settings.command.default_removed": "Rule %s will no longer be set on restart", + "carpet.settings.command.current_value": "當前的值" }