diff --git a/src/main/java/de/extio/game_engine/module/AbstractClientModule.java b/src/main/java/de/extio/game_engine/module/AbstractClientModule.java index 0895968..85cc9f8 100644 --- a/src/main/java/de/extio/game_engine/module/AbstractClientModule.java +++ b/src/main/java/de/extio/game_engine/module/AbstractClientModule.java @@ -31,11 +31,11 @@ public abstract class AbstractClientModule extends AbstractModule { } - public void runUiPreEvents() { + public void runUiPre() { } - public void runUiPostEvents() { + public void runUiPost() { } diff --git a/src/main/java/de/extio/game_engine/module/ModuleExecutor.java b/src/main/java/de/extio/game_engine/module/ModuleExecutor.java new file mode 100644 index 0000000..4a9c02a --- /dev/null +++ b/src/main/java/de/extio/game_engine/module/ModuleExecutor.java @@ -0,0 +1,7 @@ +package de.extio.game_engine.module; + +public interface ModuleExecutor { + + void execute(); + +} diff --git a/src/main/java/de/extio/game_engine/module/ModuleExecutorImpl.java b/src/main/java/de/extio/game_engine/module/ModuleExecutorImpl.java new file mode 100644 index 0000000..dd3b1ae --- /dev/null +++ b/src/main/java/de/extio/game_engine/module/ModuleExecutorImpl.java @@ -0,0 +1,62 @@ +package de.extio.game_engine.module; + +import java.util.concurrent.StructuredTaskScope; +import java.util.concurrent.StructuredTaskScope.Joiner; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ModuleExecutorImpl implements ModuleExecutor { + + private static final Logger LOGGER = LoggerFactory.getLogger(ModuleExecutorImpl.class); + + private final ModuleManager moduleManager; + + public ModuleExecutorImpl(final ModuleManager moduleManager) { + this.moduleManager = moduleManager; + } + + @Override + public void execute() { + this.runTasks(module -> () -> { + if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) { + this.invokeSafe(clientModule, AbstractClientModule::runUiPre); + } + }); + this.runTasks(module -> () -> { + this.invokeSafe(module, AbstractModule::run); + }); + this.runTasks(module -> () -> { + if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) { + this.invokeSafe(clientModule, AbstractClientModule::runUiPost); + } + }); + } + + private void invokeSafe(final T module, final Consumer consumer) { + try { + consumer.accept(module); + } + catch (final Exception exc) { + LOGGER.error("Error in module " + module.getClass().getName(), exc); + } + } + + private void runTasks(final Function taskSupplier) { + 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); + } + } + scope.join(); + } + catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + +} diff --git a/src/main/java/de/extio/game_engine/module/ModuleManagerAutoConfiguration.java b/src/main/java/de/extio/game_engine/module/ModuleManagerAutoConfiguration.java index 1651874..e2f22a5 100644 --- a/src/main/java/de/extio/game_engine/module/ModuleManagerAutoConfiguration.java +++ b/src/main/java/de/extio/game_engine/module/ModuleManagerAutoConfiguration.java @@ -10,11 +10,17 @@ import org.springframework.context.annotation.Bean; @AutoConfiguration @ConditionalOnProperty(name = "game-engine.module.enabled", havingValue = "true", matchIfMissing = true) public class ModuleManagerAutoConfiguration { - + @Bean @ConditionalOnMissingBean ModuleManager moduleManager(final List initialModules) { return new ModuleManagerImpl(initialModules); } - + + @Bean + @ConditionalOnMissingBean + ModuleExecutor moduleExecutor(final ModuleManager moduleManager) { + return new ModuleExecutorImpl(moduleManager); + } + } 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 5ea9973..b393034 100644 --- a/src/main/java/de/extio/game_engine/module/ModuleManagerImpl.java +++ b/src/main/java/de/extio/game_engine/module/ModuleManagerImpl.java @@ -6,13 +6,13 @@ import java.util.Deque; import java.util.List; import java.util.function.Consumer; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; public class ModuleManagerImpl implements InitializingBean, ModuleManager { - private static final Logger LOGGER = LogManager.getLogger(); + private static final Logger LOGGER = LoggerFactory.getLogger(ModuleManagerImpl.class); private final List modulesInitial; diff --git a/src/test/java/de/extio/game_engine/module/ModuleExecutorImplTest.java b/src/test/java/de/extio/game_engine/module/ModuleExecutorImplTest.java new file mode 100644 index 0000000..c72299e --- /dev/null +++ b/src/test/java/de/extio/game_engine/module/ModuleExecutorImplTest.java @@ -0,0 +1,125 @@ +package de.extio.game_engine.module; + +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class ModuleExecutorImplTest { + + @Mock + private ModuleManager moduleManager; + + private ModuleExecutorImpl moduleExecutor; + + @BeforeEach + public void setUp() { + this.moduleExecutor = new ModuleExecutorImpl(this.moduleManager); + } + + @Test + public void testExecute_CallsAllPhasesForActiveAndDisplayedClientModule() { + final AbstractClientModule clientModule = mock(AbstractClientModule.class); + when(this.moduleManager.getModulesActive()).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_CallsOnlyRunForActiveNonClientModule() { + final AbstractModule module = mock(AbstractModule.class); + when(this.moduleManager.getModulesActive()).thenReturn(List.of(module)); + + this.moduleExecutor.execute(); + + verify(module).run(); + verify(module, never()).onActivate(); // Just checking it doesn't call other things + } + + @Test + public void testExecute_CallsOnlyRunForActiveButNotDisplayedClientModule() { + final AbstractClientModule clientModule = mock(AbstractClientModule.class); + when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule)); + when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of()); + + this.moduleExecutor.execute(); + + verify(clientModule, never()).runUiPre(); + verify(clientModule).run(); + verify(clientModule, never()).runUiPost(); + } + + @Test + public void testExecute_HandlesExceptionInModule() { + 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)); + + this.moduleExecutor.execute(); + + verify(module1).run(); + verify(module2).run(); + } + + @Test + public void testExecute_HandlesExceptionInUiPre() { + 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.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule)); + + this.moduleExecutor.execute(); + + verify(clientModule).runUiPre(); + verify(clientModule).run(); + verify(clientModule).runUiPost(); + } + + @Test + public void testExecute_EmptyModules() { + when(this.moduleManager.getModulesActive()).thenReturn(List.of()); + + this.moduleExecutor.execute(); + + verify(this.moduleManager, times(3)).getModulesActive(); + } + + @Test + public void testExecute_MultipleModules() { + 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.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(); + } +}