feat(module): Implement module management system with loading, unloading, and state management

This commit is contained in:
extio-de
2025-12-22 19:49:04 +01:00
parent 5f38d38581
commit 8c9c4d85e6
7 changed files with 946 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
package de.extio.game_engine.module;
/**
* Root of all UI client modules
*/
public abstract class AbstractClientModule extends AbstractModule {
final static int MODULE_PRIORITY_LOW = 25;
final static int MODULE_PRIORITY_NORMAL = 50;
final static int MODULE_PRIORITY_HIGH = 75;
public AbstractClientModule() {
}
public int getPriority() {
return MODULE_PRIORITY_NORMAL;
}
public boolean isAlwaysDisplay() {
return false;
}
public void onShow() {
}
public void onHide() {
}
public void runUiPreEvents() {
}
public void runUiPostEvents() {
}
}

View File

@@ -0,0 +1,43 @@
package de.extio.game_engine.module;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Root of all modules
*/
public abstract class AbstractModule {
protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractModule.class);
public AbstractModule() {
}
public void onLoad() {
}
public void onUnload() {
}
public void onActivate() {
}
public void onDeactivate() {
}
public void run() {
}
@Override
public String toString() {
return this.getClass().getSimpleName();
}
}

View File

@@ -0,0 +1,41 @@
package de.extio.game_engine.module;
import java.util.List;
public interface ModuleManager {
void loadModule(Class<? extends AbstractModule> clazz);
void unloadModule(Class<? extends AbstractModule> clazz);
List<AbstractModule> getModulesAll();
List<AbstractModule> getModulesActive();
List<AbstractClientModule> getModulesActiveClientModules();
List<AbstractClientModule> getModulesDisplayedClientModules();
void changeActiveState(String className, boolean active);
void changeActiveState(Class<? extends AbstractModule> clazz, boolean active);
void deactivateAll();
void changeDisplayState(String className, boolean active);
void changeDisplayState(Class<? extends AbstractClientModule> clazz, boolean display);
boolean isDisplayed(String className);
boolean isDisplayed(Class<? extends AbstractClientModule> clazz);
void hideAll();
void hideExcept(String... classNames);
void restoreVisibility();
boolean isModal();
}

View File

@@ -0,0 +1,18 @@
package de.extio.game_engine.module;
import java.util.List;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnProperty(name = "game-engine.module.enabled", havingValue = "true", matchIfMissing = true)
public class ModuleManagerAutoConfiguration {
@Bean
ModuleManager moduleManager(final List<AbstractModule> initialModules) {
return new ModuleManagerImpl(initialModules);
}
}

View File

@@ -0,0 +1,297 @@
package de.extio.game_engine.module;
import java.util.ArrayDeque;
import java.util.ArrayList;
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.springframework.beans.factory.InitializingBean;
public class ModuleManagerImpl implements InitializingBean, ModuleManager {
private static final Logger LOGGER = LogManager.getLogger();
private final List<AbstractModule> modulesInitial;
private final List<AbstractModule> modulesAll = new ArrayList<>();
private List<AbstractModule> modulesAllView = List.of();
private List<AbstractModule> modulesActiveView = List.of();
private List<AbstractClientModule> modulesActiveClientModulesView = List.of();
private List<AbstractClientModule> modulesDisplayedClientModulesView = List.of();
private final List<AbstractModule> modulesActive = new ArrayList<>();
private final List<AbstractClientModule> modulesDisplayed = new ArrayList<>();
private final Deque<List<AbstractClientModule>> lastVisibleStates = new ArrayDeque<>();
public ModuleManagerImpl(final List<AbstractModule> initialModules) {
this.modulesInitial = initialModules;
}
@Override
public void afterPropertiesSet() throws Exception {
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.clear();
}
@Override
public List<AbstractModule> getModulesAll() {
return this.modulesAllView;
}
@Override
public List<AbstractModule> getModulesActive() {
return this.modulesActiveView;
}
@Override
public List<AbstractClientModule> getModulesActiveClientModules() {
return this.modulesActiveClientModulesView;
}
@Override
public List<AbstractClientModule> getModulesDisplayedClientModules() {
return modulesDisplayedClientModulesView;
}
@Override
public void loadModule(final Class<? extends AbstractModule> clazz) {
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);
}
catch (final Exception e) {
LOGGER.error("Error loading module " + clazz.getName(), e);
}
}
@Override
public void unloadModule(final Class<? extends AbstractModule> clazz) {
try {
final AbstractModule module = this.modulesAll.stream().filter(mod -> clazz.isInstance(mod)).findAny().orElse(null);
if (module == null) {
LOGGER.debug("AbstractModule not loaded: " + clazz.getName());
return;
}
this.changeActiveState(clazz, false);
this.modulesAll.remove(module);
this.sortModules(this.modulesAll);
this.modulesAllView = List.copyOf(this.modulesAll);
this.invokeSafe(module, m -> m.onUnload());
LOGGER.debug("Unloaded module " + clazz.getName());
}
catch (final Exception e) {
LOGGER.error("Error unloading module " + clazz.getName(), e);
}
}
@Override
public void changeActiveState(final String className, final boolean active) {
try {
final var clazz = Class.forName(className);
this.changeActiveState(clazz.asSubclass(AbstractModule.class), active);
}
catch (final ClassNotFoundException e) {
LOGGER.error("Class not found: " + className, e);
}
}
@Override
public void changeActiveState(final Class<? extends AbstractModule> clazz, final boolean active) {
this.modulesActiveView.stream()
.filter(module -> clazz.isAssignableFrom(module.getClass()))
.findAny()
.ifPresentOrElse(module -> {
if (!active) {
if (module instanceof final AbstractClientModule clientModule) {
this.changeDisplayState(clientModule.getClass(), false);
}
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);
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());
});
}
});
}
@Override
public void deactivateAll() {
for (final AbstractModule module : this.modulesActiveView) {
this.changeActiveState(module.getClass(), false);
}
this.lastVisibleStates.clear();
}
@Override
public void changeDisplayState(final String className, final boolean active) {
try {
final var clazz = Class.forName(className);
this.changeDisplayState(clazz.asSubclass(AbstractClientModule.class), active);
}
catch (final ClassNotFoundException e) {
LOGGER.error("Class not found: " + className, e);
}
}
@Override
public void changeDisplayState(final Class<? extends AbstractClientModule> 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);
this.invokeSafe(clientModule, m -> ((AbstractClientModule) m).onShow());
}
else if (!doDisplay && curDisplayed) {
this.modulesDisplayed.removeIf(mod -> clazz.isAssignableFrom(mod.getClass()));
this.invokeSafe(clientModule, m -> ((AbstractClientModule) m).onHide());
}
this.modulesDisplayedClientModulesView = List.copyOf(this.modulesDisplayed);
break;
}
}
}
@Override
public boolean isDisplayed(final String className) {
try {
final var clazz = Class.forName(className);
return this.isDisplayed(clazz.asSubclass(AbstractClientModule.class));
}
catch (final ClassNotFoundException e) {
LOGGER.error("Class not found: " + className, e);
return false;
}
}
@Override
public boolean isDisplayed(final Class<? extends AbstractClientModule> clazz) {
return this.modulesDisplayed.stream().anyMatch(m -> clazz.isAssignableFrom(m.getClass()));
}
@Override
public void hideAll() {
this.lastVisibleStates.clear();
for (final var module : List.copyOf(this.modulesDisplayed)) {
if ((module instanceof final AbstractClientModule clientModule) && !clientModule.isAlwaysDisplay()) {
this.changeDisplayState(clientModule.getClass(), false);
}
}
}
@Override
@SafeVarargs
public final void hideExcept(final String... classNames) {
final List<AbstractClientModule> state = new ArrayList<>();
final var keep = new java.util.HashSet<>(java.util.Arrays.asList(classNames));
for (final var module : List.copyOf(this.modulesDisplayed)) {
if (module instanceof final AbstractClientModule clientModule &&
!clientModule.isAlwaysDisplay() &&
!keep.contains(clientModule.getClass().getName())) {
this.changeDisplayState(clientModule.getClass(), false);
state.add(clientModule);
}
}
if (!state.isEmpty()) {
this.lastVisibleStates.push(state);
}
}
@Override
public void restoreVisibility() {
if (this.lastVisibleStates.isEmpty()) {
return;
}
for (final AbstractClientModule module : this.lastVisibleStates.pop()) {
this.changeDisplayState(module.getClass(), true);
}
}
@Override
public boolean isModal() {
return !this.lastVisibleStates.isEmpty();
}
private void invokeSafe(final AbstractModule module, final Consumer<AbstractModule> consumer) {
try {
consumer.accept(module);
}
catch (final Exception exc) {
LOGGER.error("Error in module " + module.getClass().getName(), exc);
}
}
private void sortModules(final List<AbstractModule> modules) {
modules.sort((o1, o2) -> {
if (o1 instanceof final AbstractClientModule a1 && o2 instanceof final AbstractClientModule a2) {
return Integer.compare(a2.getPriority(), a1.getPriority());
}
return Boolean.compare(o1 instanceof AbstractClientModule, o2 instanceof AbstractClientModule);
});
}
}

View File

@@ -3,3 +3,4 @@ de.extio.game_engine.keyboard.KeycodeAutoConfiguration
de.extio.game_engine.renderer.RendererAutoConfiguration
de.extio.game_engine.renderer.g2d.G2DAutoConfiguration
de.extio.game_engine.storage.StorageServiceAutoConfiguration
de.extio.game_engine.module.ModuleManagerAutoConfiguration