diff --git a/src/main/java/bwapi/AsyncClientData.java b/src/main/java/bwapi/AsyncClientData.java index a3345f2b..dd6e9a8e 100644 --- a/src/main/java/bwapi/AsyncClientData.java +++ b/src/main/java/bwapi/AsyncClientData.java @@ -60,18 +60,6 @@ class AsyncGameData extends GameData { super(myOffset); } - private int deltaRange2() { - return buffer != null && buffer.isPacked() ? DELTA_RANGE2 : 0; - } - - private int deltaRange3() { - return buffer != null && buffer.isPacked() ? DELTA_RANGE3 : 0; - } - - private int deltaRange4() { - return buffer != null && buffer.isPacked() ? DELTA_RANGE4 : 0; - } - @Override int getGroundHeight(int i, int j) { int offset = myOffset + 3447004 + 4 * 1 * j + 4 * 256 * i; @@ -100,42 +88,6 @@ void setIsBuildable(int i, int j, boolean value) { getStaticBuffer().putByte(myOffset + 4757724 + 1 * 1 * j + 1 * 256 * i, (byte) (value ? 1 : 0)); } @Override - boolean isVisible(int i, int j) { - int offset = myOffset + 4823260 - deltaRange2() + 1 * 1 * j + 1 * 256 * i; - return buffer.getByte(offset) != 0; - } - @Override - void setIsVisible(int i, int j, boolean value) { - buffer.putByte(myOffset + 4823260 - deltaRange2() + 1 * 1 * j + 1 * 256 * i, (byte) (value ? 1 : 0)); - } - @Override - boolean isExplored(int i, int j) { - int offset = myOffset + 4888796 - deltaRange2() + 1 * 1 * j + 1 * 256 * i; - return buffer.getByte(offset) != 0; - } - @Override - void setIsExplored(int i, int j, boolean value) { - buffer.putByte(myOffset + 4888796 - deltaRange2() + 1 * 1 * j + 1 * 256 * i, (byte) (value ? 1 : 0)); - } - @Override - boolean getHasCreep(int i, int j) { - int offset = myOffset + 4954332 - deltaRange2() + 1 * 1 * j + 1 * 256 * i; - return buffer.getByte(offset) != 0; - } - @Override - void setHasCreep(int i, int j, boolean value) { - buffer.putByte(myOffset + 4954332 - deltaRange2() + 1 * 1 * j + 1 * 256 * i, (byte) (value ? 1 : 0)); - } - @Override - boolean isOccupied(int i, int j) { - int offset = myOffset + 5019868 - deltaRange2() + 1 * 1 * j + 1 * 256 * i; - return buffer.getByte(offset) != 0; - } - @Override - void setIsOccupied(int i, int j, boolean value) { - buffer.putByte(myOffset + 5019868 - deltaRange2() + 1 * 1 * j + 1 * 256 * i, (byte) (value ? 1 : 0)); - } - @Override short getMapTileRegionId(int i, int j) { int offset = myOffset + 5085404 + 2 * 1 * j + 2 * 256 * i; return getStaticBuffer().getShort(offset); @@ -185,178 +137,5 @@ RegionData getRegions(int i) { int offset = myOffset + 5246480 + 1068 * 1 * i; return new RegionData(offset); } - @Override - int getStartLocationCount() { - int offset = myOffset + 10586480 - deltaRange3(); - return buffer.getInt(offset); - } - @Override - void setStartLocationCount(int value) { - buffer.putInt(myOffset + 10586480 - deltaRange3(), value); - } - @Override - Position getStartLocations(int i) { - int offset = myOffset + 10586484 - deltaRange3() + 8 * 1 * i; - return new Position(offset); - } - @Override - boolean isInGame() { - int offset = myOffset + 10586548 - deltaRange3(); - return buffer.getByte(offset) != 0; - } - @Override - void setIsInGame(boolean value) { - buffer.putByte(myOffset + 10586548 - deltaRange3(), (byte) (value ? 1 : 0)); - } - @Override - boolean isMultiplayer() { - int offset = myOffset + 10586549 - deltaRange3(); - return buffer.getByte(offset) != 0; - } - @Override - void setIsMultiplayer(boolean value) { - buffer.putByte(myOffset + 10586549 - deltaRange3(), (byte) (value ? 1 : 0)); - } - @Override - boolean isBattleNet() { - int offset = myOffset + 10586550 - deltaRange3(); - return buffer.getByte(offset) != 0; - } - @Override - void setIsBattleNet(boolean value) { - buffer.putByte(myOffset + 10586550 - deltaRange3(), (byte) (value ? 1 : 0)); - } - @Override - boolean isPaused() { - int offset = myOffset + 10586551 - deltaRange3(); - return buffer.getByte(offset) != 0; - } - @Override - void setIsPaused(boolean value) { - buffer.putByte(myOffset + 10586551 - deltaRange3(), (byte) (value ? 1 : 0)); - } - @Override - boolean isReplay() { - int offset = myOffset + 10586552 - deltaRange3(); - return buffer.getByte(offset) != 0; - } - @Override - void setIsReplay(boolean value) { - buffer.putByte(myOffset + 10586552 - deltaRange3(), (byte) (value ? 1 : 0)); - } - @Override - int getSelectedUnitCount() { - int offset = myOffset + 10586556 - deltaRange3(); - return buffer.getInt(offset); - } - @Override - void setSelectedUnitCount(int value) { - buffer.putInt(myOffset + 10586556 - deltaRange3(), value); - } - @Override - int getSelectedUnits(int i) { - int offset = myOffset + 10586560 - deltaRange3() + 4 * 1 * i; - return buffer.getInt(offset); - } - @Override - void setSelectedUnits(int i, int value) { - buffer.putInt(myOffset + 10586560 - deltaRange3() + 4 * 1 * i, value); - } - @Override - int getSelf() { - int offset = myOffset + 10586608 - deltaRange3(); - return buffer.getInt(offset); - } - @Override - void setSelf(int value) { - buffer.putInt(myOffset + 10586608 - deltaRange3(), value); - } - @Override - int getEnemy() { - int offset = myOffset + 10586612 - deltaRange3(); - return buffer.getInt(offset); - } - @Override - void setEnemy(int value) { - buffer.putInt(myOffset + 10586612 - deltaRange3(), value); - } - @Override - int getNeutral() { - int offset = myOffset + 10586616 - deltaRange3(); - return buffer.getInt(offset); - } - @Override - void setNeutral(int value) { - buffer.putInt(myOffset + 10586616 - deltaRange3(), value); - } - @Override - int getEventCount() { - int offset = myOffset + 10586620 - deltaRange3(); - return buffer.getInt(offset); - } - @Override - void setEventCount(int value) { - buffer.putInt(myOffset + 10586620 - deltaRange3(), value); - } - @Override - Event getEvents(int i) { - int offset = myOffset + 10586624 - deltaRange3() + 12 * 1 * i; - return new Event(offset); - } - @Override - int getEventStringCount() { - int offset = myOffset + 10706624 - deltaRange3(); - return buffer.getInt(offset); - } - @Override - void setEventStringCount(int value) { - buffer.putInt(myOffset + 10706624 - deltaRange3(), value); - } - @Override - String getEventStrings(int i) { - int offset = myOffset + 10706628 - deltaRange3() + 1 * 256 * i; - return buffer.getString(offset, 256); - } - @Override - void setEventStrings(int i, String value) { - buffer.putString(myOffset + 10706628 - deltaRange3() + 1 * 256 * i, 256, value); - } - @Override - int getStringCount() { - int offset = myOffset + 10962628 - deltaRange3(); - return buffer.getInt(offset); - } - @Override - void setStringCount(int value) { - buffer.putInt(myOffset + 10962628 - deltaRange3(), value); - } - @Override - int getCommandCount() { - int offset = myOffset + 32242636 - deltaRange4(); - return buffer.getInt(offset); - } - @Override - void setCommandCount(int value) { - buffer.putInt(myOffset + 32242636 - deltaRange4(), value); - } - @Override - Command getCommands(int i) { - int offset = myOffset + 32242640 - deltaRange4() + 12 * 1 * i; - return new Command(offset); - } - @Override - int getUnitCommandCount() { - int offset = myOffset + 32482640 - deltaRange4(); - return buffer.getInt(offset); - } - @Override - void setUnitCommandCount(int value) { - buffer.putInt(myOffset + 32482640 - deltaRange4(), value); - } - @Override - UnitCommand getUnitCommands(int i) { - int offset = myOffset + 32482644 - deltaRange4() + 24 * 1 * i; - return new UnitCommand(offset); - } } } diff --git a/src/main/java/bwapi/ClientData.java b/src/main/java/bwapi/ClientData.java index ef9e18e5..a294fcd6 100644 --- a/src/main/java/bwapi/ClientData.java +++ b/src/main/java/bwapi/ClientData.java @@ -3,7 +3,7 @@ import com.sun.jna.Pointer; class ClientData { - WrappedBuffer buffer; + volatile WrappedBuffer buffer; GameData gameData; ClientData() { gameData = createGameData(0); diff --git a/src/main/java/bwapi/WrappedBuffer.java b/src/main/java/bwapi/WrappedBuffer.java index da6ce166..ba172473 100644 --- a/src/main/java/bwapi/WrappedBuffer.java +++ b/src/main/java/bwapi/WrappedBuffer.java @@ -35,52 +35,67 @@ boolean isPacked() { return packed; } + private long addressAt(int offset) { + // Translate on the selected buffer: unsafe async can switch layouts during a read. + if (packed && offset >= AsyncClientData.STATICTILES_END) { + if (offset >= AsyncClientData.STATICTILES_END && offset < AsyncClientData.REGION_START) { + offset -= AsyncClientData.DELTA_RANGE2; + } else if (offset >= AsyncClientData.REGION_END && offset < AsyncClientData.STRINGSSHAPES_START) { + offset -= AsyncClientData.DELTA_RANGE3; + } else if (offset >= AsyncClientData.STRINGSHAPES_END && offset < AsyncClientData.UNITFINDER_START) { + offset -= AsyncClientData.DELTA_RANGE4; + } + } + return address + offset; + } + byte getByte(final int offset) { - return unsafe.getByte(address + offset); + return unsafe.getByte(addressAt(offset)); } void putByte(final int offset, final byte value) { - unsafe.putByte(address + offset, value); + unsafe.putByte(addressAt(offset), value); } short getShort(final int offset) { - return unsafe.getShort(address + offset); + return unsafe.getShort(addressAt(offset)); } void putShort(final int offset, final short value) { - unsafe.putShort(address + offset, value); + unsafe.putShort(addressAt(offset), value); } int getInt(final int offset) { - return unsafe.getInt(address + offset); + return unsafe.getInt(addressAt(offset)); } void putInt(final int offset, final int value) { - unsafe.putInt(address + offset, value); + unsafe.putInt(addressAt(offset), value); } double getDouble(final int offset) { - return unsafe.getDouble(address + offset); + return unsafe.getDouble(addressAt(offset)); } void putDouble(final int offset, final double value) { - unsafe.putDouble(address + offset, value); + unsafe.putDouble(addressAt(offset), value); } String getString(final int offset, final int maxLen) { final char[] buf = new char[maxLen]; - long pos = offset + address; + final long start = addressAt(offset); + long pos = start; for (int i = 0; i < maxLen; i++) { byte b = unsafe.getByte(pos); if (b == 0) break; buf[i] = (char) (b & 0xff); pos++; } - return new String(buf, 0, (int) (pos - offset - address)); + return new String(buf, 0, (int) (pos - start)); } void putString(final int offset, final int maxLen, final String string) { - long pos = offset + address; + long pos = addressAt(offset); for (int i = 0; i < Math.min(string.length(), maxLen - 1); i++) { unsafe.putByte(pos, (byte) string.charAt(i)); pos++; diff --git a/src/test/java/DumpToClient.java b/src/test/java/DumpToClient.java index 0272ceed..c800d88e 100644 --- a/src/test/java/DumpToClient.java +++ b/src/test/java/DumpToClient.java @@ -95,7 +95,7 @@ public static void main(String[] args) throws IOException { out.println("import com.sun.jna.Pointer;"); out.println(""); out.println("class ClientData {"); - out.println(" WrappedBuffer buffer;"); + out.println(" volatile WrappedBuffer buffer;"); out.println(" GameData gameData;"); out.println(" ClientData() {"); out.println(" gameData = createGameData(0);"); @@ -127,7 +127,7 @@ public static void main(String[] args) throws IOException { out.printf(" %s(int myOffset) {\n", s.name); out.println(" this.myOffset = myOffset;"); out.println(" }"); - s.variables.forEach(v -> generateVariable(out, s, v, bufName, null, false)); + s.variables.forEach(v -> generateVariable(out, s, v, bufName, false)); out.println(" }"); }); out.println("}"); @@ -201,35 +201,13 @@ public static void main(String[] args) throws IOException { out.println(" super(myOffset);"); out.println(" }"); out.println(""); - out.println(" private int deltaRange2() {"); - out.println(" return buffer != null && buffer.isPacked() ? DELTA_RANGE2 : 0;"); - out.println(" }"); - out.println(""); - out.println(" private int deltaRange3() {"); - out.println(" return buffer != null && buffer.isPacked() ? DELTA_RANGE3 : 0;"); - out.println(" }"); - out.println(""); - out.println(" private int deltaRange4() {"); - out.println(" return buffer != null && buffer.isPacked() ? DELTA_RANGE4 : 0;"); - out.println(" }"); - out.println(""); - Struct gameData = structs.get("GameData"); for (Variable v : gameData.variables) { boolean isStaticTile = v.offset >= 3447004 && v.offset < 4823260; - boolean isDynamicTile = v.offset >= 4823260 && v.offset < 5085404; boolean isStaticRegion = v.offset >= 5085404 && v.offset < 10586480; - boolean isDynamicGame = v.offset >= 10586480 && v.offset < 10962632; - boolean isDynamicCommand = v.offset >= 32242636 && v.offset < 32962644; if (isStaticTile || isStaticRegion) { - generateVariable(out, gameData, v, "getStaticBuffer()", null, true); - } else if (isDynamicTile) { - generateVariable(out, gameData, v, "buffer", "deltaRange2()", true); - } else if (isDynamicGame) { - generateVariable(out, gameData, v, "buffer", "deltaRange3()", true); - } else if (isDynamicCommand) { - generateVariable(out, gameData, v, "buffer", "deltaRange4()", true); + generateVariable(out, gameData, v, "getStaticBuffer()", true); } } out.println(" }"); @@ -241,7 +219,7 @@ public static void main(String[] args) throws IOException { StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); } - private static void generateVariable(PrintWriter out, Struct s, Variable v, String bufName, String delta, boolean isOverride) { + private static void generateVariable(PrintWriter out, Struct s, Variable v, String bufName, boolean isOverride) { if (isOverride) { out.println(" @Override"); } @@ -314,17 +292,9 @@ private static void generateVariable(PrintWriter out, Struct s, Variable v, Stri } offset *= v.arraySizes.get(i); } - if (delta != null) { - offsetString = "myOffset + " + v.offset + " - " + delta + " + " + String.join(" + ", index); - } else { - offsetString = "myOffset + " + v.offset + " + " + String.join(" + ", index); - } + offsetString = "myOffset + " + v.offset + " + " + String.join(" + ", index); } else { - if (delta != null) { - offsetString = "myOffset + " + v.offset + " - " + delta; - } else { - offsetString = "myOffset + " + v.offset; - } + offsetString = "myOffset + " + v.offset; } String paramString = String.join(", ", params); out.printf("%s) {\n", paramString); diff --git a/src/test/java/bwapi/AsyncClientDataTest.java b/src/test/java/bwapi/AsyncClientDataTest.java index 10981d5d..038f197a 100644 --- a/src/test/java/bwapi/AsyncClientDataTest.java +++ b/src/test/java/bwapi/AsyncClientDataTest.java @@ -6,6 +6,87 @@ public class AsyncClientDataTest { + @Test + public void retainedViewsSurviveLiveAndPackedHandoffs() { + WrappedBuffer live = new WrappedBuffer(ClientData.GameData.SIZE); + // Padding lets the unfixed code fail assertions instead of reading past native memory. + WrappedBuffer packed = new WrappedBuffer(ClientData.GameData.SIZE, true); + ClientData source = new ClientData(); + source.setBuffer(live); + source.gameData().getEvents(0).setType(EventType.MatchFrame); + source.gameData().getStartLocations(0).setX(123); + source.gameData().getCommands(0).setType(CommandType.SetLocalSpeed); + source.gameData().getUnitCommands(0).setUnitIndex(456); + new FrameBuffer(new BWClientConfiguration.Builder().withAsyncFrameBufferCapacity(1).build()) + .copyBuffer(live, packed); + + AsyncClientData data = new AsyncClientData(live); + data.setBuffer(live); + ClientData.Event event = data.gameData().getEvents(0); + ClientData.Position position = data.gameData().getStartLocations(0); + ClientData.Command command = data.gameData().getCommands(0); + ClientData.UnitCommand unitCommand = data.gameData().getUnitCommands(0); + data.setBuffer(packed); + assertEquals(EventType.MatchFrame, event.getType()); + assertEquals(123, position.getX()); + assertEquals(CommandType.SetLocalSpeed, command.getType()); + assertEquals(456, unitCommand.getUnitIndex()); + + event.setType(EventType.MatchEnd); + position.setX(789); + unitCommand.setUnitIndex(987); + ClientData.Event packedEvent = data.gameData().getEvents(0); + data.setBuffer(live); + assertEquals(EventType.MatchFrame, packedEvent.getType()); + assertEquals(123, position.getX()); + assertEquals(456, unitCommand.getUnitIndex()); + data.setBuffer(packed); + assertEquals(EventType.MatchEnd, event.getType()); + assertEquals(789, position.getX()); + assertEquals(987, unitCommand.getUnitIndex()); + } + + @Test + public void handoffDuringReadKeepsTheSelectedBuffersLayout() { + class SwitchingBuffer extends WrappedBuffer { + Runnable handoff; + + SwitchingBuffer(boolean packed) { + // Keep a stale full-layout offset inside the allocation on the unfixed code. + super(ClientData.GameData.SIZE, packed); + } + + @Override boolean isPacked() { + boolean packed = super.isPacked(); + if (handoff != null) handoff.run(); + return packed; + } + + @Override byte getByte(int offset) { + if (handoff != null) handoff.run(); + return super.getByte(offset); + } + } + + SwitchingBuffer live = new SwitchingBuffer(false); + SwitchingBuffer packed = new SwitchingBuffer(true); + ClientData source = new ClientData(); + source.setBuffer(live); + source.gameData().setIsVisible(0, 0, true); + new FrameBuffer(new BWClientConfiguration.Builder().withAsyncFrameBufferCapacity(1).build()) + .copyBuffer(live, packed); + AsyncClientData data = new AsyncClientData(live); + data.setBuffer(live); + live.handoff = () -> data.setBuffer(packed); + assertTrue(data.gameData().isVisible(0, 0)); + assertSame(packed, data.buffer); + + live.handoff = null; + packed.handoff = () -> data.setBuffer(live); + assertTrue(data.gameData().isVisible(0, 0)); + assertSame(live, data.buffer); + } + @Test public void frameBufferAllocatesReducedMemory() { // Full GameData buffer is ~33 MB diff --git a/src/test/java/bwapi/SynchronizationEnvironment.java b/src/test/java/bwapi/SynchronizationEnvironment.java index dc7ab100..04e934a5 100644 --- a/src/test/java/bwapi/SynchronizationEnvironment.java +++ b/src/test/java/bwapi/SynchronizationEnvironment.java @@ -22,6 +22,10 @@ class SynchronizationEnvironment { private final Map onFrames; SynchronizationEnvironment(final BWClientConfiguration bwClientConfiguration) { + this(bwClientConfiguration, GameBuilder.binToBufferUnchecked(GameBuilder.DEFAULT_BUFFER_PATH)); + } + + SynchronizationEnvironment(final BWClientConfiguration bwClientConfiguration, final WrappedBuffer newGameState) { BWEventListener listener = mock(BWEventListener.class); configuration = bwClientConfiguration; @@ -32,7 +36,6 @@ class SynchronizationEnvironment { bwapiDelayMs = 0; onFrames = new HashMap<>(); - WrappedBuffer newGameState = GameBuilder.binToBufferUnchecked(GameBuilder.DEFAULT_BUFFER_PATH); when(client.mapFile()).thenReturn(newGameState); when(client.liveClientData()).thenReturn(new ClientData()); client.liveClientData().setBuffer(client.mapFile()); diff --git a/src/test/java/bwapi/SynchronizationTest.java b/src/test/java/bwapi/SynchronizationTest.java index 56536ded..877d2f43 100644 --- a/src/test/java/bwapi/SynchronizationTest.java +++ b/src/test/java/bwapi/SynchronizationTest.java @@ -2,6 +2,9 @@ import org.junit.Test; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.IntStream; import static org.junit.Assert.*; @@ -30,6 +33,40 @@ private void assertWithin(String message, double expected, double actual, double measureApproximateEquality(expected, actual, margin)); } + @Test + public void asyncUnsafe_StartsBotBeforeCopyCompletes() { + CountDownLatch botStarted = new CountDownLatch(1); + AtomicBoolean overlapped = new AtomicBoolean(); + WrappedBuffer live = new WrappedBuffer(ClientData.GameData.SIZE) { + private boolean firstCopy = true; + + @Override long getAddress() { + if (firstCopy) { + firstCopy = false; + try { + // Hold the first copy until the bot starts reading the live frame. + overlapped.set(botStarted.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + } + } + return super.getAddress(); + } + }; + WrappedBuffer fixture = GameBuilder.binToBufferUnchecked(GameBuilder.DEFAULT_BUFFER_PATH); + fixture.getBuffer().rewind(); + live.getBuffer().put(fixture.getBuffer()); + BWClientConfiguration config = new BWClientConfiguration.Builder() + .withAsync(true) + .withAsyncUnsafe(true) + .withAsyncFrameBufferCapacity(2) + .build(); + SynchronizationEnvironment environment = new SynchronizationEnvironment(config, live); + environment.onFrame(0, botStarted::countDown); + environment.runGame(6); + assertTrue("Unsafe async must overlap bot execution with the frame copy", overlapped.get()); + } + @Test public void sync_IfException_ThrowException() { BWClientConfiguration config = new BWClientConfiguration.Builder()