diff --git a/src/main/java/de/extio/game_engine/module/AbstractModule.java b/src/main/java/de/extio/game_engine/module/AbstractModule.java index 0964a09..09d569b 100644 --- a/src/main/java/de/extio/game_engine/module/AbstractModule.java +++ b/src/main/java/de/extio/game_engine/module/AbstractModule.java @@ -1,5 +1,7 @@ package de.extio.game_engine.module; +import java.util.List; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,6 +32,9 @@ public abstract class AbstractModule { } + public List executorCallbackSubscriptions() { + return null; + } public void run() { diff --git a/src/main/java/de/extio/game_engine/module/ModuleExecutorCallbacks.java b/src/main/java/de/extio/game_engine/module/ModuleExecutorCallbacks.java new file mode 100644 index 0000000..49a0b6d --- /dev/null +++ b/src/main/java/de/extio/game_engine/module/ModuleExecutorCallbacks.java @@ -0,0 +1,11 @@ +package de.extio.game_engine.module; + +public enum ModuleExecutorCallbacks { + + UI_PRE, + + RUN, + + UI_POST; + +} 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 7ff6bda..00473eb 100644 --- a/src/main/java/de/extio/game_engine/module/ModuleExecutorImpl.java +++ b/src/main/java/de/extio/game_engine/module/ModuleExecutorImpl.java @@ -20,18 +20,15 @@ public class ModuleExecutorImpl implements ModuleExecutor { @Override public void execute() { - - // TODO Subscriptions for modules to run only on certain conditions - - this.runTasks(module -> () -> { + this.runTasks(ModuleExecutorCallbacks.UI_PRE, module -> () -> { if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) { this.invokeSafe(clientModule, AbstractClientModule::runUiPre); } }); - this.runTasks(module -> () -> { + this.runTasks(ModuleExecutorCallbacks.RUN, module -> () -> { this.invokeSafe(module, AbstractModule::run); }); - this.runTasks(module -> () -> { + this.runTasks(ModuleExecutorCallbacks.UI_POST, module -> () -> { if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) { this.invokeSafe(clientModule, AbstractClientModule::runUiPost); } @@ -47,12 +44,19 @@ public class ModuleExecutorImpl implements ModuleExecutor { } } - private void runTasks(final Function taskSupplier) { + private void runTasks(final ModuleExecutorCallbacks callbackType, final Function taskSupplier) { + final var subscribers = this.moduleManager.getSubscribersForCallback(callbackType); + if (subscribers.isEmpty()) { + return; + } + try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) { - for (final var module : this.moduleManager.getModulesActive()) { - final var task = taskSupplier.apply(module); - if (task != null) { - scope.fork(task); + synchronized (subscribers) { + for (final var module : subscribers) { + final var task = taskSupplier.apply(module); + if (task != null) { + scope.fork(task); + } } } scope.join(); diff --git a/src/main/java/de/extio/game_engine/module/ModuleManager.java b/src/main/java/de/extio/game_engine/module/ModuleManager.java index 73ce36d..2918a9a 100644 --- a/src/main/java/de/extio/game_engine/module/ModuleManager.java +++ b/src/main/java/de/extio/game_engine/module/ModuleManager.java @@ -38,4 +38,6 @@ public interface ModuleManager { boolean isModal(); + List getSubscribersForCallback(ModuleExecutorCallbacks callback); + } diff --git a/src/main/java/de/extio/game_engine/module/ModuleManagerImpl.java b/src/main/java/de/extio/game_engine/module/ModuleManagerImpl.java index de0cd0d..2375fc8 100644 --- a/src/main/java/de/extio/game_engine/module/ModuleManagerImpl.java +++ b/src/main/java/de/extio/game_engine/module/ModuleManagerImpl.java @@ -1,9 +1,13 @@ package de.extio.game_engine.module; -import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Deque; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Stack; import java.util.function.Consumer; import org.slf4j.Logger; @@ -18,8 +22,8 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { private final List modulesInitial; - private final List modulesAll = new ArrayList<>(); - + private final List modulesAll = Collections.synchronizedList(new ArrayList<>()); + private final RendererWorkingSet rendererWorkingSet; private List modulesAllView = List.of(); @@ -30,11 +34,13 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { private List modulesDisplayedClientModulesView = List.of(); - private final List modulesActive = new ArrayList<>(); + private final List modulesActive = Collections.synchronizedList(new ArrayList<>()); - private final List modulesDisplayed = new ArrayList<>(); + private final List modulesDisplayed = Collections.synchronizedList(new ArrayList<>()); - private final Deque> lastVisibleStates = new ArrayDeque<>(); + private final Stack> lastVisibleStates = new Stack<>(); + + private final Map> executorCallbackMap = Collections.synchronizedMap(new EnumMap<>(ModuleExecutorCallbacks.class)); public ModuleManagerImpl(final List initialModules, final RendererWorkingSet rendererWorkingSet) { this.modulesInitial = initialModules; @@ -46,10 +52,7 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { if (this.modulesInitial == null || this.modulesInitial.isEmpty()) { return; } - this.modulesInitial.forEach(module -> this.invokeSafe(module, AbstractModule::onLoad)); - this.modulesAll.addAll(this.modulesInitial); - this.sortModules(this.modulesAll); - this.modulesAllView = List.copyOf(this.modulesAll); + this.modulesInitial.forEach(this::load); this.modulesInitial.clear(); } @@ -74,30 +77,41 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { } @Override - public void loadModule(final Class clazz) { - if (this.modulesAll.stream().anyMatch(module -> clazz.isInstance(module))) { - LOGGER.debug("AbstractModule already loaded: " + clazz.getName()); - return; + public synchronized void loadModule(final Class clazz) { + synchronized (this.modulesAll) { + if (this.modulesAll.stream().anyMatch(module -> clazz.isInstance(module))) { + LOGGER.debug("AbstractModule already loaded: " + clazz.getName()); + return; + } } LOGGER.debug("Loading module " + clazz.getName()); try { final AbstractModule module = AbstractModule.class.cast(clazz.getDeclaredConstructor().newInstance()); - this.invokeSafe(module, m -> m.onLoad()); - this.modulesAll.add(module); - this.sortModules(this.modulesAll); - this.modulesAllView = List.copyOf(this.modulesAll); + this.load(module); } catch (final Exception e) { LOGGER.error("Error loading module " + clazz.getName(), e); } } + private void load(final AbstractModule module) { + this.invokeSafe(module, m -> m.onLoad()); + this.modulesAll.add(module); + this.sortModules(this.modulesAll); + synchronized (this.modulesAll) { + this.modulesAllView = List.copyOf(this.modulesAll); + } + } + @Override - public void unloadModule(final Class clazz) { + public synchronized void unloadModule(final Class clazz) { try { - final AbstractModule module = this.modulesAll.stream().filter(mod -> clazz.isInstance(mod)).findAny().orElse(null); + AbstractModule module; + synchronized (this.modulesAll) { + module = this.modulesAll.stream().filter(mod -> clazz.isInstance(mod)).findAny().orElse(null); + } if (module == null) { LOGGER.debug("AbstractModule not loaded: " + clazz.getName()); @@ -107,7 +121,9 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { this.changeActiveState(clazz, false); this.modulesAll.remove(module); this.sortModules(this.modulesAll); - this.modulesAllView = List.copyOf(this.modulesAll); + synchronized (this.modulesAll) { + this.modulesAllView = List.copyOf(this.modulesAll); + } this.invokeSafe(module, m -> m.onUnload()); if (module instanceof final AbstractClientModule clientModule) { @@ -133,7 +149,7 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { } @Override - public void changeActiveState(final Class clazz, final boolean active) { + public synchronized void changeActiveState(final Class clazz, final boolean active) { this.modulesActiveView.stream() .filter(module -> clazz.isAssignableFrom(module.getClass())) .findAny() @@ -145,39 +161,59 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { this.invokeSafe(module, AbstractModule::onDeactivate); this.modulesActive.remove(module); this.sortModules(this.modulesActive); - this.modulesActiveView = List.copyOf(this.modulesActive); - this.modulesActiveClientModulesView = this.modulesActive.stream() - .filter(m -> m instanceof AbstractClientModule) - .map(m -> (AbstractClientModule) m) - .toList(); - this.modulesDisplayedClientModulesView = List.copyOf(this.modulesDisplayed); + synchronized (this.modulesActive) { + this.modulesActiveView = List.copyOf(this.modulesActive); + this.modulesActiveClientModulesView = this.modulesActive.stream() + .filter(m -> m instanceof AbstractClientModule) + .map(m -> (AbstractClientModule) m) + .toList(); + } if (module instanceof final AbstractClientModule clientModule) { this.rendererWorkingSet.clear(clientModule.getClass()); } + synchronized (this.executorCallbackMap) { + final var subscriptions = module.executorCallbackSubscriptions(); + if (subscriptions != null) { + for (final var subscription : subscriptions) { + executorCallbackMap.computeIfAbsent(subscription, k -> Collections.synchronizedList(new ArrayList<>())).remove(module); + } + } + } LOGGER.debug("Deactivated " + clazz.getName()); } }, () -> { if (active) { - this.modulesAll.stream() - .filter(module -> clazz.isAssignableFrom(module.getClass())) - .findFirst() - .ifPresent(module -> { - this.invokeSafe(module, AbstractModule::onActivate); - if (module instanceof final AbstractClientModule clientModule && clientModule.isAlwaysDisplay()) { - this.changeDisplayState(clientModule.getClass(), true); - } - this.modulesActive.add(module); - this.sortModules(this.modulesActive); - this.modulesActiveView = List.copyOf(this.modulesActive); - this.modulesActiveClientModulesView = this.modulesActive.stream() - .filter(m -> m instanceof AbstractClientModule) - .map(m -> (AbstractClientModule) m) - .toList(); - this.modulesDisplayedClientModulesView = List.copyOf(this.modulesDisplayed); - - LOGGER.debug("Activated " + clazz.getName()); - }); + synchronized (this.modulesAll) { + this.modulesAll.stream() + .filter(module -> clazz.isAssignableFrom(module.getClass())) + .findFirst() + .ifPresent(module -> { + this.invokeSafe(module, AbstractModule::onActivate); + if (module instanceof final AbstractClientModule clientModule && clientModule.isAlwaysDisplay()) { + this.changeDisplayState(clientModule.getClass(), true); + } + this.modulesActive.add(module); + this.sortModules(this.modulesActive); + synchronized (this.modulesActive) { + this.modulesActiveView = List.copyOf(this.modulesActive); + this.modulesActiveClientModulesView = this.modulesActive.stream() + .filter(m -> m instanceof AbstractClientModule) + .map(m -> (AbstractClientModule) m) + .toList(); + } + synchronized (this.executorCallbackMap) { + final var subscriptions = module.executorCallbackSubscriptions(); + if (subscriptions != null) { + for (final var subscription : subscriptions) { + executorCallbackMap.computeIfAbsent(subscription, k -> Collections.synchronizedList(new ArrayList<>())).add(module); + } + } + } + + LOGGER.debug("Activated " + clazz.getName()); + }); + } } }); } @@ -202,25 +238,29 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { } @Override - public void changeDisplayState(final Class clazz, final boolean display) { - for (final var module : this.modulesAll) { - if (module instanceof final AbstractClientModule clientModule && clazz.isAssignableFrom(module.getClass())) { - final boolean curDisplayed = this.modulesDisplayed.stream().anyMatch(mod -> clazz.isAssignableFrom(mod.getClass())); - - final boolean doDisplay = display || clientModule.isAlwaysDisplay(); - if (doDisplay && !curDisplayed) { - this.modulesDisplayed.add(clientModule); - clientModule.setDisplayed(true); - this.invokeSafe(clientModule, m -> ((AbstractClientModule) m).onShow()); + public synchronized void changeDisplayState(final Class clazz, final boolean display) { + synchronized (this.modulesAll) { + synchronized (this.modulesDisplayed) { + for (final var module : this.modulesAll) { + if (module instanceof final AbstractClientModule clientModule && clazz.isAssignableFrom(module.getClass())) { + final boolean curDisplayed = this.modulesDisplayed.stream().anyMatch(mod -> clazz.isAssignableFrom(mod.getClass())); + + final boolean doDisplay = display || clientModule.isAlwaysDisplay(); + if (doDisplay && !curDisplayed) { + this.modulesDisplayed.add(clientModule); + clientModule.setDisplayed(true); + this.invokeSafe(clientModule, m -> ((AbstractClientModule) m).onShow()); + } + else if (!doDisplay && curDisplayed) { + clientModule.setDisplayed(false); + this.modulesDisplayed.removeIf(mod -> clazz.isAssignableFrom(mod.getClass())); + this.invokeSafe(clientModule, m -> ((AbstractClientModule) m).onHide()); + } + this.modulesDisplayedClientModulesView = List.copyOf(this.modulesDisplayed); + + break; + } } - else if (!doDisplay && curDisplayed) { - clientModule.setDisplayed(false); - this.modulesDisplayed.removeIf(mod -> clazz.isAssignableFrom(mod.getClass())); - this.invokeSafe(clientModule, m -> ((AbstractClientModule) m).onHide()); - } - this.modulesDisplayedClientModulesView = List.copyOf(this.modulesDisplayed); - - break; } } } @@ -239,9 +279,9 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { @Override public boolean isDisplayed(final Class clazz) { - final var modules = this.modulesDisplayed; + final var modules = this.modulesDisplayedClientModulesView; for (int i = 0; i < modules.size(); i++) { - if (clazz.isAssignableFrom(this.modulesDisplayed.get(i).getClass())) { + if (clazz.isAssignableFrom(modules.get(i).getClass())) { return true; } } @@ -252,7 +292,8 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { public void hideAll() { this.lastVisibleStates.clear(); - for (final var module : List.copyOf(this.modulesDisplayed)) { + final var modules = this.modulesDisplayedClientModulesView; + for (final var module : modules) { if ((module instanceof final AbstractClientModule clientModule) && !clientModule.isAlwaysDisplay()) { this.changeDisplayState(clientModule.getClass(), false); } @@ -263,9 +304,10 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { @SafeVarargs public final void hideExcept(final String... classNames) { final List state = new ArrayList<>(); - final var keep = new java.util.HashSet<>(java.util.Arrays.asList(classNames)); + final var keep = new HashSet<>(Arrays.asList(classNames)); - for (final var module : List.copyOf(this.modulesDisplayed)) { + final var modules = this.modulesDisplayedClientModulesView; + for (final var module : modules) { if (module instanceof final AbstractClientModule clientModule && !clientModule.isAlwaysDisplay() && !keep.contains(clientModule.getClass().getName())) { @@ -285,8 +327,10 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { return; } - for (final AbstractClientModule module : this.lastVisibleStates.pop()) { - this.changeDisplayState(module.getClass(), true); + synchronized (this.lastVisibleStates) { + for (final AbstractClientModule module : this.lastVisibleStates.pop()) { + this.changeDisplayState(module.getClass(), true); + } } } @@ -295,6 +339,11 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager { return !this.lastVisibleStates.isEmpty(); } + @Override + public List getSubscribersForCallback(final ModuleExecutorCallbacks callback) { + return this.executorCallbackMap.getOrDefault(callback, List.of()); + } + private void invokeSafe(final AbstractModule module, final Consumer consumer) { try { consumer.accept(module); diff --git a/src/test/java/de/extio/game_engine/module/ModuleExecutorImplTest.java b/src/test/java/de/extio/game_engine/module/ModuleExecutorImplTest.java index c72299e..8d12bc6 100644 --- a/src/test/java/de/extio/game_engine/module/ModuleExecutorImplTest.java +++ b/src/test/java/de/extio/game_engine/module/ModuleExecutorImplTest.java @@ -24,9 +24,11 @@ public class ModuleExecutorImplTest { } @Test - public void testExecute_CallsAllPhasesForActiveAndDisplayedClientModule() { + public void testExecute_CallsAllPhasesForDisplayedClientModuleSubscribedToAllPhases() { final AbstractClientModule clientModule = mock(AbstractClientModule.class); - when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule)); when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule)); this.moduleExecutor.execute(); @@ -37,20 +39,24 @@ public class ModuleExecutorImplTest { } @Test - public void testExecute_CallsOnlyRunForActiveNonClientModule() { + public void testExecute_CallsOnlyRunForNonClientModule() { final AbstractModule module = mock(AbstractModule.class); - when(this.moduleManager.getModulesActive()).thenReturn(List.of(module)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of()); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(module)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of()); this.moduleExecutor.execute(); verify(module).run(); - verify(module, never()).onActivate(); // Just checking it doesn't call other things + verify(module, never()).onActivate(); } @Test - public void testExecute_CallsOnlyRunForActiveButNotDisplayedClientModule() { + public void testExecute_SkipsUiPhaseForNotDisplayedClientModule() { final AbstractClientModule clientModule = mock(AbstractClientModule.class); - when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule)); when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of()); this.moduleExecutor.execute(); @@ -61,12 +67,14 @@ public class ModuleExecutorImplTest { } @Test - public void testExecute_HandlesExceptionInModule() { + public void testExecute_HandlesExceptionInRunPhase() { final AbstractModule module1 = mock(AbstractModule.class); final AbstractModule module2 = mock(AbstractModule.class); doThrow(new RuntimeException("Test Exception")).when(module1).run(); - when(this.moduleManager.getModulesActive()).thenReturn(List.of(module1, module2)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of()); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(module1, module2)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of()); this.moduleExecutor.execute(); @@ -79,7 +87,9 @@ public class ModuleExecutorImplTest { final AbstractClientModule clientModule = mock(AbstractClientModule.class); doThrow(new RuntimeException("UI Pre Exception")).when(clientModule).runUiPre(); - when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule)); when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule)); this.moduleExecutor.execute(); @@ -90,36 +100,226 @@ public class ModuleExecutorImplTest { } @Test - public void testExecute_EmptyModules() { - when(this.moduleManager.getModulesActive()).thenReturn(List.of()); + public void testExecute_NoSubscribersForCallback() { + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of()); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of()); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of()); this.moduleExecutor.execute(); - verify(this.moduleManager, times(3)).getModulesActive(); + verify(this.moduleManager).getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE); + verify(this.moduleManager).getSubscribersForCallback(ModuleExecutorCallbacks.RUN); + verify(this.moduleManager).getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST); } @Test - public void testExecute_MultipleModules() { + public void testExecute_MultipleModulesWithMixedSubscriptions() { final AbstractClientModule clientModule1 = mock(AbstractClientModule.class); final AbstractClientModule clientModule2 = mock(AbstractClientModule.class); final AbstractModule module1 = mock(AbstractModule.class); - when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule1, clientModule2, module1)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule1, clientModule2)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule1, clientModule2, module1)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule1)); when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule1)); this.moduleExecutor.execute(); - // clientModule1: displayed, so all phases verify(clientModule1).runUiPre(); verify(clientModule1).run(); verify(clientModule1).runUiPost(); - // clientModule2: active but not displayed, so only run verify(clientModule2, never()).runUiPre(); verify(clientModule2).run(); verify(clientModule2, never()).runUiPost(); - // module1: only run verify(module1).run(); } + + @Test + public void testExecute_ModuleSubscribedOnlyToUiPre() { + final AbstractClientModule clientModule = mock(AbstractClientModule.class); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of()); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of()); + when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule)); + + this.moduleExecutor.execute(); + + verify(clientModule).runUiPre(); + verify(clientModule, never()).run(); + verify(clientModule, never()).runUiPost(); + } + + @Test + public void testExecute_ModuleSubscribedOnlyToRun() { + final AbstractModule module = mock(AbstractModule.class); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of()); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(module)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of()); + + this.moduleExecutor.execute(); + + verify(module).run(); + } + + @Test + public void testExecute_ClientModuleSubscribedOnlyToUiPost() { + final AbstractClientModule clientModule = mock(AbstractClientModule.class); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of()); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of()); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule)); + + this.moduleExecutor.execute(); + + verify(clientModule, never()).runUiPre(); + verify(clientModule, never()).run(); + verify(clientModule).runUiPost(); + } + + @Test + public void testExecute_ClientModuleSubscribedToUiPreAndRun_NotUiPost() { + final AbstractClientModule clientModule = mock(AbstractClientModule.class); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of()); + when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule)); + + this.moduleExecutor.execute(); + + verify(clientModule).runUiPre(); + verify(clientModule).run(); + verify(clientModule, never()).runUiPost(); + } + + @Test + public void testExecute_ClientModuleSubscribedToUiPreAndUiPost_NotRun() { + final AbstractClientModule clientModule = mock(AbstractClientModule.class); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of()); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule)); + + this.moduleExecutor.execute(); + + verify(clientModule).runUiPre(); + verify(clientModule, never()).run(); + verify(clientModule).runUiPost(); + } + + @Test + public void testExecute_ClientModuleSubscribedToRunAndUiPost_NotUiPre() { + final AbstractClientModule clientModule = mock(AbstractClientModule.class); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of()); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule)); + + this.moduleExecutor.execute(); + + verify(clientModule, never()).runUiPre(); + verify(clientModule).run(); + verify(clientModule).runUiPost(); + } + + @Test + public void testExecute_HandleExceptionInUiPost() { + final AbstractClientModule clientModule = mock(AbstractClientModule.class); + doThrow(new RuntimeException("UI Post Exception")).when(clientModule).runUiPost(); + + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule)); + + this.moduleExecutor.execute(); + + verify(clientModule).runUiPre(); + verify(clientModule).run(); + verify(clientModule).runUiPost(); + } + + @Test + public void testExecute_HandleExceptionInRun() { + final AbstractClientModule clientModule = mock(AbstractClientModule.class); + doThrow(new RuntimeException("Run Exception")).when(clientModule).run(); + + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule)); + + this.moduleExecutor.execute(); + + verify(clientModule).runUiPre(); + verify(clientModule).run(); + verify(clientModule).runUiPost(); + } + + @Test + public void testExecute_MultipleExceptionsInDifferentPhases() { + final AbstractModule module1 = mock(AbstractModule.class); + final AbstractModule module2 = mock(AbstractModule.class); + final AbstractClientModule clientModule = mock(AbstractClientModule.class); + + doThrow(new RuntimeException("Exception in module1")).when(module1).run(); + doThrow(new RuntimeException("Exception in clientModule")).when(clientModule).runUiPre(); + + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(module1, module2)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of()); + when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule)); + + this.moduleExecutor.execute(); + + verify(clientModule).runUiPre(); + verify(module1).run(); + verify(module2).run(); + } + + @Test + public void testExecute_ClientModuleDisplayedButNotSubscribedToUiPhases() { + final AbstractClientModule clientModule = mock(AbstractClientModule.class); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of()); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of()); + + this.moduleExecutor.execute(); + + verify(clientModule, never()).runUiPre(); + verify(clientModule).run(); + verify(clientModule, never()).runUiPost(); + } + + @Test + public void testExecute_ClientModuleNotDisplayedButSubscribedToRunOnly() { + final AbstractClientModule clientModule = mock(AbstractClientModule.class); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of()); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of()); + + this.moduleExecutor.execute(); + + verify(clientModule, never()).runUiPre(); + verify(clientModule).run(); + verify(clientModule, never()).runUiPost(); + } + + @Test + public void testExecute_MultipleNonClientModulesSubscribedToDifferentCallbacks() { + final AbstractModule module1 = mock(AbstractModule.class); + final AbstractModule module2 = mock(AbstractModule.class); + final AbstractModule module3 = mock(AbstractModule.class); + + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(module1)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(module1, module2, module3)); + when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(module3)); + + this.moduleExecutor.execute(); + + verify(module1).run(); + verify(module2).run(); + verify(module3).run(); + } } diff --git a/src/test/java/de/extio/game_engine/module/ModuleManagerTest.java b/src/test/java/de/extio/game_engine/module/ModuleManagerTest.java index 8e22b66..b6d4ca8 100644 --- a/src/test/java/de/extio/game_engine/module/ModuleManagerTest.java +++ b/src/test/java/de/extio/game_engine/module/ModuleManagerTest.java @@ -408,6 +408,95 @@ public class ModuleManagerTest { assertFalse(this.moduleManager.isModal()); } + @Test + public void testGetSubscribersForCallback_NoSubscribers() { + final List subscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE); + + assertTrue(subscribers.isEmpty()); + } + + @Test + public void testGetSubscribersForCallback_WithSubscriber() { + this.moduleManager.loadModule(CallbackSubscriberModule.class); + this.moduleManager.changeActiveState(CallbackSubscriberModule.class, true); + + final List subscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE); + + assertEquals(1, subscribers.size()); + assertTrue(subscribers.stream().anyMatch(m -> m instanceof CallbackSubscriberModule)); + } + + @Test + public void testGetSubscribersForCallback_MultipleCallbacks() { + this.moduleManager.loadModule(MultiCallbackSubscriberModule.class); + this.moduleManager.changeActiveState(MultiCallbackSubscriberModule.class, true); + + final List uiPreSubscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE); + final List runSubscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN); + final List uiPostSubscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST); + + assertEquals(1, uiPreSubscribers.size()); + assertEquals(1, runSubscribers.size()); + assertEquals(1, uiPostSubscribers.size()); + + assertTrue(uiPreSubscribers.get(0) instanceof MultiCallbackSubscriberModule); + assertTrue(runSubscribers.get(0) instanceof MultiCallbackSubscriberModule); + assertTrue(uiPostSubscribers.get(0) instanceof MultiCallbackSubscriberModule); + } + + @Test + public void testGetSubscribersForCallback_UnsubscribedCallbackEmpty() { + this.moduleManager.loadModule(CallbackSubscriberModule.class); + this.moduleManager.changeActiveState(CallbackSubscriberModule.class, true); + + final List runSubscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN); + + assertTrue(runSubscribers.isEmpty()); + } + + @Test + public void testGetSubscribersForCallback_RemovedOnDeactivate() { + this.moduleManager.loadModule(CallbackSubscriberModule.class); + this.moduleManager.changeActiveState(CallbackSubscriberModule.class, true); + + List subscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE); + assertEquals(1, subscribers.size()); + + this.moduleManager.changeActiveState(CallbackSubscriberModule.class, false); + + subscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE); + assertTrue(subscribers.isEmpty()); + } + + @Test + public void testGetSubscribersForCallback_MultipleModules() { + this.moduleManager.loadModule(CallbackSubscriberModule.class); + this.moduleManager.loadModule(MultiCallbackSubscriberModule.class); + this.moduleManager.changeActiveState(CallbackSubscriberModule.class, true); + this.moduleManager.changeActiveState(MultiCallbackSubscriberModule.class, true); + + final List uiPreSubscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE); + + assertEquals(2, uiPreSubscribers.size()); + assertTrue(uiPreSubscribers.stream().anyMatch(m -> m instanceof CallbackSubscriberModule)); + assertTrue(uiPreSubscribers.stream().anyMatch(m -> m instanceof MultiCallbackSubscriberModule)); + } + + @Test + public void testGetSubscribersForCallback_ReactivateModule() { + this.moduleManager.loadModule(CallbackSubscriberModule.class); + this.moduleManager.changeActiveState(CallbackSubscriberModule.class, true); + this.moduleManager.changeActiveState(CallbackSubscriberModule.class, false); + + List subscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE); + assertTrue(subscribers.isEmpty()); + + this.moduleManager.changeActiveState(CallbackSubscriberModule.class, true); + + subscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE); + assertEquals(1, subscribers.size()); + } + static class TestModule extends AbstractModule { boolean onLoadCalled = false; @@ -504,4 +593,26 @@ public class ModuleManagerTest { } + static class CallbackSubscriberModule extends AbstractModule { + + @Override + public java.util.List executorCallbackSubscriptions() { + return java.util.List.of(ModuleExecutorCallbacks.UI_PRE); + } + + } + + static class MultiCallbackSubscriberModule extends AbstractModule { + + @Override + public java.util.List executorCallbackSubscriptions() { + return java.util.List.of( + ModuleExecutorCallbacks.UI_PRE, + ModuleExecutorCallbacks.RUN, + ModuleExecutorCallbacks.UI_POST + ); + } + + } + }