Files
fabric-carpet/src/main/java/carpet/script/CarpetScriptServer.java
T
altrisi cc7a9c01be Annotated Scarpet (#772)
* Progress by now.

* Output conversions plus a few extra random things

* More progress, mostly list converting and a way to get converters

* Apply ValueConverters, refactor valueconverter getter to

accept types with annotations and generics, made listconverter pass
those params and annotations recursively. And probably more things.

* Some extra docs, the 100% most important thing

* Added MapConverter, strict Strings and Texts, some docs additions

and commented out heuristics

* Rework StrictParam to use SimpleTypeConverters and be saved in a map

* Minor changes and docs & added non-fully-functional context "converter"

* Added support for variable value count converters, multi-value eaters &

0 value eaters (Context).

* Docs update, enable Context converter, fix minor TODOs

* Move (most) parameter annotations into a single class,

remove deprecated ValueCaster#cast, update some docs.

* Small refactor of Param.Strict registering, added boolean to it

* Added PairConverter, moved Context and LV ValueConverters to Params

* Document public methods of AnnotationParser

* Add a lot more docs, make it ready for the first preview PR

* Move ValueConverter population outside of makeFunction,

check lv size for > max arguments, minor doc updates and workaround for
Mercury#28 until released

* Added optional parameters (declared using Java's Optional),

made evalAndConvert check for the presence of the next iteration,
updated test class

* Generalize Value OutputConverter

* ListConverter and MapConverter now follow ValueConverter contract of

incorrect value handling, returning a complete null for an incorrect
value anywhere. Also small fix to OptionalConverter and docs.
Getting ready for checking null in AnnotationParser!

* Change reflection's #invoke to a MethodHandle to boost speed by a lot

Commented

* Dedupe code, update and remove outdated todos, add 2 new ValueCasters

* Merge lambdas in display_title2 test function to be more accurate

* Check nulls somehow, I guess

* Move base function logic into a class to stop

passing hundreds of parameters into static functions and therefore simplify it

* Reduce visibility and fix a bug with PairConverter

* Hide Context provider in usage report

* Add support for primitive varargs

* Added TheLazyT converter

* Drop #getPrefixedTypeName and use 'something' as name of values

* Use value again because of varargs, document that

* Remove a completed TODO

* Remove old disabled heuristic value type name getter

* Fix TheLazyT

* Preparing locators before touching current Carpet infrastructure

* Add notes about non-implementedness of remaining locators

* Added APIs to register custom ValueConverters

* Mention new APIs in javadocs

* Adapt to new Carpet changes and deprecated non-implemented annotations

* Update example function for display_title()

* Better implementation for player_list example

* Fix example error message

* FunctionArgument no longer has generics

* Modernize Text converters

* OutputConverter is final, plus other small changes

* Few non-behavioural changes

* Rename LazyFunction to ScarpetFunction

* Drop LazyValue in parameters, not in return

Updating docs was a nightmare

* CARPET CHANGE - Make Fluff.AbstractLazyFunction.unpackArgs call unpackLazy (was duped)

* Make errors more like current Scarpet errors

* Allow customizing ContextType when evaluating arguments

* Add a few missing docs, comment out outdated ValueCaster doc

It's been a while, wow

* Fix for new Carpet signature :irritatered:

Please don't break API methods! Extensions may be using them, and they
will just crash the game or behave in unexpected ways else!

* Format all code to Carpet's. Powered by Eclipse

Reviewed to be sure it's ok. No functional changes, except from a small correction

* Always use full name for Context.Type

* Add parsing for all current Scarpet api and language classes

And remove test functions

* Allow void return type

* Fix no param methods

Whoops!

* Remove outdated comment and fix small javadoc linting problem

* Drop Param.TheLazyT in favour of just Context.Type type

And small performance improvement to varargs varargs since fastutil's toArray is crap

* Quote functions in error messages

* Make SimpleTypeConverter independent of ValueCaster

'Real world' usage demonstrated it can be annoying else, mostly for wrapper only values where you just want the wrapped object
2021-05-21 01:33:00 -03:00

520 lines
19 KiB
Java

package carpet.script;
import carpet.CarpetSettings;
import carpet.script.annotation.AnnotationParser;
import carpet.script.api.Auxiliary;
import carpet.script.api.BlockIterators;
import carpet.script.api.Entities;
import carpet.script.api.Inventories;
import carpet.script.api.Scoreboards;
import carpet.script.api.WorldAccess;
import carpet.script.bundled.BundledModule;
import carpet.CarpetServer;
import carpet.script.bundled.FileModule;
import carpet.script.bundled.Module;
import carpet.script.exception.IntegrityException;
import carpet.script.exception.InvalidCallbackException;
import carpet.script.language.Arithmetic;
import carpet.script.language.ControlFlow;
import carpet.script.language.DataStructures;
import carpet.script.language.Functions;
import carpet.script.language.Loops;
import carpet.script.language.Sys;
import carpet.script.language.Threading;
import carpet.script.value.FunctionValue;
import carpet.script.value.Value;
import carpet.utils.CarpetProfiler;
import carpet.utils.Messenger;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.tree.CommandNode;
import net.fabricmc.api.EnvType;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.WorldSavePath;
import net.minecraft.util.math.BlockPos;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
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;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static net.minecraft.server.command.CommandManager.argument;
import static net.minecraft.server.command.CommandManager.literal;
public class CarpetScriptServer
{
//make static for now, but will change that later:
public static final Logger LOG = LogManager.getLogger("scarpet");
public final MinecraftServer server;
public CarpetScriptHost globalHost;
public Map<String, CarpetScriptHost> modules;
public Set<String> unloadableModules;
public long tickStart;
public boolean stopAll;
public int tickDepth;
private Set<String> holyMoly;
public CarpetEventServer events;
private static final List<Module> bundledModuleData = new ArrayList<>();
private static final List<Module> ruleModuleData = new ArrayList<>();
/**
* Registers a Scarpet App to be always available to be loaded
* in the /script load list.
* @see BundledModule#fromPath(String, String, boolean)
*
* @param app The {@link BundledModule} of the app
*/
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).
* Libraries should be registered with {@link #registerBuiltInScript(BundledModule)} instead
* @see BundledModule#fromPath(String, String, boolean)
*
* @param app The {@link BundledModule} of the app.
*/
public static void registerSettingsApp(BundledModule app) {
ruleModuleData.add(app);
}
static
{
registerBuiltInScript(BundledModule.carpetNative("camera", false));
registerBuiltInScript(BundledModule.carpetNative("overlay", false));
registerBuiltInScript(BundledModule.carpetNative("event_test", false));
registerBuiltInScript(BundledModule.carpetNative("stats_test", false));
registerBuiltInScript(BundledModule.carpetNative("math", true));
registerBuiltInScript(BundledModule.carpetNative("chunk_display", false));
registerBuiltInScript(BundledModule.carpetNative("ai_tracker", false));
registerBuiltInScript(BundledModule.carpetNative("draw_beta", false));
registerBuiltInScript(BundledModule.carpetNative("shapes", true));
registerBuiltInScript(BundledModule.carpetNative("distance_beta", false));
}
public CarpetScriptServer(MinecraftServer server)
{
this.server = server;
init();
}
private void init()
{
ScriptHost.systemGlobals.clear();
events = new CarpetEventServer(server);
modules = new HashMap<>();
unloadableModules = new HashSet<String>();
tickStart = 0L;
stopAll = false;
holyMoly = server.getCommandManager().getDispatcher().getRoot().getChildren().stream().map(CommandNode::getName).collect(Collectors.toSet());
globalHost = CarpetScriptHost.create(this, null, false, null, p -> true, false);
}
public void initializeForWorld()
{
CarpetServer.settingsManager.initializeScarpetRules();
CarpetServer.extensions.forEach(e -> {
if (e.customSettingsManager() != null) {
e.customSettingsManager().initializeScarpetRules();
}
});
if (CarpetSettings.scriptsAutoload)
{
for (String moduleName: listAvailableModules(false))
{
addScriptHost(server.getCommandSource(), moduleName, null, true, true, false);
}
}
CarpetEventServer.Event.START.onTick();
}
public Module getModule(String name, boolean allowLibraries)
{
try {
Path folder = server.getSavePath(WorldSavePath.ROOT).resolve("scripts");
if (!Files.exists(folder))
Files.createDirectories(folder);
Optional<Path>
scriptPath = Files.list(folder)
.filter(script ->
script.getFileName().toString().equalsIgnoreCase(name+".sc") ||
(allowLibraries && script.getFileName().toString().equalsIgnoreCase(name+".scl"))
).findFirst();
if (scriptPath.isPresent())
return new FileModule(scriptPath.get());
if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT)
{
Path globalFolder = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts");
if (!Files.exists(globalFolder))
Files.createDirectories(globalFolder);
scriptPath = Files.walk(globalFolder)
.filter(script -> script.getFileName().toString().equalsIgnoreCase(name + ".sc") ||
(allowLibraries && script.getFileName().toString().equalsIgnoreCase(name + ".scl")))
.findFirst();
if (scriptPath.isPresent())
return new FileModule(scriptPath.get());
}
} catch (IOException e) {
CarpetSettings.LOG.error("Exception while loading the app: ", e);
}
for (Module moduleData : bundledModuleData)
{
if (moduleData.getName().equalsIgnoreCase(name) && (allowLibraries || !moduleData.isLibrary()))
{
return moduleData;
}
}
return null;
}
public Module getRuleModule(String name)
{
for (Module moduleData : ruleModuleData)
{
if (moduleData.getName().equalsIgnoreCase(name))
{
return moduleData;
}
}
return null;
}
public List<String> listAvailableModules(boolean includeBuiltIns)
{
List<String> moduleNames = new ArrayList<>();
if (includeBuiltIns)
{
for (Module mi : bundledModuleData)
{
if (!mi.isLibrary() && !mi.getName().endsWith("_beta")) moduleNames.add(mi.getName());
}
}
try {
Path worldScripts = server.getSavePath(WorldSavePath.ROOT).resolve("scripts");
if (!Files.exists(worldScripts))
Files.createDirectories(worldScripts);
Files.list(worldScripts)
.filter(f -> f.toString().endsWith(".sc"))
.forEach(f -> moduleNames.add(f.getFileName().toString().replaceFirst("\\.sc$","").toLowerCase(Locale.ROOT)));
if (includeBuiltIns && (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT))
{
Path globalScripts = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts");
if (!Files.exists(globalScripts))
Files.createDirectories(globalScripts);
Files.walk(globalScripts, FileVisitOption.FOLLOW_LINKS)
.filter(f -> f.toString().endsWith(".sc"))
.forEach(f -> moduleNames.add(f.getFileName().toString().replaceFirst("\\.sc$","").toLowerCase(Locale.ROOT)));
}
} catch (IOException e) {
CarpetSettings.LOG.error("Exception while searching for apps: ", e);
}
return moduleNames;
}
public ScriptHost getHostByName(String name)
{
if (name == null)
return globalHost;
return modules.get(name);
}
public boolean addScriptHost(ServerCommandSource source, String name, Predicate<ServerCommandSource> commandValidator,
boolean perPlayer, boolean autoload, boolean isRuleApp)
{
CarpetProfiler.ProfilerToken currentSection = CarpetProfiler.start_section(null, "Scarpet load", CarpetProfiler.TYPE.GENERAL);
if (commandValidator == null) commandValidator = p -> true;
long start = System.nanoTime();
name = name.toLowerCase(Locale.ROOT);
boolean reload = false;
if (modules.containsKey(name))
{
if (isRuleApp) return false;
removeScriptHost(source, name, false, isRuleApp);
reload = true;
}
Module module = isRuleApp ? getRuleModule(name) : getModule(name, false);
if (module == null)
{
Messenger.m(source, "r Failed to add "+name+" app");
return false;
}
CarpetScriptHost newHost = CarpetScriptHost.create(this, module, perPlayer, source, commandValidator, isRuleApp);
if (newHost == null)
{
Messenger.m(source, "r Failed to add "+name+" app");
return false;
}
// config needs to be read as we load the app since some events use that info
//if (!newHost.readConfig())
//{
// Messenger.m(source, "r Failed to add "+name+" app: invalid app config (via '__config()' function)");
// return false;
//}
if (module.getCode() == null)
{
Messenger.m(source, "r Unable to load "+name+" app - not found");
return false;
}
modules.put(name, newHost);
if (!isRuleApp) unloadableModules.add(name);
if (autoload && !newHost.persistenceRequired)
{
removeScriptHost(source, name, false, false);
return false;
}
//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, isRuleApp);
return false;
}
else if (isCommandAdded)
{
CarpetServer.settingsManager.notifyPlayersCommandsChanged();
if (!isRuleApp) Messenger.m(source, "gi "+name+" app "+action+" with /"+name+" command");
}
else
{
if (!isRuleApp) Messenger.m(source, "gi "+name+" app "+action);
}
if (newHost.isPerUser())
{
// that will provide player hosts right at the startup
newHost.retrieveForExecution(source, null);
}
else
{
// global app - calling start now.
FunctionValue onStart = newHost.getFunction("__on_start");
if (onStart != null) newHost.callNow(onStart, Collections.emptyList());
}
CarpetProfiler.end_current_section(currentSection);
long end = System.nanoTime();
CarpetSettings.LOG.info("App "+name+" loaded in "+(end-start)/1000000+" ms");
return true;
}
public boolean isInvalidCommandRoot(String appName)
{
return holyMoly.contains(appName);
}
public boolean removeScriptHost(ServerCommandSource source, String name, boolean notifySource, boolean isRuleApp)
{
name = name.toLowerCase(Locale.ROOT);
if (!modules.containsKey(name) || (!isRuleApp && !unloadableModules.contains(name)))
{
if (notifySource) Messenger.m(source, "r No such app found: ", "wb " + name);
return false;
}
// stop all events associated with name
events.removeAllHostEvents(modules.get(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;
}
public boolean runEventCall(ServerCommandSource sender, String hostname, String optionalTarget, FunctionValue udf, List<Value> argv)
{
CarpetScriptHost host = globalHost;
if (hostname != null)
host = modules.get(hostname);
if (host == null) return false;
// dummy call for player apps that reside on the global copy - do not run them, but report as successes.
if (host.isPerUser() && optionalTarget==null) return true;
ServerPlayerEntity target = null;
if (optionalTarget != null)
{
target = sender.getMinecraftServer().getPlayerManager().getPlayer(optionalTarget);
if (target == null) return false;
}
int successes = signal(sender, target, hostname, udf, argv, true );
return successes >= 0;
}
public void runScheduledCall(BlockPos origin, ServerCommandSource source, String hostname, CarpetScriptHost host, FunctionValue udf, List<Value> argv)
{
if (hostname != null && !modules.containsKey(hostname)) // well - scheduled call app got unloaded
return;
try
{
host.callUDF(origin, source, udf, argv);
}
catch (NullPointerException | InvalidCallbackException ignored)
{
}
}
/**
* returns number of successful calls in the host or -1 if failed exceptionally
* @param sender
* @param optionalRecipient
* @param hostname
* @param udf
* @param argv
* @param reportFails - returns -1 when any call failed
* @return
*/
public int signal(ServerCommandSource sender, ServerPlayerEntity optionalRecipient, String hostname, FunctionValue udf, List<Value> argv, boolean reportFails)
{
if (hostname == null)
{
if (optionalRecipient != null) return 0;
try
{
globalHost.callUDF(BlockPos.ORIGIN, sender, udf, argv);
}
catch (NullPointerException | InvalidCallbackException npe)
{
return reportFails?-1:0;
}
return 1;
}
String hostRecipient = optionalRecipient == null?null:optionalRecipient.getEntityName();
ServerCommandSource source = (optionalRecipient == null)? sender : optionalRecipient.getCommandSource().withLevel(CarpetSettings.runPermissionLevel);
int successes = 0;
for (CarpetScriptHost host : modules.get(hostname).retrieveForExecution(sender, hostRecipient))
{
// getPlayer will always return nonnull cause retrieve for execution only returns hosts for existing players.
ServerCommandSource executingSource = host.perUser
? source.getMinecraftServer().getPlayerManager().getPlayer(host.user).getCommandSource()
: source.getMinecraftServer().getCommandSource();
try
{
host.callUDF(BlockPos.ORIGIN, source.withLevel(CarpetSettings.runPermissionLevel), udf, argv);
}
catch (NullPointerException | InvalidCallbackException | IntegrityException npe)
{
if (reportFails) return -1;
continue;
}
successes ++;
}
return successes;
}
public void tick()
{
CarpetProfiler.ProfilerToken token;
token = CarpetProfiler.start_section(null, "Scarpet schedule", CarpetProfiler.TYPE.GENERAL);
events.tick();
CarpetProfiler.end_current_section(token);
token = CarpetProfiler.start_section(null, "Scarpet app data", CarpetProfiler.TYPE.GENERAL);
for (CarpetScriptHost host : modules.values())
{
host.tick();
}
CarpetProfiler.end_current_section(token);
}
public void onClose()
{
CarpetEventServer.Event.SHUTDOWN.onTick();
for (ScriptHost host : modules.values())
{
host.onClose();
}
}
public void onPlayerJoin(ServerPlayerEntity player)
{
modules.values().forEach(h ->
{
if (h.isPerUser())
{
try
{
h.retrieveOwnForExecution(player.getCommandSource());
}
catch (CommandSyntaxException ignored)
{
}
}
});
}
static class TransferData
{
boolean perUser;
Predicate<ServerCommandSource> commandValidator;
boolean isRuleApp;
private TransferData(CarpetScriptHost host)
{
perUser = host.perUser;
commandValidator = host.commandValidator;
isRuleApp = host.isRuleApp;
}
}
public void reload(MinecraftServer server)
{
Map<String, TransferData> apps = new HashMap<>();
modules.forEach((s, h) -> apps.put(s, new TransferData(h)));
apps.keySet().forEach(s -> removeScriptHost(server.getCommandSource(), s, false, false));
CarpetEventServer.Event.clearAllBuiltinEvents();
init();
apps.forEach((s, data) -> addScriptHost(server.getCommandSource(), s,data.commandValidator, data.perUser,false, data.isRuleApp));
}
public void reAddCommands()
{
modules.values().forEach(host -> host.addAppCommands(s -> {}));
}
public static void parseFunctionClasses()
{
// Language
AnnotationParser.parseFunctionClass(Arithmetic.class);
AnnotationParser.parseFunctionClass(ControlFlow.class);
AnnotationParser.parseFunctionClass(DataStructures.class);
AnnotationParser.parseFunctionClass(Functions.class);
AnnotationParser.parseFunctionClass(Loops.class);
AnnotationParser.parseFunctionClass(Sys.class);
AnnotationParser.parseFunctionClass(Threading.class);
// API
AnnotationParser.parseFunctionClass(Auxiliary.class);
AnnotationParser.parseFunctionClass(BlockIterators.class);
AnnotationParser.parseFunctionClass(Entities.class);
AnnotationParser.parseFunctionClass(Inventories.class);
AnnotationParser.parseFunctionClass(Scoreboards.class);
AnnotationParser.parseFunctionClass(carpet.script.language.Threading.class);
AnnotationParser.parseFunctionClass(WorldAccess.class);
}
}