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>
This commit is contained in:
altrisiandgnembon authored and GitHub committed 2021-04-18 02:36:29 -03:00
1 parent 0e84ef5d65
commit b7b7667484
5 files changed
+135 -2

No files matched your search

+1
View File
@@ -685,6 +685,7 @@ Available options in the scarpet app space:
* `server_banned_players` - list of banned player names * `server_banned_players` - list of banned player names
* `server_banned_ips` - list of banned IP addresses * `server_banned_ips` - list of banned IP addresses
* `server_dev_environment` - boolean indicating whether this server is in a development environment. * `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 System related properties
* `java_max_memory` - maximum allowed memory accessible by JVM * `java_max_memory` - maximum allowed memory accessible by JVM
+25 -1
View File
@@ -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 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 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. `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 * `'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), 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 `'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 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, 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('*')`, 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. which only returns players in current dimension, or use `in_dimension(expr)` function.
@@ -28,6 +28,12 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; 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.Entity;
import net.minecraft.entity.EntityType; import net.minecraft.entity.EntityType;
import net.minecraft.nbt.Tag; import net.minecraft.nbt.Tag;
@@ -47,6 +53,8 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.function.BiPredicate;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -283,6 +291,18 @@ public class CarpetScriptHost extends ScriptHost
Map<Value, Value> config = ((MapValue) ret).getMap(); Map<Value, Value> config = ((MapValue) ret).getMap();
setPerPlayer(config.getOrDefault(new StringValue("scope"), new StringValue("player")).getString().equalsIgnoreCase("player")); setPerPlayer(config.getOrDefault(new StringValue("scope"), new StringValue("player")).getString().equalsIgnoreCase("player"));
persistenceRequired = config.getOrDefault(new StringValue("stay_loaded"), Value.TRUE).getBoolean(); 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; appConfig = config;
} }
@@ -364,6 +384,85 @@ public class CarpetScriptHost extends ScriptHost
} }
return addLegacyCommand(notifier); 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<String, BiPredicate<SemanticVersion, SemanticVersion>> VERSION_PREFIXES = new HashMap<String, BiPredicate<SemanticVersion, SemanticVersion>>() {{
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<Value, Value> requirements = ((MapValue)reqs).getMap();
for (Entry<Value, Value> 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<SemanticVersion, SemanticVersion> tester = null;
String remainingRequirementString = requirementString;
for (Entry<String, BiPredicate<SemanticVersion, SemanticVersion>> 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<Text> notifier) private Boolean addLegacyCommand(Consumer<Text> notifier)
{ {
@@ -270,12 +270,14 @@ public class CarpetScriptServer
} }
//addEvents(source, name); //addEvents(source, name);
String action = reload?"reloaded":"loaded"; String action = reload?"reloaded":"loaded";
Boolean isCommandAdded = newHost.addAppCommands(s -> { Boolean isCommandAdded = newHost.addAppCommands(s -> {
if (!isRuleApp) Messenger.m(source, s); if (!isRuleApp) Messenger.m(source, s);
}); });
if (isCommandAdded == null) // error should be dispatched if (isCommandAdded == null) // error should be dispatched
{ {
removeScriptHost(source, name, false, false); removeScriptHost(source, name, false, isRuleApp);
return false;
} }
else if (isCommandAdded) else if (isCommandAdded)
{ {
@@ -15,6 +15,7 @@ import carpet.settings.ParsedRule;
import carpet.settings.SettingsManager; import carpet.settings.SettingsManager;
import com.sun.management.OperatingSystemMXBean; import com.sun.management.OperatingSystemMXBean;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.SharedConstants; import net.minecraft.SharedConstants;
import net.minecraft.util.WorldSavePath; import net.minecraft.util.WorldSavePath;
import net.minecraft.world.GameRules; import net.minecraft.world.GameRules;
@@ -107,6 +108,12 @@ public class SystemInfo {
return whitelist; return whitelist;
}); });
put("server_dev_environment", c-> new NumericValue(FabricLoader.getInstance().isDevelopmentEnvironment())); put("server_dev_environment", c-> new NumericValue(FabricLoader.getInstance().isDevelopmentEnvironment()));
put("server_mods", c -> {
Map<Value, Value> 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_max_memory", c -> new NumericValue(Runtime.getRuntime().maxMemory()));
put("java_allocated_memory", c -> new NumericValue(Runtime.getRuntime().totalMemory())); put("java_allocated_memory", c -> new NumericValue(Runtime.getRuntime().totalMemory()));