diff --git a/docs/scarpet/api/Auxiliary.md b/docs/scarpet/api/Auxiliary.md index 4cccc0d3..3f2ddcd9 100644 --- a/docs/scarpet/api/Auxiliary.md +++ b/docs/scarpet/api/Auxiliary.md @@ -469,6 +469,7 @@ Available options in the scarpet app space: * `world_seed` - a numeric seed of the world * `world_path` - full path to the world saves folder * `world_folder` - name of the direct folder in the saves that holds world files + * `world_carpet_rules` - returns all Carpet rules in a map form (`rule`->`value`). Includes rules from extensions with their namespace (`namespace:rule`->`value`). You can later listen to rule changes with the `on_carpet_rule_change(rule, newValue)` event. Relevant gameplay related properties * `game_difficulty` - current difficulty of the game: `'peacefu'`, `'easy'`, `'normal'`, or `'hard'` diff --git a/docs/scarpet/api/Events.md b/docs/scarpet/api/Events.md index ebf74a39..b1baba69 100644 --- a/docs/scarpet/api/Events.md +++ b/docs/scarpet/api/Events.md @@ -43,6 +43,9 @@ at the same time. ### `__on_lightning(block, mode)` Triggered right after a lightning strikes. Lightning entity as well as potential horseman trap would already be spawned at that point. `mode` is `true` if the lightning did cause a trap to spawn. + +### `__on_carpet_rule_changes(rule, new_value)` +Triggered when a Carpet rule is changed. It includes extension rules, which will be namespaced as `namespace:rule`. ## Player events diff --git a/src/main/java/carpet/CarpetServer.java b/src/main/java/carpet/CarpetServer.java index be4e3f3a..265821af 100644 --- a/src/main/java/carpet/CarpetServer.java +++ b/src/main/java/carpet/CarpetServer.java @@ -84,7 +84,7 @@ public class CarpetServer implements ClientModInitializer,DedicatedServerModInit public static void onServerLoadedWorlds(MinecraftServer minecraftServer) { extensions.forEach(e -> e.onServerLoadedWorlds(minecraftServer)); - scriptServer.loadAllWorldScripts(); + scriptServer.initializeForWorld(); } public static void tick(MinecraftServer server) diff --git a/src/main/java/carpet/commands/ScriptCommand.java b/src/main/java/carpet/commands/ScriptCommand.java index d1971c69..6b8e9a1a 100644 --- a/src/main/java/carpet/commands/ScriptCommand.java +++ b/src/main/java/carpet/commands/ScriptCommand.java @@ -244,13 +244,13 @@ public class ScriptCommand suggests( (cc, bb) -> suggestMatching(CarpetServer.scriptServer.listAvailableModules(true),bb)). executes((cc) -> { - boolean success = CarpetServer.scriptServer.addScriptHost(cc.getSource(), StringArgumentType.getString(cc, "app"), true, false); + boolean success = CarpetServer.scriptServer.addScriptHost(cc.getSource(), StringArgumentType.getString(cc, "app"), true, false, false); return success?1:0; }). then(literal("global"). executes((cc) -> { - boolean success = CarpetServer.scriptServer.addScriptHost(cc.getSource(), StringArgumentType.getString(cc, "app"), false, false); + boolean success = CarpetServer.scriptServer.addScriptHost(cc.getSource(), StringArgumentType.getString(cc, "app"), false, false, false); return success?1:0; } ) @@ -258,10 +258,10 @@ public class ScriptCommand ); LiteralArgumentBuilder f = literal("unload").requires( (player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE) ). then(argument("app", StringArgumentType.word()). - suggests( (cc, bb) -> suggestMatching(CarpetServer.scriptServer.modules.keySet(),bb)). + suggests( (cc, bb) -> suggestMatching(CarpetServer.scriptServer.unloadableModules,bb)). executes((cc) -> { - boolean success =CarpetServer.scriptServer.removeScriptHost(cc.getSource(), StringArgumentType.getString(cc, "app"), true); + boolean success =CarpetServer.scriptServer.removeScriptHost(cc.getSource(), StringArgumentType.getString(cc, "app"), true, false); return success?1:0; })); diff --git a/src/main/java/carpet/mixins/ArmorStandEntity_scarpetMarkerMixin.java b/src/main/java/carpet/mixins/ArmorStandEntity_scarpetMarkerMixin.java index ef3af312..5c6afd16 100644 --- a/src/main/java/carpet/mixins/ArmorStandEntity_scarpetMarkerMixin.java +++ b/src/main/java/carpet/mixins/ArmorStandEntity_scarpetMarkerMixin.java @@ -30,7 +30,7 @@ public abstract class ArmorStandEntity_scarpetMarkerMixin extends LivingEntity @Inject(method = "readCustomDataFromTag", at = @At("HEAD")) private void checkScarpetMarkerUnloaded(CallbackInfo ci) { - if ((CarpetSettings.scriptsAutoload || !CarpetSettings.commandScript.equals("false")) && !world.isClient) + if (!world.isClient) { if (getScoreboardTags().contains(Auxiliary.MARKER_STRING)) { diff --git a/src/main/java/carpet/script/CarpetEventServer.java b/src/main/java/carpet/script/CarpetEventServer.java index c21b8e16..81d886f7 100644 --- a/src/main/java/carpet/script/CarpetEventServer.java +++ b/src/main/java/carpet/script/CarpetEventServer.java @@ -11,6 +11,7 @@ import carpet.script.value.NBTSerializableValue; import carpet.script.value.NumericValue; import carpet.script.value.StringValue; import carpet.script.value.Value; +import carpet.settings.ParsedRule; import carpet.utils.Messenger; import net.minecraft.block.BlockState; import net.minecraft.entity.EntityType; @@ -653,6 +654,25 @@ public class CarpetEventServer ); } }; + public static final Event CARPET_RULE_CHANGES = new Event("carpet_rule_changes", 2, true) + { + @Override + public void onCarpetRuleChanges(ParsedRule rule, ServerCommandSource source) + { + String identifier = rule.settingsManager.getIdentifier(); + final String namespace; + if (!identifier.equals("carpet")) + { + namespace = identifier+":"; + } else { namespace = "";} + handler.call( + () -> Arrays.asList( + new StringValue(namespace+rule.name), + new StringValue(rule.getAsString()) + ), () -> source + ); + } + }; public static String getLoadEvent(EntityType et) { @@ -721,6 +741,7 @@ public class CarpetEventServer public void onWorldEvent(ServerWorld world, BlockPos pos) { } public void onWorldEventFlag(ServerWorld world, BlockPos pos, int flag) { } + public void onCarpetRuleChanges(ParsedRule rule, ServerCommandSource source) { } } diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java index e60d48c2..fbdb6fc2 100644 --- a/src/main/java/carpet/script/CarpetScriptServer.java +++ b/src/main/java/carpet/script/CarpetScriptServer.java @@ -26,6 +26,7 @@ import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -41,17 +42,30 @@ public class CarpetScriptServer public final MinecraftServer server; public CarpetScriptHost globalHost; public Map modules; + public Set unloadableModules; public long tickStart; public boolean stopAll; private Set holyMoly; public CarpetEventServer events; private static final List bundledModuleData = new ArrayList<>(); + private static final List ruleModuleData = new ArrayList<>(); public static void registerBuiltInScript(BundledModule app) { bundledModuleData.add(app); } + + /** + * Registers a Scarpet App to be used as a Rule App + * (to be controlled with the value of a Carpet rule) + * + * @param app is the BundledModule of an app. Libraries + * should be registered as builtInScripts instead + */ + public static void registerRuleScript(BundledModule app) { + ruleModuleData.add(app); + } static { @@ -75,20 +89,27 @@ public class CarpetScriptServer ScriptHost.systemGlobals.clear(); events = new CarpetEventServer(server); modules = new HashMap<>(); + unloadableModules = new HashSet(); tickStart = 0L; stopAll = false; holyMoly = server.getCommandManager().getDispatcher().getRoot().getChildren().stream().map(CommandNode::getName).collect(Collectors.toSet()); globalHost = CarpetScriptHost.create(this, null, false, null); } - public void loadAllWorldScripts() + public void initializeForWorld() { + CarpetServer.settingsManager.initializeScarpetRules(); + CarpetServer.extensions.forEach(e -> { + if (e.customSettingsManager() != null) { + e.customSettingsManager().initializeScarpetRules(); + } + }); if (CarpetSettings.scriptsAutoload) { Messenger.m(server.getCommandSource(), "Auto-loading world scarpet apps"); for (String moduleName: listAvailableModules(false)) { - addScriptHost(server.getCommandSource(), moduleName, true, true); + addScriptHost(server.getCommandSource(), moduleName, true, true, false); } } @@ -119,6 +140,18 @@ public class CarpetScriptServer } return null; } + + public Module getRuleModule(String name) + { + for (Module moduleData : ruleModuleData) + { + if (moduleData.getName().equalsIgnoreCase(name)) + { + return moduleData; + } + } + return null; + } public List listAvailableModules(boolean includeBuiltIns) { @@ -152,17 +185,18 @@ public class CarpetScriptServer return modules.get(name); } - public boolean addScriptHost(ServerCommandSource source, String name, boolean perPlayer, boolean autoload) + public boolean addScriptHost(ServerCommandSource source, String name, boolean perPlayer, boolean autoload, boolean isRuleApp) { //TODO add per player modules to support player actions better on a server name = name.toLowerCase(Locale.ROOT); boolean reload = false; if (modules.containsKey(name)) { - removeScriptHost(source, name, false); + if (isRuleApp) return false; + removeScriptHost(source, name, false, isRuleApp); reload = true; } - Module module = getModule(name, false); + Module module = isRuleApp ? getRuleModule(name) : getModule(name, false); if (module == null) { Messenger.m(source, "r Failed to add "+name+" app"); @@ -194,18 +228,19 @@ public class CarpetScriptServer } modules.put(name, newHost); + if (!isRuleApp) unloadableModules.add(name); if (autoload && !newHost.persistenceRequired) { - removeScriptHost(source, name, false); + removeScriptHost(source, name, false, false); return false; } //addEvents(source, name); - addLegacyCommand(source, name, reload); // this needs to be moved to config reader, only supporting legacy command here + addLegacyCommand(source, name, reload, !isRuleApp); // this needs to be moved to config reader, only supporting legacy command here return true; } - private void addLegacyCommand(ServerCommandSource source, String hostName, boolean isReload) + private void addLegacyCommand(ServerCommandSource source, String hostName, boolean isReload, boolean notifySource) { CarpetScriptHost host = modules.get(hostName); String loaded = isReload?"reloaded":"loaded"; @@ -215,7 +250,7 @@ public class CarpetScriptServer } if (host.getFunction("__command") == null) { - Messenger.m(source, "gi "+hostName+" app "+loaded+"."); + if (notifySource) Messenger.m(source, "gi "+hostName+" app "+loaded+"."); return; } if (holyMoly.contains(hostName)) @@ -261,7 +296,7 @@ public class CarpetScriptServer }))); } } - Messenger.m(source, "gi "+hostName+" app "+loaded+" with /"+hostName+" command"); + if (notifySource) Messenger.m(source, "gi "+hostName+" app "+loaded+" with /"+hostName+" command"); server.getCommandManager().getDispatcher().register(command); CarpetServer.settingsManager.notifyPlayersCommandsChanged(); } @@ -308,10 +343,10 @@ public class CarpetScriptServer - public boolean removeScriptHost(ServerCommandSource source, String name, boolean notifySource) + public boolean removeScriptHost(ServerCommandSource source, String name, boolean notifySource, boolean isRuleApp) { name = name.toLowerCase(Locale.ROOT); - if (!modules.containsKey(name)) + if (!modules.containsKey(name) || (!isRuleApp && !unloadableModules.contains(name))) { if (notifySource) Messenger.m(source, "r No such app found: ", "wb " + name); return false; @@ -320,6 +355,7 @@ public class CarpetScriptServer events.removeAllHostEvents(name); modules.get(name).onClose(); modules.remove(name); + if (!isRuleApp) unloadableModules.remove(name); CarpetServer.settingsManager.notifyPlayersCommandsChanged(); if (notifySource) Messenger.m(source, "gi Removed "+name+" app"); return true; @@ -367,9 +403,9 @@ public class CarpetScriptServer { Map apps = new HashMap<>(); modules.forEach((s, h) -> apps.put(s, h.perUser)); - apps.keySet().forEach(s -> removeScriptHost(server.getCommandSource(), s, false)); + apps.keySet().forEach(s -> removeScriptHost(server.getCommandSource(), s, false, false)); events.clearAll(); init(); - apps.forEach((s, pp) -> addScriptHost(server.getCommandSource(), s, pp, false)); + apps.forEach((s, pp) -> addScriptHost(server.getCommandSource(), s, pp, false, false)); } } diff --git a/src/main/java/carpet/script/utils/SystemInfo.java b/src/main/java/carpet/script/utils/SystemInfo.java index 87c9e0d7..2dc048db 100644 --- a/src/main/java/carpet/script/utils/SystemInfo.java +++ b/src/main/java/carpet/script/utils/SystemInfo.java @@ -1,5 +1,6 @@ package carpet.script.utils; +import carpet.CarpetServer; import carpet.script.CarpetContext; import carpet.script.CarpetScriptHost; import carpet.script.value.ListValue; @@ -7,11 +8,14 @@ import carpet.script.value.MapValue; import carpet.script.value.NumericValue; import carpet.script.value.StringValue; import carpet.script.value.Value; +import carpet.settings.ParsedRule; +import carpet.settings.SettingsManager; import com.sun.management.OperatingSystemMXBean; import net.minecraft.util.WorldSavePath; import java.lang.management.ManagementFactory; import java.nio.file.Path; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -102,6 +106,23 @@ public class SystemInfo { OperatingSystemMXBean.class); return new NumericValue(osBean.getProcessCpuLoad()); }); + put("world_carpet_rules", c -> { + Collection> rules = CarpetServer.settingsManager.getRules(); + MapValue carpetRules = new MapValue(Collections.emptyList()); + rules.forEach(rule -> { + carpetRules.put(new StringValue(rule.name), new StringValue(rule.getAsString())); + }); + CarpetServer.extensions.forEach(e -> { + SettingsManager manager = e.customSettingsManager(); + if (manager == null) return; + + Collection> extensionRules = manager.getRules(); + extensionRules.forEach(rule -> { + carpetRules.put(new StringValue(manager.getIdentifier()+":"+rule.name), new StringValue(rule.getAsString())); + }); + }); + return carpetRules; + }); diff --git a/src/main/java/carpet/settings/ParsedRule.java b/src/main/java/carpet/settings/ParsedRule.java index 54d352b7..b441c5f8 100644 --- a/src/main/java/carpet/settings/ParsedRule.java +++ b/src/main/java/carpet/settings/ParsedRule.java @@ -4,6 +4,7 @@ import carpet.CarpetServer; import carpet.utils.Translations; import carpet.utils.Messenger; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; import net.minecraft.server.command.ServerCommandSource; import java.lang.reflect.Constructor; @@ -12,6 +13,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.Set; import static carpet.utils.Translations.tr; @@ -19,6 +21,7 @@ public final class ParsedRule implements Comparable { public final Field field; public final String name; public final String description; + public final String scarpetApp; public final ImmutableList extraInfo; public final ImmutableList categories; public final ImmutableList options; @@ -28,8 +31,10 @@ public final class ParsedRule implements Comparable { public final List> validators; public final T defaultValue; public final String defaultAsString; + public final SettingsManager settingsManager; + private static final Set> NUMBER_CLASSES = Sets.newHashSet(byte.class, short.class, int.class, long.class, float.class, double.class); - ParsedRule(Field field, Rule rule) + ParsedRule(Field field, Rule rule, SettingsManager settingsManager) { this.field = field; this.name = rule.name().isEmpty() ? field.getName() : rule.name(); @@ -38,6 +43,8 @@ public final class ParsedRule implements Comparable { this.isStrict = rule.strict(); this.extraInfo = ImmutableList.copyOf(rule.extra()); this.categories = ImmutableList.copyOf(rule.category()); + this.scarpetApp = rule.scarpetApp(); + this.settingsManager = settingsManager; this.validators = new ArrayList<>(); for (Class v : rule.validate()) this.validators.add((Validator) callConstructor(v)); @@ -50,6 +57,10 @@ public final class ParsedRule implements Comparable { this.validators.add((Validator) callConstructor(Validator._COMMAND_LEVEL_VALIDATOR.class)); } } + if (!scarpetApp.isEmpty()) + { + this.validators.add((Validator) callConstructor(Validator._SCARPET.class)); + } this.isClient = categories.contains(RuleCategory.CLIENT); if (this.isClient) { @@ -105,7 +116,7 @@ public final class ParsedRule implements Comparable { public ParsedRule set(ServerCommandSource source, String value) { - if (CarpetServer.settingsManager != null && CarpetServer.settingsManager.locked) + if (settingsManager != null && settingsManager.locked) return null; if (type == String.class) { @@ -156,7 +167,7 @@ public final class ParsedRule implements Comparable { if (!value.equals(get()) || source == null) { this.field.set(null, value); - if (source != null) CarpetServer.settingsManager.notifyRuleChanged(source, this, stringValue); + if (source != null) settingsManager.notifyRuleChanged(source, this, stringValue); } } catch (IllegalAccessException e) @@ -187,7 +198,7 @@ public final class ParsedRule implements Comparable { public boolean getBoolValue() { if (type == boolean.class) return (Boolean) get(); - if (type.isAssignableFrom(Number.class)) return ((Number) get()).doubleValue() > 0; + if (NUMBER_CLASSES.contains(type)) return ((Number) get()).doubleValue() > 0; return false; } diff --git a/src/main/java/carpet/settings/Rule.java b/src/main/java/carpet/settings/Rule.java index 6cf571e4..e0364e6c 100644 --- a/src/main/java/carpet/settings/Rule.java +++ b/src/main/java/carpet/settings/Rule.java @@ -51,6 +51,14 @@ public @interface Rule * 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. + * Consider telling the rule name so users can edit globals + * (in case there are relevant globals to edit ofc) + */ + String scarpetApp() default ""; /** * The class of the validator checked when the rule is changed. diff --git a/src/main/java/carpet/settings/SettingsManager.java b/src/main/java/carpet/settings/SettingsManager.java index bf62e5d1..05da31f4 100644 --- a/src/main/java/carpet/settings/SettingsManager.java +++ b/src/main/java/carpet/settings/SettingsManager.java @@ -43,6 +43,7 @@ import java.util.TreeMap; import java.util.stream.Collectors; import static carpet.utils.Translations.tr; +import static carpet.script.CarpetEventServer.Event.CARPET_RULE_CHANGES; import static net.minecraft.command.CommandSource.suggestMatching; import static net.minecraft.server.command.CommandManager.argument; import static net.minecraft.server.command.CommandManager.literal; @@ -71,7 +72,10 @@ public class SettingsManager this.fancyName = fancyName; } - + public String getIdentifier() { + return identifier; + } + public void attachServer(MinecraftServer server) { this.server = server; @@ -90,7 +94,7 @@ public class SettingsManager { Rule rule = f.getAnnotation(Rule.class); if (rule == null) continue; - ParsedRule parsed = new ParsedRule(f, rule); + ParsedRule parsed = new ParsedRule(f, rule, this); rules.put(parsed.name, parsed); } } @@ -104,6 +108,30 @@ public class SettingsManager { observers.forEach(observer -> observer.accept(source, rule, userTypedValue)); ServerNetworkHandler.updateRuleWithConnectedClients(rule); + switchScarpetRule(source, rule); + CARPET_RULE_CHANGES.onCarpetRuleChanges(rule, source); + } + + void switchScarpetRule(ServerCommandSource source, ParsedRule rule) + { + if (!rule.scarpetApp.isEmpty()) + { + if (rule.getBoolValue() || (rule.type == String.class && !rule.get().equals("false"))) + { + CarpetServer.scriptServer.addScriptHost(source, rule.scarpetApp, false, false, true); + } else { + CarpetServer.scriptServer.removeScriptHost(source, rule.scarpetApp, false, true); + } + } + } + + public void initializeScarpetRules() { + for (ParsedRule rule : rules.values()) + { + if (!rule.scarpetApp.isEmpty()) { + switchScarpetRule(server.getCommandSource(), rule); + } + } } public Iterable getCategories() diff --git a/src/main/java/carpet/settings/Validator.java b/src/main/java/carpet/settings/Validator.java index 3f48fff0..6f90bd60 100644 --- a/src/main/java/carpet/settings/Validator.java +++ b/src/main/java/carpet/settings/Validator.java @@ -57,6 +57,17 @@ public abstract class Validator } public String description() { return "Can be limited to 'ops' only, or a custom permission level";} } + + public static class _SCARPET extends Validator { + @Override + public T validate(ServerCommandSource source, ParsedRule currentRule, T newValue, String string) + { + return newValue; + } + public String description() { + return "It controls an accompanying Scarpet App"; + } + } public static class WIP extends Validator {