diff --git a/de.peeeq.wurstscript/build.gradle b/de.peeeq.wurstscript/build.gradle index ff96d0c5d..14ea1217b 100644 --- a/de.peeeq.wurstscript/build.gradle +++ b/de.peeeq.wurstscript/build.gradle @@ -108,7 +108,7 @@ dependencies { implementation 'com.github.albfernandez:juniversalchardet:2.4.0' implementation 'org.xerial:sqlite-jdbc:3.46.1.3' implementation 'com.github.inwc3:jmpq3:29b55f2c32' - implementation 'com.github.inwc3:wc3libs:cc49c8e63c' + implementation 'com.github.inwc3:wc3libs:6a96a79595' implementation('com.github.wurstscript:wurstsetup:393cf5ea39') { exclude group: 'org.eclipse.jgit', module: 'org.eclipse.jgit' exclude group: 'org.eclipse.jgit', module: 'org.eclipse.jgit.ssh.apache' diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ProjectConfigBuilder.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ProjectConfigBuilder.java index b6e3dbf85..6c9f9162c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ProjectConfigBuilder.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ProjectConfigBuilder.java @@ -82,8 +82,17 @@ public static MapRequest.CompilationResult apply(WurstProjectConfigData projectC applyBuildMapData(projectConfig, mapScript, buildDir, w3data, w3I, result, configHash, outputScriptName); } else if (!configNeedsApplying) { WLogger.info("Using cached w3i configuration"); - // Still need to set the result.script correctly - result.script = mapScript; + // Prefer the previously-injected script (with correct config() body) over the + // raw map script. If it doesn't exist yet (e.g. first Lua build after a JASS-only + // cache), fall through to re-inject so the config() body is never stale. + File cachedInjectedScript = new File(buildDir, outputScriptName); + if (cachedInjectedScript.exists()) { + result.script = cachedInjectedScript; + } else if (StringUtils.isNotBlank(buildMapData.getName())) { + WLogger.info("Cached injected script missing, re-injecting config"); + applyBuildMapData(projectConfig, mapScript, buildDir, w3data, w3I, result, configHash, outputScriptName); + } + // else result.script stays as mapScript (no wurst.build name configured) } result.w3i = new File(buildDir, "war3map.w3i"); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java index e77d2562d..49c208124 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java @@ -612,13 +612,19 @@ protected void injectMapData(WurstGui gui, Optional testMap, CompilationRe mapScriptName = "war3map.j"; } - // Delete old scripts + // Delete old scripts (both root and scripts/ subdirectory locations) if (mpqEditor.hasFile("war3map.j")) { mpqEditor.deleteFile("war3map.j"); } + if (mpqEditor.hasFile("scripts\\war3map.j")) { + mpqEditor.deleteFile("scripts\\war3map.j"); + } if (mpqEditor.hasFile("war3map.lua")) { mpqEditor.deleteFile("war3map.lua"); } + if (mpqEditor.hasFile("scripts\\war3map.lua")) { + mpqEditor.deleteFile("scripts\\war3map.lua"); + } // Insert new script mpqEditor.insertFile(mapScriptName, result.script); @@ -696,6 +702,10 @@ protected File executeBuildMapPipeline(ModelManager modelManager, WurstGui gui, gui.sendProgress("Finalizing map"); try (MpqEditor mpq = MpqEditorFactory.getEditor(Optional.of(targetMapFile))) { if (mpq != null) { + // Strip internal Wurst cache files — they are dev-only metadata and should + // not be present in the distributed map. + if (mpq.hasFile("wurst_cache_manifest.txt")) mpq.deleteFile("wurst_cache_manifest.txt"); + if (mpq.hasFile("wurst_object_cache.txt")) mpq.deleteFile("wurst_object_cache.txt"); mpq.closeWithCompression(); } } @@ -823,6 +833,9 @@ protected byte[] extractMapScript(Optional mapCopy) throws Exception { if (mpqEditor.hasFile("war3map.j")) { return mpqEditor.extractFile("war3map.j"); } + if (mpqEditor.hasFile("scripts\\war3map.j")) { + return mpqEditor.extractFile("scripts\\war3map.j"); + } } return null; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java index 06798af24..40640b61f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imoptimizer/ImInliner.java @@ -482,6 +482,11 @@ private boolean isInlineCandidate(ImFunction f) { // this is only relevant for lua, because in JASS they are eliminated before inlining return false; } + if (translator.luaInitFunctions.containsKey(f)) { + // Lua package init functions must stay as ImFunctionCall nodes so StmtTranslation + // can wrap them in xpcall. Inlining removes the call site and loses the guard. + return false; + } return true; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java index 267e5607f..f9a9ecbfc 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java @@ -59,7 +59,14 @@ public class ImTranslator { private final ImProg imProg; - final Map initFuncMap = new Object2ObjectLinkedOpenHashMap<>(); + public final Map initFuncMap = new Object2ObjectLinkedOpenHashMap<>(); + + /** + * When targeting Lua, package init functions that should be called directly via xpcall + * rather than through the JASS TriggerEvaluate thread-isolation pattern. + * Populated during translateProg() when isLuaTarget() is true. + */ + public final Map luaInitFunctions = new Object2ObjectLinkedOpenHashMap<>(); private final Map thisVarMap = new Object2ObjectLinkedOpenHashMap<>(); @@ -528,16 +535,23 @@ private void finishInitFunctions() { } Set calledInitializers = Sets.newLinkedHashSet(); - ImVar initTrigVar = prepareTrigger(); + if (isLuaTarget()) { + // In Lua mode, xpcall handles error isolation; no trigger handle is needed. + for (WPackage p : Utils.sortByName(initFuncMap.keySet())) { + callInitFunc(calledInitializers, p, null); + } + } else { + ImVar initTrigVar = prepareTrigger(); - for (WPackage p : Utils.sortByName(initFuncMap.keySet())) { - callInitFunc(calledInitializers, p, initTrigVar); - } + for (WPackage p : Utils.sortByName(initFuncMap.keySet())) { + callInitFunc(calledInitializers, p, initTrigVar); + } - ImFunction native_DestroyTrigger = getNativeFunc("DestroyTrigger"); - if (native_DestroyTrigger != null) { - getMainFunc().getBody().add(JassIm.ImFunctionCall(emptyTrace, native_DestroyTrigger, ImTypeArguments(), - JassIm.ImExprs(JassIm.ImVarAccess(initTrigVar)), false, CallType.NORMAL)); + ImFunction native_DestroyTrigger = getNativeFunc("DestroyTrigger"); + if (native_DestroyTrigger != null) { + getMainFunc().getBody().add(JassIm.ImFunctionCall(emptyTrace, native_DestroyTrigger, ImTypeArguments(), + JassIm.ImExprs(JassIm.ImVarAccess(initTrigVar)), false, CallType.NORMAL)); + } } } @@ -563,7 +577,7 @@ private ImFunction getNativeFunc(String funcName) { return getFuncFor(Utils.getFirst(wurstFunc).getDef()); } - private void callInitFunc(Set calledInitializers, WPackage p, ImVar initTrigVar) { +private void callInitFunc(Set calledInitializers, WPackage p, @Nullable ImVar initTrigVar) { Preconditions.checkNotNull(p); if (calledInitializers.contains(p)) { return; @@ -580,6 +594,14 @@ private void callInitFunc(Set calledInitializers, WPackage p, ImVar in if (initFunc.getBody().size() == 0) { return; } + if (isLuaTarget()) { + // In Lua mode, xpcall replaces TriggerEvaluate for error isolation without WC3 handle overhead. + // Record the init function so the Lua translator can wrap it with xpcall. + luaInitFunctions.put(initFunc, p.getName()); + getMainFunc().getBody().add(ImFunctionCall(initFunc.getTrace(), initFunc, ImTypeArguments(), ImExprs(), false, CallType.NORMAL)); + return; + } + boolean successful = createInitFuncCall(p, initTrigVar, initFunc); if (!successful) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java index 916e7709c..76843e035 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java @@ -13,7 +13,7 @@ public class ExprTranslation { public static final String TYPE_ID = "__typeId__"; public static final String WURST_SUPERTYPES = "__wurst_supertypes"; - private static final String WURST_ABORT_THREAD_SENTINEL = "__wurst_abort_thread"; + static final String WURST_ABORT_THREAD_SENTINEL = "__wurst_abort_thread"; private static final Set LUA_HANDLE_TO_INDEX = Set.of( "widgetToIndex", "unitToIndex", "destructableToIndex", "itemToIndex", "abilityToIndex", "forceToIndex", "groupToIndex", "triggerToIndex", "triggeractionToIndex", "triggerconditionToIndex", @@ -89,7 +89,7 @@ public static LuaExpr translate(ImFuncRef e, LuaTranslator tr) { return LuaAst.LuaExprFunctionAbstraction(LuaAst.LuaParams(dots), callbackBody); } - private static String callErrorFunc(LuaTranslator tr, String msg) { + static String callErrorFunc(LuaTranslator tr, String msg) { LuaFunction ef = tr.getErrorFunc(); if (ef != null) { if (ef.getParams().size() == 2) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java index 1995353f1..d8f6b5edd 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java @@ -218,7 +218,7 @@ public LuaMethod initFor(ImClass a) { LuaFunction ensureRealFunction = LuaAst.LuaFunction(uniqueName("realEnsure"), LuaAst.LuaParams(), LuaAst.LuaStatements()); private final Lazy errorFunc = Lazy.create(() -> - Objects.requireNonNull(this.getProg().getFunctions().stream() + this.getProg().getFunctions().stream() .flatMap(f -> { Element trace = f.attrTrace(); if (trace instanceof FuncDef) { @@ -233,8 +233,9 @@ public LuaMethod initFor(ImClass a) { } return Stream.empty(); }) - .findFirst().orElse(null))); - private final ImTranslator imTr; + .findFirst().orElse(null)); + final ImTranslator imTr; + public LuaTranslator(ImProg prog, ImTranslator imTr) { this.prog = prog; @@ -322,7 +323,6 @@ public LuaCompilationUnit translate() { initClassTables(c); } - emitExperimentalHashtableLeakGuards(); prependDeferredMainInitToMain(); cleanStatements(); enforceLuaLocalLimits(); @@ -354,22 +354,6 @@ private void ensureWurstContextCallbackHelpers() { } } - private void emitExperimentalHashtableLeakGuards() { - deferMainInit(LuaAst.LuaLiteral("-- Wurst experimental Lua assertion guards: raw WC3 hashtable natives must not be called.")); - deferMainInit(LuaAst.LuaLiteral("do")); - deferMainInit(LuaAst.LuaLiteral(" local __wurst_guard_ok = pcall(function()")); - for (String nativeName : allHashtableNativeNames()) { - deferMainInit(LuaAst.LuaLiteral(" if " + nativeName + " ~= nil then " + nativeName - + " = function(...) error(\"Wurst Lua assertion failed: unexpected call to native " + nativeName - + ". Expected __wurst_" + nativeName + ".\") end end")); - } - deferMainInit(LuaAst.LuaLiteral(" end)")); - deferMainInit(LuaAst.LuaLiteral(" if not __wurst_guard_ok then")); - deferMainInit(LuaAst.LuaLiteral(" -- Some Lua runtimes lock native globals. Compile-time leak checks stay authoritative.")); - deferMainInit(LuaAst.LuaLiteral(" end")); - deferMainInit(LuaAst.LuaLiteral("end")); - } - private void deferMainInit(LuaStatement statement) { deferredMainInit.add(statement); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java index 93e7e7bab..629ef44a6 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java @@ -5,13 +5,40 @@ import java.util.List; +import static de.peeeq.wurstscript.translation.lua.translation.ExprTranslation.WURST_ABORT_THREAD_SENTINEL; +import de.peeeq.wurstscript.jassIm.ImFunction; + public class StmtTranslation { public static void translate(ImExpr e, List res, LuaTranslator tr) { + // In Lua mode, package init functions are called directly and wrapped with xpcall. + if (e instanceof ImFunctionCall) { + ImFunctionCall call = (ImFunctionCall) e; + if (tr.imTr.luaInitFunctions.containsKey(call.getFunc())) { + emitLuaInitXpcall(call.getFunc(), res, tr); + return; + } + } LuaExpr expr = e.translateToLua(tr); res.add(expr); } + private static void emitLuaInitXpcall(ImFunction initFunc, List res, LuaTranslator tr) { + String funcName = tr.luaFunc.getFor(initFunc).getName(); + String packageName = tr.imTr.luaInitFunctions.getOrDefault(initFunc, "?"); + String errHandler = "function(err) if err == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end" + + " BJDebugMsg(\"lua init error: \" .. tostring(err))" + + " xpcall(function() " + ExprTranslation.callErrorFunc(tr, "tostring(err)") + " end," + + " function(err2) if err2 == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end" + + " BJDebugMsg(\"error reporting error: \" .. tostring(err2)) end) end"; + res.add(LuaAst.LuaLiteral("do")); + res.add(LuaAst.LuaLiteral(" local __wurst_init_ok = xpcall(" + funcName + ", " + errHandler + ")")); + res.add(LuaAst.LuaLiteral(" if not __wurst_init_ok then")); + res.add(LuaAst.LuaLiteral(" " + ExprTranslation.callErrorFunc(tr, "\"Could not initialize package " + packageName + ".\"") + "")); + res.add(LuaAst.LuaLiteral(" end")); + res.add(LuaAst.LuaLiteral("end")); + } + public static void translate(ImExitwhen s, List res, LuaTranslator tr) { LuaIf r = LuaAst.LuaIf(s.getCondition().translateToLua(tr), LuaAst.LuaStatements(LuaAst.LuaBreak()), diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java index c1a64b2ff..9e0832362 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java @@ -477,10 +477,8 @@ public void luaHeavyBootstrapStateIsSeededFromMain() throws IOException { String beforeMain = compiled.substring(0, mainPos); assertFalse(beforeMain.contains("__wurst_objectIndexMap = ({")); assertFalse(beforeMain.contains("__wurst_string_index_map = ({")); - assertFalse(beforeMain.contains("Wurst experimental Lua assertion guards")); assertTrue(mainSection.contains("__wurst_objectIndexMap = ({")); assertTrue(mainSection.contains("__wurst_string_index_map = ({")); - assertTrue(mainSection.contains("Wurst experimental Lua assertion guards")); } @Test @@ -531,7 +529,6 @@ public void luaDeferredBootstrapRunsBeforeInitGlobalsInMain() throws IOException assertOccursBefore(mainSection, "C.__wurst_supertypes =", "initGlobals()"); assertOccursBefore(mainSection, "C.__typeId__ =", "initGlobals()"); assertOccursBefore(mainSection, "C.C_f =", "initGlobals()"); - assertOccursBefore(mainSection, "Wurst experimental Lua assertion guards", "initGlobals()"); } @Test @@ -1198,15 +1195,19 @@ public void reflectionNativeStubsAreGuardedByExistingDefinitions() throws IOExce } @Test - public void stdLibInitUsesTriggerEvaluateGuardInMain() throws IOException { + public void stdLibInitUsesXpcallInsteadOfTriggerEvaluateInMain() throws IOException { test().testLua(true).withStdLib().lines( "package Test", "init", " skip" ); - String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stdLibInitUsesTriggerEvaluateGuardInMain.lua"), Charsets.UTF_8); - assertTrue(compiled.contains("if not(TriggerEvaluate(")); - assertTrue(compiled.contains("TriggerClearConditions")); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stdLibInitUsesXpcallInsteadOfTriggerEvaluateInMain.lua"), Charsets.UTF_8); + // Package inits use direct xpcall — no WC3 trigger handle overhead + assertTrue(compiled.contains("xpcall(init_")); + assertTrue(compiled.contains("__wurst_init_ok")); + // TriggerEvaluate pattern must NOT appear for init functions + assertFalse(compiled.contains("if not(TriggerEvaluate(")); + assertFalse(compiled.contains("TriggerClearConditions")); } @Test @@ -1261,22 +1262,23 @@ public void hashtableHandleExtensionsUseWurstLuaHelpers() throws IOException { String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableHandleExtensionsUseWurstLuaHelpers.lua"), Charsets.UTF_8); assertDoesNotContainRegex(compiled, "\\bHaveSavedHandle\\("); assertContainsRegex(compiled, "\\b__wurst_HaveSavedHandle\\("); - assertTrue(compiled.contains("Wurst experimental Lua assertion guards")); } @Test - public void hashtableNativeOverrideGuardsAreRuntimeSafe() throws IOException { + public void hashtableNativesAreReplacedByWurstHelpers() throws IOException { test().testLua(true).withStdLib().lines( "package Test", "init", " let h = InitHashtable()", " h.saveInt(1, 2, 7)" ); - String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableNativeOverrideGuardsAreRuntimeSafe.lua"), Charsets.UTF_8); - assertTrue(compiled.contains("Wurst experimental Lua assertion guards")); - assertContainsRegex(compiled, "\\blocal\\s+__wurst_guard_ok\\s*=\\s*pcall\\s*\\(\\s*function\\s*\\("); - assertContainsRegex(compiled, "\\bInitHashtable\\s*=\\s*function\\s*\\("); - assertContainsRegex(compiled, "\\bSaveInteger\\s*=\\s*function\\s*\\("); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableNativesAreReplacedByWurstHelpers.lua"), Charsets.UTF_8); + // Runtime guard overrides are removed; compile-time checks are authoritative. + assertFalse(compiled.contains("Wurst experimental Lua assertion guards")); + assertFalse(compiled.contains("__wurst_guard_ok")); + // __wurst_ helper definitions must still be present. + assertContainsRegex(compiled, "\\bfunction\\s+__wurst_InitHashtable\\s*\\("); + assertContainsRegex(compiled, "\\bfunction\\s+__wurst_SaveInteger\\s*\\("); } @Test