From b7b766748441ea2c493832b91e6e3528310dd907 Mon Sep 17 00:00:00 2001 From: altrisi Date: Sun, 18 Apr 2021 07:36:29 +0200 Subject: [PATCH] Allow apps to specify mod version requirements (#798) * Allow apps to specify mod version requirements * Allow a function in requires field and move to `readConfig()` * Add `server_mods` to system_info() * Add prefixes and default to `=` * Added docs for `requires` and for `system_info('server_mods')` * Better docs for requires (mention * and available prefixes) * Update docs/scarpet/api/Overview.md * Update docs/scarpet/api/Overview.md * Update docs/scarpet/api/Overview.md Co-authored-by: gnembon <41132274+gnembon@users.noreply.github.com> --- docs/scarpet/api/Auxiliary.md | 1 + docs/scarpet/api/Overview.md | 26 ++++- .../java/carpet/script/CarpetScriptHost.java | 99 +++++++++++++++++++ .../carpet/script/CarpetScriptServer.java | 4 +- .../java/carpet/script/utils/SystemInfo.java | 7 ++ 5 files changed, 135 insertions(+), 2 deletions(-) diff --git a/docs/scarpet/api/Auxiliary.md b/docs/scarpet/api/Auxiliary.md index 869ffe5e..23809584 100644 --- a/docs/scarpet/api/Auxiliary.md +++ b/docs/scarpet/api/Auxiliary.md @@ -685,6 +685,7 @@ Available options in the scarpet app space: * `server_banned_players` - list of banned player names * `server_banned_ips` - list of banned IP addresses * `server_dev_environment` - boolean indicating whether this server is in a development environment. + * `server_mods` - map with all loaded mods mapped to their versions as strings System related properties * `java_max_memory` - maximum allowed memory accessible by JVM diff --git a/docs/scarpet/api/Overview.md b/docs/scarpet/api/Overview.md index 568c2b75..3f62e3d5 100644 --- a/docs/scarpet/api/Overview.md +++ b/docs/scarpet/api/Overview.md @@ -56,6 +56,30 @@ conflicts and ambiguities between different paths of execution. While ambiguous and they tend to execute correctly, the suggestion support works really poorly in these situations and scarpet will warn and prevent such apps from loading with an error message. If `allow_command_conflicts` is specified and `true`, then scarpet will load all provided commands regardless. +* `'requires'` - defines either a map of mod dependencies in Fabric's mod.json style, or a function to be executed. If it's a map, it will only + allow the app to load if all of the mods specified in the map meet the version criteria. If it's a function, it will prevent the app from + loading if the function does not execute to `false`, displaying whatever is returned to the user. + + Available prefixes for the version comparison are `>=`, `<=`, `>`, `<`, `~`, `^` and `=` (default if none specified), based in the spec + at [NPM docs about SemVer ranges](https://docs.npmjs.com/cli/v6/using-npm/semver#ranges) + ``` + __config() -> { + 'requires' -> { + 'carpet' -> '>=1.4.33', // Will require Carpet with a version >= 1.4.32 + 'minecraft' -> '>=1.16', // Will require Minecraft with a version >= 1.16 + 'chat-up' -> '*' // Will require any version of the chat-up mod + } + } + ``` + ``` + __config() -> { + 'requires' -> _() -> ( + d = convert_date(unix_time()); + if(d:6 == 5 && d:2 == 13, + 'Its Friday, 13th' // Will throw this if Friday 13th, will load else since `if` function returns `null` by default + ) + } + ``` * `'command_permission'` - indicates a custom permission to run the command. It can either be a number indicating permission level (from 1 to 4) or a string value, one of: `'all'` (default), `'ops'` (default opped player with permission level of 2), `'server'` - command accessible only through the server console and commandblocks, but not in chat, `'players'` - opposite @@ -286,4 +310,4 @@ if a player is in the other dimension, calls to entities and blocks around that Moreover, running commandblocks in the spawn chunks would mean that commands will always refer to the overworld blocks and entities. In case you would want to run commands across all dimensions, just run three of them, using `/execute in overworld/the_nether/the_end run script run ...` and query players using `player('*')`, -which only returns players in current dimension, or use `in_dimension(expr)` function. \ No newline at end of file +which only returns players in current dimension, or use `in_dimension(expr)` function. diff --git a/src/main/java/carpet/script/CarpetScriptHost.java b/src/main/java/carpet/script/CarpetScriptHost.java index 2f2e9a57..c265ac11 100644 --- a/src/main/java/carpet/script/CarpetScriptHost.java +++ b/src/main/java/carpet/script/CarpetScriptHost.java @@ -28,6 +28,12 @@ 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 net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.fabricmc.loader.api.SemanticVersion; +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.VersionParsingException; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityType; import net.minecraft.nbt.Tag; @@ -47,6 +53,8 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; +import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -283,6 +291,18 @@ public class CarpetScriptHost extends ScriptHost Map config = ((MapValue) ret).getMap(); setPerPlayer(config.getOrDefault(new StringValue("scope"), new StringValue("player")).getString().equalsIgnoreCase("player")); persistenceRequired = config.getOrDefault(new StringValue("stay_loaded"), Value.TRUE).getBoolean(); + // check requires + Value loadRequirements = config.get(new StringValue("requires")); + if (loadRequirements instanceof FunctionValue) + { + Value reqResult = callNow((FunctionValue) loadRequirements, Collections.emptyList()); + if (reqResult.getBoolean()) // != false or null + throw new InternalExpressionException(reqResult.getString()); + } + else + { + checkModVersionRequirements(loadRequirements); + } appConfig = config; } @@ -364,6 +384,85 @@ public class CarpetScriptHost extends ScriptHost } return addLegacyCommand(notifier); } + + // Prefixes based on FLoader's SemanticVersionPredicateParser's class prefixes under Apache 2.0, since that's not public API + private static final Map> VERSION_PREFIXES = new HashMap>() {{ + put(">=", (target, source) -> source.compareTo(target) >= 0); + put("<=", (target, source) -> source.compareTo(target) <= 0); + put(">", (target, source) -> source.compareTo(target) > 0); + put("<", (target, source) -> source.compareTo(target) < 0); + put("=", (target, source) -> source.compareTo(target) == 0); + put("~", (target, source) -> source.compareTo(target) >= 0 + && source.getVersionComponent(0) == target.getVersionComponent(0) + && source.getVersionComponent(1) == target.getVersionComponent(1)); + put("^", (target, source) -> source.compareTo(target) >= 0 + && source.getVersionComponent(0) == target.getVersionComponent(0)); + }}; + + public void checkModVersionRequirements(Value reqs) { + if (reqs == null) + return; + if (!(reqs instanceof MapValue)) + { + throw new InternalExpressionException("`requires` field must be a map of mod dependencies or a function to be executed"); + } + Map requirements = ((MapValue)reqs).getMap(); + for (Entry requirement : requirements.entrySet()) + { + boolean successful = false; + String requiredModId = requirement.getKey().getString(); + String requirementString = requirement.getValue().getString(); + + ModContainer mod = FabricLoader.getInstance().getModContainer(requiredModId).orElse(null); + if (mod != null) + { + if (requirementString.equals("*")) + continue; + Version presentVersion = mod.getMetadata().getVersion(); + BiPredicate tester = null; + String remainingRequirementString = requirementString; + for (Entry> s : VERSION_PREFIXES.entrySet()) + { + if (requirementString.startsWith(s.getKey())) + { + tester = s.getValue(); + remainingRequirementString = requirementString.substring(s.getKey().length()); + } + } + if (tester == null) + tester = VERSION_PREFIXES.get("="); + if (presentVersion instanceof SemanticVersion) + { + SemanticVersion requiredVersion = null; + try { + requiredVersion = SemanticVersion.parse(remainingRequirementString); + } + catch (VersionParsingException e) + { + throw new InternalExpressionException("Failed to parse semantic version for '" + requiredModId + "' in 'requires': " + e.getMessage()); + } + if (tester.test(requiredVersion, (SemanticVersion) presentVersion)) + successful = true; + } + else + { + if (FabricLoader.getInstance().isDevelopmentEnvironment()) + successful = true; // Autoversioning breaks semver in dev (loads as ${version}) + else + { + if (!(tester == VERSION_PREFIXES.get("="))) + throw new InternalExpressionException("Mod '"+requiredModId+"' doesn't use semversion, thus can only check version equality"); + if (presentVersion.getFriendlyString().equals(remainingRequirementString)) + successful = true; + } + } + } + if (!successful) + { + throw new InternalExpressionException(getName()+" requires " + requiredModId + " version " + requirementString + " in order to load"); + } + } + } private Boolean addLegacyCommand(Consumer notifier) { diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java index 9ddcf503..10faab8f 100644 --- a/src/main/java/carpet/script/CarpetScriptServer.java +++ b/src/main/java/carpet/script/CarpetScriptServer.java @@ -270,12 +270,14 @@ public class CarpetScriptServer } //addEvents(source, name); String action = reload?"reloaded":"loaded"; + Boolean isCommandAdded = newHost.addAppCommands(s -> { if (!isRuleApp) Messenger.m(source, s); }); if (isCommandAdded == null) // error should be dispatched { - removeScriptHost(source, name, false, false); + removeScriptHost(source, name, false, isRuleApp); + return false; } else if (isCommandAdded) { diff --git a/src/main/java/carpet/script/utils/SystemInfo.java b/src/main/java/carpet/script/utils/SystemInfo.java index 36d00396..b85ae7fb 100644 --- a/src/main/java/carpet/script/utils/SystemInfo.java +++ b/src/main/java/carpet/script/utils/SystemInfo.java @@ -15,6 +15,7 @@ import carpet.settings.ParsedRule; import carpet.settings.SettingsManager; import com.sun.management.OperatingSystemMXBean; import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; import net.minecraft.SharedConstants; import net.minecraft.util.WorldSavePath; import net.minecraft.world.GameRules; @@ -107,6 +108,12 @@ public class SystemInfo { return whitelist; }); put("server_dev_environment", c-> new NumericValue(FabricLoader.getInstance().isDevelopmentEnvironment())); + put("server_mods", c -> { + Map ret = new HashMap<>(); + for (ModContainer mod : FabricLoader.getInstance().getAllMods()) + ret.put(new StringValue(mod.getMetadata().getName()), new StringValue(mod.getMetadata().getVersion().getFriendlyString())); + return MapValue.wrap(ret); + }); put("java_max_memory", c -> new NumericValue(Runtime.getRuntime().maxMemory())); put("java_allocated_memory", c -> new NumericValue(Runtime.getRuntime().totalMemory()));