diff --git a/src/main/java/de/extio/game_engine/module/ModuleExecutorImpl.java b/src/main/java/de/extio/game_engine/module/ModuleExecutorImpl.java index dd3b1ae..7ff6bda 100644 --- a/src/main/java/de/extio/game_engine/module/ModuleExecutorImpl.java +++ b/src/main/java/de/extio/game_engine/module/ModuleExecutorImpl.java @@ -20,6 +20,9 @@ public class ModuleExecutorImpl implements ModuleExecutor { @Override public void execute() { + + // TODO Subscriptions for modules to run only on certain conditions + this.runTasks(module -> () -> { if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) { this.invokeSafe(clientModule, AbstractClientModule::runUiPre); diff --git a/src/main/java/de/extio/game_engine/renderer/Renderer.java b/src/main/java/de/extio/game_engine/renderer/Renderer.java index 76a3742..d2fd9fa 100644 --- a/src/main/java/de/extio/game_engine/renderer/Renderer.java +++ b/src/main/java/de/extio/game_engine/renderer/Renderer.java @@ -1,14 +1,10 @@ package de.extio.game_engine.renderer; -import java.util.List; - -import de.extio.game_engine.renderer.model.RenderingBo; - public interface Renderer { void show(); - void run(List renderingBOs) throws InterruptedException; + void run() throws InterruptedException; void reset(); diff --git a/src/main/java/de/extio/game_engine/renderer/RendererAutoConfiguration.java b/src/main/java/de/extio/game_engine/renderer/RendererAutoConfiguration.java index d7cd659..8369fc0 100644 --- a/src/main/java/de/extio/game_engine/renderer/RendererAutoConfiguration.java +++ b/src/main/java/de/extio/game_engine/renderer/RendererAutoConfiguration.java @@ -15,6 +15,10 @@ import org.springframework.context.annotation.Bean; import de.extio.game_engine.event.EventService; import de.extio.game_engine.keyboard.KeycodeRegistry; import de.extio.game_engine.renderer.model.RenderingBo; +import de.extio.game_engine.renderer.work.RendererWorkingSet; +import de.extio.game_engine.renderer.work.RendererWorkingSetImpl; +import de.extio.game_engine.renderer.work.RenderingBoPool; +import de.extio.game_engine.renderer.work.RenderingBoPoolImpl; import de.extio.game_engine.storage.StorageService; @AutoConfiguration @@ -36,6 +40,12 @@ public class RendererAutoConfiguration { return new RenderingBoPoolImpl(mapping); } + + @Bean + @ConditionalOnMissingBean + RendererWorkingSet rendererWorkingSet(final RenderingBoPool rendererBoPool) { + return new RendererWorkingSetImpl(rendererBoPool); + } @Bean RendererData rendererData(final ApplicationContext applicationContext, @@ -44,9 +54,10 @@ public class RendererAutoConfiguration { final RenderingBoPool renderingBoPool, final KeycodeRegistry keycodeRegistry, final EventService eventService, - final StorageService storageService) { + final StorageService storageService, + final RendererWorkingSet rendererWorkingSet) { - final var rendererData = new RendererData(applicationContext, renderer, rendererControl, renderingBoPool, keycodeRegistry, eventService, storageService); + final var rendererData = new RendererData(applicationContext, renderer, rendererControl, renderingBoPool, keycodeRegistry, eventService, storageService, rendererWorkingSet); renderer.setRendererData(rendererData); rendererControl.setRendererData(rendererData); renderingBoPool.setRendererData(rendererData); diff --git a/src/main/java/de/extio/game_engine/renderer/RendererData.java b/src/main/java/de/extio/game_engine/renderer/RendererData.java index b90e34c..d36d02d 100644 --- a/src/main/java/de/extio/game_engine/renderer/RendererData.java +++ b/src/main/java/de/extio/game_engine/renderer/RendererData.java @@ -9,6 +9,8 @@ import de.extio.game_engine.event.EventService; import de.extio.game_engine.keyboard.KeycodeRegistry; import de.extio.game_engine.renderer.model.options.UiOptions; import de.extio.game_engine.renderer.model.options.VideoOptions; +import de.extio.game_engine.renderer.work.RendererWorkingSet; +import de.extio.game_engine.renderer.work.RenderingBoPool; import de.extio.game_engine.storage.StorageService; public class RendererData { @@ -32,10 +34,12 @@ public class RendererData { private final EventService eventService; private final StorageService storageService; + + private final RendererWorkingSet rendererWorkingSet; private RendererLoop rendererLoop; - public RendererData(final ApplicationContext applicationContext, final Renderer renderer, final RendererControl rendererControl, final RenderingBoPool renderingBoPool, final KeycodeRegistry keycodeRegistry, final EventService eventService, final StorageService storageService) { + public RendererData(final ApplicationContext applicationContext, final Renderer renderer, final RendererControl rendererControl, final RenderingBoPool renderingBoPool, final KeycodeRegistry keycodeRegistry, final EventService eventService, final StorageService storageService, final RendererWorkingSet rendererWorkingSet) { this.applicationContext = applicationContext; this.renderer = renderer; this.rendererControl = rendererControl; @@ -43,6 +47,7 @@ public class RendererData { this.keycodeRegistry = keycodeRegistry; this.eventService = eventService; this.storageService = storageService; + this.rendererWorkingSet = rendererWorkingSet; } public ApplicationContext getApplicationContext() { @@ -96,4 +101,8 @@ public class RendererData { public StorageService getStorageService() { return storageService; } + + public RendererWorkingSet getRendererWorkingSet() { + return rendererWorkingSet; + } } diff --git a/src/main/java/de/extio/game_engine/renderer/RendererLoop.java b/src/main/java/de/extio/game_engine/renderer/RendererLoop.java index fd0af0b..10cdf74 100644 --- a/src/main/java/de/extio/game_engine/renderer/RendererLoop.java +++ b/src/main/java/de/extio/game_engine/renderer/RendererLoop.java @@ -1,7 +1,6 @@ package de.extio.game_engine.renderer; import java.awt.event.KeyEvent; -import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; @@ -41,7 +40,8 @@ public class RendererLoop implements Runnable { while (!this.shutdown) { try { this.rendererData.nextFrame(); - this.rendererData.getRenderer().run(new ArrayList<>()); // TODO: Pass actual renderables + // TODO run modules here + this.rendererData.getRenderer().run(); exceptionCount = 0; } catch (final InterruptedException e) { diff --git a/src/main/java/de/extio/game_engine/renderer/container/EnhancedWindow.java b/src/main/java/de/extio/game_engine/renderer/container/EnhancedWindow.java index 28b3363..511735e 100644 --- a/src/main/java/de/extio/game_engine/renderer/container/EnhancedWindow.java +++ b/src/main/java/de/extio/game_engine/renderer/container/EnhancedWindow.java @@ -3,7 +3,7 @@ package de.extio.game_engine.renderer.container; import java.util.function.Function; import de.extio.game_engine.renderer.RendererControl; -import de.extio.game_engine.renderer.RenderingBoPool; +import de.extio.game_engine.renderer.work.RenderingBoPool; import de.extio.game_engine.spatial2.model.Area2; /** diff --git a/src/main/java/de/extio/game_engine/renderer/container/ScrollBar.java b/src/main/java/de/extio/game_engine/renderer/container/ScrollBar.java index 86d09c0..ca7121e 100644 --- a/src/main/java/de/extio/game_engine/renderer/container/ScrollBar.java +++ b/src/main/java/de/extio/game_engine/renderer/container/ScrollBar.java @@ -6,13 +6,13 @@ import java.util.function.Predicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import de.extio.game_engine.renderer.RenderingBoPool; import de.extio.game_engine.renderer.model.RenderingBo; import de.extio.game_engine.renderer.model.RenderingBoLayer; import de.extio.game_engine.renderer.model.bo.ControlRenderingBo; import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.SliderControl; import de.extio.game_engine.renderer.model.color.RgbaColor; import de.extio.game_engine.renderer.model.event.UiControlEvent; +import de.extio.game_engine.renderer.work.RenderingBoPool; import de.extio.game_engine.spatial2.model.Area2; import de.extio.game_engine.spatial2.model.CoordI2; @@ -57,8 +57,8 @@ public class ScrollBar { public void render(final List renderingBo, final Area2 renderingArea, final int numElements, final RenderingBoPool renderingBoPool) { this.lastNumElements = numElements; - final var bo = renderingBoPool.acquire(ControlRenderingBo.class) - .setId(this.name) + final var bo = renderingBoPool.acquire(this.name + "_Slider", ControlRenderingBo.class) + .setControlId(this.name) .setType(SliderControl.class) .setCustomData(Boolean.FALSE) .setCustomData2(Double.valueOf(1.0D - this.scrollPosition)) diff --git a/src/main/java/de/extio/game_engine/renderer/container/Window.java b/src/main/java/de/extio/game_engine/renderer/container/Window.java index 02f9ddb..284f023 100644 --- a/src/main/java/de/extio/game_engine/renderer/container/Window.java +++ b/src/main/java/de/extio/game_engine/renderer/container/Window.java @@ -9,12 +9,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.extio.game_engine.renderer.RendererControl; -import de.extio.game_engine.renderer.RenderingBoPool; import de.extio.game_engine.renderer.model.RenderingBo; import de.extio.game_engine.renderer.model.RenderingBoLayer; import de.extio.game_engine.renderer.model.bo.ControlRenderingBo; import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.WindowCloseButtonControl; import de.extio.game_engine.renderer.model.color.RgbaColor; +import de.extio.game_engine.renderer.work.RenderingBoPool; import de.extio.game_engine.renderer.model.bo.DrawWindowRenderingBo; import de.extio.game_engine.spatial2.WorldUtils2; import de.extio.game_engine.spatial2.model.Area2; @@ -173,7 +173,7 @@ public class Window implements HasPositionAndDimension2, AutoCloseable { this.updateArea(); } - var bo = this.renderingBoPool.acquire(DrawWindowRenderingBo.class) + var bo = this.renderingBoPool.acquire(this.name + "_Window", DrawWindowRenderingBo.class) .setThickBorder(this.draggable) .withDimensionAbsolute(this.area.getDimension()) .withPositionAbsoluteAnchorTopLeft(this.area.getPosition()) @@ -184,9 +184,9 @@ public class Window implements HasPositionAndDimension2, AutoCloseable { consumer.accept(renderingBo, bo); if (this.drawCloseButton) { - bo = this.renderingBoPool.acquire(ControlRenderingBo.class) + bo = this.renderingBoPool.acquire(this.name + "_Close", ControlRenderingBo.class) .setType(WindowCloseButtonControl.class) - .setId(this.name + "_Close") + .setControlId(this.name + "_Close") .setCustomData(this.draggable) .setEnabled(true) .setVisible(true) diff --git a/src/main/java/de/extio/game_engine/renderer/g2d/G2DRenderer.java b/src/main/java/de/extio/game_engine/renderer/g2d/G2DRenderer.java index 6e1afa7..4cd529b 100644 --- a/src/main/java/de/extio/game_engine/renderer/g2d/G2DRenderer.java +++ b/src/main/java/de/extio/game_engine/renderer/g2d/G2DRenderer.java @@ -8,9 +8,8 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.util.HashSet; +import java.util.ArrayList; import java.util.List; -import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -25,7 +24,6 @@ import de.extio.game_engine.renderer.RendererData; import de.extio.game_engine.renderer.g2d.bo.rendering.G2DAbstractRenderingBo; import de.extio.game_engine.renderer.g2d.bo.rendering.G2DDrawBackground; import de.extio.game_engine.renderer.g2d.bo.rendering.G2DDrawFpsHistory; -import de.extio.game_engine.renderer.g2d.bo.rendering.G2DDrawImage; import de.extio.game_engine.renderer.g2d.control.G2DDrawControl; import de.extio.game_engine.renderer.g2d.control.G2DDrawControlTooltip; import de.extio.game_engine.renderer.model.RenderingBo; @@ -43,10 +41,12 @@ public class G2DRenderer implements Renderer { private final RingBuffer fpsHistory = new RingBuffer<>(90); - private final Set> releasedBoClasses = new HashSet<>(); - private final StringBuilder fpsStringBuilder = new StringBuilder(15); + private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); + + private final List renderingBOs = new ArrayList<>(); + private volatile G2DMainFrame mainFrame; private RendererData rendererData; @@ -61,8 +61,6 @@ public class G2DRenderer implements Renderer { private long frameDur; - private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); - private String title; private volatile boolean takeScreenshot; @@ -104,7 +102,7 @@ public class G2DRenderer implements Renderer { public void reset() { G2DDrawControl.reset(); } - + @Override public void shutdown() { LOGGER.info("shutdown()"); @@ -125,7 +123,7 @@ public class G2DRenderer implements Renderer { } @Override - public void run(final List renderingBOs) throws InterruptedException { + public void run() throws InterruptedException { try { this.rwLock.readLock().lockInterruptibly(); try { @@ -150,8 +148,9 @@ public class G2DRenderer implements Renderer { screenshotGraphics.setRenderingHints(G2DRenderingHintFactory.createDefault()); } - this.addStaticRenderingBOs(renderingBOs); - renderingBOs.sort((bo0, bo1) -> bo0.getLayer().compareTo(bo1.getLayer())); + this.addStaticRenderingBOs(); + this.rendererData.getRendererWorkingSet().getLiveSet(this.renderingBOs); + this.renderingBOs.sort((bo0, bo1) -> bo0.getLayer().compareTo(bo1.getLayer())); Graphics2D screenGraphics = null; try { @@ -166,11 +165,11 @@ public class G2DRenderer implements Renderer { screenGraphics.setColor(Color.BLACK); screenGraphics.fillRect(0, 0, viewPortDimension.getX(), viewPortDimension.getY()); - for (final RenderingBo renderingBO : renderingBOs) { - if (renderingBO instanceof G2DAbstractRenderingBo) { - ((G2DAbstractRenderingBo) renderingBO).render(screenGraphics, this.rendererData.getRendererControl().getScaleFactor(), false); - if (takeScreenshot_ && ((G2DAbstractRenderingBo) renderingBO).isScreenshotRelevant()) { - ((G2DAbstractRenderingBo) renderingBO).render(screenshotGraphics, this.rendererData.getRendererControl().getScaleFactor(), true); + for (final RenderingBo renderingBO : this.renderingBOs) { + if (renderingBO instanceof final G2DAbstractRenderingBo g2dAbstractRenderingBo) { + g2dAbstractRenderingBo.render(screenGraphics, this.rendererData.getRendererControl().getScaleFactor(), false); + if (takeScreenshot_ && g2dAbstractRenderingBo.isScreenshotRelevant()) { + g2dAbstractRenderingBo.render(screenshotGraphics, this.rendererData.getRendererControl().getScaleFactor(), true); } } } @@ -192,14 +191,6 @@ public class G2DRenderer implements Renderer { if (screenGraphics != null) { screenGraphics.dispose(); } - - this.releasedBoClasses.clear(); - for (final RenderingBo renderingBO : renderingBOs) { - if (this.releasedBoClasses.add(renderingBO.getClass()) && renderingBO instanceof final G2DAbstractRenderingBo g2dRenderingBo) { - g2dRenderingBo.closeStatic(); - } - this.rendererData.getRenderingBoPool().release(renderingBO); - } } if (takeScreenshot_) { @@ -207,6 +198,7 @@ public class G2DRenderer implements Renderer { } } finally { + this.renderingBOs.clear(); this.rwLock.readLock().unlock(); } } @@ -217,13 +209,14 @@ public class G2DRenderer implements Renderer { } } - private void addStaticRenderingBOs(final List renderingBOs) { - renderingBOs.add(this.rendererData.getRenderingBoPool().acquire(G2DDrawBackground.class)); - renderingBOs.add(this.rendererData.getRenderingBoPool().acquire(G2DDrawControlTooltip.class)); - this.drawStatistics(renderingBOs); + private void addStaticRenderingBOs() { + this.rendererData.getRendererWorkingSet().add("G2DRenderer", this.rendererData.getRenderingBoPool().acquire("G2DRenderer_background", G2DDrawBackground.class)); + this.rendererData.getRendererWorkingSet().add("G2DRenderer", this.rendererData.getRenderingBoPool().acquire("G2DRenderer_control", G2DDrawControlTooltip.class)); + this.drawStatistics(); + this.rendererData.getRendererWorkingSet().commit("G2DRenderer", false); } - private void drawStatistics(final List renderingBOs) { + private void drawStatistics() { if (!this.rendererData.getUiOptions().isDrawFps()) { return; } @@ -236,20 +229,20 @@ public class G2DRenderer implements Renderer { fpsStringBuilder.append(this.frameDur); fpsStringBuilder.append("ms"); - final var drawFont = this.rendererData.getRenderingBoPool().acquire(DrawFontRenderingBo.class) + final var drawFont = this.rendererData.getRenderingBoPool().acquire("g2DRenderer_fpsText", DrawFontRenderingBo.class) .setText(fpsStringBuilder.toString()) .setSize(14) .setColor(RgbaColor.WHITE) .setLayer(RenderingBoLayer.TOP) .withPositionAbsoluteAnchorTopRight(100, 35); - renderingBOs.add(drawFont); + this.rendererData.getRendererWorkingSet().add("G2DRenderer", drawFont); this.fpsHistory.add(Integer.valueOf((int) this.frameDur)); - final var drawFpsHistory = this.rendererData.getRenderingBoPool().acquire(G2DDrawFpsHistory.class) + final var drawFpsHistory = this.rendererData.getRenderingBoPool().acquire("g2DRenderer_fpsHistory", G2DDrawFpsHistory.class) .setHistory(this.fpsHistory) .setLayer(RenderingBoLayer.TOP) .withPositionAbsoluteAnchorTopRight(100, 50); - renderingBOs.add(drawFpsHistory); + this.rendererData.getRendererWorkingSet().add("G2DRenderer", drawFpsHistory); } private void frameCap() throws InterruptedException { diff --git a/src/main/java/de/extio/game_engine/renderer/g2d/bo/rendering/G2DAbstractRenderingBo.java b/src/main/java/de/extio/game_engine/renderer/g2d/bo/rendering/G2DAbstractRenderingBo.java index 969edce..efe61f1 100644 --- a/src/main/java/de/extio/game_engine/renderer/g2d/bo/rendering/G2DAbstractRenderingBo.java +++ b/src/main/java/de/extio/game_engine/renderer/g2d/bo/rendering/G2DAbstractRenderingBo.java @@ -10,13 +10,14 @@ public abstract class G2DAbstractRenderingBo extends AbstractRenderingBo { public G2DAbstractRenderingBo(final RenderingBoLayer layer) { super(layer); } - + public boolean isScreenshotRelevant() { return true; } public abstract void render(Graphics2D graphics, double scaleFactor, boolean force); + @Override public void closeStatic() { } diff --git a/src/main/java/de/extio/game_engine/renderer/g2d/bo/rendering/G2DDrawBackground.java b/src/main/java/de/extio/game_engine/renderer/g2d/bo/rendering/G2DDrawBackground.java index bc0d64c..1ca6468 100644 --- a/src/main/java/de/extio/game_engine/renderer/g2d/bo/rendering/G2DDrawBackground.java +++ b/src/main/java/de/extio/game_engine/renderer/g2d/bo/rendering/G2DDrawBackground.java @@ -80,9 +80,6 @@ public class G2DDrawBackground extends G2DAbstractRenderingBo { this.loadImage(graphics, index, backgroundName); } if (G2DDrawBackground.BACKGROUND_IMAGE == null) { - if (index == 0) { - this.fillBlack(graphics, viewPort); - } return; } else if (BACKGROUND_IMAGE.validate(graphics.getDeviceConfiguration()) != VolatileImage.IMAGE_OK) { @@ -162,11 +159,6 @@ public class G2DDrawBackground extends G2DAbstractRenderingBo { } } - private void fillBlack(final Graphics2D graphics, final CoordI2 viewPort) { - graphics.setColor(Color.BLACK); - graphics.fillRect(0, 0, viewPort.getX(), viewPort.getY()); - } - private void loadImage(final Graphics2D graphics, final int index, final String backgroundName) { if (G2DDrawBackground.BACKGROUND_IMAGE != null) { G2DDrawBackground.BACKGROUND_IMAGE.flush(); diff --git a/src/main/java/de/extio/game_engine/renderer/g2d/control/G2DDrawControl.java b/src/main/java/de/extio/game_engine/renderer/g2d/control/G2DDrawControl.java index 191b3d6..79d7e33 100644 --- a/src/main/java/de/extio/game_engine/renderer/g2d/control/G2DDrawControl.java +++ b/src/main/java/de/extio/game_engine/renderer/g2d/control/G2DDrawControl.java @@ -39,7 +39,7 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen public static Map> CONTROL_GROUPS = new HashMap<>(); - private String id; + private String controlId; private Class clazz; @@ -68,14 +68,14 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen } @Override - public ControlRenderingBo setId(final String id) { - this.id = id; + public ControlRenderingBo setControlId(final String id) { + this.controlId = id; return this; } @Override - public String getId() { - return this.id; + public String getControlId() { + return this.controlId; } @Override @@ -146,7 +146,7 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen @Override public void render(final Graphics2D graphics, final double scaleFactor, final boolean force) { - var control = (G2DBaseControlImpl) CACHED_CONTROLS.get(this.id); + var control = (G2DBaseControlImpl) CACHED_CONTROLS.get(this.controlId); if (control == null) { if (this.clazz == ButtonControl.class) { control = new G2DButtonControlImpl(); @@ -189,7 +189,7 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen control.setScaleFactor(scaleFactor) .setControlGroup(this.controlGroup) .setRendererData(this.rendererData) - .setId(this.id); + .setControlId(this.controlId); if (this.clazz == TextfieldControl.class && this.customData instanceof Boolean) { ((TextfieldControl) control).setMultiLine(((Boolean) this.customData).booleanValue()); } @@ -197,7 +197,7 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen control.build(); - CACHED_CONTROLS.put(this.id, control); + CACHED_CONTROLS.put(this.controlId, control); if (this.controlGroup != null && !this.controlGroup.isEmpty()) { var controls = CONTROL_GROUPS.get(this.controlGroup); if (controls == null) { @@ -315,7 +315,7 @@ public class G2DDrawControl extends G2DAbstractRenderingBo implements ControlRen public void close() throws Exception { super.close(); - this.id = null; + this.controlId = null; this.clazz = null; this.caption = null; this.controlGroup = null; diff --git a/src/main/java/de/extio/game_engine/renderer/g2d/control/impl/G2DBaseControlImpl.java b/src/main/java/de/extio/game_engine/renderer/g2d/control/impl/G2DBaseControlImpl.java index 9b11fab..881c852 100644 --- a/src/main/java/de/extio/game_engine/renderer/g2d/control/impl/G2DBaseControlImpl.java +++ b/src/main/java/de/extio/game_engine/renderer/g2d/control/impl/G2DBaseControlImpl.java @@ -72,7 +72,7 @@ public abstract class G2DBaseControlImpl implements BaseControl { } @Override - public BaseControl setId(final String id) { + public BaseControl setControlId(final String id) { this.id = id; return this; } diff --git a/src/main/java/de/extio/game_engine/renderer/g2d/control/impl/G2DTableControlImpl.java b/src/main/java/de/extio/game_engine/renderer/g2d/control/impl/G2DTableControlImpl.java index 4eb2a78..7e7a5db 100644 --- a/src/main/java/de/extio/game_engine/renderer/g2d/control/impl/G2DTableControlImpl.java +++ b/src/main/java/de/extio/game_engine/renderer/g2d/control/impl/G2DTableControlImpl.java @@ -234,9 +234,9 @@ public class G2DTableControlImpl extends G2DBaseControlImpl implements TableCont } @Override - public BaseControl setId(final String id) { - this.scrollbar.setId(id + "_scroll"); - return super.setId(id); + public BaseControl setControlId(final String id) { + this.scrollbar.setControlId(id + "_scroll"); + return super.setControlId(id); } @Override diff --git a/src/main/java/de/extio/game_engine/renderer/g2d/control/impl/G2DTextfieldControlImpl2.java b/src/main/java/de/extio/game_engine/renderer/g2d/control/impl/G2DTextfieldControlImpl2.java index 9a3e7f7..9568791 100644 --- a/src/main/java/de/extio/game_engine/renderer/g2d/control/impl/G2DTextfieldControlImpl2.java +++ b/src/main/java/de/extio/game_engine/renderer/g2d/control/impl/G2DTextfieldControlImpl2.java @@ -353,9 +353,9 @@ public class G2DTextfieldControlImpl2 extends G2DBaseControlImpl implements Text } @Override - public BaseControl setId(final String id) { - this.scrollbar.setId(id + "_scroll"); - return super.setId(id); + public BaseControl setControlId(final String id) { + this.scrollbar.setControlId(id + "_scroll"); + return super.setControlId(id); } } diff --git a/src/main/java/de/extio/game_engine/renderer/model/AbstractRenderingBo.java b/src/main/java/de/extio/game_engine/renderer/model/AbstractRenderingBo.java index b091543..32e2f16 100644 --- a/src/main/java/de/extio/game_engine/renderer/model/AbstractRenderingBo.java +++ b/src/main/java/de/extio/game_engine/renderer/model/AbstractRenderingBo.java @@ -11,6 +11,8 @@ public abstract class AbstractRenderingBo implements RenderingBo { protected final static Logger LOGGER = LoggerFactory.getLogger(AbstractRenderingBo.class); + protected String id; + protected RendererData rendererData; protected RgbaColor color; @@ -35,7 +37,17 @@ public abstract class AbstractRenderingBo implements RenderingBo { this.layer = layer; this.defaultLayer = layer; } - + + @Override + public void setId(final String id) { + this.id = id; + } + + @Override + public String getId() { + return this.id; + } + @Override public RenderingBo withPositionRelative(final int x, final int y) { this.localX = x; @@ -203,6 +215,7 @@ public abstract class AbstractRenderingBo implements RenderingBo { @Override public void close() throws Exception { + this.id = null; this.color = null; this.x = 0; this.y = 0; @@ -212,6 +225,11 @@ public abstract class AbstractRenderingBo implements RenderingBo { this.height = 0; this.layer = this.defaultLayer; } + + @Override + public void closeStatic() { + + } @Override public void setRendererData(final RendererData RendererData) { diff --git a/src/main/java/de/extio/game_engine/renderer/model/RenderingBo.java b/src/main/java/de/extio/game_engine/renderer/model/RenderingBo.java index e572c02..1fe7dd9 100644 --- a/src/main/java/de/extio/game_engine/renderer/model/RenderingBo.java +++ b/src/main/java/de/extio/game_engine/renderer/model/RenderingBo.java @@ -10,9 +10,11 @@ import de.extio.game_engine.spatial2.model.HasPosition2; * It is recommended to view my technical presentation on youtube about the game to learn more about how rendering works. */ public interface RenderingBo extends AutoCloseable { - - void setRendererData(RendererData RendererData); - + + void setId(String id); + + String getId(); + /** * Sets a color you want to use for rendering. It depends on the rendering bo how this information is treated. */ @@ -104,5 +106,9 @@ public interface RenderingBo extends AutoCloseable { * Positions this bo relative to it's parent. This can be for example game world for positioning tiles or a composite entity. Unit is usually not pixels but the position in the tile raster. */ RenderingBo withPositionRelative(HasPosition2 position); - + + void closeStatic(); + + void setRendererData(RendererData RendererData); + } diff --git a/src/main/java/de/extio/game_engine/renderer/model/bo/ControlRenderingBo.java b/src/main/java/de/extio/game_engine/renderer/model/bo/ControlRenderingBo.java index beee3fd..967878f 100644 --- a/src/main/java/de/extio/game_engine/renderer/model/bo/ControlRenderingBo.java +++ b/src/main/java/de/extio/game_engine/renderer/model/bo/ControlRenderingBo.java @@ -11,9 +11,9 @@ import de.extio.game_engine.renderer.model.color.RgbaColor; */ public interface ControlRenderingBo extends RenderingBoHasDimension { - ControlRenderingBo setId(String id); + ControlRenderingBo setControlId(String id); - String getId(); + String getControlId(); /** * Sets the type of the control. See class tree of BaseControl @@ -63,7 +63,7 @@ public interface ControlRenderingBo extends RenderingBoHasDimension { boolean getInUse(); - BaseControl setId(String id); + BaseControl setControlId(String id); BaseControl setCaption(String caption); diff --git a/src/main/java/de/extio/game_engine/renderer/work/RendererWorkingSet.java b/src/main/java/de/extio/game_engine/renderer/work/RendererWorkingSet.java new file mode 100644 index 0000000..9a317c6 --- /dev/null +++ b/src/main/java/de/extio/game_engine/renderer/work/RendererWorkingSet.java @@ -0,0 +1,22 @@ +package de.extio.game_engine.renderer.work; + +import java.util.List; +import java.util.Map; + +import de.extio.game_engine.renderer.model.RenderingBo; + +public interface RendererWorkingSet { + + void add(String producer, RenderingBo work); + + void add(String producer, List work); + + RenderingBo get(String producer, String id); + + Map getUncommittedWork(String producer); + + Map commit(String producer, boolean clone); + + void getLiveSet(List combinedLiveSet); + +} diff --git a/src/main/java/de/extio/game_engine/renderer/work/RendererWorkingSetImpl.java b/src/main/java/de/extio/game_engine/renderer/work/RendererWorkingSetImpl.java new file mode 100644 index 0000000..36bb30f --- /dev/null +++ b/src/main/java/de/extio/game_engine/renderer/work/RendererWorkingSetImpl.java @@ -0,0 +1,129 @@ +package de.extio.game_engine.renderer.work; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReference; + +import de.extio.game_engine.renderer.model.RenderingBo; + +public class RendererWorkingSetImpl implements RendererWorkingSet { + + private final ConcurrentMap workingSet = new ConcurrentHashMap<>(); + + private final BlockingQueue> mapsPool = new ArrayBlockingQueue<>(100); + + private final BlockingQueue>> setsPool = new ArrayBlockingQueue<>(100); + + private final RenderingBoPool rendererBoPool; + + public RendererWorkingSetImpl(final RenderingBoPool rendererBoPool) { + this.rendererBoPool = rendererBoPool; + } + + @Override + public void add(final String producer, final RenderingBo work) { + this.getWorkingSetByProducer(producer) + .next() + .put(work.getId(), work); + } + + @Override + public void add(final String producer, final List work) { + final var nextMap = this.getWorkingSetByProducer(producer).next(); + for (final RenderingBo bo : work) { + nextMap.put(bo.getId(), bo); + } + } + + @Override + public Map getUncommittedWork(final String producer) { + return this.getWorkingSetByProducer(producer).next(); + } + + @Override + public RenderingBo get(final String producer, final String id) { + return this.getWorkingSetByProducer(producer).next().get(id); + } + + @Override + public Map commit(final String producer, final boolean clone) { + final AtomicReference> previousWork = new AtomicReference<>(); + final RendererWork rendererWork = this.workingSet.compute(producer, (k, v) -> { + if (v == null) { + return new RendererWork(this.obtainMapFromPool(), this.obtainMapFromPool()); + } + previousWork.set(v.live()); + if (clone) { + final Map newNext = this.obtainMapFromPool(); + newNext.putAll(v.next()); + return new RendererWork(v.next(), newNext); + } + else { + return new RendererWork(v.next(), this.obtainMapFromPool()); + } + }); + + final var previousWorkMap = previousWork.get(); + if (previousWorkMap != null) { + final var releasedBoClasses = this.obtainSetFromPool(); + for (final RenderingBo renderingBO : previousWorkMap.values()) { + if (releasedBoClasses.add(renderingBO.getClass())) { + renderingBO.closeStatic(); + } + this.rendererBoPool.release(renderingBO); + } + + this.returnMapToPool(previousWorkMap); + this.returnSetToPool(releasedBoClasses); + } + + return rendererWork.next(); + } + + @Override + public void getLiveSet(final List combinedLiveSet) { + this.workingSet.values().forEach(rendererWork -> combinedLiveSet.addAll(rendererWork.live().values())); + } + + private RendererWork getWorkingSetByProducer(final String producer) { + return this.workingSet.computeIfAbsent(producer, k -> new RendererWork(this.obtainMapFromPool(), this.obtainMapFromPool())); + } + + private Map obtainMapFromPool() { + var map = this.mapsPool.poll(); + if (map == null) { + map = Collections.synchronizedMap(new HashMap<>()); + } + return map; + } + + private void returnMapToPool(final Map map) { + map.clear(); + this.mapsPool.offer(map); + } + + private Set> obtainSetFromPool() { + var set = this.setsPool.poll(); + if (set == null) { + set = new HashSet<>(); + } + return set; + } + + private void returnSetToPool(final Set> set) { + set.clear(); + this.setsPool.offer(set); + } + + private static record RendererWork(Map live, Map next) { + + } +} diff --git a/src/main/java/de/extio/game_engine/renderer/RenderingBoPool.java b/src/main/java/de/extio/game_engine/renderer/work/RenderingBoPool.java similarity index 89% rename from src/main/java/de/extio/game_engine/renderer/RenderingBoPool.java rename to src/main/java/de/extio/game_engine/renderer/work/RenderingBoPool.java index 906e39a..f71fcd0 100644 --- a/src/main/java/de/extio/game_engine/renderer/RenderingBoPool.java +++ b/src/main/java/de/extio/game_engine/renderer/work/RenderingBoPool.java @@ -1,5 +1,6 @@ -package de.extio.game_engine.renderer; +package de.extio.game_engine.renderer.work; +import de.extio.game_engine.renderer.RendererData; import de.extio.game_engine.renderer.model.RenderingBo; /** @@ -14,7 +15,7 @@ public interface RenderingBoPool { /** * Acquires a rendering business object by type from the pool. */ - T acquire(Class clazz); + T acquire(String id, Class clazz); /** * Releases a rendering business object back to the pool. This is usually managed by the renderer. diff --git a/src/main/java/de/extio/game_engine/renderer/RenderingBoPoolImpl.java b/src/main/java/de/extio/game_engine/renderer/work/RenderingBoPoolImpl.java similarity index 87% rename from src/main/java/de/extio/game_engine/renderer/RenderingBoPoolImpl.java rename to src/main/java/de/extio/game_engine/renderer/work/RenderingBoPoolImpl.java index 288c92c..b9593b0 100644 --- a/src/main/java/de/extio/game_engine/renderer/RenderingBoPoolImpl.java +++ b/src/main/java/de/extio/game_engine/renderer/work/RenderingBoPoolImpl.java @@ -1,10 +1,11 @@ -package de.extio.game_engine.renderer; +package de.extio.game_engine.renderer.work; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; import java.util.Stack; +import de.extio.game_engine.renderer.RendererData; import de.extio.game_engine.renderer.model.RenderingBo; public class RenderingBoPoolImpl implements RenderingBoPool { @@ -34,14 +35,14 @@ public class RenderingBoPoolImpl implements RenderingBoPool { volatile Stack pooled; } - + public RenderingBoPoolImpl(final Map, Class> mapping) { this.mapping = mapping; } - + @Override @SuppressWarnings("unchecked") - public T acquire(final Class clazz) { + public synchronized T acquire(final String id, final Class clazz) { Stack pooled; Class impl; @@ -71,8 +72,8 @@ public class RenderingBoPoolImpl implements RenderingBoPool { this.lastMapping.pooled = pooled; } + T instance; if (pooled.isEmpty()) { - T instance; try { instance = clazz.cast(impl.getDeclaredConstructor().newInstance()); } @@ -80,14 +81,17 @@ public class RenderingBoPoolImpl implements RenderingBoPool { throw new RuntimeException(e); } instance.setRendererData(this.rendererData); - return instance; + } + else { + instance = (T) pooled.pop(); } - return (T) pooled.pop(); + instance.setId(id); + return instance; } @Override - public void release(final RenderingBo obj) { + public synchronized void release(final RenderingBo obj) { try { obj.close(); @@ -114,10 +118,10 @@ public class RenderingBoPoolImpl implements RenderingBoPool { throw new RuntimeException(e); } } - + @Override public void setRendererData(final RendererData rendererData) { - this.rendererData = rendererData; + this.rendererData = rendererData; } } diff --git a/src/test/java/de/extio/game_engine/renderer/work/RendererWorkingSetImplTest.java b/src/test/java/de/extio/game_engine/renderer/work/RendererWorkingSetImplTest.java new file mode 100644 index 0000000..1c463e8 --- /dev/null +++ b/src/test/java/de/extio/game_engine/renderer/work/RendererWorkingSetImplTest.java @@ -0,0 +1,378 @@ +package de.extio.game_engine.renderer.work; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.extio.game_engine.renderer.RendererData; +import de.extio.game_engine.renderer.model.RenderingBo; +import de.extio.game_engine.renderer.model.RenderingBoLayer; +import de.extio.game_engine.renderer.model.color.RgbaColor; +import de.extio.game_engine.spatial2.model.HasPosition2; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class RendererWorkingSetImplTest { + + private RendererWorkingSetImpl workingSet; + + private RenderingBoPool renderingBoPool; + + @BeforeEach + void setUp() { + this.renderingBoPool = mock(RenderingBoPool.class); + this.workingSet = new RendererWorkingSetImpl(this.renderingBoPool); + } + + @Test + void addSingleAddsToUncommittedWork() { + final RenderingBo bo1 = mock(RenderingBo.class); + when(bo1.getId()).thenReturn("bo1"); + this.workingSet.add("producer", bo1); + + final var uncommitted = this.workingSet.getUncommittedWork("producer"); + assertNotNull(uncommitted); + assertEquals(Map.of("bo1", bo1), uncommitted); + } + + @Test + void addListAddsAllToUncommittedWork() { + final RenderingBo bo1 = mock(RenderingBo.class); + when(bo1.getId()).thenReturn("bo1"); + final RenderingBo bo2 = mock(RenderingBo.class); + when(bo2.getId()).thenReturn("bo2"); + + this.workingSet.add("producer", List.of(bo1, bo2)); + + final var uncommitted = this.workingSet.getUncommittedWork("producer"); + assertEquals(Map.of("bo1", bo1, "bo2", bo2), uncommitted); + } + + @Test + void commitWithoutCloneMovesUncommittedToLiveAndClearsUncommitted() { + final RenderingBo bo1 = mock(RenderingBo.class); + when(bo1.getId()).thenReturn("bo1"); + final RenderingBo bo2 = mock(RenderingBo.class); + when(bo2.getId()).thenReturn("bo2"); + + this.workingSet.add("producer", bo1); + this.workingSet.add("producer", bo2); + + final var returnedUncommitted = this.workingSet.commit("producer", false); + final var currentUncommitted = this.workingSet.getUncommittedWork("producer"); + + assertSame(currentUncommitted, returnedUncommitted); + assertTrue(currentUncommitted.isEmpty()); + + final List live = new ArrayList<>(); + this.workingSet.getLiveSet(live); + assertEquals(2, live.size()); + assertTrue(live.containsAll(List.of(bo1, bo2))); + } + + @Test + void commitWithCloneCopiesUncommittedIntoNewUncommitted() { + final RenderingBo bo1 = mock(RenderingBo.class); + when(bo1.getId()).thenReturn("bo1"); + final RenderingBo bo2 = mock(RenderingBo.class); + when(bo2.getId()).thenReturn("bo2"); + final RenderingBo bo3 = mock(RenderingBo.class); + when(bo3.getId()).thenReturn("bo3"); + + this.workingSet.add("producer", bo1); + this.workingSet.add("producer", bo2); + + final var returnedUncommitted = this.workingSet.commit("producer", true); + final var currentUncommitted = this.workingSet.getUncommittedWork("producer"); + + assertSame(currentUncommitted, returnedUncommitted); + assertEquals(Map.of("bo1", bo1, "bo2", bo2), currentUncommitted); + + final List live = new ArrayList<>(); + this.workingSet.getLiveSet(live); + assertEquals(2, live.size()); + assertTrue(live.containsAll(List.of(bo1, bo2))); + + this.workingSet.add("producer", bo3); + final List liveAfterAdd = new ArrayList<>(); + this.workingSet.getLiveSet(liveAfterAdd); + assertEquals(2, liveAfterAdd.size()); + assertTrue(liveAfterAdd.containsAll(List.of(bo1, bo2))); + } + + @Test + void liveSetContainsAllProducersCommittedWork() { + final RenderingBo bo1 = mock(RenderingBo.class); + when(bo1.getId()).thenReturn("bo1"); + final RenderingBo bo2 = mock(RenderingBo.class); + when(bo2.getId()).thenReturn("bo2"); + final RenderingBo bo3 = mock(RenderingBo.class); + when(bo3.getId()).thenReturn("bo3"); + + this.workingSet.add("p1", bo1); + this.workingSet.add("p1", bo2); + this.workingSet.add("p2", bo3); + + this.workingSet.commit("p1", false); + this.workingSet.commit("p2", false); + + final List live = new ArrayList<>(); + this.workingSet.getLiveSet(live); + assertEquals(3, live.size()); + assertTrue(live.containsAll(List.of(bo1, bo2, bo3))); + } + + @Test + void commitOnUnknownProducerCreatesEmptyState() { + final var returnedUncommitted = this.workingSet.commit("unknown", false); + assertNotNull(returnedUncommitted); + assertTrue(returnedUncommitted.isEmpty()); + + final List live = new ArrayList<>(); + this.workingSet.getLiveSet(live); + assertTrue(live.isEmpty()); + } + + @Test + void secondCommitReleasesPreviousLiveWorkAndCallsCloseStaticOncePerClass() { + final var bo1 = new TestBoA(); + bo1.setId("bo1"); + final var bo2 = new TestBoA(); + bo2.setId("bo2"); + + this.workingSet.add("producer", bo1); + this.workingSet.add("producer", bo2); + this.workingSet.commit("producer", false); + + final var bo3 = new TestBoB(); + bo3.setId("bo3"); + this.workingSet.add("producer", bo3); + this.workingSet.commit("producer", false); + + verify(this.renderingBoPool, times(1)).release(bo1); + verify(this.renderingBoPool, times(1)).release(bo2); + + // closeStatic() is called exactly once per class, so one of bo1 or bo2 has it called + final int totalCloseStaticCallsForTestBoA = bo1.closeStaticCalls() + bo2.closeStaticCalls(); + assertEquals(1, totalCloseStaticCallsForTestBoA); + assertEquals(0, bo3.closeStaticCalls()); + } + + @Test + void getReturnsRenderingBoFromUncommittedWork() { + final RenderingBo bo1 = mock(RenderingBo.class); + when(bo1.getId()).thenReturn("bo1"); + final RenderingBo bo2 = mock(RenderingBo.class); + when(bo2.getId()).thenReturn("bo2"); + + this.workingSet.add("producer", bo1); + this.workingSet.add("producer", bo2); + + final var retrieved1 = this.workingSet.get("producer", "bo1"); + final var retrieved2 = this.workingSet.get("producer", "bo2"); + final var notFound = this.workingSet.get("producer", "nonexistent"); + + assertSame(bo1, retrieved1); + assertSame(bo2, retrieved2); + assertTrue(notFound == null); + } + + @Test + void getRetrievesFromUncommittedWorkOnly() { + final RenderingBo bo1 = mock(RenderingBo.class); + when(bo1.getId()).thenReturn("bo1"); + final RenderingBo bo2 = mock(RenderingBo.class); + when(bo2.getId()).thenReturn("bo2"); + + this.workingSet.add("producer", bo1); + this.workingSet.add("producer", bo2); + final var committedWork = this.workingSet.commit("producer", false); + + // After commit without clone, the uncommitted work should be empty + assertTrue(committedWork.isEmpty()); + + // get() only retrieves from next set, not live set, so it returns null after commit + final var retrieved1 = this.workingSet.get("producer", "bo1"); + final var retrieved2 = this.workingSet.get("producer", "bo2"); + + assertTrue(retrieved1 == null); + assertTrue(retrieved2 == null); + } + + @Test + void addWithSameIdReplacesExistingRenderingBo() { + final RenderingBo bo1 = mock(RenderingBo.class); + when(bo1.getId()).thenReturn("bo1"); + final RenderingBo bo1_replacement = mock(RenderingBo.class); + when(bo1_replacement.getId()).thenReturn("bo1"); + + this.workingSet.add("producer", bo1); + final var uncommittedBefore = this.workingSet.getUncommittedWork("producer"); + assertEquals(1, uncommittedBefore.size()); + assertSame(bo1, uncommittedBefore.get("bo1")); + + this.workingSet.add("producer", bo1_replacement); + final var uncommittedAfter = this.workingSet.getUncommittedWork("producer"); + assertEquals(1, uncommittedAfter.size()); + assertSame(bo1_replacement, uncommittedAfter.get("bo1")); + } + + private static abstract class TestBoBase implements RenderingBo { + + private final AtomicInteger closeStaticCalls = new AtomicInteger(); + + private String id; + + @Override + public void setId(final String id) { + this.id = id; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public RenderingBo setColor(final RgbaColor color) { + return this; + } + + @Override + public int getX() { + return 0; + } + + @Override + public int getY() { + return 0; + } + + @Override + public int getLocalX() { + return 0; + } + + @Override + public int getLocalY() { + return 0; + } + + @Override + public RenderingBo setLayer(final RenderingBoLayer layer) { + return this; + } + + @Override + public RenderingBoLayer getLayer() { + return RenderingBoLayer.UI0_0; + } + + @Override + public RenderingBo withPositionAbsoluteAnchorTopLeft(final int x, final int y) { + return this; + } + + @Override + public RenderingBo withPositionAbsoluteAnchorTopLeft(final HasPosition2 position) { + return this; + } + + @Override + public RenderingBo withPositionAbsoluteAnchorTopRight(final int x, final int y) { + return this; + } + + @Override + public RenderingBo withPositionAbsoluteAnchorTopRight(final HasPosition2 position) { + return this; + } + + @Override + public RenderingBo withPositionAbsoluteAnchorBottomLeft(final int x, final int y) { + return this; + } + + @Override + public RenderingBo withPositionAbsoluteAnchorBottomLeft(final HasPosition2 position) { + return this; + } + + @Override + public RenderingBo withPositionAbsoluteAnchorBottomRight(final int x, final int y) { + return this; + } + + @Override + public RenderingBo withPositionAbsoluteAnchorBottomRight(final HasPosition2 position) { + return this; + } + + @Override + public RenderingBo withPositionPercentual(final double x, final double y) { + return this; + } + + @Override + public RenderingBo withPositionIncrementalAbsolute(final int x, final int y) { + return this; + } + + @Override + public RenderingBo withPositionIncrementalAbsolute(final HasPosition2 position) { + return this; + } + + @Override + public RenderingBo withPositionIncrementalPercentual(final double x, final double y) { + return this; + } + + @Override + public RenderingBo withPositionRelative(final int x, final int y) { + return this; + } + + @Override + public RenderingBo withPositionRelative(final HasPosition2 position) { + return this; + } + + @Override + public void closeStatic() { + this.closeStaticCalls.incrementAndGet(); + } + + public int closeStaticCalls() { + return this.closeStaticCalls.get(); + } + + @Override + public void setRendererData(final RendererData RendererData) { + } + + @Override + public void close() { + } + } + + private static final class TestBoA extends TestBoBase { + + } + + private static final class TestBoB extends TestBoBase { + + } +}