From 23fde939244750d75fafb1e2c595e41231cbb33e Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 13 Jan 2026 22:52:23 +0100 Subject: [PATCH] more robust JHCR pipeline --- .../languageserver/ProjectConfigBuilder.java | 13 +- .../languageserver/requests/MapRequest.java | 62 ++++++-- .../tests/HotReloadPipelineTests.java | 137 ++++++++++++++++++ 3 files changed, 193 insertions(+), 19 deletions(-) create mode 100644 de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HotReloadPipelineTests.java 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 cc7e1530e..f271c67cb 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 @@ -36,6 +36,13 @@ public class ProjectConfigBuilder { public static MapRequest.CompilationResult apply(WurstProjectConfigData projectConfig, File targetMap, File mapScript, File buildDir, RunArgs runArgs, W3InstallationData w3data) throws IOException { + return apply(projectConfig, targetMap, mapScript, buildDir, runArgs, w3data, MapRequest.BUILD_CONFIGURED_SCRIPT_NAME); + } + + public static MapRequest.CompilationResult apply(WurstProjectConfigData projectConfig, File targetMap, + File mapScript, File buildDir, + RunArgs runArgs, W3InstallationData w3data, + String outputScriptName) throws IOException { if (projectConfig.getProjectName().isEmpty()) { throw new RequestFailedException(MessageType.Error, "wurst.build is missing projectName."); } @@ -72,7 +79,7 @@ public static MapRequest.CompilationResult apply(WurstProjectConfigData projectC // Only apply buildMapData if config changed or name is present if (configNeedsApplying && StringUtils.isNotBlank(buildMapData.getName())) { WLogger.info("Applying buildMapData config"); - applyBuildMapData(projectConfig, mapScript, buildDir, w3data, w3I, result, configHash); + 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 @@ -170,10 +177,10 @@ private static String calculateProjectConfigHash(WurstProjectConfigData projectC private static void applyBuildMapData(WurstProjectConfigData projectConfig, File mapScript, File buildDir, W3InstallationData w3data, W3I w3I, MapRequest.CompilationResult result, - String configHash) throws IOException { + String configHash, String outputScriptName) throws IOException { // Apply w3i config values prepareW3I(projectConfig, w3I); - result.script = new File(buildDir, "war3mapj_with_config.j.txt"); + result.script = new File(buildDir, outputScriptName); try (FileInputStream inputStream = new FileInputStream(mapScript)) { StringWriter sw = new StringWriter(); 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 87b4fc9d2..9da4b02ae 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 @@ -68,6 +68,11 @@ public abstract class MapRequest extends UserRequest { private static Long lastMapModified = 0L; private static String lastMapPath = ""; + public static final String BUILD_CONFIGURED_SCRIPT_NAME = "01_war3mapj_with_config.j.txt"; + public static final String BUILD_COMPILED_JASS_NAME = "02_compiled.j.txt"; + public static final String BUILD_COMPILED_LUA_NAME = "02_compiled.lua"; + public static final String BUILD_JHCR_SCRIPT_NAME = "03_jhcr_war3map.j"; + /** * makes the compilation slower, but more safe by discarding results from the editor and working on a copy of the model */ @@ -160,7 +165,7 @@ protected File compileMap(File projectFolder, WurstGui gui, Optional mapCo String compiledMapScript = sb.toString(); File buildDir = getBuildDir(); - File outFile = new File(buildDir, "compiled.lua"); + File outFile = new File(buildDir, BUILD_COMPILED_LUA_NAME); Files.write(compiledMapScript.getBytes(Charsets.UTF_8), outFile); return outFile; @@ -178,7 +183,7 @@ protected File compileMap(File projectFolder, WurstGui gui, Optional mapCo JassPrinter printer = new JassPrinter(!runArgs.isOptimize(), jassProg.get()); String compiledMapScript = printer.printProg(); File buildDir = getBuildDir(); - File outFile = new File(buildDir, "compiled.j.txt"); + File outFile = new File(buildDir, BUILD_COMPILED_JASS_NAME); Files.write(compiledMapScript.getBytes(Charsets.UTF_8), outFile); if (!runArgs.isDisablePjass()) { @@ -208,7 +213,7 @@ protected File compileMap(File projectFolder, WurstGui gui, Optional mapCo } } - private File runJassHotCodeReload(File mapScript) throws IOException, InterruptedException { + protected File runJassHotCodeReload(File mapScript) throws IOException, InterruptedException { File buildDir = getBuildDir(); File commonJ = new File(buildDir, "common.j"); File blizzardJ = new File(buildDir, "blizzard.j"); @@ -224,7 +229,7 @@ private File runJassHotCodeReload(File mapScript) throws IOException, Interrupte ProcessBuilder pb = new ProcessBuilder(langServer.getConfigProvider().getJhcrExe(), "init", commonJ.getName(), blizzardJ.getName(), mapScript.getName()); pb.directory(buildDir); Utils.exec(pb, Duration.ofSeconds(30), System.err::println); - return new File(buildDir, "jhcr_war3map.j"); + return renameJhcrOutput(buildDir); } /** @@ -410,7 +415,7 @@ protected CompilationResult compileScript(ModelManager modelManager, WurstGui gu if (runArgs.isHotReload()) { result = new CompilationResult(); - result.script = new File(buildDir, "war3mapj_with_config.j.txt"); + result.script = new File(buildDir, BUILD_CONFIGURED_SCRIPT_NAME); if (!result.script.exists()) { result.script = new File(new File(workspaceRoot.getFile(), "wurst"), "war3map.j"); } @@ -419,9 +424,9 @@ protected CompilationResult compileScript(ModelManager modelManager, WurstGui gu } } else { timeTaker.beginPhase("load map script"); - File scriptFile = loadMapScript(testMap, modelManager, gui); + File scriptFile = loadMapScript(getMapForScriptExtraction(testMap), modelManager, gui); timeTaker.endPhase(); - result = applyProjectConfig(gui, testMap, buildDir, projectConfigData, scriptFile); + result = applyProjectConfig(gui, testMap, buildDir, projectConfigData, scriptFile, BUILD_CONFIGURED_SCRIPT_NAME); } // Compile the script @@ -526,7 +531,7 @@ private static boolean startsWith(byte[] data, byte[] prefix) { return true; } - private File loadMapScript(Optional mapCopy, ModelManager modelManager, WurstGui gui) throws Exception { + protected File loadMapScript(Optional mapCopy, ModelManager modelManager, WurstGui gui) throws Exception { File scriptFile = new File(new File(workspaceRoot.getFile(), "wurst"), "war3map.j"); // If runargs are no extract, either use existing or throw error // Otherwise try loading from map, if map was saved with wurst, try existing script, otherwise error @@ -548,12 +553,7 @@ private File loadMapScript(Optional mapCopy, ModelManager modelManager, Wu lastMapModified = MapRequest.mapLastModified; lastMapPath = MapRequest.mapPath; System.out.println("Map not cached yet, extracting script"); - byte[] extractedScript = null; - try (@Nullable MpqEditor mpqEditor = MpqEditorFactory.getEditor(mapCopy, true)) { - if (mpqEditor.hasFile("war3map.j")) { - extractedScript = mpqEditor.extractFile("war3map.j"); - } - } + byte[] extractedScript = extractMapScript(mapCopy); if (extractedScript == null) { if (scriptFile.exists()) { String msg = "No war3map.j in map file, using old extracted file"; @@ -589,12 +589,13 @@ private File loadMapScript(Optional mapCopy, ModelManager modelManager, Wu return scriptFile; } - private CompilationResult applyProjectConfig(WurstGui gui, Optional testMap, File buildDir, WurstProjectConfigData projectConfig, File scriptFile) { + protected CompilationResult applyProjectConfig(WurstGui gui, Optional testMap, File buildDir, WurstProjectConfigData projectConfig, File scriptFile, + String outputScriptName) { AtomicReference result = new AtomicReference<>(); gui.sendProgress("Applying Map Config..."); timeTaker.measure("Applying Map Config", () -> { try { - result.set(ProjectConfigBuilder.apply(projectConfig, testMap.get(), scriptFile, buildDir, runArgs, w3data)); + result.set(ProjectConfigBuilder.apply(projectConfig, testMap.get(), scriptFile, buildDir, runArgs, w3data, outputScriptName)); } catch (IOException e) { throw new RuntimeException(e); } @@ -602,6 +603,35 @@ private CompilationResult applyProjectConfig(WurstGui gui, Optional testMa return result.get(); } + protected Optional getMapForScriptExtraction(Optional mapCopy) { + if (map.isPresent()) { + return map; + } + return mapCopy; + } + + protected byte[] extractMapScript(Optional mapCopy) throws Exception { + if (!mapCopy.isPresent()) { + return null; + } + try (@Nullable MpqEditor mpqEditor = MpqEditorFactory.getEditor(mapCopy, true)) { + if (mpqEditor.hasFile("war3map.j")) { + return mpqEditor.extractFile("war3map.j"); + } + } + return null; + } + + protected File renameJhcrOutput(File buildDir) throws IOException { + File rawJhcrScript = new File(buildDir, "jhcr_war3map.j"); + if (!rawJhcrScript.exists()) { + throw new IOException("Could not find file " + rawJhcrScript.getAbsolutePath()); + } + File renamedJhcrScript = new File(buildDir, BUILD_JHCR_SCRIPT_NAME); + java.nio.file.Files.move(rawJhcrScript.toPath(), renamedJhcrScript.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + return renamedJhcrScript; + } + private W3InstallationData getBestW3InstallationData() throws RequestFailedException { if (Orient.isLinuxSystem()) { // no Warcraft installation supported on Linux 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 new file mode 100644 index 000000000..25fef5af0 --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HotReloadPipelineTests.java @@ -0,0 +1,137 @@ +package tests.wurstscript.tests; + +import de.peeeq.wurstio.languageserver.BufferManager; +import de.peeeq.wurstio.languageserver.ModelManagerImpl; +import de.peeeq.wurstio.languageserver.WFile; +import de.peeeq.wurstio.languageserver.WurstLanguageServer; +import de.peeeq.wurstio.languageserver.requests.MapRequest; +import de.peeeq.wurstio.utils.FileUtils; +import de.peeeq.wurstscript.gui.WurstGui; +import de.peeeq.wurstscript.gui.WurstGuiLogger; +import org.testng.annotations.Test; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +public class HotReloadPipelineTests { + + @Test + public void hotReloadExtractionUsesSourceMap() throws Exception { + File projectFolder = new File("./temp/testProject_hotrun_extract/"); + File wurstFolder = new File(projectFolder, "wurst"); + newCleanFolder(wurstFolder); + + File sourceMap = new File(projectFolder, "source_map.w3x"); + File cachedMap = new File(projectFolder, "cached_map.w3x"); + Files.write(sourceMap.toPath(), new byte[] {0x01}); + Files.write(cachedMap.toPath(), new byte[] {0x02}); + + WurstLanguageServer langServer = new WurstLanguageServer(); + TestMapRequest request = new TestMapRequest( + langServer, + Optional.of(sourceMap), + List.of(), + WFile.create(projectFolder), + Map.of( + sourceMap, "source script".getBytes(StandardCharsets.UTF_8), + cachedMap, "cached script".getBytes(StandardCharsets.UTF_8) + ) + ); + + MapRequest.mapLastModified = System.currentTimeMillis(); + MapRequest.mapPath = sourceMap.getAbsolutePath(); + + Optional extractionMap = request.getMapForScriptExtractionForTest(Optional.of(cachedMap)); + File scriptFile = request.loadMapScriptForTest(extractionMap, new ModelManagerImpl(projectFolder, new BufferManager()), new WurstGuiLogger()); + + assertEquals(extractionMap.orElse(null), sourceMap); + assertEquals(request.getLastExtractedMap(), sourceMap); + assertEquals(Files.readString(scriptFile.toPath()), "source script"); + } + + @Test + public void jhcrPipelineRenamesOutputScript() throws Exception { + File projectFolder = new File("./temp/testProject_jhcr_output/"); + File wurstFolder = new File(projectFolder, "wurst"); + File buildDir = new File(projectFolder, "_build"); + newCleanFolder(wurstFolder); + buildDir.mkdirs(); + + File sourceMap = new File(projectFolder, "source_map.w3x"); + WurstLanguageServer langServer = new WurstLanguageServer(); + TestMapRequest request = new TestMapRequest( + langServer, + Optional.of(sourceMap), + List.of("-hotstart"), + WFile.create(projectFolder), + new HashMap<>() + ); + + File rawJhcrScript = new File(buildDir, "jhcr_war3map.j"); + Files.writeString(rawJhcrScript.toPath(), "jhcr output"); + + File renamed = request.renameJhcrOutputForTest(buildDir); + + assertEquals(request.isHotStartmapForTest(), true); + assertNotNull(renamed); + assertEquals(renamed.getName(), MapRequest.BUILD_JHCR_SCRIPT_NAME); + } + + private void newCleanFolder(File f) throws Exception { + FileUtils.deleteRecursively(f); + Files.createDirectories(f.toPath()); + } + + private static final class TestMapRequest extends MapRequest { + private final Map scriptByMap; + private File lastExtractedMap; + + private TestMapRequest(WurstLanguageServer langServer, Optional map, List compileArgs, WFile workspaceRoot, + Map scriptByMap) { + super(langServer, map, compileArgs, workspaceRoot, Optional.empty(), Optional.empty()); + this.scriptByMap = scriptByMap; + } + + @Override + public Object execute(de.peeeq.wurstio.languageserver.ModelManager modelManager) { + return null; + } + + @Override + protected byte[] extractMapScript(Optional mapCopy) { + if (mapCopy.isEmpty()) { + return null; + } + lastExtractedMap = mapCopy.get(); + return scriptByMap.get(mapCopy.get()); + } + + private File getLastExtractedMap() { + return lastExtractedMap; + } + + private boolean isHotStartmapForTest() { + return runArgs.isHotStartmap(); + } + + private Optional getMapForScriptExtractionForTest(Optional mapCopy) { + return getMapForScriptExtraction(mapCopy); + } + + private File loadMapScriptForTest(Optional mapCopy, ModelManagerImpl modelManager, WurstGui gui) throws Exception { + return loadMapScript(mapCopy, modelManager, gui); + } + + private File renameJhcrOutputForTest(File buildDir) throws Exception { + return renameJhcrOutput(buildDir); + } + } +}