Add platform auto-detection and UI flagging for Windows games
Build & Deploy / build-and-deploy (push) Successful in 49s
Build & Deploy / build-and-deploy (push) Successful in 49s
- Game.java: add 'platform' field ('dos'/'windows') + 'isPlayable' helper
- UploadResource.java: detect EXE type by reading PE/NE header during upload
- GameResource.java: allow PATCH to update platform
- GameCard.svelte: show 🪟 Win badge for Windows games (blue style)
- GameDetail.svelte: show platform badge, disable Play for Windows with
'Unplayable' button; add platform dropdown to edit form
- Play.svelte: show blocking overlay for Windows games with 'Try anyway'
option for users who want to attempt it
This commit is contained in:
@@ -23,6 +23,9 @@ public class Game {
|
||||
|
||||
private String setupExe; // relative path to SETUP.EXE (null if none)
|
||||
|
||||
/** Platform type: "dos" for pure DOS, "windows" for Windows 3.x/9x+ (requires emulated Windows), null if unknown. */
|
||||
private String platform;
|
||||
|
||||
private Integer igdbId;
|
||||
private String igdbCoverId;
|
||||
|
||||
@@ -85,6 +88,14 @@ public class Game {
|
||||
/** Returns true if a SETUP.EXE (or variant) was detected in the game archive. */
|
||||
public boolean isHasSetup() { return setupExe != null && !setupExe.isBlank(); }
|
||||
|
||||
public String getPlatform() { return platform; }
|
||||
public void setPlatform(String platform) { this.platform = platform; }
|
||||
|
||||
/** Returns true if the game is a pure DOS executable that can run natively in DOSBox. */
|
||||
public boolean isPlayable() {
|
||||
return ready && (platform == null || "dos".equals(platform));
|
||||
}
|
||||
|
||||
public Integer getIgdbId() { return igdbId; }
|
||||
public void setIgdbId(Integer igdbId) { this.igdbId = igdbId; }
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ public class GameResource {
|
||||
if (updates.containsKey("publisher")) game.setPublisher((String) updates.get("publisher"));
|
||||
if (updates.containsKey("description")) game.setDescription((String) updates.get("description"));
|
||||
if (updates.containsKey("rating")) game.setRating(asDouble(updates.get("rating")));
|
||||
if (updates.containsKey("platform")) game.setPlatform((String) updates.get("platform"));
|
||||
|
||||
svc.save(game);
|
||||
return Response.ok(game).build();
|
||||
|
||||
@@ -91,9 +91,13 @@ public class UploadResource {
|
||||
createBundle(extractDir, setupExe, setupBundlePath);
|
||||
}
|
||||
|
||||
// Detect platform (DOS vs Windows) by reading the main EXE header
|
||||
String platform = detectPlatform(Path.of(mainExe));
|
||||
|
||||
// Save metadata
|
||||
Game game = new Game(gameId, title);
|
||||
game.setBundleFile(bundleFile);
|
||||
game.setPlatform(platform);
|
||||
game.setSetupExe(setupExe != null ? extractDir.relativize(Path.of(setupExe)).toString() : null);
|
||||
game.setReady(true);
|
||||
svc.save(game);
|
||||
@@ -382,6 +386,64 @@ public class UploadResource {
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether an executable targets DOS or Windows by reading its PE/NE header.
|
||||
* <ul>
|
||||
* <li>Pure MZ (no NE/PE/LE) → "dos"</li>
|
||||
* <li>NE (Windows 3.x), PE (Win32), LE (VxD) → "windows"</li>
|
||||
* <li>Cannot read → null</li>
|
||||
* </ul>
|
||||
*/
|
||||
static String detectPlatform(Path exePath) {
|
||||
String name = exePath.getFileName().toString().toLowerCase();
|
||||
// .com and .bat files are always DOS
|
||||
if (name.endsWith(".com") || name.endsWith(".bat")) return "dos";
|
||||
|
||||
try (var fis = Files.newInputStream(exePath)) {
|
||||
byte[] buf = new byte[0x80];
|
||||
int read = fis.readNBytes(buf, 0, buf.length);
|
||||
if (read < 2) return null;
|
||||
|
||||
// Must start with MZ
|
||||
if (buf[0] != 0x4D || buf[1] != 0x5A) return null;
|
||||
|
||||
// Read e_lfanew at offset 0x3C
|
||||
if (read < 0x40) return "dos"; // too short for PE/NE, assume DOS
|
||||
int peOffset = (buf[0x3C] & 0xFF)
|
||||
| ((buf[0x3D] & 0xFF) << 8)
|
||||
| ((buf[0x3E] & 0xFF) << 16)
|
||||
| ((buf[0x3F] & 0xFF) << 24);
|
||||
|
||||
// If signature is beyond our buffer, read it
|
||||
byte sig1, sig2;
|
||||
if (peOffset + 2 <= read) {
|
||||
sig1 = buf[peOffset];
|
||||
sig2 = buf[peOffset + 1];
|
||||
} else {
|
||||
try (var fis2 = Files.newInputStream(exePath)) {
|
||||
fis2.skip(peOffset);
|
||||
sig1 = (byte) fis2.read();
|
||||
sig2 = (byte) fis2.read();
|
||||
}
|
||||
}
|
||||
|
||||
// PE = Portable Executable (Win32)
|
||||
// NE = New Executable (Windows 3.x)
|
||||
// LE = Linear Executable (VxD, OS/2)
|
||||
// LX = Extended Linear Executable (OS/2 2.x)
|
||||
if ((sig1 == 0x50 && sig2 == 0x45) // PE
|
||||
|| (sig1 == 0x4E && sig2 == 0x45) // NE
|
||||
|| (sig1 == 0x4C && sig2 == 0x45) // LE
|
||||
|| (sig1 == 0x4C && sig2 == 0x58)) // LX
|
||||
return "windows";
|
||||
|
||||
return "dos"; // Plain MZ without known Windows signature
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Could not read EXE header for " + exePath + ": " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── File Utilities ──────────────────────────────────────────
|
||||
|
||||
private void deleteDir(Path dir) throws IOException {
|
||||
|
||||
Reference in New Issue
Block a user