From a1574049bca3979ef408877104561acbe303f68f Mon Sep 17 00:00:00 2001 From: Jasper Geurtz Date: Thu, 5 Nov 2026 20:52:20 +0100 Subject: [PATCH] Reduce Async FrameBuffer memory usage from ~33MB to ~4.8MB per slot (#90) - Add packed buffer support to WrappedBuffer - In FrameBuffer, pack only dynamic game data (4 slices totaling ~4.8MB) instead of copying the full 33MB buffer, and remove frame-0 loop that copied 33MB to all buffers - Auto-generate AsyncClientData via DumpToClient: - Route dynamic game state reads/writes through offset deltas - Route static map/region data reads to the live shared memory buffer - Centralize slice boundaries and buffer constants in AsyncClientData as single source of truth for FrameBuffer - Keep synchronous mode fully backward-compatible using ClientData directly - Wire AsyncClientData in BotWrapperAsync - Add AsyncClientDataTest to verify memory size reduction and correct reading of static vs dynamic data --- src/main/java/bwapi/AsyncClientData.java | 362 ++++++++++++++ src/main/java/bwapi/BotWrapperAsync.java | 7 +- src/main/java/bwapi/ClientData.java | 72 +-- src/main/java/bwapi/FrameBuffer.java | 61 +-- src/main/java/bwapi/Game.java | 6 +- src/main/java/bwapi/WrappedBuffer.java | 11 + src/test/java/DumpToClient.java | 448 ++++++++++++------ src/test/java/bwapi/AsyncClientDataTest.java | 146 ++++++ .../bwapi/SynchronizationEnvironment.java | 2 +- 9 files changed, 872 insertions(+), 243 deletions(-) create mode 100644 src/main/java/bwapi/AsyncClientData.java create mode 100644 src/test/java/bwapi/AsyncClientDataTest.java diff --git a/src/main/java/bwapi/AsyncClientData.java b/src/main/java/bwapi/AsyncClientData.java new file mode 100644 index 00000000..a3345f2b --- /dev/null +++ b/src/main/java/bwapi/AsyncClientData.java @@ -0,0 +1,362 @@ +package bwapi; + +/** + * Asynchronous client data that routes static information to the main/static shared buffer + * and dynamic information to the packed dynamic buffer. + * Automatically generated by DumpToClient. + */ +class AsyncClientData extends ClientData { + static final int STATICTILES_START = 3447004; + static final int STATICTILES_END = 4823260; + static final int REGION_START = 5085404; + static final int REGION_END = 10586480; + static final int STRINGSSHAPES_START = 10962632; + static final int STRINGSHAPES_END = 32242636; + static final int UNITFINDER_START = 32962644; + + static final int DYNAMIC_SLICE1_SIZE = STATICTILES_START; + static final int DYNAMIC_SLICE2_SIZE = REGION_START - STATICTILES_END; + static final int DYNAMIC_SLICE3_SIZE = STRINGSSHAPES_START - REGION_END; + static final int DYNAMIC_SLICE4_SIZE = UNITFINDER_START - STRINGSHAPES_END; + + static final int DEST_SLICE1_OFFSET = 0; + static final int DEST_SLICE2_OFFSET = DYNAMIC_SLICE1_SIZE; + static final int DEST_SLICE3_OFFSET = DEST_SLICE2_OFFSET + DYNAMIC_SLICE2_SIZE; + static final int DEST_SLICE4_OFFSET = DEST_SLICE3_OFFSET + DYNAMIC_SLICE3_SIZE; + + static final int BUFFER_SIZE = DEST_SLICE4_OFFSET + DYNAMIC_SLICE4_SIZE; + + static final int DELTA_RANGE2 = STATICTILES_END - STATICTILES_START; + static final int DELTA_RANGE3 = DELTA_RANGE2 + (REGION_END - REGION_START); + static final int DELTA_RANGE4 = DELTA_RANGE3 + (STRINGSHAPES_END - STRINGSSHAPES_START); + + private WrappedBuffer staticBuffer; + + AsyncClientData() { + super(); + } + + AsyncClientData(WrappedBuffer staticBuffer) { + super(); + this.staticBuffer = staticBuffer; + } + + void setStaticBuffer(WrappedBuffer staticBuffer) { + this.staticBuffer = staticBuffer; + } + + @Override + WrappedBuffer getStaticBuffer() { + return staticBuffer != null ? staticBuffer : buffer; + } + + @Override + GameData createGameData(int offset) { + return new AsyncGameData(offset); + } + + class AsyncGameData extends GameData { + AsyncGameData(int myOffset) { + 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; + return getStaticBuffer().getInt(offset); + } + @Override + void setGetGroundHeight(int i, int j, int value) { + getStaticBuffer().putInt(myOffset + 3447004 + 4 * 1 * j + 4 * 256 * i, value); + } + @Override + boolean isWalkable(int i, int j) { + int offset = myOffset + 3709148 + 1 * 1 * j + 1 * 1024 * i; + return getStaticBuffer().getByte(offset) != 0; + } + @Override + void setIsWalkable(int i, int j, boolean value) { + getStaticBuffer().putByte(myOffset + 3709148 + 1 * 1 * j + 1 * 1024 * i, (byte) (value ? 1 : 0)); + } + @Override + boolean isBuildable(int i, int j) { + int offset = myOffset + 4757724 + 1 * 1 * j + 1 * 256 * i; + return getStaticBuffer().getByte(offset) != 0; + } + @Override + 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); + } + @Override + void setMapTileRegionId(int i, int j, short value) { + getStaticBuffer().putShort(myOffset + 5085404 + 2 * 1 * j + 2 * 256 * i, value); + } + @Override + short getMapSplitTilesMiniTileMask(int i) { + int offset = myOffset + 5216476 + 2 * 1 * i; + return getStaticBuffer().getShort(offset); + } + @Override + void setMapSplitTilesMiniTileMask(int i, short value) { + getStaticBuffer().putShort(myOffset + 5216476 + 2 * 1 * i, value); + } + @Override + short getMapSplitTilesRegion1(int i) { + int offset = myOffset + 5226476 + 2 * 1 * i; + return getStaticBuffer().getShort(offset); + } + @Override + void setMapSplitTilesRegion1(int i, short value) { + getStaticBuffer().putShort(myOffset + 5226476 + 2 * 1 * i, value); + } + @Override + short getMapSplitTilesRegion2(int i) { + int offset = myOffset + 5236476 + 2 * 1 * i; + return getStaticBuffer().getShort(offset); + } + @Override + void setMapSplitTilesRegion2(int i, short value) { + getStaticBuffer().putShort(myOffset + 5236476 + 2 * 1 * i, value); + } + @Override + int getRegionCount() { + int offset = myOffset + 5246476; + return getStaticBuffer().getInt(offset); + } + @Override + void setRegionCount(int value) { + getStaticBuffer().putInt(myOffset + 5246476, value); + } + @Override + 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/BotWrapperAsync.java b/src/main/java/bwapi/BotWrapperAsync.java index 17e361dc..ef82988a 100644 --- a/src/main/java/bwapi/BotWrapperAsync.java +++ b/src/main/java/bwapi/BotWrapperAsync.java @@ -29,7 +29,12 @@ class BotWrapperAsync extends BotWrapper { void startNewGame(WrappedBuffer liveData, PerformanceMetrics performanceMetrics) { frameBuffer.initialize(liveData, performanceMetrics); - super.startNewGame(liveData, performanceMetrics); + this.performanceMetrics = performanceMetrics; + AsyncClientData asyncClientData = new AsyncClientData(liveData); + botGame = new Game(asyncClientData); + botGame.setConfiguration(configuration); + botGame.botClientData().setBuffer(liveData); + gameOver = false; liveClientData.setBuffer(liveData); this.liveData = liveData; diff --git a/src/main/java/bwapi/ClientData.java b/src/main/java/bwapi/ClientData.java index 5c957b25..ef9e18e5 100644 --- a/src/main/java/bwapi/ClientData.java +++ b/src/main/java/bwapi/ClientData.java @@ -2,21 +2,28 @@ import com.sun.jna.Pointer; -final class ClientData { - private WrappedBuffer buffer; - private GameData gameData; +class ClientData { + WrappedBuffer buffer; + GameData gameData; ClientData() { - gameData = new ClientData.GameData(0); + gameData = createGameData(0); + } + GameData createGameData(int offset) { + return new ClientData.GameData(offset); } GameData gameData() { return gameData; } void setBuffer(WrappedBuffer buffer) { - this.buffer = buffer; + this.buffer = buffer; } void setPointer(Pointer pointer) { setBuffer(new WrappedBuffer(pointer, GameData.SIZE)); } + WrappedBuffer getStaticBuffer() { + return buffer; + } + class UnitCommand { static final int SIZE = 24; private int myOffset; @@ -68,7 +75,7 @@ void setExtra(int value) { } class GameData { static final int SIZE = 33017048; - private int myOffset; + protected int myOffset; GameData(int myOffset) { this.myOffset = myOffset; } @@ -784,94 +791,94 @@ class RegionData { } int getId() { int offset = myOffset + 0; - return buffer.getInt(offset); + return getStaticBuffer().getInt(offset); } void setId(int value) { - buffer.putInt(myOffset + 0, value); + getStaticBuffer().putInt(myOffset + 0, value); } int islandID() { int offset = myOffset + 4; - return buffer.getInt(offset); + return getStaticBuffer().getInt(offset); } void setIslandID(int value) { - buffer.putInt(myOffset + 4, value); + getStaticBuffer().putInt(myOffset + 4, value); } int getCenter_x() { int offset = myOffset + 8; - return buffer.getInt(offset); + return getStaticBuffer().getInt(offset); } void setCenter_x(int value) { - buffer.putInt(myOffset + 8, value); + getStaticBuffer().putInt(myOffset + 8, value); } int getCenter_y() { int offset = myOffset + 12; - return buffer.getInt(offset); + return getStaticBuffer().getInt(offset); } void setCenter_y(int value) { - buffer.putInt(myOffset + 12, value); + getStaticBuffer().putInt(myOffset + 12, value); } int getPriority() { int offset = myOffset + 16; - return buffer.getInt(offset); + return getStaticBuffer().getInt(offset); } void setPriority(int value) { - buffer.putInt(myOffset + 16, value); + getStaticBuffer().putInt(myOffset + 16, value); } int getLeftMost() { int offset = myOffset + 20; - return buffer.getInt(offset); + return getStaticBuffer().getInt(offset); } void setLeftMost(int value) { - buffer.putInt(myOffset + 20, value); + getStaticBuffer().putInt(myOffset + 20, value); } int getRightMost() { int offset = myOffset + 24; - return buffer.getInt(offset); + return getStaticBuffer().getInt(offset); } void setRightMost(int value) { - buffer.putInt(myOffset + 24, value); + getStaticBuffer().putInt(myOffset + 24, value); } int getTopMost() { int offset = myOffset + 28; - return buffer.getInt(offset); + return getStaticBuffer().getInt(offset); } void setTopMost(int value) { - buffer.putInt(myOffset + 28, value); + getStaticBuffer().putInt(myOffset + 28, value); } int getBottomMost() { int offset = myOffset + 32; - return buffer.getInt(offset); + return getStaticBuffer().getInt(offset); } void setBottomMost(int value) { - buffer.putInt(myOffset + 32, value); + getStaticBuffer().putInt(myOffset + 32, value); } int getNeighborCount() { int offset = myOffset + 36; - return buffer.getInt(offset); + return getStaticBuffer().getInt(offset); } void setNeighborCount(int value) { - buffer.putInt(myOffset + 36, value); + getStaticBuffer().putInt(myOffset + 36, value); } int getNeighbors(int i) { int offset = myOffset + 40 + 4 * 1 * i; - return buffer.getInt(offset); + return getStaticBuffer().getInt(offset); } void setNeighbors(int i, int value) { - buffer.putInt(myOffset + 40 + 4 * 1 * i, value); + getStaticBuffer().putInt(myOffset + 40 + 4 * 1 * i, value); } boolean isAccessible() { int offset = myOffset + 1064; - return buffer.getByte(offset) != 0; + return getStaticBuffer().getByte(offset) != 0; } void setIsAccessible(boolean value) { - buffer.putByte(myOffset + 1064, (byte) (value ? 1 : 0)); + getStaticBuffer().putByte(myOffset + 1064, (byte) (value ? 1 : 0)); } boolean isHigherGround() { int offset = myOffset + 1065; - return buffer.getByte(offset) != 0; + return getStaticBuffer().getByte(offset) != 0; } void setIsHigherGround(boolean value) { - buffer.putByte(myOffset + 1065, (byte) (value ? 1 : 0)); + getStaticBuffer().putByte(myOffset + 1065, (byte) (value ? 1 : 0)); } } class ForceData { @@ -2002,4 +2009,3 @@ void setReplayID(int value) { } } } - diff --git a/src/main/java/bwapi/FrameBuffer.java b/src/main/java/bwapi/FrameBuffer.java index d245f70b..82ccc8c3 100644 --- a/src/main/java/bwapi/FrameBuffer.java +++ b/src/main/java/bwapi/FrameBuffer.java @@ -11,7 +11,7 @@ * Circular buffer of game states. */ class FrameBuffer { - private static final int BUFFER_SIZE = ClientData.GameData.SIZE; + static final int BUFFER_SIZE = AsyncClientData.BUFFER_SIZE; private static final Unsafe unsafe = UnsafeTools.getUnsafe(); private WrappedBuffer liveData; @@ -30,7 +30,7 @@ class FrameBuffer { this.capacity = configuration.getAsyncFrameBufferCapacity(); this.configuration = configuration; while(dataBuffer.size() < capacity) { - dataBuffer.add(new WrappedBuffer(BUFFER_SIZE)); + dataBuffer.add(new WrappedBuffer(BUFFER_SIZE, true)); } } @@ -107,17 +107,15 @@ void enqueueFrame() { performanceMetrics.getIntentionallyBlocking().stopTiming(); } finally { lockSize.unlock(); }; - // For the first frame of the game, populate all buffers completely - // This is to ensure all buffers have access to immutable data like regions/walkability/buildability - // Afterwards, we want to shorten this process by only copying important and mutable data + // Copy dynamic game information into the current frame buffer slot. + // Static information (e.g. walkability/buildability/regions) is accessed + // directly from the main shared memory buffer by AsyncClientData. if (stepGame == 0) { - for (WrappedBuffer frameBuffer : dataBuffer) { - copyBuffer(liveData, frameBuffer, true); - } + copyBuffer(liveData, dataBuffer.get(indexGame())); } else { performanceMetrics.getCopyingToBuffer().time(() -> { WrappedBuffer dataTarget = dataBuffer.get(indexGame()); - copyBuffer(liveData, dataTarget, false); + copyBuffer(liveData, dataTarget); }); } @@ -153,45 +151,10 @@ void dequeue() { } finally { lockSize.unlock(); } } - /** - * - * @param source Address to copy from - * @param destination Address to copy to - * @param size Number of bytes to copy - */ - private void copyBuffer(WrappedBuffer source, WrappedBuffer destination, long offset, int size) { - long addressSource = source.getAddress() + offset; - long addressDestination = destination.getAddress() + offset; - unsafe.copyMemory(addressSource, addressDestination, size); - } - - void copyBuffer(WrappedBuffer source, WrappedBuffer destination, boolean copyEverything) { - /* - The speed at which we copy data into the frame buffer is a major cost of JBWAPI's asynchronous operation. - Copy times observed in the wild for the complete buffer usually range from 2.6ms - 19ms - but are prone to large amounts of variance. - - The normal Java way to execute this copy is via ByteBuffer.put(), which has reasonably good performance characteristics. - */ - - if (copyEverything) { - copyBuffer(source, destination, 0, FrameBuffer.BUFFER_SIZE); - } else { - // After the buffer has been filled the first time, - // we can omit copying blocks of data which are unused or which don't change after game start. - // These blocks account for *most* of the 33MB shared memory, - // so omitting them drastically reduces the copy duration - final int STATICTILES_START = 3447004; // getGroundHeight, isWalkable, isBuildable - final int STATICTILES_END = 4823260; - final int REGION_START = 5085404; // getMapTileRegionId, ..., getRegions - final int REGION_END = 10586480; - final int STRINGSSHAPES_START = 10962632; // getStringCount, ... getShapes - final int STRINGSHAPES_END = 32242636; - final int UNITFINDER_START = 32962644; - copyBuffer(source, destination, 0, STATICTILES_START); - copyBuffer(source, destination, STATICTILES_END, REGION_START - STATICTILES_END); - copyBuffer(source, destination, REGION_END, STRINGSSHAPES_START - REGION_END); - copyBuffer(source, destination, STRINGSHAPES_END, UNITFINDER_START - STRINGSHAPES_END); - } + void copyBuffer(WrappedBuffer source, WrappedBuffer destination) { + unsafe.copyMemory(source.getAddress() + 0, destination.getAddress() + AsyncClientData.DEST_SLICE1_OFFSET, AsyncClientData.DYNAMIC_SLICE1_SIZE); + unsafe.copyMemory(source.getAddress() + AsyncClientData.STATICTILES_END, destination.getAddress() + AsyncClientData.DEST_SLICE2_OFFSET, AsyncClientData.DYNAMIC_SLICE2_SIZE); + unsafe.copyMemory(source.getAddress() + AsyncClientData.REGION_END, destination.getAddress() + AsyncClientData.DEST_SLICE3_OFFSET, AsyncClientData.DYNAMIC_SLICE3_SIZE); + unsafe.copyMemory(source.getAddress() + AsyncClientData.STRINGSHAPES_END, destination.getAddress() + AsyncClientData.DEST_SLICE4_OFFSET, AsyncClientData.DYNAMIC_SLICE4_SIZE); } } diff --git a/src/main/java/bwapi/Game.java b/src/main/java/bwapi/Game.java index 17a5a3ca..7fee0c1f 100644 --- a/src/main/java/bwapi/Game.java +++ b/src/main/java/bwapi/Game.java @@ -106,7 +106,11 @@ public final class Game { final SideEffectQueue sideEffects = new SideEffectQueue(); Game() { - clientData = new ClientData(); + this(new ClientData()); + } + + Game(ClientData clientData) { + this.clientData = clientData; } void setConfiguration(BWClientConfiguration configuration) { diff --git a/src/main/java/bwapi/WrappedBuffer.java b/src/main/java/bwapi/WrappedBuffer.java index fa9fe809..da6ce166 100644 --- a/src/main/java/bwapi/WrappedBuffer.java +++ b/src/main/java/bwapi/WrappedBuffer.java @@ -11,17 +11,28 @@ class WrappedBuffer { private final ByteBuffer buffer; private final long address; + private final boolean packed; private static final Unsafe unsafe = UnsafeTools.getUnsafe(); WrappedBuffer(final int size) { + this(size, false); + } + + WrappedBuffer(final int size, final boolean packed) { buffer = ByteBuffer.allocateDirect(size); address = UnsafeTools.getAddress(buffer); + this.packed = packed; } WrappedBuffer(final Pointer pointer, final int size) { this.buffer = pointer.getByteBuffer(0, size); this.address = Pointer.nativeValue(pointer); + this.packed = false; + } + + boolean isPacked() { + return packed; } byte getByte(final int offset) { diff --git a/src/test/java/DumpToClient.java b/src/test/java/DumpToClient.java index 16ef449f..0272ceed 100644 --- a/src/test/java/DumpToClient.java +++ b/src/test/java/DumpToClient.java @@ -1,6 +1,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; @@ -91,11 +92,16 @@ public static void main(String[] args) throws IOException { try (PrintWriter out = new PrintWriter(sw)) { out.println("package bwapi;"); out.println(""); - out.println("final class ClientData {"); - out.println(" private WrappedBuffer buffer;"); - out.println(" private GameData gameData;"); + out.println("import com.sun.jna.Pointer;"); + out.println(""); + out.println("class ClientData {"); + out.println(" WrappedBuffer buffer;"); + out.println(" GameData gameData;"); out.println(" ClientData() {"); - out.println(" gameData = new ClientData.GameData(0);"); + out.println(" gameData = createGameData(0);"); + out.println(" }"); + out.println(" GameData createGameData(int offset) {"); + out.println(" return new ClientData.GameData(offset);"); out.println(" }"); out.println(" GameData gameData() {"); out.println(" return gameData;"); @@ -106,174 +112,300 @@ public static void main(String[] args) throws IOException { out.println(" void setPointer(Pointer pointer) {"); out.println(" setBuffer(new WrappedBuffer(pointer, GameData.SIZE));"); out.println(" }"); + out.println(" WrappedBuffer getStaticBuffer() {"); + out.println(" return buffer;"); + out.println(" }"); out.println(""); structs.values().forEach(s -> { + String bufName = s.name.equals("RegionData") ? "getStaticBuffer()" : "buffer"; out.printf(" class %s {\n", s.name); out.printf(" static final int SIZE = %d;\n", s.size); - out.println(" private int myOffset;"); + // GameData.myOffset needs to be accessible from AsyncGameData subclass + String access = s.name.equals("GameData") ? "protected" : "private"; + out.printf(" %s int myOffset;\n", access); out.printf(" %s(int myOffset) {\n", s.name); out.println(" this.myOffset = myOffset;"); out.println(" }"); - s.variables.forEach(v -> { - out.print(" "); - switch (v.type) { - case INT: - out.print("int"); - break; - case CHAR: - if (v.arraySizes.isEmpty()) { - out.print("char"); - } else { - out.print("String"); - } - break; - case ENUM: - out.print(v.enumName); - break; - case DOUBLE: - out.print("double"); - break; - case STRUCT: - out.print(v.structRef.name); - break; - case BOOLEAN: - out.print("boolean"); - break; - case UNSIGNED_INT: - out.print("int"); - break; - case UNSIGNED_SHORT: - out.print("short"); - break; - } - String camelCaseName = - v.name.substring(0, 1).toUpperCase() + v.name.substring(1); - if (v.name.startsWith("is") || v.name.startsWith("get")) { - out.printf(" %s(", v.name); - } else { - out.printf(" get%s(", camelCaseName); - } - List params = new ArrayList<>(); - String offsetString; - int arrayIndices = v.arraySizes.size(); - if (v.type == Type.CHAR) { - arrayIndices--; - } - if (arrayIndices > 0) { - List index = new ArrayList<>(); - int offset = 1; - for (int i = v.arraySizes.size() - 1; i >= 0; i--) { - int size = 1; - switch (v.type) { - case UNSIGNED_SHORT: - size *= 2; - break; - case UNSIGNED_INT: - case INT: - case ENUM: - size *= 4; - break; - case STRUCT: - size *= v.structRef.size; - break; - } - if (i < arrayIndices) { - params.add("int " + (char) ('i' + arrayIndices - 1 - i)); - index.add( - size + " * " + offset + " * " + (char) ('i' + i)); - } - offset *= v.arraySizes.get(i); - } - offsetString = "myOffset + " + v.offset + " + " + String.join(" + ", index); - } else { - offsetString = "myOffset + " + v.offset; - } - String paramString = String.join(", ", params); - out.printf("%s) {\n", paramString); - out.printf(" int offset = %s;\n", offsetString); - out.print(" return "); - switch (v.type) { - case UNSIGNED_INT: - case INT: - out.print("buffer.getInt(offset)"); - break; - case CHAR: - out.printf("buffer.getString(offset, %d)", - v.arraySizes.get(v.arraySizes.size() - 1)); - break; - case ENUM: - out.print(v.enumName + ".idToEnum[buffer.getInt(offset)]"); - break; - case DOUBLE: - out.print("buffer.getDouble(offset)"); - break; - case STRUCT: - out.printf("new %s(offset)", v.structRef.name); - break; - case BOOLEAN: - out.print("buffer.getByte(offset) != 0"); - break; - case UNSIGNED_SHORT: - out.print("buffer.getShort(offset)"); - break; - } - out.println(";"); - out.println(" }"); - - if (v.type != Type.STRUCT && (v.type != Type.CHAR || !v.arraySizes.isEmpty())) { - if (paramString.isEmpty()) { - out.printf(" void set%s(", camelCaseName); - } else { - out.printf(" void set%s(%s, ", camelCaseName, paramString); - } - switch (v.type) { - case INT: - case UNSIGNED_INT: - out.println("int value) {"); - out.printf(" buffer.putInt(%s, value);\n", - offsetString); - break; - case ENUM: - out.printf("%s value) {\n", v.enumName); - out.printf(" buffer.putInt(%s, value.id);\n", - offsetString); - break; - case UNSIGNED_SHORT: - out.println("short value) {"); - out.printf(" buffer.putShort(%s, value);\n", - offsetString); - break; - case DOUBLE: - out.println("double value) {"); - out.printf(" buffer.putDouble(%s, value);\n", - offsetString); - break; - case BOOLEAN: - out.println("boolean value) {"); - out.printf( - " buffer.putByte(%s, (byte) (value ? 1 : 0));\n", - offsetString); - break; - case CHAR: - out.println("String value) {"); - int maxLength = v.arraySizes.get(v.arraySizes.size() - 1); - out.printf( - " buffer.putString(%s, %d, value);\n", - offsetString, - maxLength); - break; - } - out.println(" }"); - } - }); + s.variables.forEach(v -> generateVariable(out, s, v, bufName, null, false)); out.println(" }"); }); out.println("}"); } + String clientDataText = sw.toString().replace("\r\n", "\n").replace("\n", "\r\n"); Files.write(Paths.get("src/main/java/bwapi/ClientData.java"), - Collections.singleton(sw.toString()), + clientDataText.getBytes(StandardCharsets.UTF_8), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + + StringWriter swAsync = new StringWriter(); + try (PrintWriter out = new PrintWriter(swAsync)) { + out.println("package bwapi;"); + out.println(""); + out.println("/**"); + out.println(" * Asynchronous client data that routes static information to the main/static shared buffer"); + out.println(" * and dynamic information to the packed dynamic buffer."); + out.println(" * Automatically generated by DumpToClient."); + out.println(" */"); + out.println("class AsyncClientData extends ClientData {"); + out.println(" static final int STATICTILES_START = 3447004;"); + out.println(" static final int STATICTILES_END = 4823260;"); + out.println(" static final int REGION_START = 5085404;"); + out.println(" static final int REGION_END = 10586480;"); + out.println(" static final int STRINGSSHAPES_START = 10962632;"); + out.println(" static final int STRINGSHAPES_END = 32242636;"); + out.println(" static final int UNITFINDER_START = 32962644;"); + out.println(""); + out.println(" static final int DYNAMIC_SLICE1_SIZE = STATICTILES_START;"); + out.println(" static final int DYNAMIC_SLICE2_SIZE = REGION_START - STATICTILES_END;"); + out.println(" static final int DYNAMIC_SLICE3_SIZE = STRINGSSHAPES_START - REGION_END;"); + out.println(" static final int DYNAMIC_SLICE4_SIZE = UNITFINDER_START - STRINGSHAPES_END;"); + out.println(""); + out.println(" static final int DEST_SLICE1_OFFSET = 0;"); + out.println(" static final int DEST_SLICE2_OFFSET = DYNAMIC_SLICE1_SIZE;"); + out.println(" static final int DEST_SLICE3_OFFSET = DEST_SLICE2_OFFSET + DYNAMIC_SLICE2_SIZE;"); + out.println(" static final int DEST_SLICE4_OFFSET = DEST_SLICE3_OFFSET + DYNAMIC_SLICE3_SIZE;"); + out.println(""); + out.println(" static final int BUFFER_SIZE = DEST_SLICE4_OFFSET + DYNAMIC_SLICE4_SIZE;"); + out.println(""); + out.println(" static final int DELTA_RANGE2 = STATICTILES_END - STATICTILES_START;"); + out.println(" static final int DELTA_RANGE3 = DELTA_RANGE2 + (REGION_END - REGION_START);"); + out.println(" static final int DELTA_RANGE4 = DELTA_RANGE3 + (STRINGSHAPES_END - STRINGSSHAPES_START);"); + out.println(""); + out.println(" private WrappedBuffer staticBuffer;"); + out.println(""); + out.println(" AsyncClientData() {"); + out.println(" super();"); + out.println(" }"); + out.println(""); + out.println(" AsyncClientData(WrappedBuffer staticBuffer) {"); + out.println(" super();"); + out.println(" this.staticBuffer = staticBuffer;"); + out.println(" }"); + out.println(""); + out.println(" void setStaticBuffer(WrappedBuffer staticBuffer) {"); + out.println(" this.staticBuffer = staticBuffer;"); + out.println(" }"); + out.println(""); + out.println(" @Override"); + out.println(" WrappedBuffer getStaticBuffer() {"); + out.println(" return staticBuffer != null ? staticBuffer : buffer;"); + out.println(" }"); + out.println(""); + out.println(" @Override"); + out.println(" GameData createGameData(int offset) {"); + out.println(" return new AsyncGameData(offset);"); + out.println(" }"); + out.println(""); + out.println(" class AsyncGameData extends GameData {"); + out.println(" AsyncGameData(int myOffset) {"); + 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); + } + } + out.println(" }"); + out.println("}"); + } + String asyncClientDataText = swAsync.toString().replace("\r\n", "\n"); + Files.write(Paths.get("src/main/java/bwapi/AsyncClientData.java"), + asyncClientDataText.getBytes(StandardCharsets.UTF_8), + StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } + + private static void generateVariable(PrintWriter out, Struct s, Variable v, String bufName, String delta, boolean isOverride) { + if (isOverride) { + out.println(" @Override"); + } + out.print(" "); + switch (v.type) { + case INT: + out.print("int"); + break; + case CHAR: + if (v.arraySizes.isEmpty()) { + out.print("char"); + } else { + out.print("String"); + } + break; + case ENUM: + out.print(v.enumName); + break; + case DOUBLE: + out.print("double"); + break; + case STRUCT: + out.print(v.structRef.name); + break; + case BOOLEAN: + out.print("boolean"); + break; + case UNSIGNED_INT: + out.print("int"); + break; + case UNSIGNED_SHORT: + out.print("short"); + break; + } + String camelCaseName = + v.name.substring(0, 1).toUpperCase() + v.name.substring(1); + if (v.name.startsWith("is") || v.name.startsWith("get")) { + out.printf(" %s(", v.name); + } else { + out.printf(" get%s(", camelCaseName); + } + List params = new ArrayList<>(); + String offsetString; + int arrayIndices = v.arraySizes.size(); + if (v.type == Type.CHAR) { + arrayIndices--; + } + if (arrayIndices > 0) { + List index = new ArrayList<>(); + int offset = 1; + for (int i = v.arraySizes.size() - 1; i >= 0; i--) { + int size = 1; + switch (v.type) { + case UNSIGNED_SHORT: + size *= 2; + break; + case UNSIGNED_INT: + case INT: + case ENUM: + size *= 4; + break; + case STRUCT: + size *= v.structRef.size; + break; + } + if (i < arrayIndices) { + params.add("int " + (char) ('i' + arrayIndices - 1 - i)); + index.add( + size + " * " + offset + " * " + (char) ('i' + i)); + } + offset *= v.arraySizes.get(i); + } + if (delta != null) { + offsetString = "myOffset + " + v.offset + " - " + delta + " + " + String.join(" + ", index); + } else { + offsetString = "myOffset + " + v.offset + " + " + String.join(" + ", index); + } + } else { + if (delta != null) { + offsetString = "myOffset + " + v.offset + " - " + delta; + } else { + offsetString = "myOffset + " + v.offset; + } + } + String paramString = String.join(", ", params); + out.printf("%s) {\n", paramString); + out.printf(" int offset = %s;\n", offsetString); + out.print(" return "); + switch (v.type) { + case UNSIGNED_INT: + case INT: + out.printf("%s.getInt(offset)", bufName); + break; + case CHAR: + out.printf("%s.getString(offset, %d)", + bufName, v.arraySizes.get(v.arraySizes.size() - 1)); + break; + case ENUM: + out.printf("%s.idToEnum[%s.getInt(offset)]", v.enumName, bufName); + break; + case DOUBLE: + out.printf("%s.getDouble(offset)", bufName); + break; + case STRUCT: + out.printf("new %s(offset)", v.structRef.name); + break; + case BOOLEAN: + out.printf("%s.getByte(offset) != 0", bufName); + break; + case UNSIGNED_SHORT: + out.printf("%s.getShort(offset)", bufName); + break; + } + out.println(";"); + out.println(" }"); + + if (v.type != Type.STRUCT && (v.type != Type.CHAR || !v.arraySizes.isEmpty())) { + if (isOverride) { + out.println(" @Override"); + } + if (paramString.isEmpty()) { + out.printf(" void set%s(", camelCaseName); + } else { + out.printf(" void set%s(%s, ", camelCaseName, paramString); + } + switch (v.type) { + case INT: + case UNSIGNED_INT: + out.println("int value) {"); + out.printf(" %s.putInt(%s, value);\n", + bufName, offsetString); + break; + case ENUM: + out.printf("%s value) {\n", v.enumName); + out.printf(" %s.putInt(%s, value.id);\n", + bufName, offsetString); + break; + case UNSIGNED_SHORT: + out.println("short value) {"); + out.printf(" %s.putShort(%s, value);\n", + bufName, offsetString); + break; + case DOUBLE: + out.println("double value) {"); + out.printf(" %s.putDouble(%s, value);\n", + bufName, offsetString); + break; + case BOOLEAN: + out.println("boolean value) {"); + out.printf( + " %s.putByte(%s, (byte) (value ? 1 : 0));\n", + bufName, offsetString); + break; + case CHAR: + out.println("String value) {"); + int maxLength = v.arraySizes.get(v.arraySizes.size() - 1); + out.printf( + " %s.putString(%s, %d, value);\n", + bufName, offsetString, + maxLength); + break; + } + out.println(" }"); + } } enum Type { diff --git a/src/test/java/bwapi/AsyncClientDataTest.java b/src/test/java/bwapi/AsyncClientDataTest.java new file mode 100644 index 00000000..10981d5d --- /dev/null +++ b/src/test/java/bwapi/AsyncClientDataTest.java @@ -0,0 +1,146 @@ +package bwapi; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class AsyncClientDataTest { + + @Test + public void frameBufferAllocatesReducedMemory() { + // Full GameData buffer is ~33 MB + assertEquals(33017048, ClientData.GameData.SIZE); + + // Packed dynamic FrameBuffer is ~4.8 MB + assertEquals(4805308, FrameBuffer.BUFFER_SIZE); + + assertTrue("FrameBuffer size must be significantly smaller than full GameData size", + FrameBuffer.BUFFER_SIZE < ClientData.GameData.SIZE / 6); + } + + @Test + public void staticDataReadsFromStaticBuffer() { + WrappedBuffer mainBuffer = new WrappedBuffer(ClientData.GameData.SIZE); + WrappedBuffer dynamicBuffer = new WrappedBuffer(FrameBuffer.BUFFER_SIZE, true); + + // Populate static data in the main buffer + ClientData mainData = new ClientData(); + mainData.setBuffer(mainBuffer); + + mainData.gameData().setGetGroundHeight(10, 20, 2); + mainData.gameData().setIsWalkable(15, 25, true); + mainData.gameData().setIsBuildable(12, 22, true); + mainData.gameData().setMapTileRegionId(5, 7, (short) 42); + mainData.gameData().setRegionCount(3); + mainData.gameData().getRegions(0).setId(101); + mainData.gameData().getRegions(0).setIslandID(999); + mainData.gameData().getRegions(0).setCenter_x(320); + mainData.gameData().getRegions(0).setCenter_y(240); + + // AsyncClientData reads static data from staticBuffer and dynamic data from dynamicBuffer + AsyncClientData asyncData = new AsyncClientData(mainBuffer); + asyncData.setBuffer(dynamicBuffer); + + assertEquals(2, asyncData.gameData().getGroundHeight(10, 20)); + assertTrue(asyncData.gameData().isWalkable(15, 25)); + assertTrue(asyncData.gameData().isBuildable(12, 22)); + assertEquals((short) 42, asyncData.gameData().getMapTileRegionId(5, 7)); + assertEquals(3, asyncData.gameData().getRegionCount()); + + ClientData.RegionData region = asyncData.gameData().getRegions(0); + assertEquals(101, region.getId()); + assertEquals(999, region.islandID()); + assertEquals(320, region.getCenter_x()); + assertEquals(240, region.getCenter_y()); + } + + @Test + public void dynamicDataCopiesAndReadsCorrectlyAcrossRanges() { + WrappedBuffer mainBuffer = new WrappedBuffer(ClientData.GameData.SIZE); + WrappedBuffer packedBuffer = new WrappedBuffer(FrameBuffer.BUFFER_SIZE, true); + + // Setup mock source data in mainBuffer + ClientData mainData = new ClientData(); + mainData.setBuffer(mainBuffer); + + // Range 1 (units, frameCount) + mainData.gameData().setFrameCount(1234); + mainData.gameData().getUnits(0).setHitPoints(40); + mainData.gameData().getUnits(0).setPositionX(640); + mainData.gameData().getUnits(0).setPositionY(480); + + // Range 2 (dynamic tiles) + mainData.gameData().setIsVisible(5, 10, true); + mainData.gameData().setIsExplored(5, 10, true); + mainData.gameData().setHasCreep(5, 10, true); + mainData.gameData().setIsOccupied(5, 10, true); + + // Range 3 (events, game state) + mainData.gameData().setIsInGame(true); + mainData.gameData().setStartLocationCount(2); + mainData.gameData().getStartLocations(0).setX(16); + mainData.gameData().getStartLocations(0).setY(32); + mainData.gameData().setEventCount(1); + mainData.gameData().getEvents(0).setType(EventType.MatchFrame); + mainData.gameData().setEventStringCount(1); + mainData.gameData().setEventStrings(0, "TestMessage"); + + // Range 4 (commands) + mainData.gameData().setCommandCount(5); + mainData.gameData().setUnitCommandCount(10); + + // Perform packed frame buffer copy + BWClientConfiguration config = new BWClientConfiguration.Builder().build(); + FrameBuffer frameBuffer = new FrameBuffer(config); + frameBuffer.copyBuffer(mainBuffer, packedBuffer); + + // Wire AsyncClientData + AsyncClientData asyncData = new AsyncClientData(mainBuffer); + asyncData.setBuffer(packedBuffer); + + // Verify Range 1 + assertEquals(1234, asyncData.gameData().getFrameCount()); + assertEquals(40, asyncData.gameData().getUnits(0).getHitPoints()); + assertEquals(640, asyncData.gameData().getUnits(0).getPositionX()); + assertEquals(480, asyncData.gameData().getUnits(0).getPositionY()); + + // Verify Range 2 + assertTrue(asyncData.gameData().isVisible(5, 10)); + assertTrue(asyncData.gameData().isExplored(5, 10)); + assertTrue(asyncData.gameData().getHasCreep(5, 10)); + assertTrue(asyncData.gameData().isOccupied(5, 10)); + assertFalse(asyncData.gameData().isVisible(6, 10)); + + // Verify Range 3 + assertTrue(asyncData.gameData().isInGame()); + assertEquals(2, asyncData.gameData().getStartLocationCount()); + assertEquals(16, asyncData.gameData().getStartLocations(0).getX()); + assertEquals(32, asyncData.gameData().getStartLocations(0).getY()); + assertEquals(1, asyncData.gameData().getEventCount()); + assertEquals(EventType.MatchFrame, asyncData.gameData().getEvents(0).getType()); + assertEquals("TestMessage", asyncData.gameData().getEventStrings(0)); + + // Verify Range 4 + assertEquals(5, asyncData.gameData().getCommandCount()); + assertEquals(10, asyncData.gameData().getUnitCommandCount()); + } + + @Test + public void asyncClientDataFallsBackCorrectlyWithFullBuffer() { + // Tests asyncUnsafe mode where bot is placed directly on the full 33MB liveData buffer + WrappedBuffer fullBuffer = new WrappedBuffer(ClientData.GameData.SIZE); + assertFalse(fullBuffer.isPacked()); + + ClientData syncData = new ClientData(); + syncData.setBuffer(fullBuffer); + syncData.gameData().setIsVisible(10, 20, true); + syncData.gameData().setIsInGame(true); + + AsyncClientData asyncData = new AsyncClientData(fullBuffer); + asyncData.setBuffer(fullBuffer); // Unpacked full buffer + + // Should read with zero delta offset and match syncData + assertTrue(asyncData.gameData().isVisible(10, 20)); + assertTrue(asyncData.gameData().isInGame()); + } +} diff --git a/src/test/java/bwapi/SynchronizationEnvironment.java b/src/test/java/bwapi/SynchronizationEnvironment.java index fc4ecc11..dc7ab100 100644 --- a/src/test/java/bwapi/SynchronizationEnvironment.java +++ b/src/test/java/bwapi/SynchronizationEnvironment.java @@ -88,7 +88,7 @@ void runGame(int onEndFrame) { if (configuration.getAsync()) { final long MEGABYTE = 1024 * 1024; long memoryFree = Runtime.getRuntime().freeMemory() / MEGABYTE; - long memoryRequired = configuration.getAsyncFrameBufferCapacity() * ClientData.GameData.SIZE / MEGABYTE; + long memoryRequired = (long) configuration.getAsyncFrameBufferCapacity() * FrameBuffer.BUFFER_SIZE / MEGABYTE; assertTrue( "Unit test needs to be run with sufficient memory to allocate frame buffer. Has " + memoryFree