• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wurstscript / WurstScript / 204

19 Oct 2023 11:41AM CUT coverage: 63.756% (-0.002%) from 63.758%
204

push

circleci

Frotty
fix JHCR war3map.j file handling

17246 of 27050 relevant lines covered (63.76%)

0.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java
1
package de.peeeq.wurstio.languageserver.requests;
2

3
import com.google.common.base.Charsets;
4
import com.google.common.io.Files;
5
import config.WurstProjectConfigData;
6
import de.peeeq.wurstio.Pjass;
7
import de.peeeq.wurstio.TimeTaker;
8
import de.peeeq.wurstio.UtilsIO;
9
import de.peeeq.wurstio.WurstCompilerJassImpl;
10
import de.peeeq.wurstio.languageserver.ConfigProvider;
11
import de.peeeq.wurstio.languageserver.ModelManager;
12
import de.peeeq.wurstio.languageserver.ProjectConfigBuilder;
13
import de.peeeq.wurstio.languageserver.WFile;
14
import de.peeeq.wurstio.mpq.MpqEditor;
15
import de.peeeq.wurstio.mpq.MpqEditorFactory;
16
import de.peeeq.wurstio.utils.W3InstallationData;
17
import de.peeeq.wurstscript.RunArgs;
18
import de.peeeq.wurstscript.WLogger;
19
import de.peeeq.wurstscript.ast.CompilationUnit;
20
import de.peeeq.wurstscript.ast.WImport;
21
import de.peeeq.wurstscript.ast.WPackage;
22
import de.peeeq.wurstscript.ast.WurstModel;
23
import de.peeeq.wurstscript.attributes.CompileError;
24
import de.peeeq.wurstscript.gui.WurstGui;
25
import de.peeeq.wurstscript.jassAst.JassProg;
26
import de.peeeq.wurstscript.jassprinter.JassPrinter;
27
import de.peeeq.wurstscript.luaAst.LuaCompilationUnit;
28
import de.peeeq.wurstscript.parser.WPos;
29
import de.peeeq.wurstscript.utils.LineOffsets;
30
import de.peeeq.wurstscript.utils.Utils;
31
import net.moonlightflower.wc3libs.bin.app.W3I;
32
import net.moonlightflower.wc3libs.port.Orient;
33
import org.eclipse.lsp4j.MessageParams;
34
import org.eclipse.lsp4j.MessageType;
35
import org.eclipse.lsp4j.services.LanguageClient;
36
import org.jetbrains.annotations.Nullable;
37

38
import java.io.File;
39
import java.io.FileNotFoundException;
40
import java.io.IOException;
41
import java.nio.channels.NonWritableChannelException;
42
import java.nio.charset.StandardCharsets;
43
import java.nio.file.Path;
44
import java.nio.file.Paths;
45
import java.time.Duration;
46
import java.util.*;
47
import java.util.concurrent.CompletableFuture;
48
import java.util.concurrent.atomic.AtomicReference;
49
import java.util.stream.Collectors;
50

51
public abstract class MapRequest extends UserRequest<Object> {
52
    protected final ConfigProvider configProvider;
53
    protected final @Nullable
54
    Optional<File> map;
55
    protected final List<String> compileArgs;
56
    protected final WFile workspaceRoot;
57
    protected final RunArgs runArgs;
58
    protected final Optional<String> wc3Path;
59
    protected final W3InstallationData w3data;
60
    protected final TimeTaker timeTaker;
61

62
    /**
63
     * makes the compilation slower, but more safe by discarding results from the editor and working on a copy of the model
64
     */
65
    protected SafetyLevel safeCompilation = SafetyLevel.KindOfSafe;
×
66

67
    public TimeTaker getTimeTaker() {
68
        return timeTaker;
×
69
    }
70

71

72
    enum SafetyLevel {
×
73
        QuickAndDirty, KindOfSafe;
×
74
    }
75

76
    public static class CompilationResult {
×
77
        public File script;
78
        public File w3i;
79
    }
80

81
    public MapRequest(ConfigProvider configProvider, Optional<File> map, List<String> compileArgs, WFile workspaceRoot,
82
            Optional<String> wc3Path) {
×
83
        this.configProvider = configProvider;
×
84
        this.map = map;
×
85
        this.compileArgs = compileArgs;
×
86
        this.workspaceRoot = workspaceRoot;
×
87
        this.runArgs = new RunArgs(compileArgs);
×
88
        this.wc3Path = wc3Path;
×
89
        this.w3data = getBestW3InstallationData();
×
90
        if (runArgs.isMeasureTimes()) {
×
91
            this.timeTaker = new TimeTaker.Recording();
×
92
        } else {
93
            this.timeTaker = new TimeTaker.Default();
×
94
        }
95
    }
×
96

97
    @Override
98
    public void handleException(LanguageClient languageClient, Throwable err, CompletableFuture<Object> resFut) {
99
        if (err instanceof RequestFailedException) {
×
100
            RequestFailedException rfe = (RequestFailedException) err;
×
101
            languageClient.showMessage(new MessageParams(rfe.getMessageType(), rfe.getMessage()));
×
102
            resFut.complete(new Object());
×
103
        } else {
×
104
            super.handleException(languageClient, err, resFut);
×
105
        }
106
    }
×
107

108
    protected File compileMap(File projectFolder, WurstGui gui, Optional<File> mapCopy, RunArgs runArgs, WurstModel model,
109
                              WurstProjectConfigData projectConfigData, boolean isProd) {
110
        try (@Nullable MpqEditor mpqEditor = MpqEditorFactory.getEditor(mapCopy)) {
×
111
            if (mpqEditor != null && !mpqEditor.canWrite()) {
×
112
                WLogger.severe("The supplied map is invalid/corrupted/protected and Wurst cannot write to it.\n" +
×
113
                    "Please supply a valid .w3x input map that can be opened in the world editor.");
114
                throw new NonWritableChannelException();
×
115
            }
116
            WurstCompilerJassImpl compiler = new WurstCompilerJassImpl(timeTaker, projectFolder, gui, mpqEditor, runArgs);
×
117
            compiler.setMapFile(mapCopy);
×
118
            purgeUnimportedFiles(model);
×
119

120
            gui.sendProgress("Check program");
×
121
            compiler.checkProg(model);
×
122

123
            if (gui.getErrorCount() > 0) {
×
124
                throw new RequestFailedException(MessageType.Warning, "Could not compile project: ", gui.getErrorList().get(0));
×
125
            }
126

127
            print("translating program ... ");
×
128
            compiler.translateProgToIm(model);
×
129

130
            if (gui.getErrorCount() > 0) {
×
131
                throw new RequestFailedException(MessageType.Error, "Could not compile project (error in translation): " + gui.getErrorList().get(0));
×
132
            }
133

134
            timeTaker.measure("Runinng Compiletime Functions", () -> compiler.runCompiletime(projectConfigData, isProd, runArgs.isCompiletimeCache()));
×
135

136
            if (runArgs.isLua()) {
×
137
                print("translating program to Lua ... ");
×
138
                Optional<LuaCompilationUnit> luaCode = Optional.ofNullable(compiler.transformProgToLua());
×
139

140
                if (!luaCode.isPresent()) {
×
141
                    print("Could not compile project\n");
×
142
                    throw new RuntimeException("Could not compile project (error in LUA translation)");
×
143
                }
144

145
                StringBuilder sb = new StringBuilder();
×
146
                luaCode.get().print(sb, 0);
×
147

148
                String compiledMapScript = sb.toString();
×
149
                File buildDir = getBuildDir();
×
150
                File outFile = new File(buildDir, "compiled.lua");
×
151
                Files.write(compiledMapScript.getBytes(Charsets.UTF_8), outFile);
×
152
                return outFile;
×
153

154
            } else {
155
                print("translating program to jass ... ");
×
156
                compiler.transformProgToJass();
×
157

158
                Optional<JassProg> jassProg = Optional.ofNullable(compiler.getProg());
×
159
                if (!jassProg.isPresent()) {
×
160
                    print("Could not compile project\n");
×
161
                    throw new RuntimeException("Could not compile project (error in JASS translation)");
×
162
                }
163

164
                gui.sendProgress("Printing program");
×
165
                JassPrinter printer = new JassPrinter(!runArgs.isOptimize(), jassProg.get());
×
166
                String compiledMapScript = printer.printProg();
×
167
                File buildDir = getBuildDir();
×
168
                File outFile = new File(buildDir, "compiled.j.txt");
×
169
                Files.write(compiledMapScript.getBytes(Charsets.UTF_8), outFile);
×
170

171
                if (!runArgs.isDisablePjass()) {
×
172
                    gui.sendProgress("Running PJass");
×
173
                    Pjass.Result pJassResult = Pjass.runPjass(outFile,
×
174
                        new File(buildDir, "common.j").getAbsolutePath(),
×
175
                        new File(buildDir, "blizzard.j").getAbsolutePath());
×
176
                    WLogger.info(pJassResult.getMessage());
×
177
                    if (!pJassResult.isOk()) {
×
178
                        for (CompileError err : pJassResult.getErrors()) {
×
179
                            gui.sendError(err);
×
180
                        }
×
181
                        throw new RuntimeException("Could not compile project (PJass error)");
×
182
                    }
183
                }
184

185
                if (runArgs.isHotStartmap()) {
×
186
                    gui.sendProgress("Running JHCR");
×
187
                    return runJassHotCodeReload(outFile);
×
188
                }
189
                return outFile;
×
190
            }
191
        } catch (Exception e) {
×
192
            throw new RuntimeException(e);
×
193
        }
194
    }
195

196
    private File runJassHotCodeReload(File mapScript) throws IOException, InterruptedException {
197
        File buildDir = getBuildDir();
×
198
        File commonJ = new File(buildDir, "common.j");
×
199
        File blizzardJ = new File(buildDir, "blizzard.j");
×
200

201
        if (!commonJ.exists()) {
×
202
            throw new IOException("Could not find file " + commonJ.getAbsolutePath());
×
203
        }
204

205
        if (!blizzardJ.exists()) {
×
206
            throw new IOException("Could not find file " + blizzardJ.getAbsolutePath());
×
207
        }
208

209
        ProcessBuilder pb = new ProcessBuilder(configProvider.getJhcrExe(), "init", commonJ.getName(), blizzardJ.getName(), mapScript.getName());
×
210
        pb.directory(buildDir);
×
211
        Utils.exec(pb, Duration.ofSeconds(30), System.err::println);
×
212
        return new File(buildDir, "jhcr_war3map.j");
×
213
    }
214

215
    /**
216
     * removes everything compilation unit which is neither
217
     * - inside a wurst folder
218
     * - a jass file
219
     * - imported by a file in a wurst folder
220
     */
221
    private void purgeUnimportedFiles(WurstModel model) {
222

223
        Set<CompilationUnit> imported = model.stream()
×
224
            .filter(cu -> isInWurstFolder(cu.getCuInfo().getFile()) || cu.getCuInfo().getFile().endsWith(".j")).collect(Collectors.toSet());
×
225
        addImports(imported, imported);
×
226

227
        model.removeIf(cu -> !imported.contains(cu));
×
228
    }
×
229

230
    private boolean isInWurstFolder(String file) {
231
        Path p = Paths.get(file);
×
232
        Path w;
233
        try {
234
            w = workspaceRoot.getPath();
×
235
        } catch (FileNotFoundException e) {
×
236
            return false;
×
237
        }
×
238
        return p.startsWith(w)
×
239
            && java.nio.file.Files.exists(p)
×
240
            && Utils.isWurstFile(file);
×
241
    }
242

243
    protected File getBuildDir() {
244
        File buildDir;
245
        try {
246
            buildDir = new File(workspaceRoot.getFile(), "_build");
×
247
        } catch (FileNotFoundException e) {
×
248
            throw new RuntimeException("Cannot get build dir", e);
×
249
        }
×
250
        if (!buildDir.exists()) {
×
251
            UtilsIO.mkdirs(buildDir);
×
252
        }
253
        return buildDir;
×
254
    }
255

256
    private void addImports(Set<CompilationUnit> result, Set<CompilationUnit> toAdd) {
257
        Set<CompilationUnit> imported =
×
258
            toAdd.stream()
×
259
                .flatMap((CompilationUnit cu) -> cu.getPackages().stream())
×
260
                .flatMap((WPackage p) -> p.getImports().stream())
×
261
                .map(WImport::attrImportedPackage)
×
262
                .filter(Objects::nonNull)
×
263
                .map(WPackage::attrCompilationUnit)
×
264
                .collect(Collectors.toSet());
×
265
        boolean changed = result.addAll(imported);
×
266
        if (changed) {
×
267
            // recursive call terminates, as there are only finitely many compilation units
268
            addImports(result, imported);
×
269
        }
270
    }
×
271

272
    protected void print(String s) {
273
        WLogger.info(s);
×
274
    }
×
275

276
    protected void println(String s) {
277
        WLogger.info(s);
×
278
    }
×
279

280

281
    protected File compileScript(WurstGui gui, ModelManager modelManager, List<String> compileArgs, Optional<File> mapCopy,
282
                                 WurstProjectConfigData projectConfigData, boolean isProd, File scriptFile) throws Exception {
283
        RunArgs runArgs = new RunArgs(compileArgs);
×
284
        gui.sendProgress("Compiling Script");
×
285
        print("Compile Script : ");
×
286
        for (File dep : modelManager.getDependencyWurstFiles()) {
×
287
            WLogger.info("dep: " + dep.getPath());
×
288
        }
×
289
        print("Dependencies done.");
×
290
        modelManager.syncCompilationUnit(WFile.create(scriptFile));
×
291
        print("Processed mapscript");
×
292
        if (safeCompilation != RunMap.SafetyLevel.QuickAndDirty) {
×
293
            // it is safer to rebuild the project, instead of taking the current editor state
294
            gui.sendProgress("Cleaning project");
×
295
            modelManager.clean();
×
296
            gui.sendProgress("Building project");
×
297
            modelManager.buildProject();
×
298
        }
299

300
        if (modelManager.hasErrors()) {
×
301
            for (CompileError compileError : modelManager.getParseErrors()) {
×
302
                gui.sendError(compileError);
×
303
            }
×
304
            throw new RequestFailedException(MessageType.Warning, "Cannot run code with syntax errors.");
×
305
        }
306

307
        WurstModel model = modelManager.getModel();
×
308
        if (safeCompilation != RunMap.SafetyLevel.QuickAndDirty) {
×
309
            // compilation will alter the model (e.g. remove unused imports),
310
            // so it is safer to create a copy
311
            model = ModelManager.copy(model);
×
312
        }
313

314
        return compileMap(modelManager.getProjectPath(), gui, mapCopy, runArgs, model, projectConfigData, isProd);
×
315
    }
316

317
    protected CompilationResult compileScript(ModelManager modelManager, WurstGui gui, Optional<File> testMap, WurstProjectConfigData projectConfigData, File buildDir, boolean isProd) throws Exception {
318
        if (testMap.isPresent() && testMap.get().exists()) {
×
319
            boolean deleteOk = testMap.get().delete();
×
320
            if (!deleteOk) {
×
321
                throw new RequestFailedException(MessageType.Error, "Could not delete old mapfile: " + testMap);
×
322
            }
323
        }
324
        if (map.isPresent() && testMap.isPresent()) {
×
325
            Files.copy(map.get(), testMap.get());
×
326
        }
327

328
        CompilationResult result;
329

330
        if (runArgs.isHotReload()) {
×
331
            // For hot reload use cached war3map if it exists
332
            result = new CompilationResult();
×
333
            result.script = new File(buildDir, "war3mapj_with_config.j.txt");
×
334
            if (!result.script.exists()) {
×
335
                result.script = new File(new File(workspaceRoot.getFile(), "wurst"), "war3map.j");
×
336
            }
337
            if (!result.script.exists()) {
×
338
                throw new RequestFailedException(MessageType.Error, "Could not find war3map.j file");
×
339
            }
340
        } else {
341
            File scriptFile = loadMapScript(testMap, modelManager, gui);
×
342
            result = applyProjectConfig(gui, testMap, buildDir, projectConfigData, scriptFile);
×
343
        }
344

345

346

347
        // first compile the script:
348
        result.script = compileScript(gui, modelManager, compileArgs, testMap, projectConfigData, isProd, result.script);
×
349

350
        Optional<WurstModel> model = Optional.ofNullable(modelManager.getModel());
×
351
        if (!model.isPresent()
×
352
                || model
353
                    .get()
×
354
                    .stream()
×
355
                    .noneMatch((CompilationUnit cu) -> cu.getCuInfo().getFile().endsWith("war3map.j"))) {
×
356
            println("No 'war3map.j' file could be found inside the map nor inside the wurst folder");
×
357
            println("If you compile the map with WurstPack once, this file should be in your wurst-folder. ");
×
358
            println("We will try to start the map now, but it will probably fail. ");
×
359
        }
360
        return result;
×
361
    }
362

363
    private File loadMapScript(Optional<File> mapCopy, ModelManager modelManager, WurstGui gui) throws Exception {
364
        File scriptFile = new File(new File(workspaceRoot.getFile(), "wurst"), "war3map.j");
×
365
        // If runargs are no extract, either use existing or throw error
366
        // Otherwise try loading from map, if map was saved with wurst, try existing script, otherwise error
367
        if (!mapCopy.isPresent() || runArgs.isNoExtractMapScript()) {
×
368
            if (scriptFile.exists()) {
×
369
                modelManager.syncCompilationUnit(WFile.create(scriptFile));
×
370
                return scriptFile;
×
371
            } else {
372
                throw new CompileError(new WPos("", new LineOffsets(), 0, 0),
×
373
                    "RunArg noExtractMapScript is set but no mapscript is provided inside the wurst folder");
374
            }
375
        }
376
        WLogger.info("extracting mapscript");
×
377
        byte[] extractedScript = null;
×
378
        try (@Nullable MpqEditor mpqEditor = MpqEditorFactory.getEditor(mapCopy, true)) {
×
379
            if (mpqEditor.hasFile("war3map.j")) {
×
380
                extractedScript = mpqEditor.extractFile("war3map.j");
×
381
            }
382
        }
383
        if (extractedScript == null) {
×
384
            if (scriptFile.exists()) {
×
385
                String msg = "No war3map.j in map file, using old extracted file";
×
386
                WLogger.info(msg);
×
387
            } else {
×
388
                CompileError err = new CompileError(new WPos(mapCopy.toString(), new LineOffsets(), 0, 0),
×
389
                    "No war3map.j found in map file.");
390
                gui.showInfoMessage(err.getMessage());
×
391
                WLogger.severe(err);
×
392
            }
×
393
        } else if (new String(extractedScript, StandardCharsets.UTF_8).startsWith(JassPrinter.WURST_COMMENT_RAW)) {
×
394
            WLogger.info("map has already been compiled with wurst");
×
395
            // file generated by wurst, do not use
396
            if (scriptFile.exists()) {
×
397
                String msg = "Cannot use war3map.j from map file, because it already was compiled with wurst. " + "Using war3map.j from Wurst directory instead.";
×
398
                WLogger.info(msg);
×
399
            } else {
×
400
                CompileError err = new CompileError(new WPos(mapCopy.toString(), new LineOffsets(), 0, 0),
×
401
                    "Cannot use war3map.j from map file, because it already was compiled with wurst. " + "Please add war3map.j to the wurst directory.");
402
                gui.showInfoMessage(err.getMessage());
×
403
                WLogger.severe(err);
×
404
            }
×
405
        } else {
406
            WLogger.info("new map, use extracted");
×
407
            // write mapfile from map to workspace
408
            Files.write(extractedScript, scriptFile);
×
409
        }
410

411
        return scriptFile;
×
412
    }
413

414
    private CompilationResult applyProjectConfig(WurstGui gui, Optional<File> testMap, File buildDir, WurstProjectConfigData projectConfig, File scriptFile) throws FileNotFoundException {
415
        AtomicReference<CompilationResult> result = new AtomicReference<>();
×
416
        gui.sendProgress("Applying Map Config...");
×
417
        timeTaker.measure("Applying Map Config", () -> {
×
418
            try {
419
                result.set(ProjectConfigBuilder.apply(projectConfig, testMap.get(), scriptFile, buildDir, runArgs, w3data));
×
420
            } catch (IOException e) {
×
421
                throw new RuntimeException(e);
×
422
            }
×
423
        });
×
424
        return result.get();
×
425
    }
426

427
    private W3InstallationData getBestW3InstallationData() throws RequestFailedException {
428
        if (Orient.isLinuxSystem()) {
×
429
            // no Warcraft installation supported on Linux
430
            return new W3InstallationData(Optional.empty(), Optional.empty());
×
431
        }
432
        if (wc3Path.isPresent()) {
×
433
            W3InstallationData w3data = new W3InstallationData(new File(wc3Path.get()));
×
434
            if (!w3data.getWc3PatchVersion().isPresent()) {
×
435
                throw new RequestFailedException(MessageType.Error, "Could not find Warcraft III installation at specified path: " + wc3Path);
×
436
            }
437

438
            return w3data;
×
439
        } else {
440
            return new W3InstallationData();
×
441
        }
442
    }
443

444
    protected void injectMapData(WurstGui gui, Optional<File> testMap, CompilationResult result) throws Exception {
445
        gui.sendProgress("Injecting map data");
×
446
        try (MpqEditor mpqEditor = MpqEditorFactory.getEditor(testMap)) {
×
447
            String mapScriptName;
448
            if (runArgs.isLua()) {
×
449
                mapScriptName = "war3map.lua";
×
450
            } else {
451
                mapScriptName = "war3map.j";
×
452
            }
453
            // delete both original mapscripts, just to be sure:
454
            mpqEditor.deleteFile("war3map.j");
×
455
            mpqEditor.deleteFile("war3map.lua");
×
456
            if (result.w3i != null) {
×
457
                mpqEditor.deleteFile(W3I.GAME_PATH.getName());
×
458
                mpqEditor.insertFile(W3I.GAME_PATH.getName(), result.w3i);
×
459
            }
460
            mpqEditor.insertFile(mapScriptName, result.script);
×
461
        }
462
    }
×
463
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc