From 44dd2966a4a9df4b74c063f5448d3793f8fe32d1 Mon Sep 17 00:00:00 2001 From: Frotty Date: Thu, 9 Apr 2026 23:29:43 +0200 Subject: [PATCH 1/2] more compiletime impls --- de.peeeq.wurstscript/build.gradle | 8 ++- .../de/peeeq/wurstio/CompilationProcess.java | 6 +-- .../ReflectionNativeProvider.java | 1 + .../jassinterpreter/mocks/PlayerMock.java | 3 ++ .../providers/AbilityProvider.java | 19 +++++++ .../providers/ConversionProvider.java | 8 +++ .../providers/ForceProvider.java | 3 ++ .../providers/OutputProvider.java | 12 +++++ .../providers/PlayerProvider.java | 10 +++- .../providers/UnitProvider.java | 52 ++++++++++++++++++- .../languageserver/requests/RunTests.java | 42 +++++++++++++-- .../java/de/peeeq/wurstscript/RunArgs.java | 7 +++ .../wurstscript/tests/InterpreterTests.java | 50 ++++++++++++++++++ 13 files changed, 212 insertions(+), 9 deletions(-) create mode 100644 de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/AbilityProvider.java diff --git a/de.peeeq.wurstscript/build.gradle b/de.peeeq.wurstscript/build.gradle index 14ea1217b..d5475a779 100644 --- a/de.peeeq.wurstscript/build.gradle +++ b/de.peeeq.wurstscript/build.gradle @@ -291,11 +291,17 @@ shadowJar { def fatJar = shadowJar.archiveFile.map { it.asFile } tasks.register('make_for_userdir', Copy) { - dependsOn 'shadowJar' + dependsOn 'shadowJar', 'make_for_userdir_wurst_compiler' from fatJar into "${System.properties['user.home']}/.wurst/" } +tasks.register('make_for_userdir_wurst_compiler', Copy) { + dependsOn 'shadowJar' + from fatJar + into "${System.properties['user.home']}/.wurst/wurst-compiler/" +} + tasks.register('make_for_wurstpack', Copy) { dependsOn 'shadowJar' from fatJar diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompilationProcess.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompilationProcess.java index a831d97db..7f135d599 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompilationProcess.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/CompilationProcess.java @@ -82,7 +82,7 @@ public CompilationProcess(WurstGui gui, RunArgs runArgs) { if (runArgs.isRunTests()) { timeTaker.measure("Run tests", - () -> runTests(compiler.getImTranslator(), compiler, runArgs.getTestTimeout())); + () -> runTests(compiler.getImTranslator(), compiler, runArgs.getTestTimeout(), runArgs.getTestFilter())); } timeTaker.measure("Run compiletime functions", () ->compiler.runCompiletime(new WurstProjectConfigData(), isProd, false)); @@ -153,12 +153,12 @@ private File writeMapscript(CharSequence mapScript) { } } - private void runTests(ImTranslator translator, WurstCompilerJassImpl compiler, int testTimeout) { + private void runTests(ImTranslator translator, WurstCompilerJassImpl compiler, int testTimeout, Optional testFilter) { PrintStream out = System.out; // tests gui.sendProgress("Running tests"); System.out.println("Running tests"); - RunTests runTests = new RunTests(Optional.empty(), 0, 0, Optional.empty(), testTimeout) { + RunTests runTests = new RunTests(Optional.empty(), 0, 0, Optional.empty(), testTimeout, testFilter) { @Override protected void print(String message) { out.print(message); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/ReflectionNativeProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/ReflectionNativeProvider.java index 522399533..e25e93af5 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/ReflectionNativeProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/ReflectionNativeProvider.java @@ -16,6 +16,7 @@ public class ReflectionNativeProvider implements NativesProvider { private final HashMap methodMap = new HashMap<>(); public ReflectionNativeProvider(AbstractInterpreter interpreter) { + addProvider(new AbilityProvider(interpreter)); addProvider(new GamecacheProvider(interpreter)); addProvider(new ForceProvider(interpreter)); addProvider(new HandleProvider(interpreter)); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/mocks/PlayerMock.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/mocks/PlayerMock.java index c527443f7..4df239749 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/mocks/PlayerMock.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/mocks/PlayerMock.java @@ -4,9 +4,12 @@ import de.peeeq.wurstscript.intermediatelang.ILconstInt; import de.peeeq.wurstscript.intermediatelang.ILconstNull; +import java.util.HashMap; + public class PlayerMock { public final ILconstInt id; public ILconst playerColor = ILconstNull.instance(); + public final HashMap techMaxAllowed = new HashMap<>(); public PlayerMock(ILconstInt p) { this.id = p; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/AbilityProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/AbilityProvider.java new file mode 100644 index 000000000..d74c00313 --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/AbilityProvider.java @@ -0,0 +1,19 @@ +package de.peeeq.wurstio.jassinterpreter.providers; + +import de.peeeq.wurstscript.intermediatelang.ILconstInt; +import de.peeeq.wurstscript.intermediatelang.ILconstString; +import de.peeeq.wurstscript.intermediatelang.interpreter.AbstractInterpreter; + +public class AbilityProvider extends Provider { + public AbilityProvider(AbstractInterpreter interpreter) { + super(interpreter); + } + + public ILconstString BlzGetAbilityIcon(ILconstInt abilCode) { + return new ILconstString(""); + } + + public ILconstString BlzGetAbilityExtendedTooltip(ILconstInt abilCode, ILconstInt level) { + return new ILconstString(""); + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/ConversionProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/ConversionProvider.java index 57d12f686..d9d892fb4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/ConversionProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/ConversionProvider.java @@ -47,6 +47,14 @@ public IlConstHandle ConvertUnitState(ILconstInt i) { return new IlConstHandle("unitstate" + i, new LinkedHashSet<>()); } + public IlConstHandle ConvertUnitIntegerField(ILconstInt i) { + return new IlConstHandle("unitintegerfield" + i, i.getVal()); + } + + public IlConstHandle ConvertUnitWeaponIntegerField(ILconstInt i) { + return new IlConstHandle("unitweaponintegerfield" + i, i.getVal()); + } + public IlConstHandle ConvertAIDifficulty(ILconstInt i) { return new IlConstHandle("aidifficulty" + i, new LinkedHashSet<>()); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/ForceProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/ForceProvider.java index b12f78850..25fe73917 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/ForceProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/ForceProvider.java @@ -33,6 +33,9 @@ public void ForceClear(IlConstHandle force) { } public ILconstBool IsPlayerInForce(IlConstHandle player, IlConstHandle force) { + if (player == null || force == null) { + return ILconstBool.FALSE; + } LinkedHashSet forceList = (LinkedHashSet) force.getObj(); return ILconstBool.instance(forceList.contains(player)); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/OutputProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/OutputProvider.java index 6f0e98ca6..4bccfaf72 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/OutputProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/OutputProvider.java @@ -18,6 +18,18 @@ public OutputProvider(AbstractInterpreter interpreter) { super(interpreter); } + public void DisplayTextToForce(IlConstHandle force, ILconstString msg) { + outStream.println(msg.getVal()); + } + + public void DisplayTimedTextToForce(IlConstHandle force, ILconstReal duration, ILconstString msg) { + outStream.println(msg.getVal()); + } + + public void DisplayTextToPlayer(IlConstHandle player, ILconstReal x, ILconstReal y, ILconstString msg) { + outStream.println(msg.getVal()); + } + public void DisplayTimedTextToPlayer(IlConstHandle player, ILconstReal x, ILconstReal y, ILconstReal duration, ILconstString msg) { outStream.println(msg.getVal()); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/PlayerProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/PlayerProvider.java index 150b0b213..18cd5dbbd 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/PlayerProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/PlayerProvider.java @@ -35,7 +35,7 @@ public ILconstInt GetPlayerNeutralAggressive() { public IlConstHandle GetLocalPlayer() { - return new IlConstHandle("Local Player", "local player"); + return Player(ILconstInt.create(0)); } public ILconstInt GetBJMaxPlayerSlots() { @@ -53,4 +53,12 @@ public void SetPlayerColor(IlConstHandle player, IlConstHandle playercolor) { public ILconst GetPlayerColor(IlConstHandle player) { return ((PlayerMock) player.getObj()).playerColor; } + + public void SetPlayerTechMaxAllowed(IlConstHandle player, ILconstInt techid, ILconstInt maximum) { + ((PlayerMock) player.getObj()).techMaxAllowed.put(techid.getVal(), maximum); + } + + public ILconstInt GetPlayerTechMaxAllowed(IlConstHandle player, ILconstInt techid) { + return ((PlayerMock) player.getObj()).techMaxAllowed.getOrDefault(techid.getVal(), ILconstInt.create(0)); + } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/UnitProvider.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/UnitProvider.java index 0ad91e1d3..433d0ad94 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/UnitProvider.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/jassinterpreter/providers/UnitProvider.java @@ -1,8 +1,11 @@ package de.peeeq.wurstio.jassinterpreter.providers; +import de.peeeq.wurstio.objectreader.ObjectHelper; import de.peeeq.wurstio.jassinterpreter.mocks.UnitMock; +import de.peeeq.wurstscript.intermediatelang.ILconstBool; import de.peeeq.wurstscript.intermediatelang.ILconstInt; import de.peeeq.wurstscript.intermediatelang.ILconstReal; +import de.peeeq.wurstscript.intermediatelang.ILconstString; import de.peeeq.wurstscript.intermediatelang.IlConstHandle; import de.peeeq.wurstscript.intermediatelang.interpreter.AbstractInterpreter; @@ -23,11 +26,58 @@ public ILconstInt GetUnitTypeId(IlConstHandle unit) { return ((UnitMock)unit.getObj()).unitid; } + public ILconstString GetUnitName(IlConstHandle unit) { + if (unit == null) { + return new ILconstString(""); + } + UnitMock unitMock = (UnitMock) unit.getObj(); + return new ILconstString(ObjectHelper.objectIdIntToString(unitMock.unitid.getVal())); + } + + public ILconstInt GetUnitGoldCost(ILconstInt unitid) { + return ILconstInt.create(0); + } + + public ILconstInt GetUnitWoodCost(ILconstInt unitid) { + return ILconstInt.create(0); + } + + public ILconstInt GetUnitPointValueByType(ILconstInt unitid) { + return ILconstInt.create(0); + } + + public ILconstInt GetFoodUsed(ILconstInt unitid) { + return ILconstInt.create(0); + } + + public ILconstInt GetUnitBuildTime(ILconstInt unitid) { + return ILconstInt.create(0); + } + + public ILconstInt BlzGetUnitIntegerField(IlConstHandle whichUnit, IlConstHandle whichField) { + return ILconstInt.create(0); + } + + public ILconstInt BlzGetUnitWeaponIntegerField(IlConstHandle whichUnit, IlConstHandle whichField, ILconstInt index) { + return ILconstInt.create(0); + } + + public ILconstBool IsUnitType(IlConstHandle whichUnit, IlConstHandle whichUnitType) { + return ILconstBool.FALSE; + } + + public void RemoveUnit(IlConstHandle unit) { + userDataMap.remove(unit); + } + public ILconstInt GetUnitUserData(IlConstHandle unit) { - return userDataMap.get(unit); + return unit == null ? ILconstInt.create(0) : userDataMap.getOrDefault(unit, ILconstInt.create(0)); } public void SetUnitUserData(IlConstHandle unit, ILconstInt userData) { + if (unit == null) { + return; + } userDataMap.put(unit, userData); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunTests.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunTests.java index 63a616648..652e18c97 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunTests.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunTests.java @@ -22,6 +22,7 @@ import de.peeeq.wurstscript.jassIm.ImProg; import de.peeeq.wurstscript.jassinterpreter.TestFailException; import de.peeeq.wurstscript.jassinterpreter.TestSuccessException; +import de.peeeq.wurstscript.parser.WPos; import de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum; import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; import de.peeeq.wurstscript.utils.Utils; @@ -45,6 +46,7 @@ public class RunTests extends UserRequest { private final int column; private final Optional testName; private final int timeoutSeconds; + private final Optional testFilter; private final List successTests = Lists.newArrayList(); private final List failTests = Lists.newArrayList(); @@ -88,15 +90,20 @@ public String getMessageWithStackFrame() { } public RunTests(Optional filename, int line, int column, Optional testName) { - this(filename, line, column, testName, 20); + this(filename, line, column, testName, 20, Optional.empty()); } public RunTests(Optional filename, int line, int column, Optional testName, int timeoutSeconds) { + this(filename, line, column, testName, timeoutSeconds, Optional.empty()); + } + + public RunTests(Optional filename, int line, int column, Optional testName, int timeoutSeconds, Optional testFilter) { this.filename = filename.map(WFile::create); this.line = line; this.column = column; this.testName = testName; this.timeoutSeconds = timeoutSeconds; + this.testFilter = testFilter; } @@ -176,6 +183,27 @@ public TestResult runTests(ImTranslator translator, ImProg imProg, Optional matched = new java.util.ArrayList<>(); + for (ImFunction f : imProg.getFunctions()) { + if (f.hasFlag(FunctionFlagEnum.IS_TEST)) { + String packageName = f.attrTrace().attrNearestPackage().tryGetNameDef().getName(); + String qualifiedName = packageName + "." + f.getName(); + if (qualifiedName.toLowerCase().contains(testFilter.get().toLowerCase())) { + matched.add(qualifiedName); + } + } + } + if (matched.isEmpty()) { + println("No tests match filter '" + testFilter.get() + "'."); + } else { + println("Filter '" + testFilter.get() + "' matched " + matched.size() + " test(s):"); + for (String name : matched) { + println(" " + name); + } + } + } + // Use try-with-resources for automatic cleanup try (ScheduledExecutorService testScheduler = Executors.newSingleThreadScheduledExecutor()) { @@ -189,9 +217,17 @@ public TestResult runTests(ImTranslator translator, ImProg imProg, Optional.."; + WPos source = f.attrTrace().attrSource(); + String file = new File(source.getFile()).toPath().normalize().toString(); + String message = "Running " + file + ":" + source.getLine() + " - " + f.getName() + ".."; println(message); WLogger.info(message); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/RunArgs.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/RunArgs.java index 9566d251f..610c2f47b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/RunArgs.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/RunArgs.java @@ -6,6 +6,7 @@ import java.io.File; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Stream; @@ -22,6 +23,7 @@ public class RunArgs { private @Nullable String workspaceroot = null; private @Nullable String inputmap = null; private @Nullable int testTimeout = 20; + private @Nullable String testFilter = null; private final List options = Lists.newArrayList(); private final List libDirs = Lists.newArrayList(); private final RunOption optionHelp; @@ -88,6 +90,7 @@ public RunArgs(String... args) { // interpreter optionRuntests = addOption("runtests", "Run all test functions found in the scripts."); optionTestTimeout = addOptionWithArg("testTimeout", "Timeout in seconds after which tests will be cancelled and considered failed, if they did not yet succeed.", arg -> testTimeout = Integer.parseInt(arg)); + addOptionWithArg("testFilter", "Only run tests whose qualified name (Package.function) contains this string (case-insensitive).", arg -> testFilter = arg); optionRunCompileTimeFunctions = addOption("runcompiletimefunctions", "Run all compiletime functions found in the scripts."); optionInjectCompiletimeObjects = addOption("injectobjects", "Injects the objects generated by compiletime functions into the map."); // optimization @@ -346,6 +349,10 @@ public int getTestTimeout() { return testTimeout; } + public Optional getTestFilter() { + return Optional.ofNullable(testFilter); + } + public boolean isMeasureTimes() { return optionMeasureTimes.isSet; } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterpreterTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterpreterTests.java index 50c33c604..a5f897bd8 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterpreterTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/InterpreterTests.java @@ -51,4 +51,54 @@ public void arrayDefault() { ); } + @Test + public void displayNativesAcceptNullForceAndLocalPlayer() { + test().withStdLib().executeProg(true).testLua(false).lines( + "package Test", + "init", + " DisplayTextToForce(null, \"force\")", + " DisplayTimedTextToForce(null, 1.0, \"timed force\")", + " DisplayTextToPlayer(GetLocalPlayer(), 0.0, 0.0, \"player\")", + " DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 1.0, \"timed player\")", + " if GetPlayerId(GetLocalPlayer()) == 0", + " testSuccess()" + ); + } + + @Test + public void setPlayerTechMaxAllowed() { + test().withStdLib().executeProg(true).testLua(false).lines( + "package Test", + "init", + " SetPlayerTechMaxAllowed(Player(0), 'hfoo', 3)", + " if GetPlayerTechMaxAllowed(Player(0), 'hfoo') == 3", + " testSuccess()" + ); + } + + @Test + public void unitAndAbilityInfoNatives() { + test().withStdLib().executeProg(true).testLua(false).lines( + "package Test", + "native GetUnitBuildTime(integer unitid) returns integer", + "init", + " let u = CreateUnit(Player(0), 'hfoo', 0.0, 0.0, 0.0)", + " RemoveUnit(u)", + " if GetUnitName(u) == \"hfoo\"", + " if GetUnitUserData(u) == 0", + " if GetUnitUserData(null) == 0", + " if GetUnitGoldCost('hfoo') == 0", + " if GetUnitWoodCost('hfoo') == 0", + " if GetUnitPointValueByType('hfoo') == 0", + " if GetFoodUsed('hfoo') == 0", + " if GetUnitBuildTime('hfoo') == 0", + " if BlzGetAbilityIcon('AHbz') == \"\"", + " if BlzGetAbilityExtendedTooltip('AHbz', 1) == \"\"", + " if BlzGetUnitIntegerField(u, ConvertUnitIntegerField('ubui')) == 0", + " if BlzGetUnitWeaponIntegerField(u, ConvertUnitWeaponIntegerField('ua1b'), 0) == 0", + " if not IsUnitType(u, ConvertUnitType(3))", + " testSuccess()" + ); + } + } From e2a9df2810606b6ff7afd71628168fc470b7a0f7 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 10 Apr 2026 13:40:49 +0200 Subject: [PATCH 2/2] support folder mode --- de.peeeq.wurstscript/build.gradle | 2 +- .../languageserver/requests/MapRequest.java | 72 +++++++++++++++++-- .../languageserver/requests/RunMap.java | 2 +- .../peeeq/wurstio/mpq/Jmpq3BasedEditor.java | 17 +++++ .../peeeq/wurstio/mpq/MpqEditorFactory.java | 4 ++ .../tests/HotReloadPipelineTests.java | 53 ++++++++++++++ 6 files changed, 144 insertions(+), 6 deletions(-) diff --git a/de.peeeq.wurstscript/build.gradle b/de.peeeq.wurstscript/build.gradle index d5475a779..ff03cd096 100644 --- a/de.peeeq.wurstscript/build.gradle +++ b/de.peeeq.wurstscript/build.gradle @@ -107,7 +107,7 @@ dependencies { implementation 'commons-lang:commons-lang:2.6' 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:jmpq3:e28f6999c0' implementation 'com.github.inwc3:wc3libs:6a96a79595' implementation('com.github.wurstscript:wurstsetup:393cf5ea39') { exclude group: 'org.eclipse.jgit', module: 'org.eclipse.jgit' 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 48b6b83a4..8c15100bc 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 @@ -395,7 +395,7 @@ protected boolean canUseCachedMap(File cachedMap) { } // Check if source map is newer - if (map.isPresent() && map.get().lastModified() > cachedMap.lastModified()) { + if (map.isPresent() && getSourceMapLastModified(map.get()) > cachedMap.lastModified()) { WLogger.info("Source map is newer than cache"); return false; } @@ -514,16 +514,69 @@ protected File ensureCachedMap(WurstGui gui) throws IOException { File sourceMap = map.get(); + long sourceLastModified = getSourceMapLastModified(sourceMap); // If cached map doesn't exist or source is newer, update cache - if (!cachedMap.exists() || sourceMap.lastModified() > cachedMap.lastModified()) { + if (!cachedMap.exists() || sourceLastModified > cachedMap.lastModified()) { WLogger.info("Updating cached map from source"); gui.sendProgress("Updating cached map"); - Files.copy(sourceMap, cachedMap); + if (sourceMap.isDirectory()) { + buildArchiveFromFolder(sourceMap, cachedMap); + } else { + Files.copy(sourceMap, cachedMap); + } } return cachedMap; } + protected static long getSourceMapLastModified(File sourceMap) { + if (!sourceMap.isDirectory()) { + return sourceMap.lastModified(); + } + try (java.util.stream.Stream files = java.nio.file.Files.walk(sourceMap.toPath())) { + return files + .map(Path::toFile) + .mapToLong(File::lastModified) + .max() + .orElse(sourceMap.lastModified()); + } catch (IOException e) { + WLogger.warning("Could not inspect folder map timestamp: " + sourceMap + " (" + e.getMessage() + ")"); + return sourceMap.lastModified(); + } + } + + private static void buildArchiveFromFolder(File sourceFolder, File cachedMap) throws IOException { + try { + java.nio.file.Files.deleteIfExists(cachedMap.toPath()); + MpqEditorFactory.createEmptyArchive(cachedMap); + try (MpqEditor mpqEditor = MpqEditorFactory.getEditor(Optional.of(cachedMap))) { + try (java.util.stream.Stream files = java.nio.file.Files.walk(sourceFolder.toPath())) { + List regularFiles = files + .filter(java.nio.file.Files::isRegularFile) + .sorted(Comparator.comparing(path -> sourceFolder.toPath().relativize(path).toString().replace('\\', '/'))) + .collect(Collectors.toList()); + for (Path file : regularFiles) { + String filenameInMpq = sourceFolder.toPath().relativize(file).toString().replace('/', '\\'); + if (isInternalMpqMetadataFile(filenameInMpq)) { + continue; + } + mpqEditor.insertFile(filenameInMpq, file.toFile()); + } + } + } + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException("Could not build MPQ archive from folder map: " + sourceFolder.getAbsolutePath(), e); + } + } + + private static boolean isInternalMpqMetadataFile(String filenameInMpq) { + return filenameInMpq.equals("(listfile)") + || filenameInMpq.equals("(attributes)") + || filenameInMpq.equals("(signature)"); + } + private void cleanupOppositeModeCacheAndOutputs() { if (cachedMapFileName.isEmpty()) { File cacheDir = new File(getBuildDir(), "cache"); @@ -685,7 +738,7 @@ protected File executeBuildMapPipeline(ModelManager modelManager, WurstGui gui, throw new RequestFailedException(MessageType.Error, map.get().getAbsolutePath() + " does not exist."); } - mapLastModified = map.get().lastModified(); + mapLastModified = getSourceMapLastModified(map.get()); mapPath = map.get().getAbsolutePath(); gui.sendProgress("Copying map"); @@ -830,6 +883,17 @@ protected byte[] extractMapScript(Optional mapCopy) throws Exception { if (!mapCopy.isPresent()) { return null; } + if (mapCopy.get().isDirectory()) { + File rootScript = new File(mapCopy.get(), "war3map.j"); + if (rootScript.exists()) { + return java.nio.file.Files.readAllBytes(rootScript.toPath()); + } + File scriptsScript = new File(new File(mapCopy.get(), "scripts"), "war3map.j"); + if (scriptsScript.exists()) { + return java.nio.file.Files.readAllBytes(scriptsScript.toPath()); + } + return null; + } try (@Nullable MpqEditor mpqEditor = MpqEditorFactory.getEditor(mapCopy, true)) { if (mpqEditor.hasFile("war3map.j")) { return mpqEditor.extractFile("war3map.j"); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunMap.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunMap.java index f36cb5e88..21c3bc5fc 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunMap.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunMap.java @@ -99,7 +99,7 @@ private String compileMap(ModelManager modelManager, WurstGui gui, WurstProjectC // first we copy in same location to ensure validity File buildDir = getBuildDir(); if (map.isPresent()) { - mapLastModified = map.get().lastModified(); + mapLastModified = getSourceMapLastModified(map.get()); mapPath = map.get().getAbsolutePath(); } if (!runArgs.isHotReload()) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/mpq/Jmpq3BasedEditor.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/mpq/Jmpq3BasedEditor.java index d3b6c7113..08e3e15f4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/mpq/Jmpq3BasedEditor.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/mpq/Jmpq3BasedEditor.java @@ -8,6 +8,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; class Jmpq3BasedEditor implements MpqEditor { private final JMpqEditor editor; @@ -25,6 +26,22 @@ public Jmpq3BasedEditor(File mpqArchive, boolean readonly) throws Exception { } + static void createEmptyArchive(File mpqArchive) throws IOException { + try { + JMpqEditor.class.getMethod("createEmptyArchive", File.class).invoke(null, mpqArchive); + } catch (NoSuchMethodException e) { + throw new IOException("JMPQ3 is missing createEmptyArchive(File); update the JMPQ3 dependency.", e); + } catch (IllegalAccessException e) { + throw new IOException("Cannot access JMPQ3 createEmptyArchive(File).", e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } + throw new IOException("JMPQ3 could not create an empty MPQ archive.", cause); + } + } + @Override public void insertFile(String filenameInMpq, byte[] contents) { getEditor().deleteFile(filenameInMpq); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/mpq/MpqEditorFactory.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/mpq/MpqEditorFactory.java index 38a7353f1..64b0967ed 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/mpq/MpqEditorFactory.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/mpq/MpqEditorFactory.java @@ -17,4 +17,8 @@ public class MpqEditorFactory { } return new Jmpq3BasedEditor(f.get(), readOnly); } + + static public void createEmptyArchive(File f) throws Exception { + Jmpq3BasedEditor.createEmptyArchive(f); + } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HotReloadPipelineTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HotReloadPipelineTests.java index 90fda54d3..ef16a0a70 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HotReloadPipelineTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HotReloadPipelineTests.java @@ -6,10 +6,14 @@ import de.peeeq.wurstio.languageserver.WFile; import de.peeeq.wurstio.languageserver.WurstLanguageServer; import de.peeeq.wurstio.languageserver.requests.MapRequest; +import de.peeeq.wurstio.mpq.MpqEditor; +import de.peeeq.wurstio.mpq.MpqEditorFactory; import de.peeeq.wurstio.utils.FileUtils; import de.peeeq.wurstscript.gui.WurstGui; import de.peeeq.wurstscript.gui.WurstGuiLogger; +import org.testng.SkipException; import org.testng.annotations.Test; +import systems.crigges.jmpq3.JMpqEditor; import java.io.File; import java.nio.charset.StandardCharsets; @@ -92,6 +96,51 @@ public void cachedMapFileNameIsModeSpecific() throws Exception { assertEquals(jassCache.getName().contains("_jass_cached.w3x"), true); } + @Test + public void folderMapInputIsMaterializedAsCachedArchive() throws Exception { + if (!jmpqCreateEmptyArchiveAvailable()) { + throw new SkipException("Requires JMPQ3 createEmptyArchive(File)."); + } + + File projectFolder = new File("./temp/testProject_folder_map_cache/"); + File wurstFolder = new File(projectFolder, "wurst"); + newCleanFolder(wurstFolder); + + File sourceMap = new File(projectFolder, "folder_map.w3x"); + Files.createDirectories(sourceMap.toPath()); + Files.writeString(new File(sourceMap, "war3map.j").toPath(), "folder script"); + File nestedFile = new File(sourceMap, "war3mapImported\\asset.txt"); + Files.createDirectories(nestedFile.getParentFile().toPath()); + Files.writeString(nestedFile.toPath(), "asset data"); + + WurstLanguageServer langServer = new WurstLanguageServer(); + TestMapRequest request = new TestMapRequest( + langServer, + Optional.of(sourceMap), + List.of(), + WFile.create(projectFolder), + Map.of() + ); + + File cachedMap = request.ensureCachedMapForTest(new WurstGuiLogger()); + + try (MpqEditor mpqEditor = MpqEditorFactory.getEditor(Optional.of(cachedMap), true)) { + assertEquals(mpqEditor.hasFile("war3map.j"), true); + assertEquals(mpqEditor.hasFile("war3mapImported\\asset.txt"), true); + assertEquals(new String(mpqEditor.extractFile("war3map.j"), StandardCharsets.UTF_8), "folder script"); + assertEquals(new String(mpqEditor.extractFile("war3mapImported\\asset.txt"), StandardCharsets.UTF_8), "asset data"); + } + } + + private boolean jmpqCreateEmptyArchiveAvailable() { + try { + JMpqEditor.class.getMethod("createEmptyArchive", File.class); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + @Test public void jhcrPipelineRenamesOutputScript() throws Exception { File projectFolder = new File("./temp/testProject_jhcr_output/"); @@ -230,5 +279,9 @@ private File renameJhcrOutputForTest(File buildDir) throws Exception { private File getCachedMapFileForTest() { return getCachedMapFile(); } + + private File ensureCachedMapForTest(WurstGui gui) throws Exception { + return ensureCachedMap(gui); + } } }