diff --git a/src/main/java/emu/grasscutter/config/ConfigContainer.java b/src/main/java/emu/grasscutter/config/ConfigContainer.java index 7694e6d9a..d019aad7b 100644 --- a/src/main/java/emu/grasscutter/config/ConfigContainer.java +++ b/src/main/java/emu/grasscutter/config/ConfigContainer.java @@ -111,6 +111,12 @@ public class ConfigContainer { public ServerRunMode runMode = ServerRunMode.HYBRID; public boolean logCommands = false; + /** + * If enabled, the 'require' Lua function will load the script's compiled varient into the context. (faster; doesn't work as well) + * If disabled, all 'require' calls will be replaced with the referenced script's source. (slower; works better) + */ + public boolean fastRequire = true; + public HTTP http = new HTTP(); public Game game = new Game(); diff --git a/src/main/java/emu/grasscutter/config/Configuration.java b/src/main/java/emu/grasscutter/config/Configuration.java index 9e4251f0b..4229824e3 100644 --- a/src/main/java/emu/grasscutter/config/Configuration.java +++ b/src/main/java/emu/grasscutter/config/Configuration.java @@ -1,11 +1,12 @@ package emu.grasscutter.config; -import static emu.grasscutter.Grasscutter.config; - import emu.grasscutter.utils.FileUtils; + import java.nio.file.Path; import java.util.Locale; +import static emu.grasscutter.Grasscutter.config; + /** * A data container for the server's configuration. * @@ -38,6 +39,7 @@ public final class Configuration extends ConfigContainer { config.server.game.gameOptions.inventoryLimits; public static final GameOptions.HandbookOptions HANDBOOK = config.server.game.gameOptions.handbook; + public static final boolean FAST_REQUIRE = config.server.fastRequire; private static final String DATA_FOLDER = config.folderStructure.data; private static final String PLUGINS_FOLDER = config.folderStructure.plugins; private static final String SCRIPTS_FOLDER = config.folderStructure.scripts; diff --git a/src/main/java/emu/grasscutter/game/ability/actions/ActionServerLuaCall.java b/src/main/java/emu/grasscutter/game/ability/actions/ActionServerLuaCall.java index 26f9134ad..6c678b579 100644 --- a/src/main/java/emu/grasscutter/game/ability/actions/ActionServerLuaCall.java +++ b/src/main/java/emu/grasscutter/game/ability/actions/ActionServerLuaCall.java @@ -6,9 +6,10 @@ import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; import emu.grasscutter.game.ability.Ability; import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.scripts.ScriptLoader; -import javax.script.Bindings; import org.luaj.vm2.LuaFunction; +import javax.script.Bindings; + @AbilityAction(AbilityModifierAction.Type.ServerLuaCall) public final class ActionServerLuaCall extends AbilityActionHandler { @Override diff --git a/src/main/java/emu/grasscutter/scripts/ScriptLoader.java b/src/main/java/emu/grasscutter/scripts/ScriptLoader.java index 2f52d3472..e58cdfeff 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptLoader.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptLoader.java @@ -1,6 +1,7 @@ package emu.grasscutter.scripts; import emu.grasscutter.*; +import emu.grasscutter.config.Configuration; import emu.grasscutter.game.dungeons.challenge.enums.*; import emu.grasscutter.game.props.*; import emu.grasscutter.game.quest.enums.QuestState; @@ -8,46 +9,54 @@ import emu.grasscutter.scripts.constants.*; import emu.grasscutter.scripts.data.SceneMeta; import emu.grasscutter.scripts.serializer.*; import emu.grasscutter.utils.FileUtils; +import lombok.Getter; +import org.luaj.vm2.*; +import org.luaj.vm2.lib.*; +import org.luaj.vm2.lib.jse.CoerceJavaToLua; +import org.luaj.vm2.script.*; + +import javax.script.*; +import java.io.IOException; import java.lang.ref.SoftReference; import java.nio.file.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; -import javax.script.*; -import lombok.Getter; -import org.luaj.vm2.*; -import org.luaj.vm2.lib.OneArgFunction; -import org.luaj.vm2.lib.jse.CoerceJavaToLua; -import org.luaj.vm2.script.LuajContext; public class ScriptLoader { private static ScriptEngineManager sm; - @Getter private static ScriptEngine engine; - private static ScriptEngineFactory factory; + @Getter private static LuaScriptEngine engine; @Getter private static Serializer serializer; @Getter private static ScriptLib scriptLib; @Getter private static LuaValue scriptLibLua; /** suggest GC to remove it if the memory is less */ + private static Map> scriptSources = + new ConcurrentHashMap<>(); private static Map> scriptsCache = new ConcurrentHashMap<>(); /** sceneId - SceneMeta */ private static Map> sceneMetaCache = new ConcurrentHashMap<>(); private static final AtomicReference currentBindings = new AtomicReference<>(null); + private static final AtomicReference currentContext = new AtomicReference<>(null); + /** + * Initializes the script engine. + */ public static synchronized void init() throws Exception { if (sm != null) { throw new Exception("Script loader already initialized"); } // Create script engine - sm = new ScriptEngineManager(); - engine = sm.getEngineByName("luaj"); - factory = getEngine().getFactory(); + ScriptLoader.sm = new ScriptEngineManager(); + var engine = ScriptLoader.engine = (LuaScriptEngine) sm.getEngineByName("luaj"); + ScriptLoader.serializer = new LuaSerializer(); - // Lua stuff - serializer = new LuaSerializer(); - var ctx = (LuajContext) engine.getContext(); + // Set the Lua context. + var ctx = new LuajContext(true, false); + ctx.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE); + engine.setContext(ctx); // Set the 'require' function handler. ctx.globals.set("require", new RequireFunction()); @@ -141,6 +150,7 @@ public class ScriptLoader { // Append the script to the context. try { var bindings = currentBindings.get(); + if (bindings != null) { ScriptLoader.eval(script, bindings); } else { @@ -158,19 +168,96 @@ public class ScriptLoader { } } + /** + * Loads the sources of a script. + * + * @param path The path of the script. + * @return The sources of the script. + */ + public static String readScript(String path) { + // Check if the path is cached. + var cached = ScriptLoader.tryGet( + ScriptLoader.scriptSources.get(path)); + if (cached.isPresent()) { + return cached.get(); + } + + // Attempt to load the script. + var scriptPath = FileUtils.getScriptPath(path); + if (!Files.exists(scriptPath)) return null; + + try { + var source = Files.readString(scriptPath); + ScriptLoader.scriptSources.put( + path, new SoftReference<>(source)); + + return source; + } catch (IOException exception) { + Grasscutter.getLogger() + .error("Loading script {} failed! - {}", path, exception.getLocalizedMessage()); + return null; + } + } + + /** + * Fetches a script and compiles it, or uses the cached varient. + * + * @param path The path of the script. + * @return The compiled script. + */ public static CompiledScript getScript(String path) { - var sc = tryGet(scriptsCache.get(path)); + // Check if the script is cached. + var sc = ScriptLoader.tryGet( + ScriptLoader.scriptsCache.get(path)); if (sc.isPresent()) { return sc.get(); } - // Grasscutter.getLogger().debug("Loading script " + path); - final Path scriptPath = FileUtils.getScriptPath(path); - if (!Files.exists(scriptPath)) return null; - try { - var script = ((Compilable) getEngine()).compile(Files.newBufferedReader(scriptPath)); - scriptsCache.put(path, new SoftReference<>(script)); + CompiledScript script; + if (Configuration.FAST_REQUIRE) { + // Attempt to load the script. + var scriptPath = FileUtils.getScriptPath(path); + if (!Files.exists(scriptPath)) return null; + + // Compile the script from the file. + var source = Files.newBufferedReader(scriptPath); + script = ScriptLoader.getEngine().compile(source); + } else { + // Load the script sources. + var sources = ScriptLoader.readScript(path); + if (sources == null) return null; + + // Check to see if the script references other scripts. + if (sources.contains("require")) { + var lines = sources.split("\n"); + var output = new StringBuilder(); + for (var line : lines) { + // Skip non-require lines. + if (!line.startsWith("require")) { + output.append(line).append("\n"); + continue; + } + + // Extract the script name. + var scriptName = line.substring(9, line.length() - 1); + // Resolve the script path. + var scriptPath = "Common/" + scriptName + ".lua"; + var scriptSource = ScriptLoader.readScript(scriptPath); + if (scriptSource == null) continue; + + // Append the script source. + output.append(scriptSource).append("\n"); + } + sources = output.toString(); + } + + // Compile the script & cache it in memory. + script = ScriptLoader.getEngine().compile(sources); + } + + // Cache the script. + ScriptLoader.scriptsCache.put(path, new SoftReference<>(script)); return script; } catch (Exception e) { Grasscutter.getLogger() diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java b/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java index f67f48264..9bb8252f9 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java @@ -3,12 +3,13 @@ package emu.grasscutter.scripts.data; import emu.grasscutter.Grasscutter; import emu.grasscutter.game.world.Position; import emu.grasscutter.scripts.ScriptLoader; -import java.util.*; -import java.util.stream.Collectors; -import javax.script.*; import lombok.*; import org.luaj.vm2.*; +import javax.script.*; +import java.util.*; +import java.util.stream.Collectors; + @ToString @Setter public final class SceneGroup { @@ -35,7 +36,8 @@ public final class SceneGroup { public SceneReplaceable is_replaceable; - private transient boolean loaded; // Not an actual variable in the scripts either + /* These are not script variables. */ + private transient boolean loaded; private transient CompiledScript script; private transient Bindings bindings; @@ -82,7 +84,7 @@ public final class SceneGroup { } // Set flag here so if there is no script, we don't call this function over and over again. this.setLoaded(true); - + // Create the bindings. this.bindings = ScriptLoader.getEngine().createBindings(); var cs =