Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,25 @@ jobs:
run: mvn -B package --file pom.xml
- name: Test with Maven
run: mvn test

windows-java8-x86:
name: Windows Java 8 x86
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- name: Set up the primary bot runtime
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '8'
architecture: 'x86'
- name: Verify 32-bit Java 8
shell: pwsh
run: |
$settings = (& java -XshowSettings:properties -version 2>&1 | Out-String)
if ($settings -notmatch 'sun.arch.data.model\s*=\s*32' -or $settings -notmatch 'java.specification.version\s*=\s*1\.8') {
throw 'Expected a 32-bit Java 8 JVM'
}
java -version
- name: Build and test with Maven
run: mvn -B package --file pom.xml
9 changes: 5 additions & 4 deletions src/main/java/bwapi/BWClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,17 @@ public void startGame(boolean autoContinue) {
*/
public void startGame(BWClientConfiguration gameConfiguration) {
this.configuration = gameConfiguration;
this.performanceMetrics = new PerformanceMetrics(configuration);
botWrapper = configuration.getAsync()
? new BotWrapperAsync(configuration, eventListener)
: new BotWrapper(configuration, eventListener);

if (client == null) {
client = new Client(this);
}
client.reconnect();

this.performanceMetrics = new PerformanceMetrics(configuration);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My naive assumption is that change is actually the most important for the startup boost (e.g. the Client starting to listen ASAP). Do you have individual timing on how much each of the 3 changes actually brings individually?

@dgant dgant Nov 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Using pre-extracted JNA DLL: -45%
  • Defer buffer allocation: -26%
  • Narrow kernel32 bindings: -7%

The deferred buffer allocation is certainly the easiest win though :)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok interesting, thanks for the numbers. I would prefer putting the "Using pre-extracted JNA DLL" (most complicated) in another PR which I can look at in more detail another time, the deferral and narrow bindings look like free wins, those LGTM for this CL, thanks a bunch!

botWrapper = configuration.getAsync()
? new BotWrapperAsync(configuration, eventListener)
: new BotWrapper(configuration, eventListener);

do {
ClientData.GameData liveGameData = client.liveClientData().gameData();
while (!liveGameData.isInGame()) {
Expand Down
20 changes: 15 additions & 5 deletions src/main/java/bwapi/ClientConnectionW32.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIOptions;

import java.io.FileNotFoundException;
Expand All @@ -15,10 +16,19 @@
class ClientConnectionW32 implements ClientConnection {
private static final int READ_WRITE = 0x1 | 0x2 | 0x4;

interface MappingKernel extends Kernel32 {
MappingKernel INSTANCE = Native.load(MappingKernel.class, W32APIOptions.DEFAULT_OPTIONS);
/**
* Opens the BWAPI server's game table and game-state shared-memory mappings.
* Binding only these two kernel32 functions avoids initializing JNA's full
* Kernel32 proxy before the client can discover a server.
Comment thread
JasperGeurtz marked this conversation as resolved.
Resolved
* Measured savings: about 7% of startup time on Windows/Java 8 x86
* with JNA 5.18.1 in fresh-JVM startup tests; actual savings depend on the machine.
*/
interface MappingKernel extends StdCallLibrary {
MappingKernel INSTANCE = Native.load("kernel32", MappingKernel.class, W32APIOptions.DEFAULT_OPTIONS);

HANDLE OpenFileMapping(int desiredAccess, boolean inherit, String name);

Pointer MapViewOfFile(HANDLE mapping, int desiredAccess, int offsetHigh, int offsetLow, int bytesToMap);
}


Expand All @@ -38,7 +48,7 @@ public void disconnect() {

@Override
public WrappedBuffer getGameTable() {
final Pointer gameTableView = Kernel32.INSTANCE.MapViewOfFile(MappingKernel.INSTANCE
final Pointer gameTableView = MappingKernel.INSTANCE.MapViewOfFile(MappingKernel.INSTANCE
.OpenFileMapping(READ_WRITE, false, "Local\\bwapi_shared_memory_game_list"), READ_WRITE,
0, 0, GameTable.SIZE);
return new WrappedBuffer(gameTableView, GameTable.SIZE);
Expand All @@ -48,7 +58,7 @@ public WrappedBuffer getGameTable() {
public WrappedBuffer getSharedMemory(int serverProcID) {
String sharedMemoryName = "Local\\bwapi_shared_memory_" + serverProcID;
try {
final Pointer mapFileView = Kernel32.INSTANCE.MapViewOfFile(MappingKernel.INSTANCE
final Pointer mapFileView = MappingKernel.INSTANCE.MapViewOfFile(MappingKernel.INSTANCE
.OpenFileMapping(READ_WRITE, false, sharedMemoryName), READ_WRITE,
0, 0, ClientData.GameData.SIZE);
return new WrappedBuffer(mapFileView, ClientData.GameData.SIZE);
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/bwapi/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -877,11 +877,13 @@ public String mapHash() {
* @return true if the mini-tile is walkable and false if it is impassable for ground units.
*/
final public boolean isWalkable(final int walkX, final int walkY) {
return isWalkable(new WalkPosition(walkX, walkY));
return walkX >= 0 && walkY >= 0 &&
walkX < mapWidth * TILE_WALK_FACTOR && walkY < mapHeight * TILE_WALK_FACTOR &&
walkable[walkX][walkY];
}

final public boolean isWalkable(final WalkPosition position) {
return position.isValid(this) && walkable[position.x][position.y];
return isWalkable(position.x, position.y);
}

/**
Expand Down
17 changes: 3 additions & 14 deletions src/main/java/bwem/ChokePoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -313,25 +313,14 @@ public boolean equals(final Object object) {
return false;
} else {
final ChokePoint that = (ChokePoint) object;
final boolean lel = this.areas.getLeft().equals(that.areas.getLeft());
final boolean ler = this.areas.getLeft().equals(that.areas.getRight());
final boolean rer = this.areas.getRight().equals(that.areas.getRight());
final boolean rel = this.areas.getRight().equals(that.areas.getLeft());
return lel && rer
|| ler && rel; /* true if area pairs are an exact match or if one pair is reversed. */
// Parallel connections can share areas or even a center, but not a graph/index.
return this.graph == that.graph && this.index == that.index;
}
}

@Override
public int hashCode() {
int idLeft = areas.getLeft().getId().intValue();
int idRight = areas.getRight().getId().intValue();
if (idLeft > idRight) {
final int idLeftTmp = idLeft;
idLeft = idRight;
idRight = idLeftTmp;
}
return Objects.hash(idLeft, idRight);
return Objects.hash(graph, index);
}

/**
Expand Down
49 changes: 49 additions & 0 deletions src/test/java/bwapi/BWClientStartupTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package bwapi;

import org.junit.Test;

import java.lang.management.BufferPoolMXBean;
import java.lang.management.ManagementFactory;

import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;

/**
* Guards the order of startup work: attempt the BWAPI connection before creating
* performance metrics or allocating the large buffers used for asynchronous frames.
* Measures direct-buffer allocation at a mocked connection boundary, so the test
* needs neither a running server nor a machine-dependent elapsed-time threshold.
*/
public class BWClientStartupTest {
// Stops startGame at the mocked connection boundary, before a game loop can run.
private static class StopAfterConnectionAttempt extends RuntimeException {
}

@Test
public void attemptsConnectionBeforeAllocatingFrameBuffersOrMetrics() {
BWClient bwClient = new BWClient(new DefaultBWListener());
Client client = mock(Client.class);
bwClient.setClient(client);
BWClientConfiguration configuration = new BWClientConfiguration.Builder()
.withAsync(true)
.withAsyncFrameBufferCapacity(3)
.build();
BufferPoolMXBean directBuffers = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class)
.stream()
.filter(pool -> "direct".equals(pool.getName()))
.findFirst()
.get();
long bytesBeforeStart = directBuffers.getMemoryUsed();
doAnswer(invocation -> {
assertTrue("Connection attempts must precede allocation of even one game-state buffer",
directBuffers.getMemoryUsed() - bytesBeforeStart < ClientData.GameData.SIZE);
assertNull(bwClient.getPerformanceMetrics());
throw new StopAfterConnectionAttempt();
}).when(client).reconnect();

assertThrows(StopAfterConnectionAttempt.class, () -> bwClient.startGame(configuration));
}
}
41 changes: 36 additions & 5 deletions src/test/java/bwapi/BWEMTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

import bwem.BWEM;
import bwem.BWMap;
import bwta.BWTA;
import bwta.Chokepoint;
import bwta.Region;
import org.junit.Test;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.*;

public class BWEMTest {
static class BWEMMap{
Expand Down Expand Up @@ -66,13 +72,38 @@ public int hashCode() {
mapData.put("(4)Roadrunner.scx", new BWEMMap(26, 12, 35));
}

@Test
public void chokePointsFromDifferentMapsAreDistinct() throws IOException {
BWEM first = new BWEM(GameBuilder.createGame());
first.initialize();
BWEM second = new BWEM(GameBuilder.createGame());
second.initialize();
assertTrue(Collections.disjoint(first.getMap().getChokePoints(), second.getMap().getChokePoints()));
}

@Test
public void checkSSCAITMaps() throws IOException {
for (String mapName : mapData.keySet()) {
Game game = GameBuilder.createGame(mapName);
BWEM bwem = new BWEM(game);
bwem.initialize();
assertEquals(new BWEMMap(bwem.getMap()), mapData.get(mapName));
BWTA.readMap(GameBuilder.createGame(mapName));
BWTA.analyze();
BWMap map = BWTA.getBWEM().getMap();
assertEquals(new BWEMMap(map), mapData.get(mapName));
assertEquals(mapName, map.getChokePoints().size(), new HashSet<>(map.getChokePoints()).size());

List<Chokepoint> chokes = BWTA.getChokepoints();
assertEquals(mapName, map.getChokePoints().size(), chokes.size());
for (Chokepoint choke : chokes) {
assertTrue(choke.getRegions().getLeft().getChokepoints().contains(choke));
assertTrue(choke.getRegions().getRight().getChokepoints().contains(choke));
}
for (Region region : BWTA.getRegions()) {
for (Chokepoint choke : region.getChokepoints()) {
assertNotNull(mapName, choke);
assertTrue(mapName, chokes.contains(choke));
assertTrue(choke.getRegions().getLeft().equals(region)
|| choke.getRegions().getRight().equals(region));
}
}
}
}
}
44 changes: 44 additions & 0 deletions src/test/java/bwapi/GameWalkabilityTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package bwapi;

import org.junit.Test;

import java.io.IOException;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class GameWalkabilityTest {
@Test
public void walkabilityQueriesMatchMapDataAcrossTheMap() throws IOException {
Game game = GameBuilder.createGame();
boolean foundWalkable = false;
boolean foundBlocked = false;
for (int x = 0; x < game.mapWidth() * 4; x++) {
for (int y = 0; y < game.mapHeight() * 4; y++) {
boolean expected = game.botClientData().gameData().isWalkable(x, y);
assertEquals(expected, game.isWalkable(x, y));
assertEquals(expected, game.isWalkable(new WalkPosition(x, y)));
foundWalkable |= expected;
foundBlocked |= !expected;
}
}
assertTrue(foundWalkable);
assertTrue(foundBlocked);
}

@Test
public void rejectsOutOfBoundsWalkCoordinates() throws IOException {
Game game = GameBuilder.createGame();
int[] invalidX = {-1, Integer.MIN_VALUE, game.mapWidth() * 4, 1 << 29, Integer.MAX_VALUE};
int[] invalidY = {-1, Integer.MIN_VALUE, game.mapHeight() * 4, 1 << 29, Integer.MAX_VALUE};
for (int x : invalidX) {
assertFalse(game.isWalkable(x, 0));
assertFalse(game.isWalkable(new WalkPosition(x, 0)));
}
for (int y : invalidY) {
assertFalse(game.isWalkable(0, y));
assertFalse(game.isWalkable(new WalkPosition(0, y)));
}
}
}
71 changes: 71 additions & 0 deletions src/test/java/bwapi/MappingKernelTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package bwapi;

import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinNT;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assume.assumeTrue;

/**
* Verifies the Windows mapping binding's JNA signatures against a real shared-memory object.
* The existing JNA Kernel32 binding creates the mapping and its first view;
* MappingKernel opens a second view. Reads and writes must agree in both directions.
* A Unicode name checks Windows string marshalling, and a missing mapping must
* return null. These tests require Windows but do not require a BWAPI server.
*/
public class MappingKernelTest {
@Before
public void requireWindows() {
assumeTrue(System.getProperty("os.name").startsWith("Windows"));
}

@Test
public void opensUnicodeNamedMappingAndSharesWritesBetweenViews() {
// Include Unicode to exercise the same W32API string mapping used by the client.
String name = "Local\\jbwapi-mapping-\u03bb-" + System.nanoTime();
int access = WinNT.SECTION_MAP_READ | WinNT.SECTION_MAP_WRITE;
WinNT.HANDLE created = Kernel32.INSTANCE.CreateFileMapping(
WinBase.INVALID_HANDLE_VALUE, null, WinNT.PAGE_READWRITE, 0, 4, name);
assertNotNull(created);
WinNT.HANDLE opened = null;
Pointer originalView = null;
Pointer clientView = null;
try {
originalView = Kernel32.INSTANCE.MapViewOfFile(created, access, 0, 0, 4);
assertNotNull(originalView);
originalView.setInt(0, 10003);

opened = ClientConnectionW32.MappingKernel.INSTANCE.OpenFileMapping(access, false, name);
assertNotNull(opened);
clientView = ClientConnectionW32.MappingKernel.INSTANCE.MapViewOfFile(opened, access, 0, 0, 4);
assertNotNull(clientView);
assertEquals(10003, clientView.getInt(0));

clientView.setInt(0, 42);
assertEquals(42, originalView.getInt(0));
} finally {
if (clientView != null) {
Kernel32.INSTANCE.UnmapViewOfFile(clientView);
}
if (originalView != null) {
Kernel32.INSTANCE.UnmapViewOfFile(originalView);
}
if (opened != null) {
Kernel32.INSTANCE.CloseHandle(opened);
}
Kernel32.INSTANCE.CloseHandle(created);
}
}

@Test
public void returnsNullForMissingMapping() {
assertNull(ClientConnectionW32.MappingKernel.INSTANCE.OpenFileMapping(
WinNT.SECTION_MAP_READ, false, "Local\\jbwapi-missing-" + System.nanoTime()));
}
}
Loading