Skip to content

Commit 9eec8a8

Browse files
authored
Handle MPQ maps without HM3W headers (#1258)
1 parent 3effd09 commit 9eec8a8

2 files changed

Lines changed: 135 additions & 7 deletions

File tree

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ProjectConfigBuilder.java

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public static MapRequest.CompilationResult apply(WurstProjectConfigData projectC
107107
w3I.write(result.w3i);
108108

109109
// Apply map header (this is cheap, so we always do it)
110-
applyMapHeader(projectConfig, targetMap);
110+
applyMapHeader(projectConfig, targetMap, w3I.getPlayers().size(), w3I.getMapName(), w3I.getFlags().toInt());
111111

112112
// Update the manifest with new config hash (must open writable to insert)
113113
try (MpqEditor mpq = MpqEditorFactory.getEditor(Optional.of(targetMap), false)) {
@@ -356,20 +356,50 @@ private static void setVolatilePlayerConfig(WurstProjectBuildPlayer wplayer, W3I
356356
}
357357
}
358358

359-
private static void applyMapHeader(WurstProjectConfigData projectConfig, File targetMap) throws IOException {
359+
private static void applyMapHeader(WurstProjectConfigData projectConfig, File targetMap,
360+
int existingPlayerCount, String existingMapName,
361+
int existingMapFlags) throws IOException {
360362
boolean shouldWrite = false;
361-
MapHeader mapHeader = MapHeader.ofFile(targetMap);
362-
if (!projectConfig.buildMapData().players().isEmpty()) {
363-
mapHeader.setMaxPlayersCount(projectConfig.buildMapData().players().size());
363+
WurstProjectBuildMapData buildMapData = projectConfig.buildMapData();
364+
if (buildMapData.players().isEmpty() && StringUtils.isBlank(buildMapData.name())) {
365+
return;
366+
}
367+
368+
// A Warcraft III map may omit the optional 512-byte HM3W prefix and start
369+
// directly with its MPQ archive. MapHeader.ofFile only reads the prefix,
370+
// so use a new header in that case; writeToMapFile will insert it before
371+
// the archive.
372+
boolean hasNoMapHeader = startsWithMpqArchive(targetMap);
373+
MapHeader mapHeader = hasNoMapHeader
374+
? new MapHeader()
375+
: MapHeader.ofFile(targetMap);
376+
if (!buildMapData.players().isEmpty()) {
377+
mapHeader.setMaxPlayersCount(buildMapData.players().size());
364378
shouldWrite = true;
379+
} else if (hasNoMapHeader) {
380+
mapHeader.setMaxPlayersCount(existingPlayerCount);
381+
}
382+
if (hasNoMapHeader && StringUtils.isBlank(buildMapData.name())) {
383+
mapHeader.setMapName(existingMapName);
365384
}
366-
if (StringUtils.isNotBlank(projectConfig.buildMapData().name())) {
367-
mapHeader.setMapName(projectConfig.buildMapData().name());
385+
if (hasNoMapHeader) {
386+
mapHeader.setFlags(existingMapFlags);
387+
}
388+
if (StringUtils.isNotBlank(buildMapData.name())) {
389+
mapHeader.setMapName(buildMapData.name());
368390
shouldWrite = true;
369391
}
370392
if (shouldWrite) {
371393
WLogger.info("Applying map header");
372394
mapHeader.writeToMapFile(targetMap);
373395
}
374396
}
397+
398+
private static boolean startsWithMpqArchive(File targetMap) throws IOException {
399+
try (InputStream input = new FileInputStream(targetMap)) {
400+
byte[] startToken = input.readNBytes(4);
401+
return startToken.length == 4
402+
&& new String(startToken, StandardCharsets.US_ASCII).startsWith("MPQ");
403+
}
404+
}
375405
}

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstBuildConfigTests.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
package tests.wurstscript.tests;
22

33
import org.wurstscript.projectconfig.WurstProjectConfigData;
4+
import org.wurstscript.projectconfig.WurstProjectBuildMapData;
5+
import org.wurstscript.projectconfig.WurstProjectBuildPlayer;
46
import de.peeeq.wurstio.languageserver.WFile;
57
import de.peeeq.wurstio.languageserver.ProjectConfigBuilder;
68
import de.peeeq.wurstio.languageserver.WurstBuildConfig;
79
import de.peeeq.wurstio.languageserver.WurstCommands;
810
import de.peeeq.wurstio.utils.W3InstallationData;
11+
import net.moonlightflower.wc3libs.bin.app.MapHeader;
912
import net.moonlightflower.wc3libs.port.GameVersion;
1013
import org.testng.annotations.Test;
1114

1215
import java.io.File;
1316
import java.lang.reflect.Method;
17+
import java.nio.charset.StandardCharsets;
1418
import java.nio.file.Files;
1519
import java.nio.file.Path;
1620
import java.util.List;
@@ -223,6 +227,100 @@ public void configInjectionPrefersPinnedPatchOverDetectedInstallVersion() throws
223227
);
224228
}
225229

230+
@Test
231+
public void mapHeaderConfigAcceptsAnArchiveWithoutAnHm3wPrefix() throws Exception {
232+
Path mapWithoutHeader = Files.createTempFile("wurst-map-without-header", ".w3x");
233+
byte[] original = new byte[1024];
234+
original[0] = 'M';
235+
original[1] = 'P';
236+
original[2] = 'Q';
237+
original[3] = 0x1a;
238+
Files.write(mapWithoutHeader, original);
239+
240+
Method applyMapHeader = ProjectConfigBuilder.class.getDeclaredMethod(
241+
"applyMapHeader", WurstProjectConfigData.class, File.class, int.class, String.class, int.class
242+
);
243+
applyMapHeader.setAccessible(true);
244+
245+
applyMapHeader.invoke(
246+
null,
247+
new WurstProjectConfigData(
248+
"Test",
249+
List.of(),
250+
new WurstProjectBuildMapData("Configured map", null, null, null, null, List.of(), List.of()),
251+
null,
252+
null
253+
),
254+
mapWithoutHeader.toFile(),
255+
3,
256+
"Existing map",
257+
0x1234
258+
);
259+
260+
assertEquals(Files.readAllBytes(mapWithoutHeader)[0], (byte) 'H');
261+
assertEquals(MapHeader.ofFile(mapWithoutHeader.toFile()).getMaxPlayersCount(), 3);
262+
assertEquals(Files.size(mapWithoutHeader), original.length + 512);
263+
}
264+
265+
@Test
266+
public void mapHeaderConfigDoesNotReadAnArchiveWhenNothingNeedsChanging() throws Exception {
267+
Path mapWithoutHeader = Files.createTempFile("wurst-map-without-header-noop", ".w3x");
268+
byte[] original = new byte[1024];
269+
original[0] = 'M';
270+
original[1] = 'P';
271+
original[2] = 'Q';
272+
original[3] = 0x1a;
273+
Files.write(mapWithoutHeader, original);
274+
275+
Method applyMapHeader = ProjectConfigBuilder.class.getDeclaredMethod(
276+
"applyMapHeader", WurstProjectConfigData.class, File.class, int.class, String.class, int.class
277+
);
278+
applyMapHeader.setAccessible(true);
279+
applyMapHeader.invoke(null, WurstProjectConfigData.empty(), mapWithoutHeader.toFile(), 0, null, 0);
280+
281+
assertEquals(Files.readAllBytes(mapWithoutHeader), original);
282+
}
283+
284+
@Test
285+
public void mapHeaderConfigPreservesExistingMapNameWhenOnlyPlayersChange() throws Exception {
286+
Path mapWithoutHeader = Files.createTempFile("wurst-map-without-header-name", ".w3x");
287+
byte[] original = new byte[1024];
288+
original[0] = 'M';
289+
original[1] = 'P';
290+
original[2] = 'Q';
291+
original[3] = 0x1a;
292+
Files.write(mapWithoutHeader, original);
293+
294+
Method applyMapHeader = ProjectConfigBuilder.class.getDeclaredMethod(
295+
"applyMapHeader", WurstProjectConfigData.class, File.class, int.class, String.class, int.class
296+
);
297+
applyMapHeader.setAccessible(true);
298+
299+
applyMapHeader.invoke(
300+
null,
301+
new WurstProjectConfigData(
302+
"Test",
303+
List.of(),
304+
new WurstProjectBuildMapData(
305+
"", null, null, null, null,
306+
List.of(new WurstProjectBuildPlayer(0, null, null, null, null)),
307+
List.of()
308+
),
309+
null,
310+
null
311+
),
312+
mapWithoutHeader.toFile(),
313+
4,
314+
"Existing map",
315+
0x1234
316+
);
317+
318+
byte[] result = Files.readAllBytes(mapWithoutHeader);
319+
assertEquals(MapHeader.ofFile(mapWithoutHeader.toFile()).getMaxPlayersCount(), 1);
320+
assertEquals(MapHeader.ofFile(mapWithoutHeader.toFile()).getFlags(), 0x1234);
321+
assertTrue(new String(result, StandardCharsets.UTF_8).contains("Existing map"));
322+
}
323+
226324
private static String calculateProjectConfigHash(File buildDir) throws Exception {
227325
Method method = ProjectConfigBuilder.class.getDeclaredMethod(
228326
"calculateProjectConfigHash",

0 commit comments

Comments
 (0)