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

wurstscript / WurstScript / 265

29 Sep 2025 09:00AM UTC coverage: 62.244% (+0.02%) from 62.222%
265

Pull #1096

circleci

Frotty
restore determinism, fix tests
Pull Request #1096: Perf improvements

17480 of 28083 relevant lines covered (62.24%)

0.62 hits per line

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

44.44
de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java
1
package de.peeeq.wurstio.intermediateLang.interpreter;
2

3
import com.google.common.collect.Maps;
4
import de.peeeq.wurstio.mpq.MpqEditor;
5
import de.peeeq.wurstio.objectreader.ObjectFileType;
6
import de.peeeq.wurstio.utils.FileUtils;
7
import de.peeeq.wurstscript.WLogger;
8
import de.peeeq.wurstscript.gui.WurstGui;
9
import de.peeeq.wurstscript.intermediatelang.interpreter.ProgramState;
10
import de.peeeq.wurstscript.jassIm.ImProg;
11
import de.peeeq.wurstscript.jassIm.ImStmt;
12
import de.peeeq.wurstscript.utils.Utils;
13
import net.moonlightflower.wc3libs.bin.ObjMod;
14
import net.moonlightflower.wc3libs.bin.Wc3BinInputStream;
15
import net.moonlightflower.wc3libs.bin.Wc3BinOutputStream;
16
import net.moonlightflower.wc3libs.bin.app.objMod.*;
17
import net.moonlightflower.wc3libs.dataTypes.app.War3Int;
18
import net.moonlightflower.wc3libs.dataTypes.app.War3String;
19
import net.moonlightflower.wc3libs.txt.WTS;
20
import org.eclipse.jdt.annotation.Nullable;
21
import org.jetbrains.annotations.NotNull;
22

23
import java.io.*;
24
import java.nio.charset.StandardCharsets;
25
import java.nio.file.Files;
26
import java.nio.file.Path;
27
import java.util.List;
28
import java.util.Map;
29
import java.util.Optional;
30

31
public class ProgramStateIO extends ProgramState {
32

33
    private static final int GENERATED_BY_WURST = 42;
34
    private @Nullable ImStmt lastStatement;
35
    private @Nullable
36
    final MpqEditor mpqEditor;
37
    private final Map<ObjectFileType, ObjMod<? extends ObjMod.Obj>> dataStoreMap = Maps.newLinkedHashMap();
1✔
38
    private int id = 0;
1✔
39
    private final Map<String, ObjMod.Obj> objDefinitions = Maps.newLinkedHashMap();
1✔
40
    private PrintStream outStream = System.err;
1✔
41
    private @Nullable WTS trigStrings = null;
1✔
42
    private final Optional<File> mapFile;
43

44
    public ProgramStateIO(Optional<File> mapFile, @Nullable MpqEditor mpqEditor, WurstGui gui, ImProg prog, boolean isCompiletime) {
45
        super(gui, prog, isCompiletime);
1✔
46
        this.mapFile = mapFile;
1✔
47
        this.mpqEditor = mpqEditor;
1✔
48
    }
1✔
49

50
    @Override
51
    public void setLastStatement(ImStmt s) {
52
        lastStatement = s;
1✔
53
    }
1✔
54

55
    @Override
56
    public @Nullable ImStmt getLastStatement() {
57
        return lastStatement;
×
58
    }
59

60
    @Override
61
    public WurstGui getGui() {
62
        return super.getGui();
×
63
    }
64

65
    private String getTrigString(int id) {
66
        return loadTrigStrings().getEntry(id);
×
67
    }
68

69
    private WTS loadTrigStrings() {
70
        WTS res = trigStrings;
×
71
        if (res != null) {
×
72
            return res;
×
73
        }
74
        try {
75
            byte[] wts = mpqEditor.extractFile("war3map.wts");
×
76
            res = new WTS(new ByteArrayInputStream(wts));
×
77
        } catch (Exception e) {
×
78
            // dummy result
79
            res = new WTS();
×
80
            WLogger.warning("Could not load trigger strings");
×
81
            WLogger.info(e);
×
82
        }
×
83
        trigStrings = res;
×
84
        return res;
×
85
    }
86

87
    ObjMod<? extends ObjMod.Obj> getDataStore(String fileExtension) {
88
        return getDataStore(ObjectFileType.fromExt(fileExtension));
1✔
89
    }
90

91

92
    private ObjMod<? extends ObjMod.Obj> getDataStore(ObjectFileType filetype) throws Error {
93
        ObjMod<? extends ObjMod.Obj> dataStore = dataStoreMap.get(filetype);
1✔
94
        if (dataStore != null) {
1✔
95
            return dataStore;
1✔
96
        }
97
        if (mpqEditor == null) {
1✔
98
            // without a map: create empty object file
99
            dataStore = filetypeToObjmod(filetype);
1✔
100
            dataStore.setFormat(ObjMod.EncodingFormat.OBJ_0x2);
1✔
101
            dataStoreMap.put(filetype, dataStore);
1✔
102
            return dataStore;
1✔
103
        }
104

105
        try {
106
            // extract specific object file:
107
            String fileName = "war3map." + filetype.getExt();
×
108
            String skinFileName = "war3mapSkin." + filetype.getExt();
×
109
            try {
110
                if (mpqEditor.hasFile(fileName)) {
×
111
                    byte[] w3_ = mpqEditor.extractFile(fileName);
×
112
                    Wc3BinInputStream in = new Wc3BinInputStream(new ByteArrayInputStream(w3_));
×
113
                    switch (filetype) {
×
114
                        case UNITS:
115
                            W3U w3u = new W3U(in);
×
116
                            if (mpqEditor.hasFile(skinFileName)) {
×
117
                                byte[] w3s_ = mpqEditor.extractFile(skinFileName);
×
118
                                Wc3BinInputStream inS = new Wc3BinInputStream(new ByteArrayInputStream(w3s_));
×
119
                                W3U skin = new W3U(inS);
×
120
                                w3u.merge(skin);
×
121
                                mpqEditor.deleteFile(skinFileName);
×
122
                            }
123
                            dataStore = w3u;
×
124
                            break;
×
125
                        case ITEMS:
126
                            W3T w3t = new W3T(in);
×
127
                            if (mpqEditor.hasFile(skinFileName)) {
×
128
                                byte[] w3s_ = mpqEditor.extractFile(skinFileName);
×
129
                                Wc3BinInputStream inS = new Wc3BinInputStream(new ByteArrayInputStream(w3s_));
×
130
                                W3T skin = new W3T(inS);
×
131
                                w3t.merge(skin);
×
132
                                mpqEditor.deleteFile(skinFileName);
×
133
                            }
134
                            dataStore = w3t;
×
135
                            break;
×
136
                        case DESTRUCTABLES:
137
                            W3B w3b = new W3B(in);
×
138
                            if (mpqEditor.hasFile(skinFileName)) {
×
139
                                byte[] w3s_ = mpqEditor.extractFile(skinFileName);
×
140
                                Wc3BinInputStream inS = new Wc3BinInputStream(new ByteArrayInputStream(w3s_));
×
141
                                W3B skin = new W3B(inS);
×
142
                                w3b.merge(skin);
×
143
                                mpqEditor.deleteFile(skinFileName);
×
144
                            }
145
                            dataStore = w3b;
×
146
                            break;
×
147
                        case DOODADS:
148
                            W3D w3d = new W3D(in);
×
149
                            if (mpqEditor.hasFile(skinFileName)) {
×
150
                                byte[] w3s_ = mpqEditor.extractFile(skinFileName);
×
151
                                Wc3BinInputStream inS = new Wc3BinInputStream(new ByteArrayInputStream(w3s_));
×
152
                                W3D skin = new W3D(inS);
×
153
                                w3d.merge(skin);
×
154
                                mpqEditor.deleteFile(skinFileName);
×
155
                            }
156
                            dataStore = w3d;
×
157
                            break;
×
158
                        case ABILITIES:
159
                            W3A w3a = new W3A(in);
×
160
                            if (mpqEditor.hasFile(skinFileName)) {
×
161
                                byte[] w3s_ = mpqEditor.extractFile(skinFileName);
×
162
                                Wc3BinInputStream inS = new Wc3BinInputStream(new ByteArrayInputStream(w3s_));
×
163
                                W3A skin = new W3A(inS);
×
164
                                w3a.merge(skin);
×
165
                                mpqEditor.deleteFile(skinFileName);
×
166
                            }
167
                            dataStore = w3a;
×
168
                            break;
×
169
                        case BUFFS:
170
                            W3H w3h = new W3H(in);
×
171
                            if (mpqEditor.hasFile(skinFileName)) {
×
172
                                byte[] w3s_ = mpqEditor.extractFile(skinFileName);
×
173
                                Wc3BinInputStream inS = new Wc3BinInputStream(new ByteArrayInputStream(w3s_));
×
174
                                W3H skin = new W3H(inS);
×
175
                                w3h.merge(skin);
×
176
                                mpqEditor.deleteFile(skinFileName);
×
177
                            }
178
                            dataStore = w3h;
×
179
                            break;
×
180
                        case UPGRADES:
181
                            W3Q w3q = new W3Q(in);
×
182
                            if (mpqEditor.hasFile(skinFileName)) {
×
183
                                byte[] w3s_ = mpqEditor.extractFile(skinFileName);
×
184
                                Wc3BinInputStream inS = new Wc3BinInputStream(new ByteArrayInputStream(w3s_));
×
185
                                W3Q skin = new W3Q(inS);
×
186
                                w3q.merge(skin);
×
187
                                mpqEditor.deleteFile(skinFileName);
×
188
                            }
189
                            dataStore = w3q;
×
190
                            break;
191
                    }
192

193
                    in.close();
×
194
                    replaceTrigStrings(dataStore);
×
195

196
                } else {
×
197
                    dataStore = filetypeToObjmod(filetype);
×
198
                    dataStore.setFormat(ObjMod.EncodingFormat.OBJ_0x2);
×
199
                }
200
            } catch (IOException | InterruptedException e) {
×
201
                // TODO maybe tell the user, that something has gone wrong
202
                WLogger.info("Could not extract file: " + fileName);
×
203
                WLogger.info(e);
×
204
                dataStore = filetypeToObjmod(filetype);
×
205
                dataStore.setFormat(ObjMod.EncodingFormat.OBJ_0x2);
×
206
            }
×
207
            dataStoreMap.put(filetype, dataStore);
×
208

209
            // clean datastore, remove all objects created by wurst
210
            deleteWurstObjects(dataStore);
×
211
        } catch (Exception e) {
×
212
            WLogger.severe(e);
×
213
            throw new Error(e);
×
214
        }
×
215

216
        return dataStore;
×
217
    }
218

219
    @NotNull
220
    private ObjMod<? extends ObjMod.Obj> filetypeToObjmod(ObjectFileType filetype) {
221
        switch (filetype) {
1✔
222
            case UNITS:
223
                return new W3U();
1✔
224
            case ITEMS:
225
                return new W3T();
1✔
226
            case DESTRUCTABLES:
227
                return new W3B();
1✔
228
            case DOODADS:
229
                return new W3D();
1✔
230
            case ABILITIES:
231
                return new W3A();
1✔
232
            case BUFFS:
233
                return new W3H();
1✔
234
            case UPGRADES:
235
                return new W3Q();
1✔
236
        }
237
        return null;
×
238
    }
239

240
    private void replaceTrigStrings(ObjMod<? extends ObjMod.Obj> dataStore) {
241
        replaceTrigStringsInTable(dataStore.getOrigObjs());
×
242
        replaceTrigStringsInTable(dataStore.getCustomObjs());
×
243
    }
×
244

245
    private void replaceTrigStringsInTable(List<? extends ObjMod.Obj> modifiedTable) {
246
        for (ObjMod.Obj od : modifiedTable) {
×
247
            for (ObjMod.Obj.Mod mod : od.getMods()) {
×
248
                if (mod.getValType() == ObjMod.ValType.STRING && mod.getVal() instanceof War3String) {
×
249
                    War3String stringVal = (War3String) mod.getVal();
×
250
                    if (stringVal.getVal().startsWith("TRIGSTR_")) {
×
251
                        try {
252
                            int id = Integer.parseInt(stringVal.getVal().substring("TRIGSTR_".length()), 10);
×
253
                            String newVal = getTrigString(id);
×
254
                            stringVal.set_val(newVal);
×
255
                        } catch (NumberFormatException e) {
×
256
                            // ignore
257
                        }
×
258
                    }
259
                }
260
            }
×
261
        }
×
262
    }
×
263

264
    private void deleteWurstObjects(ObjMod<? extends ObjMod.Obj> dataStore) {
265
        for (ObjMod.Obj next : dataStore.getCustomObjs()) {
×
266
            for (ObjMod.Obj.Mod om : next.getMods()) {
×
267
                if (om.getId().getVal().equals("wurs") && om.getVal() instanceof War3Int) {
×
268
                    War3Int val = (War3Int) om.getVal();
×
269
                    if (val.getVal() == GENERATED_BY_WURST) {
×
270
                        dataStore.removeObj(next.getId());
×
271
                        break;
×
272
                    }
273
                }
274
            }
×
275
        }
×
276
    }
×
277

278

279
    String addObjectDefinition(ObjMod.Obj objDef) {
280
        id++;
1✔
281
        String key = "obj" + id;
1✔
282
        objDefinitions.put(key, objDef);
1✔
283
        return key;
1✔
284
    }
285

286
    ObjMod.Obj getObjectDefinition(String key) {
287
        return objDefinitions.get(key);
1✔
288
    }
289

290
    @Override
291
    public void writeBack(boolean inject) {
292
        gui.sendProgress("Writing back generated objects");
1✔
293

294
        for (ObjectFileType fileType : ObjectFileType.values()) {
1✔
295
            WLogger.info("Writing back " + fileType);
1✔
296
            ObjMod<? extends ObjMod.Obj> dataStore = getDataStore(fileType);
1✔
297
            if (!dataStore.getObjsList().isEmpty()) {
1✔
298
                WLogger.info("Writing back filetype " + fileType);
1✔
299
                writebackObjectFile(dataStore, fileType, inject);
1✔
300
            } else {
301
                WLogger.info("Writing back empty for " + fileType);
1✔
302
            }
303
        }
304
        writeW3oFile();
1✔
305
    }
1✔
306

307
    private void writeW3oFile() {
308
        Optional<File> objFile = getObjectEditingOutputFolder().map(fo -> new File(fo, "wurstCreatedObjects.w3o"));
1✔
309
        try (Wc3BinOutputStream objFileStream = new Wc3BinOutputStream(objFile.get())) {
1✔
310
            objFileStream.writeInt32(1); // version
1✔
311
            for (ObjectFileType fileType : ObjectFileType.values()) {
1✔
312
                ObjMod<? extends ObjMod.Obj> dataStore = getDataStore(fileType);
1✔
313
                if (!dataStore.getObjs().isEmpty()) {
1✔
314
                    objFileStream.writeInt32(1); // exists
1✔
315
                    dataStore.write(objFileStream, ObjMod.EncodingFormat.OBJ_0x2);
1✔
316
                } else {
317
                    objFileStream.writeInt32(0); // does not exist
1✔
318
                }
319
            }
320
        } catch (Error | IOException e) {
×
321
            WLogger.severe(e);
×
322
        }
1✔
323
    }
1✔
324

325
    private void writebackObjectFile(ObjMod<? extends ObjMod.Obj> dataStore, ObjectFileType fileType, boolean inject) throws Error {
326

327
        try {
328
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
1✔
329
            Wc3BinOutputStream out = new Wc3BinOutputStream(baos);
1✔
330
            Optional<File> folder = getObjectEditingOutputFolder();
1✔
331

332
            dataStore.write(out, ObjMod.EncodingFormat.OBJ_0x2);
1✔
333

334
            out.close();
1✔
335
            byte[] w3_ = baos.toByteArray();
1✔
336

337
            exportToWurst(dataStore, fileType, new File(folder.get(), "WurstExportedObjects_" + fileType.getExt() + ".wurst.txt").toPath());
1✔
338

339
            if (inject) {
1✔
340
                if (mpqEditor == null) {
×
341
                    throw new RuntimeException("Map file must be given with '-injectobjects' option.");
×
342
                }
343
                String filenameInMpq = "war3map." + fileType.getExt();
×
344
                mpqEditor.deleteFile(filenameInMpq);
×
345
                mpqEditor.insertFile(filenameInMpq, w3_);
×
346
            }
347
        } catch (Exception e) {
×
348
            WLogger.severe(e);
×
349
            throw new Error(e);
×
350
        }
1✔
351

352
    }
1✔
353

354
    public void exportToWurst(ObjMod<? extends ObjMod.Obj> dataStore,
355
                              ObjectFileType fileType, Path outFile) throws IOException {
356
        try (BufferedWriter out = Files.newBufferedWriter(outFile, StandardCharsets.UTF_8)) {
1✔
357
            out.write("package WurstExportedObjects_" + fileType.getExt() + "\n");
1✔
358
            out.write("import ObjEditingNatives\n\n");
1✔
359

360
            out.write("// Modified Table (contains all custom objects)\n\n");
1✔
361
            exportToWurst(dataStore.getCustomObjs(), fileType, out);
1✔
362

363
            out.write("// Original Table (contains all modified default objects)\n" +
1✔
364
                "// Wurst does not support modifying default objects\n" +
365
                "// but you can copy these functions and replace 'xxxx' with a new, custom id.\n\n");
366
            exportToWurst(dataStore.getOrigObjs(), fileType, out);
1✔
367
        }
368
    }
1✔
369

370

371
    public void exportToWurst(List<? extends ObjMod.Obj> customObjs, ObjectFileType fileType, Appendable out) throws IOException {
372
        for (ObjMod.Obj obj : customObjs) {
1✔
373
            String oldId = obj.getBaseId().getVal();
1✔
374
            String newId = (obj.getNewId() != null ? obj.getNewId().getVal() : "xxxx");
1✔
375
            out.append("@compiletime function create_").append(fileType.getExt()).append("_").append(newId)
1✔
376
                .append("()\n");
1✔
377
            out.append("        let def = createObjectDefinition(\"").append(fileType.getExt()).append("\", '");
1✔
378
            out.append(newId);
1✔
379
            out.append("', '");
1✔
380
            out.append(oldId);
1✔
381
            out.append("')\n");
1✔
382
            for (ObjMod.Obj.Mod m : obj.getMods()) {
1✔
383
                if (fileType.usesLevels() && m instanceof ObjMod.Obj.ExtendedMod) {
1✔
384
                    ObjMod.Obj.ExtendedMod extendedMod = (ObjMod.Obj.ExtendedMod) m;
1✔
385
                    out.append("\t..setLvlData").append(valTypeToFuncPostfix(extendedMod.getValType())).append("(\"");
1✔
386
                    out.append(m.toString());
1✔
387
                    out.append("\", ")
1✔
388
                        .append(String.valueOf(extendedMod.getLevel()))
1✔
389
                        .append(", ")
1✔
390
                        .append(String.valueOf(extendedMod.getDataPt())).append(", ")
1✔
391
                        .append((extendedMod.getValType() == ObjMod.ValType.STRING) ?
1✔
392
                            Utils.escapeString(extendedMod.getVal().toString()) :
1✔
393
                            extendedMod.getVal().toString())
1✔
394
                        .append(")\n");
1✔
395
                } else {
1✔
396
                    out.append("\t..set").append(valTypeToFuncPostfix(m.getValType())).append("(\"");
1✔
397
                    out.append(m.toString());
1✔
398
                    out.append("\", ").append((m.getValType() == ObjMod.ValType.STRING) ?
1✔
399
                        Utils.escapeString(m.getVal().toString()) :
1✔
400
                        m.getVal().toString()).append(")\n");
1✔
401
                }
402
            }
1✔
403
            out.append("\n\n");
1✔
404
        }
1✔
405
    }
1✔
406

407
    private String valTypeToFuncPostfix(ObjMod.ValType valType) {
408
        switch (valType) {
1✔
409
            case INT:
410
                return "Int";
1✔
411
            case REAL:
412
                return "Real";
1✔
413
            case UNREAL:
414
                return "Unreal";
1✔
415
            case STRING:
416
                return "String";
1✔
417
        }
418
        return "Int";
×
419
    }
420

421

422
    private Optional<File> getObjectEditingOutputFolder() {
423
        if (!mapFile.isPresent()) {
1✔
424
            File folder = new File("_build", "objectEditingOutput");
1✔
425
            folder.mkdirs();
1✔
426
            return Optional.of(folder);
1✔
427
        }
428
        Optional<File> folder = mapFile.map(fi -> new File(fi.getParent(), "objectEditingOutput"));
×
429
        if (!folder.get().exists() && !folder.get().mkdirs()) {
×
430
            WLogger.info("Could not create folder " + folder.map(File::getAbsoluteFile));
×
431
            return Optional.empty();
×
432
        }
433
        return folder;
×
434
    }
435

436
    @Override
437
    public PrintStream getOutStream() {
438
        return outStream;
×
439
    }
440

441
    @Override
442
    public void setOutStream(PrintStream os) {
443
        outStream = os;
1✔
444
    }
1✔
445

446

447
}
448

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