Skip to content

Commit 2226644

Browse files
committed
game version vs pin version
1 parent 19e59a5 commit 2226644

6 files changed

Lines changed: 270 additions & 13 deletions

File tree

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,21 +200,29 @@ private static void applyBuildMapData(WurstProjectConfigData projectConfig, File
200200
try (FileInputStream inputStream = new FileInputStream(mapScript)) {
201201
StringWriter sw = new StringWriter();
202202

203-
if (w3data.getWc3PatchVersion().isPresent()) {
204-
w3I.injectConfigsInJassScript(inputStream, sw, w3data.getWc3PatchVersion().get());
203+
WurstBuildConfig buildConfig = buildConfigFromBuildDir(buildDir);
204+
GameVersion version = effectiveConfigInjectionVersion(buildDir, w3data);
205+
if (buildConfig.configuredGameVersion().isPresent()) {
206+
WLogger.info("Using wurst.build patch target for map config injection: " + version);
207+
} else if (w3data.getWc3PatchVersion().isPresent()) {
208+
WLogger.info("Using detected game version for map config injection: " + version);
205209
} else {
206-
GameVersion version = buildConfigFromBuildDir(buildDir).fallbackGameVersion();
207-
WLogger.info(
208-
"Failed to determine installed game version. Falling back to wurst.build patch target: " + version
209-
);
210-
w3I.injectConfigsInJassScript(inputStream, sw, version);
210+
WLogger.info("Failed to determine installed game version. Falling back to default patch target: " + version);
211211
}
212+
w3I.injectConfigsInJassScript(inputStream, sw, version);
212213

213214
byte[] scriptBytes = sw.toString().getBytes(StandardCharsets.UTF_8);
214215
Files.write(scriptBytes, result.script);
215216
}
216217
}
217218

219+
private static GameVersion effectiveConfigInjectionVersion(File buildDir, W3InstallationData w3data) {
220+
WurstBuildConfig buildConfig = buildConfigFromBuildDir(buildDir);
221+
return buildConfig.configuredGameVersion()
222+
.or(() -> w3data.getWc3PatchVersion())
223+
.orElseGet(buildConfig::fallbackGameVersion);
224+
}
225+
218226
private static WurstBuildConfig buildConfigFromBuildDir(File buildDir) {
219227
java.nio.file.Path projectRoot = buildDir.toPath().getParent();
220228
if (projectRoot == null) {

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,10 @@ public MapRequest(WurstLanguageServer langServer, Optional<File> map, List<Strin
108108
this.wc3Path = wc3Path;
109109
this.buildConfig = WurstBuildConfig.fromWorkspaceRoot(workspaceRoot);
110110
if (gameExePath.isPresent() && StringUtils.isNotBlank(gameExePath.get())) {
111-
this.w3data = new W3InstallationData(Optional.of(new File(gameExePath.get())), buildConfig.configuredGameVersion());
111+
Optional<GameVersion> configuredVersion = this instanceof RunMap
112+
? Optional.empty()
113+
: buildConfig.configuredGameVersion();
114+
this.w3data = new W3InstallationData(Optional.of(new File(gameExePath.get())), configuredVersion);
112115
} else {
113116
this.w3data = getBestW3InstallationData();
114117
}
@@ -945,17 +948,18 @@ private W3InstallationData getBestW3InstallationData() throws RequestFailedExcep
945948
if (!needsGameExe && configuredVersion.isPresent()) {
946949
return new W3InstallationData(Optional.empty(), configuredVersion);
947950
}
951+
Optional<GameVersion> versionForDiscovery = needsGameExe ? Optional.empty() : configuredVersion;
948952
if (wc3Path.isPresent() && StringUtils.isNotBlank(wc3Path.get())) {
949953
W3InstallationData w3data = new W3InstallationData(langServer, new File(wc3Path.get()),
950-
needsGameExe, configuredVersion);
954+
needsGameExe, versionForDiscovery);
951955
if (w3data.getWc3PatchVersion().isEmpty() && !configuredVersion.isPresent()) {
952956
WLogger.warning("Could not determine Warcraft III version at specified path: " + wc3Path
953957
+ ". Falling back to default launch behavior.");
954958
}
955959

956960
return w3data;
957961
} else {
958-
return new W3InstallationData(langServer, needsGameExe, configuredVersion);
962+
return new W3InstallationData(langServer, needsGameExe, versionForDiscovery);
959963
}
960964
}
961965

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/RunMap.java

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import de.peeeq.wurstio.gui.WurstGuiImpl;
88
import de.peeeq.wurstio.languageserver.ModelManager;
99
import de.peeeq.wurstio.languageserver.WFile;
10+
import de.peeeq.wurstio.languageserver.WurstBuildConfig;
1011
import de.peeeq.wurstio.languageserver.WurstLanguageServer;
12+
import de.peeeq.wurstio.utils.W3InstallationData;
1113
import de.peeeq.wurstscript.WLogger;
1214
import de.peeeq.wurstscript.attributes.CompileError;
1315
import de.peeeq.wurstscript.gui.WurstGui;
@@ -22,6 +24,7 @@
2224

2325
import javax.swing.*;
2426
import javax.swing.filechooser.FileSystemView;
27+
import java.awt.GraphicsEnvironment;
2528
import java.io.File;
2629
import java.io.FileNotFoundException;
2730
import java.io.IOException;
@@ -136,7 +139,11 @@ private void startGame(WurstGui gui, CompilationResult result) throws Exception
136139
gui.sendProgress("Starting Warcraft 3...");
137140

138141
File mapCopy = cachedMapFile.get();
139-
Optional<GameVersion> detectedGameVersion = w3data.getWc3PatchVersion();
142+
W3InstallationData launchData = resolveLaunchData();
143+
if (launchData == null) {
144+
throw new RequestFailedException(MessageType.Info, "Run canceled.");
145+
}
146+
Optional<GameVersion> detectedGameVersion = launchData.getWc3PatchVersion();
140147
if (buildConfig.shouldCopyRunMapToWarcraftMapDir(detectedGameVersion)) {
141148
mapCopy = copyToWarcraftMapDir(cachedMapFile.get());
142149
}
@@ -153,7 +160,7 @@ private void startGame(WurstGui gui, CompilationResult result) throws Exception
153160

154161
if (!path.isEmpty()) {
155162
// now start the map
156-
File gameExe = w3data.getGameExe()
163+
File gameExe = launchData.getGameExe()
157164
.orElseThrow(() -> new RequestFailedException(MessageType.Error, wc3Path + " does not exist."));
158165
List<String> cmd = Lists.newArrayList(gameExe.getAbsolutePath());
159166
Optional<String> wc3RunArgs = langServer.getConfigProvider().getWc3RunArgs();
@@ -186,6 +193,104 @@ private void startGame(WurstGui gui, CompilationResult result) throws Exception
186193
}
187194
}
188195

196+
private W3InstallationData resolveLaunchData() {
197+
W3InstallationData launchData = w3data;
198+
while (shouldWarnClientPatchMismatch(launchData)) {
199+
String projectTarget = buildConfig.wc3PatchName().orElse("configured patch");
200+
String clientTarget = launchData.getWc3PatchVersion()
201+
.map(RunMap::describeClientVersion)
202+
.orElse("unknown Warcraft III version");
203+
String message = "This project targets " + projectTarget + ", but the selected Warcraft III client looks like "
204+
+ clientTarget + ". The map may not start correctly.";
205+
WLogger.warning(message);
206+
207+
MismatchChoice choice = chooseMismatchAction(message);
208+
if (choice == MismatchChoice.CANCEL) {
209+
return null;
210+
}
211+
if (choice == MismatchChoice.CONTINUE) {
212+
return launchData;
213+
}
214+
Optional<W3InstallationData> selected = chooseAlternateGamePath();
215+
if (selected.isEmpty()) {
216+
return null;
217+
}
218+
launchData = selected.get();
219+
}
220+
if (buildConfig.wc3Patch().isPresent() && launchData.getWc3PatchVersion().isEmpty()) {
221+
WLogger.warning("Could not determine Warcraft III client version. If the map does not start, select a matching Warcraft III installation.");
222+
}
223+
return launchData;
224+
}
225+
226+
private boolean shouldWarnClientPatchMismatch(W3InstallationData launchData) {
227+
Optional<WurstBuildConfig.Wc3Patch> projectKind = buildConfig.wc3Patch();
228+
Optional<GameVersion> clientVersion = launchData.getWc3PatchVersion();
229+
if (projectKind.isEmpty() || clientVersion.isEmpty()) {
230+
return false;
231+
}
232+
return projectKind.get() != kindForVersion(clientVersion.get());
233+
}
234+
235+
private static WurstBuildConfig.Wc3Patch kindForVersion(GameVersion version) {
236+
if (version.compareTo(new GameVersion("1.29")) < 0) {
237+
return WurstBuildConfig.Wc3Patch.PRE_129;
238+
}
239+
if (version.compareTo(GameVersion.VERSION_1_32) < 0) {
240+
return WurstBuildConfig.Wc3Patch.CLASSIC;
241+
}
242+
return WurstBuildConfig.Wc3Patch.REFORGED;
243+
}
244+
245+
private static String describeClientVersion(GameVersion version) {
246+
return kindForVersion(version) + " (" + version + ")";
247+
}
248+
249+
private MismatchChoice chooseMismatchAction(String message) {
250+
if (GraphicsEnvironment.isHeadless()) {
251+
return MismatchChoice.CONTINUE;
252+
}
253+
Object[] options = {"Continue", "Choose Warcraft III folder", "Cancel"};
254+
int result = JOptionPane.showOptionDialog(
255+
null,
256+
message,
257+
"Warcraft III version mismatch",
258+
JOptionPane.DEFAULT_OPTION,
259+
JOptionPane.WARNING_MESSAGE,
260+
null,
261+
options,
262+
options[1]
263+
);
264+
if (result == 1) {
265+
return MismatchChoice.CHOOSE_OTHER;
266+
}
267+
if (result == 2 || result == JOptionPane.CLOSED_OPTION) {
268+
return MismatchChoice.CANCEL;
269+
}
270+
return MismatchChoice.CONTINUE;
271+
}
272+
273+
private Optional<W3InstallationData> chooseAlternateGamePath() {
274+
if (GraphicsEnvironment.isHeadless()) {
275+
return Optional.empty();
276+
}
277+
JFileChooser fileChooser = new JFileChooser(FileSystemView.getFileSystemView().getHomeDirectory());
278+
fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
279+
fileChooser.setDialogTitle("Select Warcraft III installation folder");
280+
int result = fileChooser.showOpenDialog(null);
281+
if (result != JFileChooser.APPROVE_OPTION) {
282+
return Optional.empty();
283+
}
284+
File selectedFolder = fileChooser.getSelectedFile();
285+
return Optional.of(new W3InstallationData(langServer, selectedFolder, true, Optional.empty()));
286+
}
287+
288+
private enum MismatchChoice {
289+
CONTINUE,
290+
CHOOSE_OTHER,
291+
CANCEL
292+
}
293+
189294

190295
private void callJhcrUpdate(File mapScript) throws IOException, InterruptedException {
191296
File mapScriptFolder = mapScript.getParentFile();

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/utils/W3InstallationData.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public class W3InstallationData {
1919

2020
private Optional<GameVersion> version = Optional.empty();
2121

22+
private boolean versionHeuristic = false;
23+
2224
private File selectedFolder;
2325

2426
private boolean shouldAskForPath = false;
@@ -37,10 +39,12 @@ public W3InstallationData(Optional<File> gameExe, Optional<GameVersion> version)
3739
if (!this.version.isPresent() && this.gameExe.isPresent()) {
3840
try {
3941
this.version = Optional.ofNullable(GameExe.getVersion(this.gameExe.get()));
42+
this.versionHeuristic = false;
4043
WLogger.info("Parsed game version from configured executable: " + this.version);
4144
} catch (IOException | RuntimeException e) {
4245
WLogger.warning("Could not parse game version from configured executable. Continuing without detected game version.");
4346
WLogger.debug("Game version parser failed: " + e);
47+
inferVersionFromExecutablePath();
4448
}
4549
}
4650
}
@@ -128,9 +132,55 @@ private void loadFromPath(File wc3Path, boolean shouldParseVersion) {
128132

129133
return Optional.empty();
130134
});
135+
if (!version.isPresent()) {
136+
inferVersionFromExecutablePath();
137+
} else {
138+
versionHeuristic = false;
139+
}
131140
WLogger.info("Parsed custom game version from executable: " + version);
132141
}
133142

143+
private void inferVersionFromExecutablePath() {
144+
if (!gameExe.isPresent()) {
145+
return;
146+
}
147+
Optional<GameVersion> inferred = inferVersionFromExecutablePath(gameExe.get());
148+
if (inferred.isPresent()) {
149+
version = inferred;
150+
versionHeuristic = true;
151+
WLogger.info("Inferred Warcraft III client family from executable path: " + version.get());
152+
}
153+
}
154+
155+
public static Optional<GameVersion> inferVersionFromExecutablePath(File exe) {
156+
if (exe == null) {
157+
return Optional.empty();
158+
}
159+
String normalized = exe.getAbsolutePath().replace('\\', '/').toLowerCase();
160+
String fileName = exe.getName().toLowerCase();
161+
if (normalized.contains("/_retail_/") || normalized.contains("/_ptr_/")) {
162+
return Optional.of(GameVersion.VERSION_1_32);
163+
}
164+
if (fileName.equals("war3.exe") || fileName.equals("frozen throne.exe")) {
165+
return Optional.of(new GameVersion("1.28"));
166+
}
167+
if (fileName.equals("warcraft iii.exe")) {
168+
File parent = exe.getParentFile();
169+
if (parent != null) {
170+
String parentName = parent.getName().toLowerCase();
171+
if (parentName.equals("x86") || parentName.equals("x86_64")) {
172+
File installDir = parent.getParentFile();
173+
if (installDir != null && new File(installDir, "_retail_").exists()) {
174+
return Optional.of(GameVersion.VERSION_1_32);
175+
}
176+
return Optional.of(GameVersion.VERSION_1_31);
177+
}
178+
}
179+
return Optional.of(new GameVersion("1.29"));
180+
}
181+
return Optional.empty();
182+
}
183+
134184
private void discoverExePath() {
135185
try {
136186
gameExe = Optional.ofNullable(new StdGameExeFinder().get());
@@ -178,6 +228,7 @@ private void showFileChooser() {
178228
private void discoverVersion() {
179229
try {
180230
version = Optional.ofNullable(new StdGameVersionFinder().get());
231+
versionHeuristic = false;
181232
WLogger.info("Parsed game version: " + version);
182233
} catch (NotFoundException e) {
183234
WLogger.warning("Game version detection failed. Pin wc3Patch in wurst.build if launch arguments look wrong.");
@@ -194,6 +245,10 @@ public Optional<GameVersion> getWc3PatchVersion() {
194245
return version;
195246
}
196247

248+
public boolean isVersionHeuristic() {
249+
return versionHeuristic;
250+
}
251+
197252
/**
198253
* @return The wc3 path or empty if none has been found
199254
*/

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,32 @@ public void pinnedRunMapUsesPatchVersionWithoutParsingExecutable() throws Except
6060

6161
TestRunMap request = new TestRunMap(project, fakeExe.toString());
6262

63-
assertEquals(request.detectedVersion().orElseThrow(), new GameVersion("1.27"));
63+
assertFalse(request.detectedVersion().isPresent());
6464
assertEquals(request.gameExe().orElseThrow().getAbsoluteFile(), fakeExe.toFile().getAbsoluteFile());
6565
}
6666

67+
@Test
68+
public void runMapInfersReforgedClientFromRetailExecutablePath() throws Exception {
69+
Path project = projectWithPatch("v1.27b");
70+
Path install = Files.createTempDirectory("warcraft-reforged");
71+
Path exe = Files.createDirectories(install.resolve("_retail_").resolve("x86_64")).resolve("Warcraft III.exe");
72+
Files.writeString(exe, "not a PE file");
73+
74+
TestRunMap request = new TestRunMap(project, exe.toString());
75+
76+
assertEquals(request.detectedVersion().orElseThrow(), GameVersion.VERSION_1_32);
77+
}
78+
79+
@Test
80+
public void runMapInfersClassicClientFromWar3ExecutablePath() throws Exception {
81+
Path project = projectWithPatch("v2.0");
82+
Path exe = Files.writeString(Files.createTempDirectory("warcraft-classic").resolve("war3.exe"), "not a PE file");
83+
84+
TestRunMap request = new TestRunMap(project, exe.toString());
85+
86+
assertEquals(request.detectedVersion().orElseThrow(), new GameVersion("1.28"));
87+
}
88+
6789
@Test
6890
public void configuredVersionSurvivesManualPathLoad() throws Exception {
6991
W3InstallationData data = new W3InstallationData(Optional.empty(), Optional.of(new GameVersion("2.0")));

0 commit comments

Comments
 (0)