From e56148559ce12f6001fd3a364209e616df6b5520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 28 Jul 2014 01:07:23 +0200 Subject: [PATCH] Source import --- .classpath | 7 + .gitignore | 18 +- .project | 17 + .settings/org.eclipse.jdt.core.prefs | 11 + README.md | 2 + src/junk/AppInitOptions.java | 133 +++++ src/junk/BaseApp.java | 184 ++++++ src/junk/DisplaySystem.java | 299 ++++++++++ src/junk/Render.java | 542 ++++++++++++++++++ src/junk/SoundSystem.java | 261 +++++++++ .../gamecore/audio/AudioModule.java | 220 +++++++ .../gamecore/audio/DeferredAudio.java | 47 ++ src/mightypork/gamecore/audio/IAudio.java | 99 ++++ .../gamecore/audio/JointVolume.java | 51 ++ .../gamecore/audio/SoundRegistry.java | 83 +++ src/mightypork/gamecore/audio/Volume.java | 29 + .../audio/players/BaseAudioPlayer.java | 105 ++++ .../gamecore/audio/players/EffectPlayer.java | 66 +++ .../gamecore/audio/players/LoopPlayer.java | 166 ++++++ src/mightypork/gamecore/core/App.java | 277 +++++++++ src/mightypork/gamecore/core/AppBackend.java | 65 +++ src/mightypork/gamecore/core/AppPlugin.java | 34 ++ .../gamecore/core/BackendModule.java | 24 + src/mightypork/gamecore/core/InitTask.java | 150 +++++ src/mightypork/gamecore/core/MainLoop.java | 130 +++++ .../gamecore/core/OptionalInitTask.java | 19 + src/mightypork/gamecore/core/WorkDir.java | 91 +++ .../gamecore/core/config/Config.java | 269 +++++++++ .../gamecore/core/config/InitTaskConfig.java | 75 +++ .../core/config/KeyStrokeProperty.java | 55 ++ .../gamecore/core/events/MainLoopRequest.java | 44 ++ .../gamecore/core/events/ShutdownEvent.java | 47 ++ .../core/events/ShutdownListener.java | 18 + .../core/init/InitTaskCrashHandler.java | 42 ++ .../gamecore/core/init/InitTaskDisplay.java | 97 ++++ .../core/init/InitTaskIonizables.java | 74 +++ .../gamecore/core/init/InitTaskLog.java | 108 ++++ .../gamecore/core/init/InitTaskLogHeader.java | 54 ++ .../gamecore/core/init/InitTaskWorkdir.java | 136 +++++ .../screenshot/InitTaskPluginScreenshot.java | 68 +++ .../plugins/screenshot/ScreenshotPlugin.java | 34 ++ .../plugins/screenshot/ScreenshotRequest.java | 22 + .../screenshot/TaskTakeScreenshot.java | 100 ++++ .../graphics/FullscreenToggleRequest.java | 16 + .../gamecore/graphics/GraphicsModule.java | 406 +++++++++++++ .../gamecore/graphics/Renderable.java | 16 + .../gamecore/graphics/Screenshot.java | 34 ++ .../gamecore/graphics/fonts/DeferredFont.java | 98 ++++ .../gamecore/graphics/fonts/FontRegistry.java | 79 +++ .../gamecore/graphics/fonts/FontRenderer.java | 213 +++++++ .../gamecore/graphics/fonts/Glyphs.java | 23 + .../gamecore/graphics/fonts/IFont.java | 76 +++ .../graphics/textures/DeferredTexture.java | 57 ++ .../graphics/textures/FilterMode.java | 12 + .../gamecore/graphics/textures/ITexture.java | 64 +++ .../gamecore/graphics/textures/QuadGrid.java | 92 +++ .../graphics/textures/TextureRegistry.java | 143 +++++ .../gamecore/graphics/textures/TxQuad.java | 164 ++++++ .../gamecore/graphics/textures/TxSheet.java | 121 ++++ .../gamecore/graphics/textures/WrapMode.java | 12 + src/mightypork/gamecore/gui/Action.java | 54 ++ src/mightypork/gamecore/gui/ActionGroup.java | 38 ++ .../gamecore/gui/ActionTrigger.java | 17 + .../gui/components/BaseComponent.java | 169 ++++++ .../gamecore/gui/components/Component.java | 95 +++ .../gui/components/DynamicWidthComponent.java | 7 + .../gui/components/InputComponent.java | 15 + .../gui/components/LayoutComponent.java | 122 ++++ .../gui/components/LinearComponent.java | 77 +++ .../gui/components/PluggableRenderable.java | 28 + .../components/input/ClickableComponent.java | 49 ++ .../components/input/ClickableWrapper.java | 63 ++ .../gui/components/input/TextButton.java | 77 +++ .../gui/components/layout/ColumnLayout.java | 42 ++ .../components/layout/ConstraintLayout.java | 36 ++ .../components/layout/FlowColumnLayout.java | 85 +++ .../gui/components/layout/FlowRowLayout.java | 84 +++ .../gui/components/layout/GridLayout.java | 78 +++ .../gui/components/layout/NullComponent.java | 18 + .../gui/components/layout/RowLayout.java | 42 ++ .../layout/linear/AbstractLinearWrapper.java | 78 +++ .../components/layout/linear/LinearGap.java | 24 + .../layout/linear/LinearLayout.java | 93 +++ .../layout/linear/LinearRectangle.java | 31 + .../layout/linear/LinearSquare.java | 20 + .../layout/linear/LinearWrapper.java | 20 + .../gui/components/painters/ImagePainter.java | 46 ++ .../gui/components/painters/QuadPainter.java | 62 ++ .../gui/components/painters/TextPainter.java | 163 ++++++ .../gui/events/LayoutChangeEvent.java | 30 + .../gui/events/LayoutChangeListener.java | 7 + .../gamecore/gui/events/ScreenRequest.java | 33 ++ .../gui/events/ScreenRequestListener.java | 15 + .../gui/events/ViewportChangeEvent.java | 44 ++ .../gui/events/ViewportChangeListener.java | 17 + .../gamecore/gui/screens/LayeredScreen.java | 112 ++++ .../gamecore/gui/screens/Overlay.java | 222 +++++++ .../gamecore/gui/screens/Screen.java | 153 +++++ .../gamecore/gui/screens/ScreenLayer.java | 50 ++ .../gamecore/gui/screens/ScreenRegistry.java | 106 ++++ .../gui/screens/impl/CrossfadeOverlay.java | 83 +++ .../gui/screens/impl/CrossfadeRequest.java | 45 ++ .../gui/screens/impl/FadingLayer.java | 151 +++++ .../gamecore/gui/screens/impl/LayerColor.java | 31 + .../gamecore/input/InputModule.java | 101 ++++ src/mightypork/gamecore/input/Key.java | 114 ++++ src/mightypork/gamecore/input/KeyBinder.java | 31 + src/mightypork/gamecore/input/KeyBinding.java | 70 +++ .../gamecore/input/KeyBindingPool.java | 71 +++ src/mightypork/gamecore/input/KeyStroke.java | 137 +++++ src/mightypork/gamecore/input/Keys.java | 374 ++++++++++++ src/mightypork/gamecore/input/Trigger.java | 13 + .../gamecore/input/events/KeyEvent.java | 84 +++ .../input/events/KeyEventHandler.java | 17 + .../input/events/MouseButtonEvent.java | 125 ++++ .../input/events/MouseButtonHandler.java | 17 + .../input/events/MouseMotionEvent.java | 56 ++ .../input/events/MouseMotionHandler.java | 17 + .../resources/BaseDeferredResource.java | 135 +++++ .../gamecore/resources/DeferredResource.java | 23 + src/mightypork/gamecore/resources/Res.java | 100 ++++ .../gamecore/resources/ResourceSetup.java | 38 ++ .../loading/AsyncResourceLoader.java | 126 ++++ .../loading/MustLoadInRenderingContext.java | 18 + .../loading/ResourceLoadRequest.java | 33 ++ .../resources/loading/ResourceLoader.java | 29 + 126 files changed, 10548 insertions(+), 12 deletions(-) create mode 100644 .classpath create mode 100644 .project create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 src/junk/AppInitOptions.java create mode 100644 src/junk/BaseApp.java create mode 100644 src/junk/DisplaySystem.java create mode 100644 src/junk/Render.java create mode 100644 src/junk/SoundSystem.java create mode 100644 src/mightypork/gamecore/audio/AudioModule.java create mode 100644 src/mightypork/gamecore/audio/DeferredAudio.java create mode 100644 src/mightypork/gamecore/audio/IAudio.java create mode 100644 src/mightypork/gamecore/audio/JointVolume.java create mode 100644 src/mightypork/gamecore/audio/SoundRegistry.java create mode 100644 src/mightypork/gamecore/audio/Volume.java create mode 100644 src/mightypork/gamecore/audio/players/BaseAudioPlayer.java create mode 100644 src/mightypork/gamecore/audio/players/EffectPlayer.java create mode 100644 src/mightypork/gamecore/audio/players/LoopPlayer.java create mode 100644 src/mightypork/gamecore/core/App.java create mode 100644 src/mightypork/gamecore/core/AppBackend.java create mode 100644 src/mightypork/gamecore/core/AppPlugin.java create mode 100644 src/mightypork/gamecore/core/BackendModule.java create mode 100644 src/mightypork/gamecore/core/InitTask.java create mode 100644 src/mightypork/gamecore/core/MainLoop.java create mode 100644 src/mightypork/gamecore/core/OptionalInitTask.java create mode 100644 src/mightypork/gamecore/core/WorkDir.java create mode 100644 src/mightypork/gamecore/core/config/Config.java create mode 100644 src/mightypork/gamecore/core/config/InitTaskConfig.java create mode 100644 src/mightypork/gamecore/core/config/KeyStrokeProperty.java create mode 100644 src/mightypork/gamecore/core/events/MainLoopRequest.java create mode 100644 src/mightypork/gamecore/core/events/ShutdownEvent.java create mode 100644 src/mightypork/gamecore/core/events/ShutdownListener.java create mode 100644 src/mightypork/gamecore/core/init/InitTaskCrashHandler.java create mode 100644 src/mightypork/gamecore/core/init/InitTaskDisplay.java create mode 100644 src/mightypork/gamecore/core/init/InitTaskIonizables.java create mode 100644 src/mightypork/gamecore/core/init/InitTaskLog.java create mode 100644 src/mightypork/gamecore/core/init/InitTaskLogHeader.java create mode 100644 src/mightypork/gamecore/core/init/InitTaskWorkdir.java create mode 100644 src/mightypork/gamecore/core/plugins/screenshot/InitTaskPluginScreenshot.java create mode 100644 src/mightypork/gamecore/core/plugins/screenshot/ScreenshotPlugin.java create mode 100644 src/mightypork/gamecore/core/plugins/screenshot/ScreenshotRequest.java create mode 100644 src/mightypork/gamecore/core/plugins/screenshot/TaskTakeScreenshot.java create mode 100644 src/mightypork/gamecore/graphics/FullscreenToggleRequest.java create mode 100644 src/mightypork/gamecore/graphics/GraphicsModule.java create mode 100644 src/mightypork/gamecore/graphics/Renderable.java create mode 100644 src/mightypork/gamecore/graphics/Screenshot.java create mode 100644 src/mightypork/gamecore/graphics/fonts/DeferredFont.java create mode 100644 src/mightypork/gamecore/graphics/fonts/FontRegistry.java create mode 100644 src/mightypork/gamecore/graphics/fonts/FontRenderer.java create mode 100644 src/mightypork/gamecore/graphics/fonts/Glyphs.java create mode 100644 src/mightypork/gamecore/graphics/fonts/IFont.java create mode 100644 src/mightypork/gamecore/graphics/textures/DeferredTexture.java create mode 100644 src/mightypork/gamecore/graphics/textures/FilterMode.java create mode 100644 src/mightypork/gamecore/graphics/textures/ITexture.java create mode 100644 src/mightypork/gamecore/graphics/textures/QuadGrid.java create mode 100644 src/mightypork/gamecore/graphics/textures/TextureRegistry.java create mode 100644 src/mightypork/gamecore/graphics/textures/TxQuad.java create mode 100644 src/mightypork/gamecore/graphics/textures/TxSheet.java create mode 100644 src/mightypork/gamecore/graphics/textures/WrapMode.java create mode 100644 src/mightypork/gamecore/gui/Action.java create mode 100644 src/mightypork/gamecore/gui/ActionGroup.java create mode 100644 src/mightypork/gamecore/gui/ActionTrigger.java create mode 100644 src/mightypork/gamecore/gui/components/BaseComponent.java create mode 100644 src/mightypork/gamecore/gui/components/Component.java create mode 100644 src/mightypork/gamecore/gui/components/DynamicWidthComponent.java create mode 100644 src/mightypork/gamecore/gui/components/InputComponent.java create mode 100644 src/mightypork/gamecore/gui/components/LayoutComponent.java create mode 100644 src/mightypork/gamecore/gui/components/LinearComponent.java create mode 100644 src/mightypork/gamecore/gui/components/PluggableRenderable.java create mode 100644 src/mightypork/gamecore/gui/components/input/ClickableComponent.java create mode 100644 src/mightypork/gamecore/gui/components/input/ClickableWrapper.java create mode 100644 src/mightypork/gamecore/gui/components/input/TextButton.java create mode 100644 src/mightypork/gamecore/gui/components/layout/ColumnLayout.java create mode 100644 src/mightypork/gamecore/gui/components/layout/ConstraintLayout.java create mode 100644 src/mightypork/gamecore/gui/components/layout/FlowColumnLayout.java create mode 100644 src/mightypork/gamecore/gui/components/layout/FlowRowLayout.java create mode 100644 src/mightypork/gamecore/gui/components/layout/GridLayout.java create mode 100644 src/mightypork/gamecore/gui/components/layout/NullComponent.java create mode 100644 src/mightypork/gamecore/gui/components/layout/RowLayout.java create mode 100644 src/mightypork/gamecore/gui/components/layout/linear/AbstractLinearWrapper.java create mode 100644 src/mightypork/gamecore/gui/components/layout/linear/LinearGap.java create mode 100644 src/mightypork/gamecore/gui/components/layout/linear/LinearLayout.java create mode 100644 src/mightypork/gamecore/gui/components/layout/linear/LinearRectangle.java create mode 100644 src/mightypork/gamecore/gui/components/layout/linear/LinearSquare.java create mode 100644 src/mightypork/gamecore/gui/components/layout/linear/LinearWrapper.java create mode 100644 src/mightypork/gamecore/gui/components/painters/ImagePainter.java create mode 100644 src/mightypork/gamecore/gui/components/painters/QuadPainter.java create mode 100644 src/mightypork/gamecore/gui/components/painters/TextPainter.java create mode 100644 src/mightypork/gamecore/gui/events/LayoutChangeEvent.java create mode 100644 src/mightypork/gamecore/gui/events/LayoutChangeListener.java create mode 100644 src/mightypork/gamecore/gui/events/ScreenRequest.java create mode 100644 src/mightypork/gamecore/gui/events/ScreenRequestListener.java create mode 100644 src/mightypork/gamecore/gui/events/ViewportChangeEvent.java create mode 100644 src/mightypork/gamecore/gui/events/ViewportChangeListener.java create mode 100644 src/mightypork/gamecore/gui/screens/LayeredScreen.java create mode 100644 src/mightypork/gamecore/gui/screens/Overlay.java create mode 100644 src/mightypork/gamecore/gui/screens/Screen.java create mode 100644 src/mightypork/gamecore/gui/screens/ScreenLayer.java create mode 100644 src/mightypork/gamecore/gui/screens/ScreenRegistry.java create mode 100644 src/mightypork/gamecore/gui/screens/impl/CrossfadeOverlay.java create mode 100644 src/mightypork/gamecore/gui/screens/impl/CrossfadeRequest.java create mode 100644 src/mightypork/gamecore/gui/screens/impl/FadingLayer.java create mode 100644 src/mightypork/gamecore/gui/screens/impl/LayerColor.java create mode 100644 src/mightypork/gamecore/input/InputModule.java create mode 100644 src/mightypork/gamecore/input/Key.java create mode 100644 src/mightypork/gamecore/input/KeyBinder.java create mode 100644 src/mightypork/gamecore/input/KeyBinding.java create mode 100644 src/mightypork/gamecore/input/KeyBindingPool.java create mode 100644 src/mightypork/gamecore/input/KeyStroke.java create mode 100644 src/mightypork/gamecore/input/Keys.java create mode 100644 src/mightypork/gamecore/input/Trigger.java create mode 100644 src/mightypork/gamecore/input/events/KeyEvent.java create mode 100644 src/mightypork/gamecore/input/events/KeyEventHandler.java create mode 100644 src/mightypork/gamecore/input/events/MouseButtonEvent.java create mode 100644 src/mightypork/gamecore/input/events/MouseButtonHandler.java create mode 100644 src/mightypork/gamecore/input/events/MouseMotionEvent.java create mode 100644 src/mightypork/gamecore/input/events/MouseMotionHandler.java create mode 100644 src/mightypork/gamecore/resources/BaseDeferredResource.java create mode 100644 src/mightypork/gamecore/resources/DeferredResource.java create mode 100644 src/mightypork/gamecore/resources/Res.java create mode 100644 src/mightypork/gamecore/resources/ResourceSetup.java create mode 100644 src/mightypork/gamecore/resources/loading/AsyncResourceLoader.java create mode 100644 src/mightypork/gamecore/resources/loading/MustLoadInRenderingContext.java create mode 100644 src/mightypork/gamecore/resources/loading/ResourceLoadRequest.java create mode 100644 src/mightypork/gamecore/resources/loading/ResourceLoader.java diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..81dc7c7 --- /dev/null +++ b/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.gitignore b/.gitignore index 32858aa..d791231 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,6 @@ -*.class - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.ear - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* +/bin/ +/target/ +/~local/ +*.log +.attach_pid* +*~ diff --git a/.project b/.project new file mode 100644 index 0000000..d60d2aa --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + GameCore + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..7341ab1 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,11 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/README.md b/README.md index e01a2b8..7ab2e7d 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,5 @@ gamecore ======== Versatile Java game engine with pluggable backends + +TODO: better readme and tutorial games diff --git a/src/junk/AppInitOptions.java b/src/junk/AppInitOptions.java new file mode 100644 index 0000000..3066eb1 --- /dev/null +++ b/src/junk/AppInitOptions.java @@ -0,0 +1,133 @@ +package junk; + + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +import mightypork.gamecore.core.AppBackend; +import mightypork.gamecore.resources.ResourceSetup; +import mightypork.gamecore.resources.loading.AsyncResourceLoader; +import mightypork.gamecore.resources.loading.ResourceLoader; + + +/** + * Init options holder class + */ +public class AppInitOptions { + + String logDir = "log"; + String logFilePrefix = "runtime"; + + String screenshotDir = "screenshots"; + + boolean busLogging = false; + + String configFile = "settings.cfg"; + String configComment = "Main config file"; + + final List resourceLists = new ArrayList<>(); + final List keyLists = new ArrayList<>(); + final List configLists = new ArrayList<>(); + + ResourceLoader resourceLoader = new AsyncResourceLoader(); + Level logLevel = Level.ALL; + public boolean sigleInstance = true; + Level logSoutLevel = Level.ALL; + + + public void setConfigFile(String filename, String comment) + { + configFile = filename; + configComment = comment; + } + + + public void addConfig(ConfigSetup cfg) + { + configLists.add(cfg); + } + + + public void addKeys(KeySetup keys) + { + keyLists.add(keys); + } + + + public void addResources(ResourceSetup res) + { + resourceLists.add(res); + } + + + public void setBackend(AppBackend backend) + { + this.backend = backend; + } + + + /** + * Set whether to run in single instance mode, or allow multiple instances.
+ * Multiple instances running can cause various collisions (eg. when writing + * config file or logging). + * + * @param sigleInstance true to allow only one instance + */ + public void setSigleInstance(boolean sigleInstance) + { + this.sigleInstance = sigleInstance; + } + + + /** + * Set working directory path. If not exists, it will be created. + * + * @param workdir work dir path + */ + public void setWorkdir(File workdir) + { + this.workdir = workdir; + } + + + public void setBusLogging(boolean yes) + { + busLogging = yes; + } + + + public void setLogOptions(String logDir, String filePrefix, int archivedCount, Level logLevel) + { + this.logDir = logDir; + this.logFilePrefix = filePrefix; + this.logArchiveCount = archivedCount; + this.logLevel = logLevel; + } + + + public void setResourceLoader(ResourceLoader resLoader) + { + resourceLoader = resLoader; + } + + + public void setScreenshotDir(String path) + { + this.screenshotDir = path; + } + + + public void setLockFile(String lockFile) + { + this.lockFile = lockFile; + } + + + public void setLogLevel(Level logLevel, Level soutLevel) + { + this.logLevel = logLevel; + this.logSoutLevel = soutLevel; + } +} diff --git a/src/junk/BaseApp.java b/src/junk/BaseApp.java new file mode 100644 index 0000000..5993226 --- /dev/null +++ b/src/junk/BaseApp.java @@ -0,0 +1,184 @@ +package junk; + + +import java.lang.Thread.UncaughtExceptionHandler; + +import mightypork.gamecore.backends.lwjgl.LwjglInputModule; +import mightypork.gamecore.core.App; +import mightypork.gamecore.core.AppBackend; +import mightypork.gamecore.core.MainLoop; +import mightypork.gamecore.core.WorkDir; +import mightypork.gamecore.core.config.Config; +import mightypork.gamecore.gui.screens.ScreenRegistry; +import mightypork.gamecore.gui.screens.impl.CrossfadeOverlay; +import mightypork.gamecore.resources.Res; +import mightypork.gamecore.resources.ResourceSetup; +import mightypork.utils.logging.Log; + + +/** + * Basic screen-based game with subsystems.
+ * This class takes care of the initialization sequence. + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class BaseApp extends App implements UncaughtExceptionHandler { + + // modules + private MainLoop gameLoop; + private ScreenRegistry screenRegistry; + + private boolean started = false; + private boolean lockObtained = false; + + // init opt holder + private final AppInitOptions opt = new AppInitOptions(); + + + /** + * Get init options + * + * @return opt holder + */ + public AppInitOptions getInitOptions() + { + if (started) { + throw new IllegalStateException("Cannot alter init options after starting the App."); + } + + return opt; + } + + + public BaseApp(AppBackend backend) { + super(backend); + } + + + /** + * Start the application + */ + @Override + public final void start() + { + initialize(); + + Log.i("Starting main loop..."); + + // open first screen !!! + started = true; + gameLoop.start(); + } + + + /** + * Init the app + */ + protected void initialize() + { + WorkDir.init(opt.workdir); + + if (opt.sigleInstance) initLock(); + lockObtained = true; + + for (final RouteSetup rs : opt.routeLists) { + WorkDir.registerRoutes(rs); + } + WorkDir.addPath("_screenshot_dir", opt.screenshotDir); + + // apply configurations + Config.init(WorkDir.getFile(opt.configFile), opt.configComment); + + // add keys to config + for (final KeySetup l : opt.keyLists) { + Config.registerKeys(l); + } + + // add options to config + for (final ConfigSetup c : opt.configLists) { + Config.registerOptions(c); + } + Config.load(); + + /* + * Display + */ + Log.f2("Initializing Display System..."); + initDisplay(gfx()); + + /* + * Audio + */ + Log.f2("Initializing Sound System..."); + soundSystem = new SoundSystem(this); + initSoundSystem(soundSystem); + + /* + * Input + */ + Log.f2("Initializing Input System..."); + inputSystem = new LwjglInputModule(this); + initInputSystem(inputSystem); + + /* + * Prepare main loop + */ + Log.f1("Creating Screen Registry and Game Loop..."); + screenRegistry = new ScreenRegistry(this); + gameLoop = createMainLoop(); + gameLoop.setRootRenderable(screenRegistry); + + /* + * Load resources + * + * Resources should be registered to registries, and AsyncResourceLoader will load them. + */ + Log.f1("Loading resources..."); + if (opt.resourceLoader != null) { + opt.resourceLoader.init(this); + } + + Res.init(this); + + for (final ResourceSetup rl : opt.resourceLists) { + Res.load(rl); + } + + /* + * Screen registry + * + * Must be after resources, because screens can request them during instantiation. + */ + Log.f2("Registering screens..."); + initScreens(screenRegistry); + } + + + /** + * Register game screens to the registry. + * + * @param screens + */ + protected void initScreens(ScreenRegistry screens) + { + screens.addOverlay(new CrossfadeOverlay(this)); + } + + + /** + * Create game loop instance + * + * @return the game loop. + */ + protected MainLoop createMainLoop() + { + return new MainLoop(this); + } + + + protected void beforeShutdown() + { + // ??? + if (lockObtained) Config.save(); + } +} diff --git a/src/junk/DisplaySystem.java b/src/junk/DisplaySystem.java new file mode 100644 index 0000000..ddd089c --- /dev/null +++ b/src/junk/DisplaySystem.java @@ -0,0 +1,299 @@ +//package junk; +// +// +//import static org.lwjgl.opengl.GL11.*; +// +//import java.nio.ByteBuffer; +// +//import mightypork.gamecore.backend.lwjgl.AwtScreenshot; +//import mightypork.gamecore.core.modules.AppAccess; +//import mightypork.gamecore.core.modules.AppModule; +//import mightypork.gamecore.render.events.DisplayReadyEvent; +//import mightypork.gamecore.render.events.ViewportChangeEvent; +//import mightypork.utils.logging.Log; +//import mightypork.utils.math.constraints.rect.Rect; +//import mightypork.utils.math.constraints.rect.RectBound; +//import mightypork.utils.math.constraints.vect.Vect; +//import mightypork.utils.math.timing.FpsMeter; +// +//import org.lwjgl.BufferUtils; +//import org.lwjgl.LWJGLException; +//import org.lwjgl.opengl.Display; +//import org.lwjgl.opengl.DisplayMode; +// +// +///** +// * Display system +// * +// * @author Ondřej Hruška (MightyPork) +// */ +//@Deprecated +//public class DisplaySystem extends AppModule implements RectBound { +// +// private DisplayMode windowDisplayMode; +// private int targetFps; +// private FpsMeter fpsMeter; +// private boolean fullscreenSwitchRequested; +// +// /** Current screen size */ +// private static final Vect screenSize = new Vect() { +// +// @Override +// public double y() +// { +// return Display.getHeight(); +// } +// +// +// @Override +// public double x() +// { +// return Display.getWidth(); +// } +// }; +// +// private static final Rect rect = Rect.make(screenSize); +// +// +// /** +// * @param app app access +// */ +// public DisplaySystem(AppAccess app) { +// super(app); +// } +// +// +// @Override +// protected void deinit() +// { +// Display.destroy(); +// } +// +// +// /** +// * Set target fps (for syncing in endFrame() call).
+// * With vsync enabled, the target fps may not be met. +// * +// * @param fps requested fps +// */ +// public void setTargetFps(int fps) +// { +// this.targetFps = fps; +// } +// +// +// /** +// * Create a main window +// * +// * @param width requested width +// * @param height requested height +// * @param resizable is resizable by the user +// * @param fullscreen is in fullscreen +// * @param title window title +// */ +// public void createMainWindow(int width, int height, boolean resizable, boolean fullscreen, String title) +// { +// try { +// Display.setDisplayMode(windowDisplayMode = new DisplayMode(width, height)); +// Display.setResizable(resizable); +// Display.setVSyncEnabled(true); +// Display.setTitle(title); +// Display.create(); +// +// fpsMeter = new FpsMeter(); +// +// if (fullscreen) { +// switchFullscreen(); +// Display.update(); +// } +// +// getEventBus().send(new DisplayReadyEvent()); +// +// } catch (final LWJGLException e) { +// throw new RuntimeException("Could not initialize screen", e); +// } +// } +// +// +// /** +// * Toggle FS if possible +// */ +// public void switchFullscreen() +// { +// fullscreenSwitchRequested = true; +// } +// +// +// private void doSwitchFullscreen() +// { +// try { +// +// if (!Display.isFullscreen()) { +// Log.f3("Entering fullscreen."); +// // save window resize +// windowDisplayMode = new DisplayMode(Display.getWidth(), Display.getHeight()); +// +// Display.setDisplayMode(Display.getDesktopDisplayMode()); +// Display.setFullscreen(true); +// Display.update(); +// } else { +// Log.f3("Leaving fullscreen."); +// Display.setDisplayMode(windowDisplayMode); +// Display.update(); +// } +// +// getEventBus().send(new ViewportChangeEvent(getSize())); +// +// } catch (final Throwable t) { +// Log.e("Failed to toggle fullscreen mode.", t); +// try { +// Display.setDisplayMode(windowDisplayMode); +// Display.update(); +// } catch (final Throwable t1) { +// throw new RuntimeException("Failed to revert failed fullscreen toggle.", t1); +// } +// } +// } +// +// +// /** +// * Take screenshot (expensive processing is done on-demand when screenshot +// * is processed). +// * +// * @return screenshot object +// */ +// public static AwtScreenshot prepareScreenshot() +// { +// glReadBuffer(GL_FRONT); +// final int width = Display.getWidth(); +// final int height = Display.getHeight(); +// final int bpp = 4; +// final ByteBuffer buffer = BufferUtils.createByteBuffer(width * height * bpp); +// glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer); +// +// final AwtScreenshot sc = new AwtScreenshot(width, height, bpp, buffer); +// +// return sc; +// } +// +// +// /** +// * @return true if close was requested (i.e. click on cross) +// */ +// public static boolean isCloseRequested() +// { +// return Display.isCloseRequested(); +// } +// +// +// /** +// * Get fullscreen state +// * +// * @return is fullscreen +// */ +// public static boolean isFullscreen() +// { +// return Display.isFullscreen(); +// } +// +// +// /** +// * Get screen size. This Vect is final and views at it can safely be made. +// * +// * @return size +// */ +// public static Vect getSize() +// { +// return screenSize; +// } +// +// +// /** +// * Get screen rect. Static version of getRect(). +// * +// * @return size +// */ +// public static Rect getBounds() +// { +// return rect; +// } +// +// +// /** +// * @return screen width +// */ +// public static int getWidth() +// { +// return screenSize.xi(); +// } +// +// +// /** +// * @return screen height +// */ +// public static int getHeight() +// { +// return screenSize.yi(); +// } +// +// +// /** +// * Start a OpenGL frame +// */ +// public void beginFrame() +// { +// // handle resize +// if (Display.wasResized()) { +// getEventBus().send(new ViewportChangeEvent(getSize())); +// } +// +// if (fullscreenSwitchRequested) { +// fullscreenSwitchRequested = false; +// doSwitchFullscreen(); +// } +// +// glLoadIdentity(); +// glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +// fpsMeter.frame(); +// } +// +// +// /** +// * End an OpenGL frame, flip buffers, sync to fps. +// */ +// public void endFrame() +// { +// Display.update(false); // don't poll input devices +// Display.sync(targetFps); +// } +// +// +// /** +// * Get screen rect. This Rect is final and views at it can safely be made. +// */ +// @Override +// public Rect getRect() +// { +// return getBounds(); +// } +// +// +// /** +// * @return current FPS +// */ +// public long getFps() +// { +// return fpsMeter.getFPS(); +// } +// +// +// /** +// * Get screen center. This vect is final and views at it can safely be made. +// * +// * @return screen center. +// */ +// public static Vect getCenter() +// { +// return rect.center(); +// } +//} diff --git a/src/junk/Render.java b/src/junk/Render.java new file mode 100644 index 0000000..7182d30 --- /dev/null +++ b/src/junk/Render.java @@ -0,0 +1,542 @@ +//package junk; +// +// +//import static org.lwjgl.opengl.GL11.*; +// +//import java.io.IOException; +// +//import mightypork.gamecore.resources.textures.FilterMode; +//import mightypork.gamecore.resources.textures.ITexture; +//import mightypork.gamecore.resources.textures.TxQuad; +//import mightypork.utils.files.FileUtils; +//import mightypork.utils.logging.Log; +//import mightypork.utils.math.color.Color; +//import mightypork.utils.math.color.pal.RGB; +//import mightypork.utils.math.constraints.rect.Rect; +//import mightypork.utils.math.constraints.rect.caching.RectDigest; +//import mightypork.utils.math.constraints.vect.Vect; +//import mightypork.utils.math.constraints.vect.VectConst; +// +//import org.lwjgl.opengl.GL11; +//import org.newdawn.slick.opengl.Texture; +//import org.newdawn.slick.opengl.TextureLoader; +// +// +///** +// * Render utilities +// * +// * @author Ondřej Hruška (MightyPork) +// */ +//@Deprecated +//public class Render { +// +// public static final VectConst AXIS_X = Vect.make(1, 0, 0); +// public static final VectConst AXIS_Y = Vect.make(0, 1, 0); +// public static final VectConst AXIS_Z = Vect.make(0, 0, 1); +// +// +// /** +// * Bind GL color +// * +// * @param color Color color +// */ +// public static void setColor(Color color) +// { +// if (color != null) glColor4d(color.r(), color.g(), color.b(), color.a()); +// } +// +// +// /** +// * Bind GL color +// * +// * @param color Color color +// * @param alpha alpha multiplier +// */ +// public static void setColor(Color color, double alpha) +// { +// if (color != null) glColor4d(color.r(), color.g(), color.b(), color.a() * alpha); +// } +// +// +// /** +// * Translate +// * +// * @param x +// * @param y +// */ +// public static void translate(double x, double y) +// { +// glTranslated(x, y, 0); +// } +// +// +// /** +// * Translate +// * +// * @param x +// * @param y +// * @param z +// */ +// public static void translate(double x, double y, double z) +// { +// glTranslated(x, y, z); +// } +// +// +// /** +// * Translate with coord +// * +// * @param coord coord +// */ +// public static void translate(Vect coord) +// { +// glTranslated(coord.x(), coord.y(), coord.z()); +// } +// +// +// /** +// * Translate with coord, discard Z +// * +// * @param coord coord +// */ +// public static void translateXY(Vect coord) +// { +// glTranslated(coord.x(), coord.y(), 0); +// } +// +// +// /** +// * Scale +// * +// * @param x +// * @param y +// */ +// public static void scale(double x, double y) +// { +// glScaled(x, y, 0); +// } +// +// +// /** +// * Scale +// * +// * @param x +// * @param y +// * @param z +// */ +// public static void scale(double x, double y, double z) +// { +// glScaled(x, y, z); +// } +// +// +// /** +// * Scale +// * +// * @param factor vector of scaling factors +// */ +// public static void scale(Vect factor) +// { +// glScaled(factor.x(), factor.y(), factor.z()); +// } +// +// +// /** +// * Scale by X factor +// * +// * @param factor scaling factor +// */ +// public static void scaleXY(double factor) +// { +// glScaled(factor, factor, 1); +// } +// +// +// /** +// * Scale by X factor +// * +// * @param factor scaling factor +// */ +// public static void scaleX(double factor) +// { +// glScaled(factor, 1, 1); +// } +// +// +// /** +// * Scale by Y factor +// * +// * @param factor scaling factor +// */ +// public static void scaleY(double factor) +// { +// glScaled(1, factor, 1); +// } +// +// +// /** +// * Scale by Z factor +// * +// * @param factor scaling factor +// */ +// public static void scaleZ(double factor) +// { +// glScaled(1, 1, factor); +// } +// +// +// /** +// * Rotate around X axis +// * +// * @param angle deg +// */ +// public static void rotateX(double angle) +// { +// rotate(angle, AXIS_X); +// } +// +// +// /** +// * Rotate around Y axis +// * +// * @param angle deg +// */ +// public static void rotateY(double angle) +// { +// rotate(angle, AXIS_Y); +// } +// +// +// /** +// * Rotate around Z axis +// * +// * @param angle deg +// */ +// public static void rotateZ(double angle) +// { +// rotate(angle, AXIS_Z); +// } +// +// +// /** +// * Rotate +// * +// * @param angle rotate angle +// * @param axis rotation axis +// */ +// public static void rotate(double angle, Vect axis) +// { +// final Vect vec = axis.norm(1); +// glRotated(angle, vec.x(), vec.y(), vec.z()); +// } +// +// private static int pushed = 0; +// /** Can be used to avoid texture binding and glBegin/glEnd in textured quads */ +// public static boolean batchTexturedQuadMode; +// +// +// /** +// * Store GL state +// */ +// public static void pushState() +// { +// pushed++; +// +// if (pushed >= 100) { +// Log.w("Suspicious number of state pushes: " + pushed); +// } +// +// GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); +// GL11.glPushClientAttrib(GL11.GL_ALL_CLIENT_ATTRIB_BITS); +// GL11.glMatrixMode(GL11.GL_MODELVIEW); +// GL11.glPushMatrix(); +// GL11.glMatrixMode(GL11.GL_PROJECTION); +// GL11.glPushMatrix(); +// GL11.glMatrixMode(GL11.GL_MODELVIEW); +// } +// +// +// /** +// * Restore Gl state +// */ +// public static void popState() +// { +// if (pushed == 0) { +// Log.w("Pop without push."); +// } +// +// pushed--; +// +// GL11.glMatrixMode(GL11.GL_PROJECTION); +// GL11.glPopMatrix(); +// GL11.glMatrixMode(GL11.GL_MODELVIEW); +// GL11.glPopMatrix(); +// GL11.glPopClientAttrib(); +// GL11.glPopAttrib(); +// } +// +// +// /** +// * Store matrix +// */ +// public static void pushMatrix() +// { +// GL11.glPushMatrix(); +// } +// +// +// /** +// * Restore Gl state +// */ +// public static void popMatrix() +// { +// GL11.glPopMatrix(); +// } +// +// +// /** +// * Load texture +// * +// * @param resourcePath +// * @param filtering filtering mode to use while loading. +// * @return the loaded texture +// */ +// public synchronized static Texture loadSlickTexture(String resourcePath, FilterMode filtering) +// { +// +// try { +// +// final String ext = FileUtils.getExtension(resourcePath).toUpperCase(); +// +// final Texture texture = TextureLoader.getTexture(ext, FileUtils.getResource(resourcePath), false, filtering.num); +// +// if (texture == null) { +// Log.w("Texture " + resourcePath + " could not be loaded."); +// } +// +// return texture; +// +// } catch (final IOException e) { +// Log.e("Loading of texture " + resourcePath + " failed.", e); +// throw new RuntimeException("Could not load texture " + resourcePath + ".", e); +// } +// +// } +// +// +// /** +// * Render quad 2D +// * +// * @param rect rectangle +// * @param color draw color +// */ +// public static void quad(Rect rect, Color color) +// { +// setColor(color); +// quad(rect); +// } +// +// +// /** +// * Render quad +// * +// * @param quad the quad to draw (px) +// */ +// public static void quad(Rect quad) +// { +// final RectDigest q = quad.digest(); +// +// // draw with color +// +// glDisable(GL_TEXTURE_2D); +// +// // quad +// glBegin(GL_QUADS); +// glVertex2d(q.left, q.bottom); +// glVertex2d(q.right, q.bottom); +// glVertex2d(q.right, q.top); +// glVertex2d(q.left, q.top); +// glEnd(); +// } +// +// +// /** +// * Draw quad with horizontal gradient +// * +// * @param quad drawn quad bounds +// * @param color1 left color +// * @param color2 right color +// */ +// public static void quadGradH(Rect quad, Color color1, Color color2) +// { +// quadColor(quad, color1, color2, color2, color1); +// } +// +// +// public static void quadColor(Rect quad, Color color) +// { +// quadColor(quad, color, color, color, color); +// } +// +// +// /** +// * Draw quad with coloured vertices. +// * +// * @param quad drawn quad bounds +// * @param colorHMinVMin +// * @param colorHMaxVMin +// * @param colorHMaxVMax +// * @param colorHMinVMax +// */ +// public static void quadColor(Rect quad, Color colorHMinVMin, Color colorHMaxVMin, Color colorHMaxVMax, Color colorHMinVMax) +// { +// final RectDigest r = quad.digest(); +// +// // draw with color +// +// glDisable(GL_TEXTURE_2D); +// +// glBegin(GL_QUADS); +// setColor(colorHMinVMax); +// glVertex2d(r.left, r.bottom); +// +// setColor(colorHMaxVMax); +// glVertex2d(r.right, r.bottom); +// +// setColor(colorHMaxVMin); +// glVertex2d(r.right, r.top); +// +// setColor(colorHMinVMin); +// glVertex2d(r.left, r.top); +// glEnd(); +// } +// +// +// /** +// * Draw quad with vertical gradient +// * +// * @param quad drawn quad bounds +// * @param color1 top color +// * @param color2 bottom color +// */ +// public static void quadGradV(Rect quad, Color color1, Color color2) +// { +// quadColor(quad, color1, color1, color2, color2); +// } +// +// +// /** +// * Render textured rect +// * +// * @param quad rectangle (px) +// * @param txquad texture quad +// */ +// public static void quadTextured(Rect quad, TxQuad txquad) +// { +// quadTextured(quad, txquad, RGB.WHITE); +// } +// +// +// /** +// * Render textured rect +// * +// * @param quad rectangle (px) +// * @param txquad texture instance +// * @param tint color tint +// */ +// public static void quadTextured(Rect quad, TxQuad txquad, Color tint) +// { +// if (!batchTexturedQuadMode) { +// glEnable(GL_TEXTURE_2D); +// txquad.tx.bind(); +// glBegin(GL_QUADS); +// setColor(tint); +// } +// +// final RectDigest q = quad.digest(); +// final RectDigest u = txquad.uvs.digest(); +// +// final double offs = 0.0001;// hack to avoid white stitching +// +// double tL = u.left + offs, tR = u.right - offs, tT = u.top + offs, tB = u.bottom - offs; +// +// // handle flip +// if (txquad.isFlippedY()) { +// final double swap = tT; +// tT = tB; +// tB = swap; +// } +// +// if (txquad.isFlippedX()) { +// final double swap = tL; +// tL = tR; +// tR = swap; +// } +// +// final double w = txquad.tx.getWidth01(); +// final double h = txquad.tx.getHeight01(); +// +// // quad with texture +// glTexCoord2d(tL * w, tB * h); +// glVertex2d(q.left, q.bottom); +// +// glTexCoord2d(tR * w, tB * h); +// glVertex2d(q.right, q.bottom); +// +// glTexCoord2d(tR * w, tT * h); +// glVertex2d(q.right, q.top); +// +// glTexCoord2d(tL * w, tT * h); +// glVertex2d(q.left, q.top); +// +// if (!batchTexturedQuadMode) glEnd(); +// } +// +// +// /** +// * Setup Ortho projection for 2D graphics +// * +// * @param size viewport size (screen size) +// */ +// public static void setupOrtho(Vect size) +// { +// // fix projection for changed size +// glMatrixMode(GL_PROJECTION); +// glLoadIdentity(); +// glViewport(0, 0, size.xi(), size.yi()); +// glOrtho(0, size.xi(), size.yi(), 0, -1000, 1000); +// +// // back to modelview +// glMatrixMode(GL_MODELVIEW); +// +// glLoadIdentity(); +// +// glDisable(GL_LIGHTING); +// +// glClearDepth(1f); +// glEnable(GL_DEPTH_TEST); +// glDepthFunc(GL_LEQUAL); +// +// glEnable(GL_NORMALIZE); +// +// glShadeModel(GL_SMOOTH); +// +// glEnable(GL_BLEND); +// glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); +// } +// +// +// public static void enterBatchTexturedQuadMode(ITexture texture) +// { +// texture.bind(); +// glBegin(GL11.GL_QUADS); +// batchTexturedQuadMode = true; +// } +// +// +// public static void leaveBatchTexturedQuadMode() +// { +// glEnd(); +// batchTexturedQuadMode = false; +// } +//} diff --git a/src/junk/SoundSystem.java b/src/junk/SoundSystem.java new file mode 100644 index 0000000..da75ea4 --- /dev/null +++ b/src/junk/SoundSystem.java @@ -0,0 +1,261 @@ +package junk; + + +//package mightypork.gamecore.resources.audio; +// +// +//import java.nio.FloatBuffer; +//import java.util.ArrayList; +//import java.util.List; +// +//import mightypork.gamecore.backend.lwjgl.SlickAudio; +//import mightypork.gamecore.core.modules.App; +//import mightypork.gamecore.resources.ResourceLoadRequest; +//import mightypork.gamecore.resources.audio.players.EffectPlayer; +//import mightypork.gamecore.resources.audio.players.LoopPlayer; +//import mightypork.gamecore.util.BufferHelper; +//import mightypork.utils.eventbus.clients.BusNode; +//import mightypork.utils.interfaces.Destroyable; +//import mightypork.utils.interfaces.Updateable; +//import mightypork.utils.logging.Log; +//import mightypork.utils.math.constraints.vect.Vect; +//import mightypork.utils.math.constraints.vect.var.VectVar; +// +//import org.lwjgl.openal.AL; +//import org.lwjgl.openal.AL10; +//import org.newdawn.slick.openal.SoundStore; +// +// +///** +// * Sound system class (only one instance should be made per application) +// * +// * @author Ondřej Hruška (MightyPork) +// */ +//@Deprecated +//public class SoundSystem extends BusNode implements Updateable, Destroyable { +// +// private static final Vect INITIAL_LISTENER_POS = Vect.ZERO; +// private static final int MAX_SOURCES = 256; +// +// private static VectVar listener = Vect.makeVar(); +// private static boolean soundSystemInited = false; +// +// +// /** +// * Set listener pos +// * +// * @param pos +// */ +// public static void setListener(Vect pos) +// { +// listener.setTo(pos); +// final FloatBuffer buf3 = BufferHelper.alloc(3); +// final FloatBuffer buf6 = BufferHelper.alloc(6); +// buf3.clear(); +// BufferHelper.fill(buf3, (float) pos.x(), (float) pos.y(), (float) pos.z()); +// AL10.alListener(AL10.AL_POSITION, buf3); +// buf3.clear(); +// BufferHelper.fill(buf3, 0, 0, 0); +// AL10.alListener(AL10.AL_VELOCITY, buf3); +// buf6.clear(); +// BufferHelper.fill(buf6, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f); +// AL10.alListener(AL10.AL_ORIENTATION, buf6); +// } +// +// +// /** +// * @return listener coordinate +// */ +// public static Vect getListener() +// { +// return listener; +// } +// +// // -- instance -- +// +// private final Volume masterVolume = new Volume(1D); +// private final Volume effectsVolume = new JointVolume(masterVolume); +// private final Volume loopsVolume = new JointVolume(masterVolume); +// +// private final List loopPlayers = new ArrayList<>(); +// private final List resources = new ArrayList<>(); +// +// +// /** +// * @param busAccess app access +// */ +// public SoundSystem() { +// +// if (!soundSystemInited) { +// soundSystemInited = true; +// +// try { +// SoundStore.get().setMaxSources(MAX_SOURCES); +// SoundStore.get().init(); +// setListener(INITIAL_LISTENER_POS); +// +// App.bus().send(new AudioReadyEvent()); +// } catch (final Throwable t) { +// Log.e("Error initializing sound system.", t); +// } +// } +// } +// +// +// @Override +// public void destroy() +// { +// for (final DeferredAudio r : resources) { +// r.destroy(); +// } +// +// SoundStore.get().clear(); +// AL.destroy(); +// } +// +// +// @Override +// public void update(double delta) +// { +// for (final Updateable lp : loopPlayers) { +// lp.update(delta); +// } +// } +// +// +// /** +// * Create effect resource +// * +// * @param resource resource path +// * @param pitch default pitch (1 = unchanged) +// * @param gain default gain (0-1) +// * @return player +// */ +// public EffectPlayer createEffect(String resource, double pitch, double gain) +// { +// return new EffectPlayer(createResource(resource), pitch, gain, effectsVolume); +// } +// +// +// /** +// * Register loop resource (music / effect loop) +// * +// * @param resource resource path +// * @param pitch default pitch (1 = unchanged) +// * @param gain default gain (0-1) +// * @param fadeIn default time for fadeIn +// * @param fadeOut default time for fadeOut +// * @return player +// */ +// public LoopPlayer createLoop(String resource, double pitch, double gain, double fadeIn, double fadeOut) +// { +// final LoopPlayer p = new LoopPlayer(createResource(resource), pitch, gain, loopsVolume); +// p.setFadeTimes(fadeIn, fadeOut); +// loopPlayers.add(p); +// return p; +// } +// +// +// /** +// * Create {@link DeferredAudio} for a resource +// * +// * @param res a resource name +// * @return the resource +// * @throws IllegalArgumentException if resource is already registered +// */ +// private DeferredAudio createResource(String res) +// { +// final DeferredAudio a = new SlickAudio(res); +// App.bus().send(new ResourceLoadRequest(a)); +// resources.add(a); +// return a; +// } +// +// +// /** +// * Fade out all loops (ie. for screen transitions) +// */ +// public void fadeOutAllLoops() +// { +// for (final LoopPlayer p : loopPlayers) { +// p.fadeOut(); +// } +// } +// +// +// /** +// * Pause all loops (leave volume unchanged) +// */ +// public void pauseAllLoops() +// { +// for (final LoopPlayer p : loopPlayers) { +// p.pause(); +// } +// } +// +// +// /** +// * Set level of master volume +// * +// * @param d level +// */ +// public void setMasterVolume(double d) +// { +// masterVolume.set(d); +// } +// +// +// /** +// * Set level of effects volume +// * +// * @param d level +// */ +// public void setEffectsVolume(double d) +// { +// effectsVolume.set(d); +// } +// +// +// /** +// * Set level of music volume +// * +// * @param d level +// */ +// public void setMusicVolume(double d) +// { +// loopsVolume.set(d); +// } +// +// +// /** +// * Get level of master volume +// * +// * @return level +// */ +// public double getMasterVolume() +// { +// return masterVolume.get(); +// } +// +// +// /** +// * Get level of effects volume +// * +// * @return level +// */ +// public double getEffectsVolume() +// { +// return effectsVolume.get(); +// } +// +// +// /** +// * Get level of music volume +// * +// * @return level +// */ +// public double getMusicVolume() +// { +// return loopsVolume.get(); +// } +//} diff --git a/src/mightypork/gamecore/audio/AudioModule.java b/src/mightypork/gamecore/audio/AudioModule.java new file mode 100644 index 0000000..9112fd3 --- /dev/null +++ b/src/mightypork/gamecore/audio/AudioModule.java @@ -0,0 +1,220 @@ +package mightypork.gamecore.audio; + + +import java.util.ArrayList; +import java.util.List; + +import mightypork.gamecore.audio.players.EffectPlayer; +import mightypork.gamecore.audio.players.LoopPlayer; +import mightypork.gamecore.core.App; +import mightypork.gamecore.core.BackendModule; +import mightypork.gamecore.resources.loading.ResourceLoadRequest; +import mightypork.utils.interfaces.Updateable; +import mightypork.utils.math.constraints.vect.Vect; + + +/** + * Abstract audio module. + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class AudioModule extends BackendModule implements Updateable { + + /** + * Set listener position + * + * @param pos listener position + */ + public abstract void setListenerPos(Vect pos); + + + /** + * Get current listener position + * + * @return listener position + */ + public abstract Vect getListenerPos(); + + // -- instance -- + + private final Volume masterVolume = new Volume(1D); + private final Volume effectsVolume = new JointVolume(masterVolume); + private final Volume loopsVolume = new JointVolume(masterVolume); + + private final List loopPlayers = new ArrayList<>(); + private final List resources = new ArrayList<>(); + + + @Override + public void destroy() + { + for (final DeferredAudio r : resources) { + r.destroy(); + } + + deinitSoundSystem(); + } + + + /** + * Deinitialize the soud system, release resources etc.
+ * Audio resources are already destroyed. + */ + protected abstract void deinitSoundSystem(); + + + @Override + public void update(double delta) + { + for (final Updateable lp : loopPlayers) { + lp.update(delta); + } + } + + + /** + * Create effect resource + * + * @param resource resource path + * @param pitch default pitch (1 = unchanged) + * @param gain default gain (0-1) + * @return player + */ + public EffectPlayer createEffect(String resource, double pitch, double gain) + { + return new EffectPlayer(createResource(resource), pitch, gain, effectsVolume); + } + + + /** + * Register loop resource (music / effect loop) + * + * @param resource resource path + * @param pitch default pitch (1 = unchanged) + * @param gain default gain (0-1) + * @param fadeIn default time for fadeIn + * @param fadeOut default time for fadeOut + * @return player + */ + public LoopPlayer createLoop(String resource, double pitch, double gain, double fadeIn, double fadeOut) + { + final LoopPlayer p = new LoopPlayer(createResource(resource), pitch, gain, loopsVolume); + p.setFadeTimes(fadeIn, fadeOut); + loopPlayers.add(p); + return p; + } + + + /** + * Create {@link DeferredAudio} for a resource, request deferred load and + * add to the resources list. + * + * @param res a resource name + * @return the resource + * @throws IllegalArgumentException if resource is already registered + */ + protected DeferredAudio createResource(String res) + { + final DeferredAudio a = doCreateResource(res); + App.bus().send(new ResourceLoadRequest(a)); + resources.add(a); + return a; + } + + + /** + * Create a backend-specific deferred audio resource + * + * @param res resource path + * @return Deferred Audio + */ + protected abstract DeferredAudio doCreateResource(String res); + + + /** + * Fade out all loops (= fade out the currently playing loops) + */ + public void fadeOutAllLoops() + { + for (final LoopPlayer p : loopPlayers) { + p.fadeOut(); + } + } + + + /** + * Pause all loops (leave volume unchanged) + */ + public void pauseAllLoops() + { + for (final LoopPlayer p : loopPlayers) { + p.pause(); + } + } + + + /** + * Set level of master volume (volume multiplier) + * + * @param volume level (0..1) + */ + public void setMasterVolume(double volume) + { + masterVolume.set(volume); + } + + + /** + * Set level of effects volume (volume multiplier) + * + * @param volume level (0..1) + */ + public void setEffectsVolume(double volume) + { + effectsVolume.set(volume); + } + + + /** + * Set level of loops volume (volume multiplier) + * + * @param volume level (0..1) + */ + public void setLoopsVolume(double volume) + { + loopsVolume.set(volume); + } + + + /** + * Get level of master volume (volume multiplier) + * + * @return level (0..1) + */ + public double getMasterVolume() + { + return masterVolume.get(); + } + + + /** + * Get level of effects volume (volume multiplier) + * + * @return level (0..1) + */ + public double getEffectsVolume() + { + return effectsVolume.get(); + } + + + /** + * Get level of loops volume (volume multiplier) + * + * @return level (0..1) + */ + public double getLoopsVolume() + { + return loopsVolume.get(); + } +} diff --git a/src/mightypork/gamecore/audio/DeferredAudio.java b/src/mightypork/gamecore/audio/DeferredAudio.java new file mode 100644 index 0000000..8dd72c1 --- /dev/null +++ b/src/mightypork/gamecore/audio/DeferredAudio.java @@ -0,0 +1,47 @@ +package mightypork.gamecore.audio; + + +import mightypork.gamecore.core.App; +import mightypork.gamecore.resources.BaseDeferredResource; +import mightypork.utils.annotations.Alias; +import mightypork.utils.math.constraints.vect.Vect; + + +/** + * Abstract deferred audio, to be extended in backend. + * + * @author Ondřej Hruška (MightyPork) + */ +@Alias(name = "Audio") +public abstract class DeferredAudio extends BaseDeferredResource implements IAudio { + + /** + * Create audio + * + * @param resourceName resource to load (when needed) + */ + public DeferredAudio(String resourceName) { + super(resourceName); + } + + + @Override + public void play(double pitch, double gain, boolean loop) + { + play(pitch, gain, loop, App.audio().getListenerPos()); + } + + + @Override + public void play(double pitch, double gain, boolean loop, double x, double y) + { + play(pitch, gain, loop, x, y, App.audio().getListenerPos().z()); + } + + + @Override + public void play(double pitch, double gain, boolean loop, Vect pos) + { + play(pitch, gain, loop, pos.x(), pos.y(), pos.z()); + } +} diff --git a/src/mightypork/gamecore/audio/IAudio.java b/src/mightypork/gamecore/audio/IAudio.java new file mode 100644 index 0000000..a89d2c1 --- /dev/null +++ b/src/mightypork/gamecore/audio/IAudio.java @@ -0,0 +1,99 @@ +package mightypork.gamecore.audio; + + +import mightypork.utils.interfaces.Destroyable; +import mightypork.utils.math.constraints.vect.Vect; + + +/** + * Audio resource interface (backend independent) + * + * @author Ondřej Hruška (MightyPork) + */ +public interface IAudio extends Destroyable { + + /** + * Pause loop (remember position and stop playing) - if was looping + */ + void pauseLoop(); + + + /** + * Resume loop (if was paused) + */ + void resumeLoop(); + + + /** + * Adjust gain for the currently playing effect (can be used for fading + * music) + * + * @param gain gain to set 0..1 + */ + void adjustGain(double gain); + + + /** + * Stop audio playback + */ + void stop(); + + + /** + * @return true if the audio is playing + */ + boolean isPlaying(); + + + /** + * @return trie if the audio is paused + */ + boolean isPaused(); + + + /** + * Play as sound effect at listener position + * + * @param pitch pitch (1 = default) + * @param gain gain (0-1) + * @param loop looping + */ + void play(double pitch, double gain, boolean loop); + + + /** + * Play as sound effect at given X-Y position + * + * @param pitch pitch (1 = default) + * @param gain gain (0-1) + * @param loop looping + * @param x + * @param y + */ + void play(double pitch, double gain, boolean loop, double x, double y); + + + /** + * Play as sound effect at given position + * + * @param pitch pitch (1 = default) + * @param gain gain (0-1) + * @param loop looping + * @param x + * @param y + * @param z + */ + void play(double pitch, double gain, boolean loop, double x, double y, double z); + + + /** + * Play as sound effect at given position + * + * @param pitch pitch (1 = default) + * @param gain gain (0-1) + * @param loop looping + * @param pos coord + */ + void play(double pitch, double gain, boolean loop, Vect pos); + +} diff --git a/src/mightypork/gamecore/audio/JointVolume.java b/src/mightypork/gamecore/audio/JointVolume.java new file mode 100644 index 0000000..ab186ac --- /dev/null +++ b/src/mightypork/gamecore/audio/JointVolume.java @@ -0,0 +1,51 @@ +package mightypork.gamecore.audio; + + +import mightypork.utils.math.Calc; + + +/** + * Volume combined of multiple volumes, combining them (multiplication). + * + * @author Ondřej Hruška (MightyPork) + */ +public class JointVolume extends Volume { + + private final Volume[] volumes; + + + /** + * Create joint volume with master gain of 1 + * + * @param volumes individual volumes to join + */ + @SafeVarargs + public JointVolume(Volume... volumes) { + super(1D); + this.volumes = volumes; + } + + + /** + * Get combined gain (multiplied) + */ + @Override + public Double get() + { + double d = super.get(); + for (final Volume v : volumes) + d *= v.get(); + + return Calc.clamp(d, 0, 1); + } + + + /** + * Set master gain + */ + @Override + public void set(Double o) + { + super.set(o); + } +} diff --git a/src/mightypork/gamecore/audio/SoundRegistry.java b/src/mightypork/gamecore/audio/SoundRegistry.java new file mode 100644 index 0000000..1aaf03d --- /dev/null +++ b/src/mightypork/gamecore/audio/SoundRegistry.java @@ -0,0 +1,83 @@ +package mightypork.gamecore.audio; + + +import java.util.HashMap; +import java.util.Map; + +import mightypork.gamecore.audio.players.EffectPlayer; +import mightypork.gamecore.audio.players.LoopPlayer; +import mightypork.gamecore.core.App; + + +/** + * Audio resource storage + * + * @author Ondřej Hruška (MightyPork) + */ +public class SoundRegistry { + + private final Map effects = new HashMap<>(); + private final Map loops = new HashMap<>(); + + + /** + * Register effect resource + * + * @param key sound key + * @param resource resource path + * @param pitch default pitch (1 = unchanged) + * @param gain default gain (0-1) + */ + public void addEffect(String key, String resource, double pitch, double gain) + { + effects.put(key, App.audio().createEffect(resource, pitch, gain)); + } + + + /** + * Register loop resource (music / effect loop) + * + * @param key sound key + * @param resource resource path + * @param pitch default pitch (1 = unchanged) + * @param gain default gain (0-1) + * @param fadeIn default time for fadeIn + * @param fadeOut default time for fadeOut + */ + public void addLoop(String key, String resource, double pitch, double gain, double fadeIn, double fadeOut) + { + loops.put(key, App.audio().createLoop(resource, pitch, gain, fadeIn, fadeOut)); + } + + + /** + * Get a loop player for key + * + * @param key sound key + * @return loop player + */ + public LoopPlayer getLoop(String key) + { + final LoopPlayer p = loops.get(key); + if (p == null) { + throw new RuntimeException("Unknown sound loop \"" + key + "\"."); + } + return p; + } + + + /** + * Get a effect player for key + * + * @param key sound key + * @return effect player + */ + public EffectPlayer getEffect(String key) + { + final EffectPlayer p = effects.get(key); + if (p == null) { + throw new RuntimeException("Unknown sound effect \"" + key + "\"."); + } + return p; + } +} diff --git a/src/mightypork/gamecore/audio/Volume.java b/src/mightypork/gamecore/audio/Volume.java new file mode 100644 index 0000000..3e8a55d --- /dev/null +++ b/src/mightypork/gamecore/audio/Volume.java @@ -0,0 +1,29 @@ +package mightypork.gamecore.audio; + + +import mightypork.utils.math.Calc; +import mightypork.utils.struct.Mutable; + + +/** + * Mutable volume 0-1 + * + * @author Ondřej Hruška (MightyPork) + */ +public class Volume extends Mutable { + + /** + * @param d initial value + */ + public Volume(Double d) { + super(d); + } + + + @Override + public void set(Double d) + { + super.set(Calc.clamp(d, 0, 1)); + } + +} diff --git a/src/mightypork/gamecore/audio/players/BaseAudioPlayer.java b/src/mightypork/gamecore/audio/players/BaseAudioPlayer.java new file mode 100644 index 0000000..78a240e --- /dev/null +++ b/src/mightypork/gamecore/audio/players/BaseAudioPlayer.java @@ -0,0 +1,105 @@ +package mightypork.gamecore.audio.players; + + +import mightypork.gamecore.audio.DeferredAudio; +import mightypork.gamecore.audio.Volume; +import mightypork.utils.interfaces.Destroyable; + + +/** + * Basic abstract player + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class BaseAudioPlayer implements Destroyable { + + /** the track */ + private final DeferredAudio audio; + + /** base gain for sfx */ + private final double baseGain; + + /** base pitch for sfx */ + private final double basePitch; + + /** dedicated volume control */ + private final Volume gainMultiplier; + + + /** + * @param track audio resource + * @param basePitch base pitch (pitch multiplier) + * @param baseGain base gain (volume multiplier) + * @param volume colume control + */ + public BaseAudioPlayer(DeferredAudio track, double basePitch, double baseGain, Volume volume) { + this.audio = track; + + this.baseGain = baseGain; + this.basePitch = basePitch; + + if (volume == null) volume = new Volume(1D); + + this.gainMultiplier = volume; + } + + + @Override + public void destroy() + { + audio.destroy(); + } + + + /** + * @return audio resource + */ + protected DeferredAudio getAudio() + { + return audio; + } + + + /** + * Get play gain, computed based on volume and given multiplier + * + * @param multiplier extra volume adjustment + * @return computed gain + */ + protected double computeGain(double multiplier) + { + return baseGain * gainMultiplier.get() * multiplier; + } + + + /** + * Get pitch + * + * @param multiplier pitch adjustment + * @return computed pitch + */ + protected double computePitch(double multiplier) + { + return basePitch * multiplier; + } + + + /** + * Get if audio is valid + * + * @return is valid + */ + protected boolean hasAudio() + { + return (audio != null); + } + + + /** + * force load the resource + */ + public void load() + { + if (hasAudio()) audio.load(); + } +} diff --git a/src/mightypork/gamecore/audio/players/EffectPlayer.java b/src/mightypork/gamecore/audio/players/EffectPlayer.java new file mode 100644 index 0000000..624ffde --- /dev/null +++ b/src/mightypork/gamecore/audio/players/EffectPlayer.java @@ -0,0 +1,66 @@ +package mightypork.gamecore.audio.players; + + +import mightypork.gamecore.audio.DeferredAudio; +import mightypork.gamecore.audio.Volume; +import mightypork.utils.math.constraints.vect.Vect; + + +/** + * Player for one-off effects + * + * @author Ondřej Hruška (MightyPork) + */ +public class EffectPlayer extends BaseAudioPlayer { + + /** + * @param track audio resource + * @param basePitch base pitch (pitch multiplier) + * @param baseGain base gain (volume multiplier) + * @param volume volume control + */ + public EffectPlayer(DeferredAudio track, double basePitch, double baseGain, Volume volume) { + super(track, (float) basePitch, (float) baseGain, volume); + } + + + /** + * Play at listener + * + * @param pitch play pitch + * @param gain play gain + */ + public void play(double pitch, double gain) + { + if (!hasAudio()) return; + + getAudio().play(computePitch(pitch), computeGain(gain), false); + } + + + /** + * Play at listener + * + * @param gain play gain + */ + public void play(double gain) + { + play(1, gain); + } + + + /** + * Play at given position + * + * @param pitch play pitch + * @param gain play gain + * @param pos play position + */ + public void play(double pitch, double gain, Vect pos) + { + if (!hasAudio()) return; + + getAudio().play(computePitch(pitch), computeGain(gain), false, pos); + } + +} diff --git a/src/mightypork/gamecore/audio/players/LoopPlayer.java b/src/mightypork/gamecore/audio/players/LoopPlayer.java new file mode 100644 index 0000000..714a6c9 --- /dev/null +++ b/src/mightypork/gamecore/audio/players/LoopPlayer.java @@ -0,0 +1,166 @@ +package mightypork.gamecore.audio.players; + + +import mightypork.gamecore.audio.DeferredAudio; +import mightypork.gamecore.audio.Volume; +import mightypork.utils.interfaces.Pauseable; +import mightypork.utils.interfaces.Updateable; +import mightypork.utils.math.animation.NumAnimated; + + +/** + * Audio loop player (with fading, good for music) + * + * @author Ondřej Hruška (MightyPork) + */ +public class LoopPlayer extends BaseAudioPlayer implements Updateable, Pauseable { + + private int sourceID = -1; + + /** animator for fade in and fade out */ + private final NumAnimated fadeAnim = new NumAnimated(0); + + private double lastUpdateGain = 0; + + /** flag that track is paused */ + private boolean paused = true; + + /** Default fadeIn time */ + private double inTime = 1; + + /** Default fadeOut time */ + private double outTime = 1; + + + /** + * @param track audio resource + * @param basePitch base pitch (pitch multiplier) + * @param baseGain base gain (volume multiplier) + * @param volume volume control + */ + public LoopPlayer(DeferredAudio track, double basePitch, double baseGain, Volume volume) { + super(track, (float) basePitch, (float) baseGain, volume); + + paused = true; + } + + + /** + * Set fading duration (seconds) + * + * @param in duration of fade-in + * @param out duration of fade-out + */ + public void setFadeTimes(double in, double out) + { + inTime = in; + outTime = out; + } + + + private void initLoop() + { + if (hasAudio() && sourceID == -1) { + getAudio().play(computePitch(1), computeGain(1), true); + getAudio().pauseLoop(); + } + } + + + @Override + public void pause() + { + if (!hasAudio() || paused) return; + + initLoop(); + + getAudio().pauseLoop(); + paused = true; + } + + + @Override + public boolean isPaused() + { + return paused; + } + + + @Override + public void resume() + { + if (!hasAudio() || !paused) return; + + initLoop(); + + paused = false; + + getAudio().adjustGain(computeGain(fadeAnim.value())); + } + + + @Override + public void update(double delta) + { + if (!hasAudio() || paused) return; + + initLoop(); + + fadeAnim.update(delta); + + final double gain = computeGain(fadeAnim.value()); + if (!paused && gain != lastUpdateGain) { + getAudio().adjustGain(gain); + lastUpdateGain = gain; + } + + if (gain == 0 && !paused) pause(); // pause on zero volume + } + + + /** + * Resume if paused, and fade in (pick up from current volume). + * + * @param secs + */ + public void fadeIn(double secs) + { + if (!hasAudio()) return; + + if (isPaused()) fadeAnim.setTo(0); + resume(); + fadeAnim.fadeIn(secs); + } + + + /** + * Fade out and pause when reached zero volume + * + * @param secs fade duration + */ + public void fadeOut(double secs) + { + if (!hasAudio()) return; + if (isPaused()) return; + fadeAnim.fadeOut(secs); + } + + + /** + * Fade in with default duration + */ + public void fadeIn() + { + fadeIn(inTime); + } + + + /** + * Fade out with default duration + */ + public void fadeOut() + { + fadeOut(outTime); + } + +} diff --git a/src/mightypork/gamecore/core/App.java b/src/mightypork/gamecore/core/App.java new file mode 100644 index 0000000..fbb9817 --- /dev/null +++ b/src/mightypork/gamecore/core/App.java @@ -0,0 +1,277 @@ +package mightypork.gamecore.core; + + +import java.util.ArrayList; +import java.util.List; + +import mightypork.gamecore.audio.AudioModule; +import mightypork.gamecore.core.config.Config; +import mightypork.gamecore.core.events.ShutdownEvent; +import mightypork.gamecore.graphics.GraphicsModule; +import mightypork.gamecore.input.InputModule; +import mightypork.utils.annotations.Stub; +import mightypork.utils.eventbus.EventBus; +import mightypork.utils.eventbus.clients.BusNode; +import mightypork.utils.eventbus.clients.DelegatingList; +import mightypork.utils.eventbus.events.DestroyEvent; +import mightypork.utils.logging.Log; + + +/** + * Game base class & static subsystem access + * + * @author MightyPork + */ +public class App extends BusNode { + + private static App instance; + + private final AppBackend backend; + private final EventBus eventBus = new EventBus(); + private boolean started = false; + + protected final DelegatingList plugins = new DelegatingList(); + protected final List initializers = new ArrayList<>(); + + + /** + * Create an app with given backend. + * + * @param backend + */ + public App(AppBackend backend) { + if (App.instance != null) { + throw new IllegalStateException("App already initialized"); + } + + // store current instance in static field + App.instance = this; + + // join the bus + this.eventBus.subscribe(this); + + // create plugin registry attached to bus + this.eventBus.subscribe(this.plugins); + + // initialize and use backend + this.backend = backend; + this.eventBus.subscribe(backend); + this.backend.bind(this); + this.backend.initialize(); + } + + + /** + * Add a plugin to the app. Plugins can eg. listen to bus events and react + * to them. + * + * @param plugin the added plugin. + */ + public void addPlugin(AppPlugin plugin) + { + if (started) { + throw new IllegalStateException("App already started, cannot add plugins."); + } + + // attach to event bus + plugins.add(plugin); + plugin.bind(this); + plugin.initialize(); + } + + + /** + * Add an initializer to the app. + * + * @param initializer + */ + public void addInitTask(InitTask initializer) + { + if (started) { + throw new IllegalStateException("App already started, cannot add initializers."); + } + + initializers.add(initializer); + } + + + /** + * Get current backend + * + * @return the backend + */ + public AppBackend getBackend() + { + return backend; + } + + + /** + * Initialize the App and start operating.
+ * This method should be called after adding all required initializers and + * plugins. + */ + public final void start() + { + if (started) { + throw new IllegalStateException("Already started."); + } + started = true; + + // pre-init hook, just in case anyone wanted to have one. + Log.f2("Calling pre-init hook..."); + preInit(); + + Log.i("=== Starting initialization sequence ==="); + + // sort initializers by order. + List orderedInitializers = InitTask.inOrder(initializers); + + for (InitTask initializer : orderedInitializers) { + Log.f1("Running init task \"" + initializer.getName() + "\"..."); + initializer.bind(this); + initializer.init(); + initializer.run(); + } + + Log.i("=== Initialization sequence completed ==="); + + // user can now start the main loop etc. + Log.f2("Calling post-init hook..."); + postInit(); + } + + + /** + * Hook called before the initialization sequence starts. + */ + @Stub + protected void preInit() + { + } + + + /** + * Hook called after the initialization sequence is finished. + */ + @Stub + protected void postInit() + { + } + + + /** + * Shut down the running instance.
+ * Deinitialize backend modules and terminate the JVM. + */ + public static void shutdown() + { + if (instance != null) { + Log.i("Dispatching Shutdown event..."); + + bus().send(new ShutdownEvent(new Runnable() { + + @Override + public void run() + { + try { + final EventBus bus = bus(); + if (bus != null) { + bus.send(new DestroyEvent()); + bus.destroy(); + } + } catch (final Throwable e) { + Log.e(e); + } + + Log.i("Shutdown completed."); + System.exit(0); + } + })); + + } else { + Log.w("App is not running."); + System.exit(0); + } + } + + + /** + * Get the currently running App instance. + * + * @return app instance + */ + public static App instance() + { + return instance; + } + + + /** + * Get graphics module from the running app's backend + * + * @return graphics module + */ + public static GraphicsModule gfx() + { + return instance.backend.getGraphics(); + } + + + /** + * Get audio module from the running app's backend + * + * @return audio module + */ + public static AudioModule audio() + { + return instance.backend.getAudio(); + } + + + /** + * Get input module from the running app's backend + * + * @return input module + */ + public static InputModule input() + { + return instance.backend.getInput(); + } + + + /** + * Get event bus instance. + * + * @return event bus + */ + public static EventBus bus() + { + return instance.eventBus; + } + + + /** + * Get the main config, if initialized. + * + * @return main config + * @throws IllegalArgumentException if there is no such config. + */ + public static Config cfg() + { + return cfg("main"); + } + + + /** + * Get a config by alias. + * + * @param alias config alias + * @return the config + * @throws IllegalArgumentException if there is no such config. + */ + public static Config cfg(String alias) + { + return Config.forAlias(alias); + } +} diff --git a/src/mightypork/gamecore/core/AppBackend.java b/src/mightypork/gamecore/core/AppBackend.java new file mode 100644 index 0000000..f2f03ed --- /dev/null +++ b/src/mightypork/gamecore/core/AppBackend.java @@ -0,0 +1,65 @@ +package mightypork.gamecore.core; + + +import mightypork.gamecore.audio.AudioModule; +import mightypork.gamecore.graphics.GraphicsModule; +import mightypork.gamecore.input.InputModule; +import mightypork.utils.eventbus.clients.BusNode; + + +/** + * Application backend interface (set of core modules).
+ * The goal of this abstraction is to allow easy migration to different + * environment with different libraries etc. It should be as simple as using + * different backend. + * + * @author MightyPork + */ +public abstract class AppBackend extends BusNode { + + protected App app; + + + /** + * Assign an app instance. + * + * @param app app + */ + public void bind(App app) + { + if (this.app != null) { + throw new IllegalStateException("App already set."); + } + this.app = app; + } + + + /** + * Initialize backend modules, add them to event bus. + */ + public abstract void initialize(); + + + /** + * Get graphics module (screen manager, texture and font loader, renderer) + * + * @return graphics module + */ + public abstract GraphicsModule getGraphics(); + + + /** + * Get audio module ( + * + * @return audio module + */ + public abstract AudioModule getAudio(); + + + /** + * Get input module + * + * @return input module + */ + public abstract InputModule getInput(); +} diff --git a/src/mightypork/gamecore/core/AppPlugin.java b/src/mightypork/gamecore/core/AppPlugin.java new file mode 100644 index 0000000..ff9783a --- /dev/null +++ b/src/mightypork/gamecore/core/AppPlugin.java @@ -0,0 +1,34 @@ +package mightypork.gamecore.core; + + +import mightypork.utils.annotations.Stub; +import mightypork.utils.eventbus.clients.BusNode; + + +/** + * App plugin. Plugins are an easy way to extend app functionality.
+ * Typically, a plugin waits for trigger event(s) and performs some action upon + * receiving them. + * + * @author Ondřej Hruška (MightyPork) + */ +public class AppPlugin extends BusNode { + + protected App app; + + + void bind(App app) + { + this.app = app; + } + + + /** + * Initialize the plugin for the given App.
+ * The plugin is already attached to the event bus. + */ + @Stub + public void initialize() + { + } +} diff --git a/src/mightypork/gamecore/core/BackendModule.java b/src/mightypork/gamecore/core/BackendModule.java new file mode 100644 index 0000000..a4dd870 --- /dev/null +++ b/src/mightypork/gamecore/core/BackendModule.java @@ -0,0 +1,24 @@ +package mightypork.gamecore.core; + + +import mightypork.utils.annotations.Stub; +import mightypork.utils.eventbus.clients.BusNode; +import mightypork.utils.interfaces.Destroyable; + + +/** + * Abstract application backend module. + * + * @author MightyPork + */ +public abstract class BackendModule extends BusNode implements Destroyable { + + public abstract void init(); + + + @Override + @Stub + public void destroy() + { + } +} diff --git a/src/mightypork/gamecore/core/InitTask.java b/src/mightypork/gamecore/core/InitTask.java new file mode 100644 index 0000000..7be6c86 --- /dev/null +++ b/src/mightypork/gamecore/core/InitTask.java @@ -0,0 +1,150 @@ +package mightypork.gamecore.core; + + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import mightypork.utils.Reflect; +import mightypork.utils.annotations.Stub; +import mightypork.utils.logging.Log; + + +/** + * App initializer. A sequence of initializers is executed once the start() + * method on App is called. Adding initializers is one way to customize the App + * behavior and features. + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class InitTask { + + protected App app; + + + /** + * Assign the initialized app instance to a protected "app" field. + * + * @param app app + */ + void bind(App app) + { + this.app = app; + } + + + /** + * An intialization method that is called before the run() method.
+ * This method should be left unimplemented in the task, and can be used to + * configure the init task when using it as anonymous inner type. + */ + @Stub + public void init() + { + } + + + /** + * Run the initalizer on app. + */ + public abstract void run(); + + + /** + * Get name of this initializer (for dependency resolver).
+ * The name should be short, snake_case and precise. + * + * @return name + */ + public abstract String getName(); + + + /** + * Get what other initializers must be already loaded before this can load.
+ * Depending on itself or creating a circular dependency will cause error.
+ * If the dependencies cannot be satisfied, the initialization sequence will + * be aborted. + * + * @return array of names of required initializers. + */ + @Stub + public String[] getDependencies() + { + return new String[] {}; + } + + + /** + * Order init tasks so that all dependencies are loaded before thye are + * needed by the tasks. + * + * @param tasks task list + * @return task list ordered + */ + public static List inOrder(List tasks) + { + List remaining = new ArrayList<>(tasks); + + List ordered = new ArrayList<>(); + Set loaded = new HashSet<>(); + + // resolve task order + int addedThisIteration = 0; + do { + for (Iterator i = remaining.iterator(); i.hasNext();) { + InitTask task = i.next(); + + String[] deps = task.getDependencies(); + if (deps == null) deps = new String[] {}; + + int unmetDepsCount = deps.length; + + for (String d : deps) { + if (loaded.contains(d)) unmetDepsCount--; + } + + if (unmetDepsCount == 0) { + ordered.add(task); + loaded.add(task.getName()); + i.remove(); + addedThisIteration++; + } + } + } while (addedThisIteration > 0); + + // check if any tasks are left out + if (remaining.size() > 0) { + + // build error message for each bad task + int badInitializers = 0; + for (InitTask task : remaining) { + if (Reflect.hasAnnotation(task.getClass(), OptionalInitTask.class)) { + continue; + } + + badInitializers++; + + String notSatisfied = ""; + + for (String d : task.getDependencies()) { + if (!loaded.contains(d)) { + + if (!notSatisfied.isEmpty()) { + notSatisfied += ", "; + } + + notSatisfied += d; + } + } + + Log.w("InitTask \"" + task.getName() + "\" - missing dependencies: " + notSatisfied); + } + + if (badInitializers > 0) throw new RuntimeException("Some InitTask dependencies could not be satisfied."); + } + + return ordered; + } +} diff --git a/src/mightypork/gamecore/core/MainLoop.java b/src/mightypork/gamecore/core/MainLoop.java new file mode 100644 index 0000000..b215045 --- /dev/null +++ b/src/mightypork/gamecore/core/MainLoop.java @@ -0,0 +1,130 @@ +package mightypork.gamecore.core; + + +import java.util.Deque; +import java.util.concurrent.ConcurrentLinkedDeque; + +import mightypork.gamecore.graphics.Renderable; +import mightypork.gamecore.gui.screens.ScreenRegistry; +import mightypork.utils.annotations.Stub; +import mightypork.utils.eventbus.clients.BusNode; +import mightypork.utils.eventbus.events.UpdateEvent; +import mightypork.utils.interfaces.Destroyable; +import mightypork.utils.logging.Log; +import mightypork.utils.math.timing.Profiler; +import mightypork.utils.math.timing.TimerDelta; + + +/** + * Delta-timed game loop with task queue etc. + * + * @author Ondřej Hruška (MightyPork) + */ +public class MainLoop extends BusNode implements Destroyable { + + private static final double MAX_TIME_TASKS = 1 / 30D; // (avoid queue from hogging timing) + private static final double MAX_DELTA = 1 / 20D; // (skip huge gaps caused by loading resources etc) + + private final Deque tasks = new ConcurrentLinkedDeque<>(); + private TimerDelta timer; + private Renderable rootRenderable; + private volatile boolean running = true; + + + /** + * Set primary renderable + * + * @param rootRenderable main {@link Renderable}, typically a + * {@link ScreenRegistry} + */ + public void setRootRenderable(Renderable rootRenderable) + { + this.rootRenderable = rootRenderable; + } + + + /** + * Start the loop + */ + public void start() + { + timer = new TimerDelta(); + + while (running) { + App.gfx().beginFrame(); + + double delta = timer.getDelta(); + if (delta > MAX_DELTA) { + Log.f3("(timing) Cropping delta: was " + delta + " , limit " + MAX_DELTA); + delta = MAX_DELTA; + } + + App.bus().sendDirect(new UpdateEvent(delta)); + + Runnable r; + final long t = Profiler.begin(); + while ((r = tasks.poll()) != null) { + Log.f3(" * Main loop task."); + r.run(); + if (Profiler.end(t) > MAX_TIME_TASKS) { + Log.f3("! Postponing main loop tasks to next cycle."); + break; + } + } + + beforeRender(); + + if (rootRenderable != null) { + rootRenderable.render(); + } + + afterRender(); + + App.gfx().endFrame(); + } + } + + + /** + * Called before render + */ + @Stub + protected void beforeRender() + { + // + } + + + /** + * Called after render + */ + @Stub + protected void afterRender() + { + // + } + + + @Override + public void destroy() + { + running = false; + } + + + /** + * Add a task to queue to be executed in the main loop (OpenGL thread) + * + * @param request task + * @param priority if true, skip other tasks + */ + public synchronized void queueTask(Runnable request, boolean priority) + { + if (priority) { + tasks.addFirst(request); + } else { + tasks.addLast(request); + } + } + +} diff --git a/src/mightypork/gamecore/core/OptionalInitTask.java b/src/mightypork/gamecore/core/OptionalInitTask.java new file mode 100644 index 0000000..39a47d0 --- /dev/null +++ b/src/mightypork/gamecore/core/OptionalInitTask.java @@ -0,0 +1,19 @@ +package mightypork.gamecore.core; + + +import java.lang.annotation.*; + + +/** + * Indicates that an {@link InitTask} can safely be ignored if it's dependencies + * are not satisfied. + * + * @author Ondřej Hruška (MightyPork) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Inherited +public @interface OptionalInitTask { + +} diff --git a/src/mightypork/gamecore/core/WorkDir.java b/src/mightypork/gamecore/core/WorkDir.java new file mode 100644 index 0000000..12bdf51 --- /dev/null +++ b/src/mightypork/gamecore/core/WorkDir.java @@ -0,0 +1,91 @@ +package mightypork.gamecore.core; + + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import mightypork.utils.logging.Log; + + +/** + * Static application workdir accessor. + * + * @author Ondřej Hruška (MightyPork) + */ +public class WorkDir { + + private static File workdir; + private static Map namedPaths = new HashMap<>(); + + + public static void init(File workdir) + { + WorkDir.workdir = workdir; + } + + + /** + * Add a path alias (dir or file), relative to the workdir. + * + * @param alias path alias + * @param path path relative to workdir + */ + public static void addPath(String alias, String path) + { + namedPaths.put(alias, path); + } + + + /** + * Get workdir folder, create if not exists. + * + * @param path dir path relative to workdir + * @return dir file + */ + public static File getDir(String path) + { + if (namedPaths.containsKey(path)) path = namedPaths.get(path); + + final File f = new File(workdir, path); + if (!f.exists()) { + if (!f.mkdirs()) { + Log.w("Could not create a directory: " + f + " (path: " + path + ")"); + } + } + + return f; + } + + + /** + * Get workdir file, create parent if not exists. + * + * @param path dir path relative to workdir + * @return dir file + */ + public static File getFile(String path) + { + if (namedPaths.containsKey(path)) path = namedPaths.get(path); + + final File f = new File(workdir, path); + + // create the parent dir + if (!f.getParent().equals(workdir)) { + f.getParentFile().mkdirs(); + } + + return f; + + } + + + /** + * @return the workdir File + */ + public static File getWorkDir() + { + return workdir; + } + +} diff --git a/src/mightypork/gamecore/core/config/Config.java b/src/mightypork/gamecore/core/config/Config.java new file mode 100644 index 0000000..d319847 --- /dev/null +++ b/src/mightypork/gamecore/core/config/Config.java @@ -0,0 +1,269 @@ +package mightypork.gamecore.core.config; + + +import java.util.HashMap; +import java.util.Map; + +import mightypork.gamecore.core.WorkDir; +import mightypork.gamecore.input.Key; +import mightypork.gamecore.input.KeyStroke; +import mightypork.utils.config.propmgr.Property; +import mightypork.utils.config.propmgr.PropertyManager; +import mightypork.utils.config.propmgr.PropertyStore; +import mightypork.utils.config.propmgr.store.PropertyFile; +import mightypork.utils.logging.Log; + + +/** + * Settings repository. + * + * @author Ondřej Hruška (MightyPork) + */ +public class Config { + + protected static Map configs = new HashMap<>(); + + private Map strokes = new HashMap<>(); + + private PropertyManager propertyManager; + + + /** + * Get a config from the static map, by given alias + * + * @param alias alias + * @return the config + */ + public static Config forAlias(String alias) + { + Config c = configs.get(alias); + + if (c == null) { + throw new IllegalArgumentException("There is no config with alias \"" + alias + "\""); + } + + return c; + } + + + /** + * Register a config by alias. + * + * @param alias config alias + * @param config the config + */ + public static void register(String alias, Config config) + { + if (configs.get(alias) != null) { + throw new IllegalArgumentException("The alias \"" + alias + "\" is already used."); + } + + configs.put(alias, config); + } + + + /** + * Initialize property manager for a file + * + * @param file config file, relative to workdir + * @param headComment file comment + */ + public Config(String file, String headComment) { + this(new PropertyFile(WorkDir.getFile(file), headComment)); + } + + + /** + * Initialize property manager for a given store + * + * @param store property store backing the property manager + */ + public Config(PropertyStore store) { + if (propertyManager != null) { + throw new IllegalStateException("Config already initialized."); + } + + propertyManager = new PropertyManager(store); + } + + + /** + * Add a keystroke property + * + * @param key key in config file + * @param defval default value (keystroke datastring) + * @param comment optional comment, can be null + */ + public void addKeyStroke(String key, String defval, String comment) + { + final KeyStrokeProperty kprop = new KeyStrokeProperty(prefixKeyStroke(key), KeyStroke.createFromString(defval), comment); + strokes.put(prefixKeyStroke(key), kprop); + propertyManager.addProperty(kprop); + } + + + /** + * Add a boolean property (flag) + * + * @param key key in config file + * @param defval default value + * @param comment optional comment, can be null + */ + public void addBoolean(String key, boolean defval, String comment) + { + propertyManager.addBoolean(key, defval, comment); + } + + + /** + * Add an integer property + * + * @param key key in config file + * @param defval default value + * @param comment optional comment, can be null + */ + public void addInteger(String key, int defval, String comment) + { + propertyManager.addInteger(key, defval, comment); + } + + + /** + * Add a double property + * + * @param key key in config file + * @param defval default value + * @param comment optional comment, can be null + */ + public void addDouble(String key, double defval, String comment) + { + propertyManager.addDouble(key, defval, comment); + } + + + /** + * Add a string property + * + * @param key key in config file + * @param defval default value + * @param comment optional comment, can be null + */ + public void addString(String key, String defval, String comment) + { + propertyManager.addString(key, defval, comment); + } + + + /** + * Add an arbitrary property (can be custom type) + * + * @param prop the property to add + */ + public void addProperty(Property prop) + { + propertyManager.addProperty(prop); + } + + + /** + * Load config from file + */ + public void load() + { + propertyManager.load(); + } + + + /** + * Save config to file + */ + public void save() + { + Log.f3("Saving config."); + propertyManager.save(); + } + + + /** + * Get an option for key + * + * @param key + * @return option value + */ + public T getValue(String key) + { + try { + if (propertyManager.getProperty(key) == null) { + throw new IllegalArgumentException("No such property: " + key); + } + + return propertyManager.getValue(key); + } catch (final ClassCastException cce) { + throw new RuntimeException("Property of incompatible type: " + key); + } + } + + + /** + * Set option to a value. Call the save() method to make the change + * permanent. + * + * @param key option key + * @param value value to set + */ + public void setValue(String key, T value) + { + if (propertyManager.getProperty(key) == null) { + throw new IllegalArgumentException("No such property: " + key); + } + + propertyManager.setValue(key, value); + } + + + /** + * Add "key." before the given config file key + * + * @param cfgKey config key + * @return key. + cfgKey + */ + private String prefixKeyStroke(String cfgKey) + { + return "key." + cfgKey; + } + + + /** + * Get keystroke for name + * + * @param cfgKey stroke identifier in config file + * @return the stroke + */ + public KeyStroke getKeyStroke(String cfgKey) + { + final KeyStrokeProperty kp = strokes.get(prefixKeyStroke(cfgKey)); + if (kp == null) { + throw new IllegalArgumentException("No such stroke: " + cfgKey); + } + + return kp.getValue(); + } + + + /** + * Set a keystroke for name + * + * @param cfgKey stroke identifier in config file + * @param key stroke key + * @param mod stroke modifiers + */ + public void setKeyStroke(String cfgKey, Key key, int mod) + { + final KeyStrokeProperty kp = strokes.get(prefixKeyStroke(cfgKey)); + if (kp == null) { + throw new IllegalArgumentException("No such stroke: " + cfgKey); + } + + kp.getValue().setTo(key, mod); + } +} diff --git a/src/mightypork/gamecore/core/config/InitTaskConfig.java b/src/mightypork/gamecore/core/config/InitTaskConfig.java new file mode 100644 index 0000000..0ced5e9 --- /dev/null +++ b/src/mightypork/gamecore/core/config/InitTaskConfig.java @@ -0,0 +1,75 @@ +package mightypork.gamecore.core.config; + + +import mightypork.gamecore.core.InitTask; +import mightypork.utils.annotations.Stub; + + +/** + * Initialize config. To apply this initializer, you must extend it. That + * ensures that the workdir initializer has already finished when the code is + * executed (such as resolving a file path for the config file). + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class InitTaskConfig extends InitTask { + + /** + * Add a config with given alias + * + * @param alias config alias + * @param config config to add + */ + protected void addConfig(String alias, Config config) + { + Config.register(alias, config); + } + + + /** + * Initialize the main config. + * + * @return the main config. + */ + protected abstract Config buildConfig(); + + + /** + * Initialize extra configs.
+ * the addConfig() method can be used to register configs. + */ + @Stub + protected void buildExtraConfigs() + { + } + + + // locked to encourage the use of the build* methods. + @Override + public final void init() + { + } + + + @Override + public final void run() + { + addConfig("main", buildConfig()); + buildExtraConfigs(); + } + + + @Override + public String getName() + { + return "config"; + } + + + @Override + public String[] getDependencies() + { + return new String[] { "workdir" }; + } + +} diff --git a/src/mightypork/gamecore/core/config/KeyStrokeProperty.java b/src/mightypork/gamecore/core/config/KeyStrokeProperty.java new file mode 100644 index 0000000..03e98df --- /dev/null +++ b/src/mightypork/gamecore/core/config/KeyStrokeProperty.java @@ -0,0 +1,55 @@ +package mightypork.gamecore.core.config; + + +import mightypork.gamecore.input.Key; +import mightypork.gamecore.input.KeyStroke; +import mightypork.gamecore.input.Keys; +import mightypork.utils.config.propmgr.Property; + + +/** + * Key property.
+ * The stored value must stay the same instance ({@link KeyStroke} is mutable).
+ * That ensures that bindings based on this keystroke are automatically updated + * when the settings change. + * + * @author Ondřej Hruška (MightyPork) + */ +public class KeyStrokeProperty extends Property { + + public KeyStrokeProperty(String key, KeyStroke defaultValue, String comment) { + super(key, defaultValue, comment); + } + + + @Override + public void fromString(String string) + { + if (string != null) { + // keep the same instance + + final Key backup_key = value.getKey(); + final int backup_mod = value.getMod(); + + value.loadFromString(string); + if (value.getKey() == Keys.NONE) { + value.setTo(backup_key, backup_mod); + } + } + } + + + @Override + public String toString() + { + return value.saveToString(); + } + + + @Override + public void setValue(Object value) + { + // keep the same instance + this.value.setTo(((KeyStroke) value).getKey(), ((KeyStroke) value).getMod()); + } +} diff --git a/src/mightypork/gamecore/core/events/MainLoopRequest.java b/src/mightypork/gamecore/core/events/MainLoopRequest.java new file mode 100644 index 0000000..7bcc546 --- /dev/null +++ b/src/mightypork/gamecore/core/events/MainLoopRequest.java @@ -0,0 +1,44 @@ +package mightypork.gamecore.core.events; + + +import mightypork.gamecore.core.MainLoop; +import mightypork.utils.eventbus.BusEvent; +import mightypork.utils.eventbus.events.flags.SingleReceiverEvent; + + +/** + * Request to execute given {@link Runnable} in main loop. + * + * @author Ondřej Hruška (MightyPork) + */ +@SingleReceiverEvent +public class MainLoopRequest extends BusEvent { + + private final Runnable task; + private final boolean priority; + + + /** + * @param task task to run on main thread in rendering context + */ + public MainLoopRequest(Runnable task) { + this(task, false); + } + + + /** + * @param task task to run on main thread in rendering context + * @param priority if true, skip other tasks in queue + */ + public MainLoopRequest(Runnable task, boolean priority) { + this.task = task; + this.priority = priority; + } + + + @Override + public void handleBy(MainLoop handler) + { + handler.queueTask(task, priority); + } +} diff --git a/src/mightypork/gamecore/core/events/ShutdownEvent.java b/src/mightypork/gamecore/core/events/ShutdownEvent.java new file mode 100644 index 0000000..8defcce --- /dev/null +++ b/src/mightypork/gamecore/core/events/ShutdownEvent.java @@ -0,0 +1,47 @@ +package mightypork.gamecore.core.events; + + +import mightypork.utils.eventbus.BusEvent; +import mightypork.utils.eventbus.EventBus; +import mightypork.utils.logging.Log; + + +/** + * Shutdown event.
+ * This event is dispatched when the App.shutdown() method is + * called. If no client consumes it, the shutdown will immediately follow.
+ * This is a way to allow clients to abort the shutdown (ie. ask user to save + * game). After the game is saved, the App.shutdown() method can be + * called again. + * + * @author Ondřej Hruška (MightyPork) + */ +public class ShutdownEvent extends BusEvent { + + private Runnable shutdownTask; + + + public ShutdownEvent(Runnable doShutdown) { + this.shutdownTask = doShutdown; + } + + + @Override + protected void handleBy(ShutdownListener handler) + { + handler.onShutdown(this); + } + + + @Override + public void onDispatchComplete(EventBus bus) + { + if (!isConsumed()) { + Log.i("Shutting down..."); + shutdownTask.run(); + } else { + Log.i("Shutdown aborted."); + } + } + +} diff --git a/src/mightypork/gamecore/core/events/ShutdownListener.java b/src/mightypork/gamecore/core/events/ShutdownListener.java new file mode 100644 index 0000000..00ebc10 --- /dev/null +++ b/src/mightypork/gamecore/core/events/ShutdownListener.java @@ -0,0 +1,18 @@ +package mightypork.gamecore.core.events; + + +/** + * Quit request listener; implementing client can abort shutdown. + * + * @author Ondřej Hruška (MightyPork) + */ +public interface ShutdownListener { + + /** + * Intercept quit request.
+ * Consume the event to abort shutdown (ie. ask user to save) + * + * @param event quit request event. + */ + void onShutdown(ShutdownEvent event); +} diff --git a/src/mightypork/gamecore/core/init/InitTaskCrashHandler.java b/src/mightypork/gamecore/core/init/InitTaskCrashHandler.java new file mode 100644 index 0000000..dcf8122 --- /dev/null +++ b/src/mightypork/gamecore/core/init/InitTaskCrashHandler.java @@ -0,0 +1,42 @@ +package mightypork.gamecore.core.init; + + +import java.lang.Thread.UncaughtExceptionHandler; + +import mightypork.gamecore.core.App; +import mightypork.gamecore.core.InitTask; +import mightypork.utils.annotations.Stub; +import mightypork.utils.logging.Log; + + +/** + * Add a crash handler to the app.
+ * For customized crash message / crash dialog etc, override the + * uncaughtException() method. + * + * @author Ondřej Hruška (MightyPork) + */ +public class InitTaskCrashHandler extends InitTask implements UncaughtExceptionHandler { + + @Override + public void run() + { + Thread.setDefaultUncaughtExceptionHandler(this); + } + + + @Override + @Stub + public void uncaughtException(Thread thread, Throwable throwable) + { + Log.e("The game has crashed.", throwable); + App.shutdown(); + } + + + @Override + public String getName() + { + return "crash_handler"; + } +} diff --git a/src/mightypork/gamecore/core/init/InitTaskDisplay.java b/src/mightypork/gamecore/core/init/InitTaskDisplay.java new file mode 100644 index 0000000..20cf522 --- /dev/null +++ b/src/mightypork/gamecore/core/init/InitTaskDisplay.java @@ -0,0 +1,97 @@ +package mightypork.gamecore.core.init; + + +import mightypork.gamecore.core.InitTask; +import mightypork.gamecore.graphics.GraphicsModule; + + +/** + * Setup main window. + * + * @author Ondřej Hruška (MightyPork) + */ +public class InitTaskDisplay extends InitTask { + + private int width = 800, height = 600, fps = 60; + private boolean resizable, fullscreen; + private String title = "Game"; + + + /** + * Set initial window size + * + * @param width width (px) + * @param height height (px) + */ + public void setSize(int width, int height) + { + this.width = width; + this.height = height; + } + + + /** + * Set whether the window should be resizable + * + * @param resizable true for resizable + */ + public void setResizable(boolean resizable) + { + this.resizable = resizable; + } + + + /** + * Set window title + * + * @param title title text + */ + public void setTitle(String title) + { + this.title = title; + } + + + /** + * Set desired framerate. + * + * @param fps FPS + */ + public void setTargetFps(int fps) + { + this.fps = fps; + } + + + /** + * Set whether the window should start in fullscreen + * + * @param fullscreen true for fullscreen + */ + public void setFullscreen(boolean fullscreen) + { + this.fullscreen = fullscreen; + } + + + @Override + public void run() + { + GraphicsModule gfx = app.getBackend().getGraphics(); + + gfx.setSize(width, height); + gfx.setResizable(resizable); + gfx.setTitle(title); + gfx.setTargetFps(fps); + + if (fullscreen) gfx.setFullscreen(true); + } + + + @Override + public String getName() + { + return "display"; + } + +} diff --git a/src/mightypork/gamecore/core/init/InitTaskIonizables.java b/src/mightypork/gamecore/core/init/InitTaskIonizables.java new file mode 100644 index 0000000..f686da0 --- /dev/null +++ b/src/mightypork/gamecore/core/init/InitTaskIonizables.java @@ -0,0 +1,74 @@ +package mightypork.gamecore.core.init; + + +import java.io.IOException; + +import mightypork.gamecore.core.InitTask; +import mightypork.utils.ion.Ion; +import mightypork.utils.ion.IonInput; +import mightypork.utils.ion.IonOutput; +import mightypork.utils.ion.IonizerBinary; +import mightypork.utils.math.algo.Coord; +import mightypork.utils.math.algo.Move; + + +/** + * Register extra ionizables added by the game library (non-native ION types).
+ * This initializer can be called anywhere in the initialization sequence. + * + * @author Ondřej Hruška (MightyPork) + */ +public class InitTaskIonizables extends InitTask { + + @Override + public void run() + { + Ion.registerIndirect(255, new IonizerBinary() { + + @Override + public void save(Coord object, IonOutput out) throws IOException + { + out.writeInt(object.x); + out.writeInt(object.y); + } + + + @Override + public Coord load(IonInput in) throws IOException + { + final int x = in.readInt(); + final int y = in.readInt(); + return new Coord(x, y); + } + + }); + + Ion.registerIndirect(254, new IonizerBinary() { + + @Override + public void save(Move object, IonOutput out) throws IOException + { + out.writeInt(object.x()); + out.writeInt(object.y()); + } + + + @Override + public Move load(IonInput in) throws IOException + { + final int x = in.readInt(); + final int y = in.readInt(); + return new Move(x, y); + } + + }); + } + + + @Override + public String getName() + { + return "ion"; + } + +} diff --git a/src/mightypork/gamecore/core/init/InitTaskLog.java b/src/mightypork/gamecore/core/init/InitTaskLog.java new file mode 100644 index 0000000..4b34e1a --- /dev/null +++ b/src/mightypork/gamecore/core/init/InitTaskLog.java @@ -0,0 +1,108 @@ +package mightypork.gamecore.core.init; + + +import java.io.File; +import java.util.logging.Level; + +import mightypork.gamecore.core.InitTask; +import mightypork.gamecore.core.WorkDir; +import mightypork.utils.logging.Log; +import mightypork.utils.logging.writers.LogWriter; +import mightypork.utils.string.StringUtil; + + +/** + * Init main logger and console log printing.
+ * Must be called after workdir is initialized. + * + * @author Ondřej Hruška (MightyPork) + */ +public class InitTaskLog extends InitTask { + + private String logDir = "log"; + private String logName = "runtime"; + private int archiveCount = 5; + + private Level levelWrite = Level.ALL; + private Level levelPrint = Level.ALL; + + + /** + * Set log directory (relative to workdir).
+ * Defaults to "log". + * + * @param logDir log directory. + */ + public void setLogDir(String logDir) + { + this.logDir = logDir; + } + + + /** + * Set log name. This name is used as a prefix for archived log files.
+ * Should contain only valid filename characters.
+ * Defaults to "runtime". + * + * @param logName log name + */ + public void setLogName(String logName) + { + if (!StringUtil.isValidFilenameString(logName)) { + throw new IllegalArgumentException("Invalid log name."); + } + + this.logName = logName; + } + + + /** + * Set number of logs to keep in the logs directory.
+ * Set to 0 to keep just the last log, -1 to keep unlimited number of logs.
+ * Defaults to 5. + * + * @param archiveCount logs to keep + */ + public void setArchiveCount(int archiveCount) + { + this.archiveCount = archiveCount; + } + + + /** + * Set logging levels (minimal level of message to be accepted)
+ * Defaults to ALL, ALL. + * + * @param levelWrite level for writing to file + * @param levelPrint level for writing to stdout / stderr + */ + public void setLevels(Level levelWrite, Level levelPrint) + { + this.levelWrite = levelWrite; + this.levelPrint = levelPrint; + } + + + @Override + public void run() + { + final LogWriter log = Log.create(logName, new File(WorkDir.getDir(logDir), logName + ".log"), archiveCount); + Log.setMainLogger(log); + Log.setLevel(levelWrite); + Log.setSysoutLevel(levelPrint); + } + + + @Override + public String getName() + { + return "log"; + } + + + @Override + public String[] getDependencies() + { + return new String[] { "workdir" }; + } +} diff --git a/src/mightypork/gamecore/core/init/InitTaskLogHeader.java b/src/mightypork/gamecore/core/init/InitTaskLogHeader.java new file mode 100644 index 0000000..96a9788 --- /dev/null +++ b/src/mightypork/gamecore/core/init/InitTaskLogHeader.java @@ -0,0 +1,54 @@ +package mightypork.gamecore.core.init; + + +import java.io.IOException; + +import mightypork.gamecore.core.InitTask; +import mightypork.gamecore.core.OptionalInitTask; +import mightypork.gamecore.core.WorkDir; +import mightypork.utils.logging.Log; + + +/** + * initializer task that writes a system info header to the log file.
+ * Must be called after log is initialized. + * + * @author Ondřej Hruška (MightyPork) + */ +@OptionalInitTask +public class InitTaskLogHeader extends InitTask { + + @Override + public void run() + { + String txt = ""; + + txt += "\n### SYSTEM INFO ###\n\n"; + txt += " Platform ...... " + System.getProperty("os.name") + "\n"; + txt += " Runtime ....... " + System.getProperty("java.runtime.name") + "\n"; + txt += " Java .......... " + System.getProperty("java.version") + "\n"; + txt += " Launch path ... " + System.getProperty("user.dir") + "\n"; + + try { + txt += " Workdir ....... " + WorkDir.getWorkDir().getCanonicalPath() + "\n"; + } catch (final IOException e) { + Log.e(e); + } + + Log.i(txt); + } + + + @Override + public String getName() + { + return "log_header"; + } + + + @Override + public String[] getDependencies() + { + return new String[] { "log", "workdir" }; + } +} diff --git a/src/mightypork/gamecore/core/init/InitTaskWorkdir.java b/src/mightypork/gamecore/core/init/InitTaskWorkdir.java new file mode 100644 index 0000000..6f986fa --- /dev/null +++ b/src/mightypork/gamecore/core/init/InitTaskWorkdir.java @@ -0,0 +1,136 @@ +package mightypork.gamecore.core.init; + + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import javax.swing.JOptionPane; + +import mightypork.gamecore.core.App; +import mightypork.gamecore.core.InitTask; +import mightypork.gamecore.core.WorkDir; +import mightypork.utils.annotations.Stub; +import mightypork.utils.files.InstanceLock; +import mightypork.utils.logging.Log; + + +/** + * Initializer that takes care of setting up the proper workdir. + * + * @author Ondřej Hruška (MightyPork) + */ +public class InitTaskWorkdir extends InitTask { + + private File workdirPath; + private boolean doLock; + private String lockFile = ".lock"; + private Map namedPaths = new HashMap<>(); + + + /** + * @param workdir path to the working directory + * @param lock whether to lock the directory (single instance mode) + */ + public InitTaskWorkdir(File workdir, boolean lock) { + this.workdirPath = workdir; + this.doLock = lock; + } + + + /** + * Set workdir root path + * + * @param path workdir path + */ + public void setWorkdirPath(File path) + { + this.workdirPath = path; + } + + + /** + * Set whether the workdir should be locked when the app is running, to + * prevent other instances from running simultaneously. + * + * @param lock + */ + public void setInstanceLock(boolean lock) + { + this.doLock = lock; + } + + + /** + * Set name of the lock file. + * + * @param lockFile + */ + public void setLockFileName(String lockFile) + { + this.lockFile = lockFile; + } + + + /** + * Add a named path + * + * @param alias path alias (snake_case) + * @param path path (relative to the workdir) + */ + public void addPath(String alias, String path) + { + namedPaths.put(alias, path); + } + + + @Override + public void run() + { + WorkDir.init(workdirPath); + + // lock working directory + if (doLock) { + final File lock = WorkDir.getFile(lockFile); + if (!InstanceLock.onFile(lock)) { + onLockError(); + return; + } + } + + for (Entry e : namedPaths.entrySet()) { + WorkDir.addPath(e.getKey(), e.getValue()); + } + } + + + /** + * Called when the lock file could not be obtained (cannot write or already + * exists).
+ * Feel free to override this method to define custom behavior. + */ + @Stub + protected void onLockError() + { + Log.e("Could not obtain lock file.\nOnly one instance can run at a time."); + + //@formatter:off + JOptionPane.showMessageDialog( + null, + "Another instance is already running.\n(Delete the "+lockFile +" file in the working directory to override)", + "Lock Error", + JOptionPane.ERROR_MESSAGE + ); + //@formatter:on + + App.shutdown(); + } + + + @Override + public String getName() + { + return "workdir"; + } +} diff --git a/src/mightypork/gamecore/core/plugins/screenshot/InitTaskPluginScreenshot.java b/src/mightypork/gamecore/core/plugins/screenshot/InitTaskPluginScreenshot.java new file mode 100644 index 0000000..76363da --- /dev/null +++ b/src/mightypork/gamecore/core/plugins/screenshot/InitTaskPluginScreenshot.java @@ -0,0 +1,68 @@ +package mightypork.gamecore.core.plugins.screenshot; + + +import mightypork.gamecore.core.InitTask; +import mightypork.gamecore.core.WorkDir; + + +/** + * Register screenshot plugin to the App. + * + * @author Ondřej Hruška (MightyPork) + */ +public class InitTaskPluginScreenshot extends InitTask { + + private String screenshotDir; + + + /** + * Initialize to use the "screenshots" directory + */ + public InitTaskPluginScreenshot() { + this("screenshots"); + } + + + /** + * Initialize to use the given directory for saving. + * + * @param dir screenshot dir (relative to workdir) + */ + public InitTaskPluginScreenshot(String dir) { + this.screenshotDir = dir; + } + + + /** + * Set screenshot directory + * + * @param dir screenshot dir (relative to workdir) + */ + public void setScreenshotDir(String dir) + { + this.screenshotDir = dir; + } + + + @Override + public void run() + { + WorkDir.addPath("_screenshot_dir", screenshotDir); + app.addPlugin(new ScreenshotPlugin()); + } + + + @Override + public String getName() + { + return "plugin_screenshot"; + } + + + @Override + public String[] getDependencies() + { + return new String[] { "workdir" }; + } + +} diff --git a/src/mightypork/gamecore/core/plugins/screenshot/ScreenshotPlugin.java b/src/mightypork/gamecore/core/plugins/screenshot/ScreenshotPlugin.java new file mode 100644 index 0000000..fb4307f --- /dev/null +++ b/src/mightypork/gamecore/core/plugins/screenshot/ScreenshotPlugin.java @@ -0,0 +1,34 @@ +package mightypork.gamecore.core.plugins.screenshot; + + +import mightypork.gamecore.core.App; +import mightypork.gamecore.core.AppPlugin; +import mightypork.gamecore.core.events.MainLoopRequest; +import mightypork.utils.Support; + + +/** + * This plugin waits for a {@link ScreenshotRequest} event.
+ * Upon receiving it, a screenshot is captured and written to file + * asynchronously. + * + * @author Ondřej Hruška (MightyPork) + */ +public class ScreenshotPlugin extends AppPlugin { + + /** + * Take screenshot. Called by the trigger event. + */ + void takeScreenshot() + { + App.bus().send(new MainLoopRequest(new Runnable() { + + @Override + public void run() + { + Runnable tts = new TaskTakeScreenshot(); + Support.runAsThread(tts); + } + })); + } +} diff --git a/src/mightypork/gamecore/core/plugins/screenshot/ScreenshotRequest.java b/src/mightypork/gamecore/core/plugins/screenshot/ScreenshotRequest.java new file mode 100644 index 0000000..be37967 --- /dev/null +++ b/src/mightypork/gamecore/core/plugins/screenshot/ScreenshotRequest.java @@ -0,0 +1,22 @@ +package mightypork.gamecore.core.plugins.screenshot; + + +import mightypork.utils.eventbus.BusEvent; +import mightypork.utils.eventbus.events.flags.SingleReceiverEvent; + + +/** + * Event used to request screenshot capture. + * + * @author MightyPork + */ +@SingleReceiverEvent +public class ScreenshotRequest extends BusEvent { + + @Override + protected void handleBy(ScreenshotPlugin handler) + { + handler.takeScreenshot(); + } + +} diff --git a/src/mightypork/gamecore/core/plugins/screenshot/TaskTakeScreenshot.java b/src/mightypork/gamecore/core/plugins/screenshot/TaskTakeScreenshot.java new file mode 100644 index 0000000..d1e095c --- /dev/null +++ b/src/mightypork/gamecore/core/plugins/screenshot/TaskTakeScreenshot.java @@ -0,0 +1,100 @@ +package mightypork.gamecore.core.plugins.screenshot; + + +import java.io.File; +import java.io.IOException; + +import mightypork.gamecore.core.App; +import mightypork.gamecore.core.WorkDir; +import mightypork.gamecore.graphics.Screenshot; +import mightypork.utils.Support; +import mightypork.utils.logging.Log; + + +/** + * Task that takes screenshot and asynchronously saves it to a file.
+ * Can be run in a separate thread, but must be instantiated in the render + * thread. + * + * @author MightyPork + */ +public class TaskTakeScreenshot implements Runnable { + + private final Screenshot scr; + + + /** + * Take screenshot. Must be called in render thread. + */ + public TaskTakeScreenshot() { + scr = App.gfx().takeScreenshot(); + } + + + @Override + public void run() + { + // generate unique filename + final File file = getScreenshotFile(); + + Log.f3("Saving screenshot to file: " + file); + + // save to disk + try { + scr.save(file); + } catch (final IOException e) { + Log.e("Failed to save screenshot.", e); + } + } + + + /** + * @return File to save the screenshot to. + */ + protected File getScreenshotFile() + { + final String fname = getBaseFilename(); + return findFreeFile(fname); + } + + + /** + * @return directory for screenshots + */ + protected File getScreenshotDirectory() + { + return WorkDir.getDir("_screenshot_dir"); + } + + + /** + * Get base filename for the screenshot, without extension. + * + * @return filename + */ + protected String getBaseFilename() + { + return Support.getTime("yyyy-MM-dd_HH-mm-ss"); + } + + + /** + * Find first free filename for the screenshot, by adding -NUMBER after the + * base filename and before extension. + * + * @param base_name base filename + * @return full path to screenshot file + */ + protected File findFreeFile(String base_name) + { + File file; + int index = 0; + while (true) { + file = new File(getScreenshotDirectory(), base_name + (index > 0 ? "-" + index : "") + ".png"); + if (!file.exists()) break; + index++; + } + return file; + } + +} diff --git a/src/mightypork/gamecore/graphics/FullscreenToggleRequest.java b/src/mightypork/gamecore/graphics/FullscreenToggleRequest.java new file mode 100644 index 0000000..eeff3b8 --- /dev/null +++ b/src/mightypork/gamecore/graphics/FullscreenToggleRequest.java @@ -0,0 +1,16 @@ +package mightypork.gamecore.graphics; + + +import mightypork.utils.eventbus.BusEvent; +import mightypork.utils.eventbus.events.flags.SingleReceiverEvent; + + +@SingleReceiverEvent +public class FullscreenToggleRequest extends BusEvent { + + @Override + protected void handleBy(GraphicsModule handler) + { + handler.switchFullscreen(); + } +} diff --git a/src/mightypork/gamecore/graphics/GraphicsModule.java b/src/mightypork/gamecore/graphics/GraphicsModule.java new file mode 100644 index 0000000..f895888 --- /dev/null +++ b/src/mightypork/gamecore/graphics/GraphicsModule.java @@ -0,0 +1,406 @@ +package mightypork.gamecore.graphics; + + +import mightypork.gamecore.core.BackendModule; +import mightypork.gamecore.graphics.textures.DeferredTexture; +import mightypork.gamecore.graphics.textures.TxQuad; +import mightypork.gamecore.gui.events.ViewportChangeEvent; +import mightypork.utils.math.color.Color; +import mightypork.utils.math.color.Grad; +import mightypork.utils.math.constraints.rect.Rect; +import mightypork.utils.math.constraints.vect.Vect; +import mightypork.utils.math.constraints.vect.VectConst; +import mightypork.utils.math.timing.FpsMeter; + + +/** + * Render and display backend module.
+ * This module takes care of setting and getting screen size and parameters, + * drawing on screen and timing render frames. + * + * @author MightyPork + */ +public abstract class GraphicsModule extends BackendModule { + + protected static final VectConst AXIS_X = Vect.make(1, 0, 0); + protected static final VectConst AXIS_Y = Vect.make(0, 1, 0); + protected static final VectConst AXIS_Z = Vect.make(0, 0, 1); + + + /** + * Set drawing color + * + * @param color color + */ + public abstract void setColor(Color color); + + + /** + * Set drawing color, adjust alpha + * + * @param color color + * @param alpha alpha multiplier + */ + public abstract void setColor(Color color, double alpha); + + + /** + * Translate by x, y + * + * @param x x offset + * @param y y offset + */ + public abstract void translate(double x, double y); + + + /** + * Translate by x, y, z + * + * @param x x offset + * @param y y offset + * @param z z offset + */ + public abstract void translate(double x, double y, double z); + + + /** + * Translate by offset vector + * + * @param offset offset coordinate + */ + public abstract void translate(Vect offset); + + + /** + * Translate by offset vector, ignore Z + * + * @param offset offset coordinate + */ + public abstract void translateXY(Vect offset); + + + /** + * Set scale for translations and coordinates + * + * @param x x scale + * @param y y scale + */ + public abstract void scale(double x, double y); + + + /** + * Set scale for translations and coordinates + * + * @param x x scale + * @param y y scale + * @param z z scale + */ + public abstract void scale(double x, double y, double z); + + + /** + * Set scale for translations and coordinates + * + * @param scale vector + */ + public abstract void scale(Vect scale); + + + /** + * Set scale for translations and coordinates (same value for X and Y scale) + * + * @param scale scaling factor + */ + public abstract void scaleXY(double scale); + + + /** + * Set X scale for translations and coordinates + * + * @param scale scaling factor + */ + public abstract void scaleX(double scale); + + + /** + * Set Y scale for translations and coordinates + * + * @param scale scaling factor + */ + public abstract void scaleY(double scale); + + + /** + * Set Z scale for translations and coordinates + * + * @param scale scaling factor + */ + public abstract void scaleZ(double scale); + + + /** + * Rotate coordinate system around X axis + * + * @param angle rotation (in degrees) + */ + public abstract void rotateX(double angle); + + + /** + * Rotate coordinate system around Y axis + * + * @param angle rotation (in degrees) + */ + public abstract void rotateY(double angle); + + + /** + * Rotate coordinate system around Z axis + * + * @param angle rotation (in degrees) + */ + public abstract void rotateZ(double angle); + + + /** + * Rotate coordinate system around given axis + * + * @param angle rotation angle + * @param axis rotation axis (unit vector) + */ + public abstract void rotate(double angle, Vect axis); + + + /** + * Store render state on stack
+ * This includes pushGeometry and pushColor. + */ + public abstract void pushState(); + + + /** + * Restore state from stack (must be pushed first)
+ * This includes popColor and popGeometry. + */ + public abstract void popState(); + + + /** + * Store current rotation and translation on stack + */ + public abstract void pushGeometry(); + + + /** + * Restore rotation and translation from stack + */ + public abstract void popGeometry(); + + + /** + * Store color on stack (so it can later be restored) + */ + public abstract void pushColor(); + + + /** + * Restore color from stack (must be pushed first) + */ + public abstract void popColor(); + + + /** + * Render 2D quad with currently set color + * + * @param rect drawn rect + */ + public abstract void quad(Rect rect); + + + /** + * Render 2D quad with given color.
+ * This may change current drawing color. + * + * @param rect rectangle + * @param color draw color + */ + public abstract void quad(Rect rect, Color color); + + + /** + * Render 2D quad with gradient.
+ * This may change current drawing color. + * + * @param rect rectangle + * @param grad gradient + */ + public abstract void quad(Rect rect, Grad grad); + + + /** + * Render textured quad with current color + * + * @param rect rectangle to draw + * @param txquad texture quad + */ + public abstract void quad(Rect rect, TxQuad txquad); + + + /** + * Render textured quad with given color + * + * @param rect rectangle to draw + * @param txquad texture instance + * @param color color tint + */ + public abstract void quad(Rect rect, TxQuad txquad, Color color); + + + /** + * Setup projection for 2D graphics, using current scren size + */ + public abstract void setupProjection(); + + + /** + * Get backend-flavoured lazy texture + * + * @param path path to texture + * @return the lazy texture + */ + public abstract DeferredTexture getLazyTexture(String path); + + + /** + * Set target fps (for syncing in endFrame() call).
+ * With vsync enabled, the target fps may not be met. + * + * @param fps requested fps + */ + public abstract void setTargetFps(int fps); + + + /** + * Set fullscreen. The fullscreen state will be changed when possible (eg. + * at the end of the current frame) and a {@link ViewportChangeEvent} will + * be fired. + * + * @param fs true for fullscreen + */ + public abstract void setFullscreen(boolean fs); + + + /** + * Request fullscreen toggle. See setFullscreen() for more info) + */ + public abstract void switchFullscreen(); + + + /** + * Get fullscreen state (note that methods changing fullscreen may not have + * immediate effect, so this method may report the old state if the + * fullscreen state has not yet been changed). + * + * @return is fullscreen + */ + public abstract boolean isFullscreen(); + + + /** + * Take screenshot (expensive processing should be done in separate thread + * when screenshot is saved).
+ * This method is utilized by the Screenshot plugin. + * + * @return screenshot object + */ + public abstract Screenshot takeScreenshot(); + + + /** + * Start a render frame - clear buffers, prepare rendering context etc. + */ + public abstract void beginFrame(); + + + /** + * End a render frame: flip buffers, sync to fps... + */ + public abstract void endFrame(); + + + /** + * Set display dimensions + * + * @param width display width (pixels) + * @param height display height (pixels) + */ + public abstract void setSize(int width, int height); + + + /** + * Set window titlebar text + * + * @param title titlebar text + */ + public abstract void setTitle(String title); + + + /** + * Enable or disable VSync + * + * @param vsync true for vsync enabled + */ + public abstract void setVSync(boolean vsync); + + + /** + * Set window resizable / fixed + * + * @param resizable true for resizable + */ + public abstract void setResizable(boolean resizable); + + + /** + * Get screen rect. Should always return the same Rect instance. + * + * @return the rect + */ + public abstract Rect getRect(); + + + /** + * Get current FPS (eg. measured by a {@link FpsMeter}) + * + * @return current FPS + */ + public abstract long getFps(); + + + /** + * Get screen center. Should always return the same Vect instance. + * + * @return screen center. + */ + public abstract Vect getCenter(); + + + /** + * Get screen size. Should always return the same Vect instance. + * + * @return size + */ + public abstract Vect getSize(); + + + /** + * @return screen width + */ + public abstract int getWidth(); + + + /** + * @return screen height + */ + public abstract int getHeight(); +} diff --git a/src/mightypork/gamecore/graphics/Renderable.java b/src/mightypork/gamecore/graphics/Renderable.java new file mode 100644 index 0000000..6c6a236 --- /dev/null +++ b/src/mightypork/gamecore/graphics/Renderable.java @@ -0,0 +1,16 @@ +package mightypork.gamecore.graphics; + + +/** + * Can be rendered + * + * @author Ondřej Hruška (MightyPork) + */ +public interface Renderable { + + /** + * Render on screen. + */ + void render(); + +} diff --git a/src/mightypork/gamecore/graphics/Screenshot.java b/src/mightypork/gamecore/graphics/Screenshot.java new file mode 100644 index 0000000..00c2a5c --- /dev/null +++ b/src/mightypork/gamecore/graphics/Screenshot.java @@ -0,0 +1,34 @@ +package mightypork.gamecore.graphics; + + +import java.io.File; +import java.io.IOException; + + +/** + *

+ * Screenshot object used to save screenshot to a file. The Screenshot object is + * created by the Graphics module. + *

+ *

+ * Screenshot typically takes a byte buffer and converts it to image before + * saving to file. This image can be cached to speed up repeated saving. + *

+ *

+ * Once created (passing byte buffer in constructor), the Screenshot should be + * safe to process (call the save() method) in separate thread. + *

+ * + * @author MightyPork + */ +public interface Screenshot { + + /** + * Process byte buffer and write image to a file.
+ * Image can be cached for future save. + * + * @param file target file + * @throws IOException on error writing to file + */ + void save(File file) throws IOException; +} diff --git a/src/mightypork/gamecore/graphics/fonts/DeferredFont.java b/src/mightypork/gamecore/graphics/fonts/DeferredFont.java new file mode 100644 index 0000000..659ec50 --- /dev/null +++ b/src/mightypork/gamecore/graphics/fonts/DeferredFont.java @@ -0,0 +1,98 @@ +package mightypork.gamecore.graphics.fonts; + + +import mightypork.gamecore.graphics.textures.FilterMode; +import mightypork.gamecore.resources.BaseDeferredResource; + + +/** + * Abstract deferred font stub. + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class DeferredFont extends BaseDeferredResource implements IFont { + + public static enum FontStyle + { + PLAIN(0), BOLD(1), ITALIC(2), BOLD_ITALIC(3); + + public int numval; + + + /** + * Font style + * + * @param style style index as in awt Font. Not using constants to be + * independent on awt. + */ + private FontStyle(int style) { + this.numval = style; + } + } + + protected double size = 12; + protected FontStyle style = FontStyle.PLAIN; + protected String chars = Glyphs.basic; + protected FilterMode filter = FilterMode.NEAREST; + protected boolean antialias = false; + protected double discardTop = 0; + protected double discardBottom = 0; + + + public DeferredFont(String resource) { + super(resource); + } + + + public void setSize(double size) + { + this.size = size; + } + + + public void setStyle(FontStyle style) + { + this.style = style; + } + + + public void setChars(String chars) + { + this.chars = chars; + } + + + public void setFilter(FilterMode filter) + { + this.filter = filter; + } + + + public void setAntialias(boolean antialias) + { + this.antialias = antialias; + } + + + @Override + public void setDiscardRatio(double top, double bottom) + { + discardTop = top; + discardBottom = bottom; + } + + + @Override + public double getTopDiscardRatio() + { + return discardTop; + } + + + @Override + public double getBottomDiscardRatio() + { + return discardBottom; + } + +} diff --git a/src/mightypork/gamecore/graphics/fonts/FontRegistry.java b/src/mightypork/gamecore/graphics/fonts/FontRegistry.java new file mode 100644 index 0000000..9c80f17 --- /dev/null +++ b/src/mightypork/gamecore/graphics/fonts/FontRegistry.java @@ -0,0 +1,79 @@ +package mightypork.gamecore.graphics.fonts; + + +import java.util.HashMap; + +import mightypork.gamecore.core.App; +import mightypork.gamecore.resources.loading.ResourceLoadRequest; +import mightypork.utils.eventbus.clients.BusNode; + + +/** + * Font loader and registry + * + * @author Ondřej Hruška (MightyPork) + */ +public class FontRegistry extends BusNode { + + private final HashMap fonts = new HashMap<>(); + private final HashMap aliases = new HashMap<>(); + + + /** + * Load a {@link DeferredLwjglFont} + * + * @param key font key + * @param font font instance + */ + public void addFont(String key, DeferredFont font) + { + App.bus().send(new ResourceLoadRequest(font)); + + fonts.put(key, font); + } + + + /** + * Add a {@link IFont} to the bank. + * + * @param key font key + * @param font font instance + */ + public void addFont(String key, IFont font) + { + fonts.put(key, font); + } + + + /** + * Add a font alias. + * + * @param alias_key alias key + * @param font_key font key + */ + public void addAlias(String alias_key, String font_key) + { + aliases.put(alias_key, font_key); + } + + + /** + * Get a loaded {@link Texture} + * + * @param key texture key + * @return the texture + */ + public IFont getFont(String key) + { + IFont f = fonts.get(key); + + if (f == null) f = fonts.get(aliases.get(key)); + + if (f == null) { + throw new RuntimeException("There's no font called " + key + "!"); + } + + return f; + } + +} diff --git a/src/mightypork/gamecore/graphics/fonts/FontRenderer.java b/src/mightypork/gamecore/graphics/fonts/FontRenderer.java new file mode 100644 index 0000000..4e3886b --- /dev/null +++ b/src/mightypork/gamecore/graphics/fonts/FontRenderer.java @@ -0,0 +1,213 @@ +package mightypork.gamecore.graphics.fonts; + + +import mightypork.gamecore.core.App; +import mightypork.utils.math.AlignX; +import mightypork.utils.math.color.Color; +import mightypork.utils.math.color.pal.RGB; +import mightypork.utils.math.constraints.rect.Rect; +import mightypork.utils.math.constraints.vect.Vect; + + +/** + * Font renderer + * + * @author Ondřej Hruška (MightyPork) + */ +public class FontRenderer { + + private IFont font; + + private Color color; + + + /** + * @param font used font + */ + public FontRenderer(IFont font) { + this(font, RGB.WHITE); + } + + + /** + * @param font used font + * @param color drawing color + */ + public FontRenderer(IFont font, Color color) { + this.font = font; + this.color = color; + } + + + /** + * Get region needed to draw text at size + * + * @param text text to draw + * @param height drawing height + * @return taken space (width, height) + */ + public Vect getNeededSpace(String text, double height) + { + return font.getNeededSpace(text).mul(getScale(height)); + } + + + /** + * Get width needed to draw text at size + * + * @param text text to draw + * @param height drawing height + * @return needed width + */ + public double getWidth(String text, double height) + { + return getNeededSpace(text, height).x(); + } + + + private double getScale(double height) + { + return height / font.getLineHeight(); + } + + + /** + * Change drawing font + * + * @param font font to use for drawing + */ + public void setFont(IFont font) + { + this.font = font; + } + + + /** + * Set drawing color + * + * @param color color + */ + public void setColor(Color color) + { + this.color = color; + } + + + /** + * Draw on screen + * + * @param text text to draw + * @param pos origin (min coord) + * @param height drawing height + * @param color drawing color + */ + public void draw(String text, Vect pos, double height, Color color) + { + App.gfx().pushGeometry(); + + final double sc = getScale(height); + + App.gfx().translate(pos.x(), pos.y()); + App.gfx().scaleXY(sc); + + font.draw(text, color); + + App.gfx().popGeometry(); + } + + + /** + * Draw on screen + * + * @param text text to draw + * @param bounds drawing bounds (height for font height, horizontal bounds + * for align) + * @param align horizontal alignment (with respect to bounds) + */ + public void draw(String text, Rect bounds, AlignX align) + { + this.draw(text, bounds, align, this.color); + } + + + /** + * Draw on screen + * + * @param text text to draw + * @param bounds drawing bounds (height for font height, horizontal bounds + * for align) + * @param align horizontal alignment (with respect to bounds) + * @param color drawing color + */ + public void draw(String text, Rect bounds, AlignX align, Color color) + { + Vect start; + + switch (align) { + case LEFT: + start = bounds.topLeft(); + break; + + case CENTER: + start = bounds.topCenter(); + break; + + case RIGHT: + default: + start = bounds.topRight(); + break; + } + + draw(text, start, bounds.height().value(), align, color); + } + + + /** + * Draw on screen + * + * @param text text to draw + * @param pos origin (min coord) + * @param height drawing height + * @param align horizontal alignment + */ + public void draw(String text, Vect pos, double height, AlignX align) + { + draw(text, pos, height, align, this.color); + } + + + /** + * Draw on screen + * + * @param text text to draw + * @param pos origin (min coord) + * @param height drawing height + * @param align horizontal alignment + * @param color drawing color + */ + public void draw(String text, Vect pos, double height, AlignX align, Color color) + { + + final double w = getWidth(text, height); + + Vect start; + + switch (align) { + case LEFT: + start = pos; + break; + + case CENTER: + start = pos.sub(w / 2D, 0); + break; + + case RIGHT: + default: + start = pos.sub(w, 0); + break; + } + + draw(text, start, height, color); + } + +} diff --git a/src/mightypork/gamecore/graphics/fonts/Glyphs.java b/src/mightypork/gamecore/graphics/fonts/Glyphs.java new file mode 100644 index 0000000..0b9b7a8 --- /dev/null +++ b/src/mightypork/gamecore/graphics/fonts/Glyphs.java @@ -0,0 +1,23 @@ +package mightypork.gamecore.graphics.fonts; + + +/** + * Glyph tables, can be used for font loading. + * + * @author Ondřej Hruška (MightyPork) + */ +public class Glyphs { + + public static final String latin = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + public static final String latin_extra = "ŒÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜŸÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿĚŠČŘŽŤŇĎŮěščřžťňďůŁłđ"; + public static final String numbers = "0123456789"; + public static final String punctuation = ".-,.?!:;\"'"; + public static final String punctuation_extra = "()¿¡»«›‹“”‘’„…"; + public static final String symbols = "[]{}#$%&§*+/<=>@\\^_|~°"; + public static final String symbols_extra = "¥€£¢`ƒ†‡ˆ‰•¤¦¨ªº¹²³¬­¯±´µ¶·¸¼½¾×÷™©­®→↓←↑"; + + public static final String basic = latin + numbers + punctuation + symbols; + public static final String extra = latin_extra + punctuation_extra + symbols_extra; + public static final String all = basic + extra; + +} diff --git a/src/mightypork/gamecore/graphics/fonts/IFont.java b/src/mightypork/gamecore/graphics/fonts/IFont.java new file mode 100644 index 0000000..ef08b1a --- /dev/null +++ b/src/mightypork/gamecore/graphics/fonts/IFont.java @@ -0,0 +1,76 @@ +package mightypork.gamecore.graphics.fonts; + + +import mightypork.utils.math.color.Color; +import mightypork.utils.math.constraints.vect.Vect; + + +/** + * Interface bor drawable font. + * + * @author Ondřej Hruška (MightyPork) + */ +public interface IFont { + + /** + * Draw without scaling at (0, 0) in given color. + * + * @param text text to draw + * @param color draw color + */ + void draw(String text, Color color); + + + /** + * Get suize needed to render give string + * + * @param text string to check + * @return coord (width, height) + */ + Vect getNeededSpace(String text); + + + /** + * @return font height + */ + int getLineHeight(); + + + /** + * @param text texted text + * @return space needed + */ + int getWidth(String text); + + + /** + * @return specified font size + */ + int getFontSize(); + + + /** + * Set what vertical ratio of the font size is blank and should be cut off + * when rendering + * + * @param top top ratio (0-1) + * @param bottom bottom ratio (0-1) + */ + void setDiscardRatio(double top, double bottom); + + + /** + * Get top discard ratio (blank unused space) + * + * @return ratio + */ + double getTopDiscardRatio(); + + + /** + * Get bottom discard ratio (blank unused space) + * + * @return ratio + */ + double getBottomDiscardRatio(); +} diff --git a/src/mightypork/gamecore/graphics/textures/DeferredTexture.java b/src/mightypork/gamecore/graphics/textures/DeferredTexture.java new file mode 100644 index 0000000..27bd0a7 --- /dev/null +++ b/src/mightypork/gamecore/graphics/textures/DeferredTexture.java @@ -0,0 +1,57 @@ +package mightypork.gamecore.graphics.textures; + + +import mightypork.gamecore.resources.BaseDeferredResource; +import mightypork.gamecore.resources.loading.MustLoadInRenderingContext; +import mightypork.utils.annotations.Alias; +import mightypork.utils.math.constraints.rect.Rect; + + +/** + * Deferred texture (to be extended by backend texture) + * + * @author Ondřej Hruška (MightyPork) + */ +@Alias(name = "Texture") +@MustLoadInRenderingContext +public abstract class DeferredTexture extends BaseDeferredResource implements ITexture { + + protected FilterMode filter = FilterMode.NEAREST; + protected WrapMode wrap = WrapMode.CLAMP; + + + /** + * @param resourcePath resource path + */ + public DeferredTexture(String resourcePath) { + super(resourcePath); + } + + + @Override + public TxQuad makeQuad(Rect uvs) + { + return new TxQuad(this, uvs); + } + + + @Override + public void setFilter(FilterMode filterMin) + { + this.filter = filterMin; + } + + + @Override + public void setWrap(WrapMode wrapping) + { + this.wrap = wrapping; + } + + + @Override + public QuadGrid grid(int x, int y) + { + return new QuadGrid(this, x, y); + } +} diff --git a/src/mightypork/gamecore/graphics/textures/FilterMode.java b/src/mightypork/gamecore/graphics/textures/FilterMode.java new file mode 100644 index 0000000..cd9c510 --- /dev/null +++ b/src/mightypork/gamecore/graphics/textures/FilterMode.java @@ -0,0 +1,12 @@ +package mightypork.gamecore.graphics.textures; + + +/** + * Texture filtering mode + * + * @author Ondřej Hruška (MightyPork) + */ +public enum FilterMode +{ + LINEAR, NEAREST; +} diff --git a/src/mightypork/gamecore/graphics/textures/ITexture.java b/src/mightypork/gamecore/graphics/textures/ITexture.java new file mode 100644 index 0000000..17a8856 --- /dev/null +++ b/src/mightypork/gamecore/graphics/textures/ITexture.java @@ -0,0 +1,64 @@ +package mightypork.gamecore.graphics.textures; + + +import mightypork.utils.interfaces.Destroyable; +import mightypork.utils.math.constraints.rect.Rect; + + +/** + * Texture interface, backend independent + * + * @author Ondřej Hruška (MightyPork) + */ +public interface ITexture extends Destroyable { + + /** + * Set filter for scaling + * + * @param filter filter + */ + void setFilter(FilterMode filter); + + + /** + * @param wrapping wrap mode + */ + void setWrap(WrapMode wrapping); + + + /** + * Get a quad from this texture of given position/size + * + * @param uvs quad rect + * @return the quad + */ + TxQuad makeQuad(Rect uvs); + + + /** + * Get a grid for given number of tiles + * + * @param x horizontal tile count + * @param y vertical tile count + * @return grid + */ + QuadGrid grid(int x, int y); + + + /** + * @return source image width (corresponding to width01) + */ + int getImageWidth(); + + + /** + * @return source image height (corresponding to height01) + */ + int getImageHeight(); + + + /** + * @return true if the image is RGBA + */ + boolean hasAlpha(); +} diff --git a/src/mightypork/gamecore/graphics/textures/QuadGrid.java b/src/mightypork/gamecore/graphics/textures/QuadGrid.java new file mode 100644 index 0000000..e63eeec --- /dev/null +++ b/src/mightypork/gamecore/graphics/textures/QuadGrid.java @@ -0,0 +1,92 @@ +package mightypork.gamecore.graphics.textures; + + +import mightypork.utils.math.constraints.rect.Rect; + + +/** + * {@link TxQuad} and {@link TxSheet} building utility + * + * @author Ondřej Hruška (MightyPork) + */ +public class QuadGrid { + + private final ITexture tx; + private final int txHeight; + private final int txWidth; + private final double tileW; + private final double tileH; + + + public QuadGrid(ITexture tx, int tilesX, int tilesY) { + this.tx = tx; + this.txWidth = tilesX; + this.txHeight = tilesY; + this.tileW = 1D / tilesX; + this.tileH = 1D / tilesY; + } + + + /** + * Make square quad at given coords (one grid cell) + * + * @param x x coordinate (cells) + * @param y y coordinate (cells) + * @return the quad + */ + public TxQuad makeQuad(double x, double y) + { + if (x < 0 || x >= txWidth || y < 0 || y >= txHeight) { + throw new IndexOutOfBoundsException("Requested invalid txquad coordinates."); + } + + return makeQuad(x, y, 1, 1); + } + + + /** + * Make square quad at given coords, with arbitrary size. Coordinates are + * multiples of cell size. + * + * @param x x coordinate (cells) + * @param y y coordinate (cells) + * @param width width (cells) + * @param height height (cells) + * @return the quad + */ + public TxQuad makeQuad(double x, double y, double width, double height) + { + if (x < 0 || x >= txWidth || y < 0 || y >= txHeight) { + throw new IndexOutOfBoundsException("Requested invalid txquad coordinates."); + } + + if (x + width > txWidth || y + height > txHeight) { + throw new IndexOutOfBoundsException("Requested invalid txquad size (would go beyond texture size)."); + } + + return tx.makeQuad(Rect.make(tileW * x, tileH * y, tileW * width, tileH * height)); + } + + + /** + * Make a sheet. + * + * @param x x origin coordinate (cells) + * @param y y origin coordinate (cells) + * @param width width (cells) + * @param height height (cells) + * @return the sheet + */ + public TxSheet makeSheet(double x, double y, double width, double height) + { + if (x < 0 || x >= txWidth || y < 0 || y >= txHeight) { + throw new IndexOutOfBoundsException("Requested invalid txquad coordinates."); + } + + if (x + width > txWidth || y + height > txHeight) { + throw new IndexOutOfBoundsException("Requested invalid txsheet size (would go beyond texture size)."); + } + + return makeQuad(x, y).makeSheet(width, height); + } +} diff --git a/src/mightypork/gamecore/graphics/textures/TextureRegistry.java b/src/mightypork/gamecore/graphics/textures/TextureRegistry.java new file mode 100644 index 0000000..1d3408b --- /dev/null +++ b/src/mightypork/gamecore/graphics/textures/TextureRegistry.java @@ -0,0 +1,143 @@ +package mightypork.gamecore.graphics.textures; + + +import java.util.HashMap; +import java.util.Map; + +import mightypork.gamecore.core.App; +import mightypork.gamecore.resources.loading.ResourceLoadRequest; +import mightypork.utils.exceptions.KeyAlreadyExistsException; +import mightypork.utils.math.constraints.rect.Rect; + + +/** + * Texture storage and quad/sheet registry. Quads and Sheets are interchangeable + * once registered. + * + * @author Ondřej Hruška (MightyPork) + */ +public class TextureRegistry { + + private final Map textures = new HashMap<>(); + private final Map sheets = new HashMap<>(); + + + /** + * Load a texture from resource, without a key. This texture will not be + * added to the bank. + * + * @param resourcePath resource path of the texture + * @param filter + * @param wrap + * @return texture reference + */ + public ITexture addTexture(String resourcePath, FilterMode filter, WrapMode wrap) + { + return addTexture(resourcePath, resourcePath, filter, wrap); + } + + + /** + * Load a texture from resource; if key is not null, the texture will be + * added to the bank. + * + * @param key texture key, can be null. + * @param resourcePath resource path of the texture + * @param filter + * @param wrap + * @return texture reference + */ + public ITexture addTexture(String key, String resourcePath, FilterMode filter, WrapMode wrap) + { + if (key != null) if (textures.containsKey(key)) throw new KeyAlreadyExistsException(); + + final DeferredTexture texture = App.gfx().getLazyTexture(resourcePath); + texture.setFilter(filter); + texture.setWrap(wrap); + + App.bus().send(new ResourceLoadRequest(texture)); + + if (key != null) { + textures.put(key, texture); + add(key, texture.makeQuad(Rect.ONE)); + } + + return texture; + } + + + /** + * Add already created quad to the quad registry + * + * @param quadKey key + * @param quad quad to add + */ + public void add(String quadKey, TxQuad quad) + { + if (sheets.containsKey(quadKey)) throw new KeyAlreadyExistsException(); + + sheets.put(quadKey, quad.makeSheet(1, 1)); + } + + + /** + * Add an already created sheet + * + * @param sheetKey key + * @param sheet sheet to add + */ + public void add(String sheetKey, TxSheet sheet) + { + if (sheets.containsKey(sheetKey)) throw new KeyAlreadyExistsException(); + + sheets.put(sheetKey, sheet); + } + + + /** + * Get a {@link TxQuad} for key; if it was added as sheet, the first quad + * ofthe sheet is returned. + * + * @param key quad key + * @return the quad + */ + public TxQuad getQuad(String key) + { + return getSheet(key).getQuad(0); // get the first + } + + + /** + * Get a loaded {@link ITexture} + * + * @param key texture key + * @return the texture + */ + public ITexture getTexture(String key) + { + final ITexture tx = textures.get(key); + + if (tx == null) throw new RuntimeException("There's no texture called \"" + key + "\"!"); + + return tx; + } + + + /** + * Get a {@link TxSheet} for key + * + * @param key sheet key + * @return the sheet + */ + public TxSheet getSheet(String key) + { + final TxSheet sh = sheets.get(key); + + if (sh == null) { + throw new RuntimeException("There's no sheet called \"" + key + "\"!"); + } + + return sh; + } + +} diff --git a/src/mightypork/gamecore/graphics/textures/TxQuad.java b/src/mightypork/gamecore/graphics/textures/TxQuad.java new file mode 100644 index 0000000..bc3efd7 --- /dev/null +++ b/src/mightypork/gamecore/graphics/textures/TxQuad.java @@ -0,0 +1,164 @@ +package mightypork.gamecore.graphics.textures; + + +import mightypork.utils.math.constraints.rect.Rect; +import mightypork.utils.math.constraints.rect.RectConst; + + +/** + * Texture Quad (describing a part of a texture) + * + * @author Ondřej Hruška (MightyPork) + */ +public class TxQuad { + + /** The texture */ + public final ITexture tx; + /** Coords in texture (0-1) */ + public final RectConst uvs; + + private boolean flipX; + private boolean flipY; + + + /** + * TxQuad from origin and size in pixels + * + * @param tx texture + * @param xPx left top X (0-1) + * @param yPx left top Y (0-1) + * @param widthPx area width (0-1) + * @param heightPx area height (0-1) + * @return new TxQuad + */ + public static TxQuad fromSizePx(ITexture tx, double xPx, double yPx, double widthPx, double heightPx) + { + final double w = tx.getImageWidth(); + final double h = tx.getImageHeight(); + + return fromSize(tx, xPx / w, yPx / h, widthPx / w, heightPx / h); + } + + + /** + * TxQuad from origin and size 0-1 + * + * @param tx texture + * @param x1 left top X (0-1) + * @param y1 left top Y (0-1) + * @param width area width (0-1) + * @param height area height (0-1) + * @return new TxQuad + */ + public static TxQuad fromSize(ITexture tx, double x1, double y1, double width, double height) + { + return new TxQuad(tx, x1, y1, x1 + width, y1 + height); + } + + + /** + * Make of coords + * + * @param tx texture + * @param x1 left top X (0-1) + * @param y1 left top Y (0-1) + * @param x2 right bottom X (0-1) + * @param y2 right bottom Y (0-1) + */ + public TxQuad(ITexture tx, double x1, double y1, double x2, double y2) { + this(tx, Rect.make(x1, y1, x2, y2)); + } + + + /** + * @param tx Texture + * @param uvs Rect of texture UVs (0-1); will be frozen. + */ + public TxQuad(ITexture tx, Rect uvs) { + this.tx = tx; + this.uvs = uvs.freeze(); + } + + + /** + * Clone another + * + * @param txQuad a copied quad + */ + public TxQuad(TxQuad txQuad) { + this.tx = txQuad.tx; + this.uvs = txQuad.uvs; + this.flipX = txQuad.flipX; + this.flipY = txQuad.flipY; + } + + + /** + * Get copy + * + * @return copy of this + */ + public TxQuad copy() + { + return new TxQuad(this); + } + + + /** + * Make a sheet starting with this quad, spannign to right and down. + * + * @param width sheet width + * @param height sheet height + * @return sheet + */ + public TxSheet makeSheet(double width, double height) + { + return new TxSheet(this, (int) Math.round(width), (int) Math.round(height)); + } + + + /** + * @return copy flipped X + */ + public TxQuad flipX() + { + final TxQuad copy = new TxQuad(this); + copy.flipX ^= true; + return copy; + } + + + /** + * @return copy flipped Y + */ + public TxQuad flipY() + { + final TxQuad copy = new TxQuad(this); + copy.flipY ^= true; + return copy; + } + + + public boolean isFlippedY() + { + return flipY; + } + + + public boolean isFlippedX() + { + return flipX; + } + + + /** + * Use the same flit/other attributes as the original txQuad + * + * @param original + */ + public void dupeAttrs(TxQuad original) + { + this.flipX = original.flipX; + this.flipY = original.flipY; + } +} diff --git a/src/mightypork/gamecore/graphics/textures/TxSheet.java b/src/mightypork/gamecore/graphics/textures/TxSheet.java new file mode 100644 index 0000000..b29e873 --- /dev/null +++ b/src/mightypork/gamecore/graphics/textures/TxSheet.java @@ -0,0 +1,121 @@ +package mightypork.gamecore.graphics.textures; + + +import java.util.Random; + +import mightypork.utils.logging.Log; + + +/** + * Basic sprite sheet + * + * @author Ondřej Hruška (MightyPork) + */ +public class TxSheet { + + private final TxQuad original; + private final TxQuad[] sprites; + private final int width; + + private final Random rand = new Random(); + private final Random randForSeed = new Random(); + private final int count; + + + public TxSheet(TxQuad tx, int width, int height) { + this.original = tx; + this.width = width; + this.count = width * height; + + this.sprites = new TxQuad[count]; + } + + + /** + * @return number of quads + */ + public int getQuadCount() + { + return count; + } + + + /** + * Get a quad based on ratio 0-1 (0: first, 1: last) + * + * @param ratio ratio + * @return quad + */ + public TxQuad getQuad(double ratio) + { + return getQuad((int) Math.round((count - 1) * ratio)); + } + + + /** + * Get quad of index + * + * @param index index + * @return the quad + */ + public TxQuad getQuad(int index) + { + if (index < 0 || index >= count) { + Log.w("Index out of bounds: " + index + ", allowed: 0.." + count); + index = index % count; + } + + // lazy - init only when needed + if (sprites[index] == null) { + final int x = index % width; + final int y = index / width; + + final double origW = original.uvs.width().value(); + final double origH = original.uvs.height().value(); + + final TxQuad txq = new TxQuad(original.tx, original.uvs.move(x * origW, y * origH)); + txq.dupeAttrs(original); + + sprites[index] = txq; + } + + return sprites[index]; + } + + + /** + * Get entirely random TxQuad from this sheet + * + * @return the picked quad + */ + public TxQuad getRandomQuad() + { + return getQuad(rand.nextInt(count)); + } + + + /** + * Get random TxQuad from this sheet + * + * @param seed random number generator seed + * @return the picked quad + */ + public TxQuad getRandomQuad(long seed) + { + randForSeed.setSeed(seed); + return getQuad(randForSeed.nextInt(count)); + } + + + /** + * Get random TxQuad from this sheet + * + * @param seed random number generator seed (double will be converted to + * long) + * @return the picked quad + */ + public TxQuad getRandomQuad(double seed) + { + return getRandomQuad(Double.doubleToLongBits(seed)); + } +} diff --git a/src/mightypork/gamecore/graphics/textures/WrapMode.java b/src/mightypork/gamecore/graphics/textures/WrapMode.java new file mode 100644 index 0000000..bd6ce41 --- /dev/null +++ b/src/mightypork/gamecore/graphics/textures/WrapMode.java @@ -0,0 +1,12 @@ +package mightypork.gamecore.graphics.textures; + + +/** + * Texture wrap mode + * + * @author Ondřej Hruška (MightyPork) + */ +public enum WrapMode +{ + CLAMP, REPEAT; +} diff --git a/src/mightypork/gamecore/gui/Action.java b/src/mightypork/gamecore/gui/Action.java new file mode 100644 index 0000000..e8ac2d2 --- /dev/null +++ b/src/mightypork/gamecore/gui/Action.java @@ -0,0 +1,54 @@ +package mightypork.gamecore.gui; + + +import mightypork.utils.interfaces.Enableable; + + +/** + * Triggered action + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class Action implements Runnable, Enableable { + + private boolean enabled = true; + + + /** + * Enable the action + * + * @param enable true to enable + */ + @Override + public final void setEnabled(boolean enable) + { + this.enabled = enable; + } + + + /** + * @return true if this action is enabled. + */ + @Override + public final boolean isEnabled() + { + return enabled; + } + + + /** + * Run the action, if it's enabled. + */ + @Override + public final void run() + { + if (enabled) execute(); + } + + + /** + * Do the work. + */ + protected abstract void execute(); + +} diff --git a/src/mightypork/gamecore/gui/ActionGroup.java b/src/mightypork/gamecore/gui/ActionGroup.java new file mode 100644 index 0000000..b7dc37c --- /dev/null +++ b/src/mightypork/gamecore/gui/ActionGroup.java @@ -0,0 +1,38 @@ +package mightypork.gamecore.gui; + + +import java.util.HashSet; +import java.util.Set; + +import mightypork.utils.interfaces.Enableable; + + +public class ActionGroup implements Enableable { + + private boolean enabled = true; + + private final Set groupMembers = new HashSet<>(); + + + @Override + public void setEnabled(boolean yes) + { + enabled = yes; + for (final Enableable e : groupMembers) + e.setEnabled(yes); + } + + + @Override + public boolean isEnabled() + { + return enabled; + } + + + public void add(Enableable action) + { + groupMembers.add(action); + } + +} diff --git a/src/mightypork/gamecore/gui/ActionTrigger.java b/src/mightypork/gamecore/gui/ActionTrigger.java new file mode 100644 index 0000000..e12d4db --- /dev/null +++ b/src/mightypork/gamecore/gui/ActionTrigger.java @@ -0,0 +1,17 @@ +package mightypork.gamecore.gui; + + +/** + * Element that can be assigned an action (ie. button); + * + * @author Ondřej Hruška (MightyPork) + */ +public interface ActionTrigger { + + /** + * Assign an action + * + * @param action action + */ + void setAction(Action action); +} diff --git a/src/mightypork/gamecore/gui/components/BaseComponent.java b/src/mightypork/gamecore/gui/components/BaseComponent.java new file mode 100644 index 0000000..be01481 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/BaseComponent.java @@ -0,0 +1,169 @@ +package mightypork.gamecore.gui.components; + + +import mightypork.gamecore.core.App; +import mightypork.gamecore.graphics.Renderable; +import mightypork.gamecore.gui.events.LayoutChangeEvent; +import mightypork.gamecore.gui.events.LayoutChangeListener; +import mightypork.utils.Support; +import mightypork.utils.annotations.Stub; +import mightypork.utils.interfaces.Enableable; +import mightypork.utils.logging.Log; +import mightypork.utils.math.color.Color; +import mightypork.utils.math.constraints.num.Num; +import mightypork.utils.math.constraints.rect.Rect; +import mightypork.utils.math.constraints.rect.RectBound; +import mightypork.utils.math.constraints.rect.caching.AbstractRectCache; +import mightypork.utils.math.constraints.rect.proxy.RectProxy; + + +/** + * {@link Renderable} with pluggable context. When caching is enabled, the + * layout update can be triggered by firing the {@link LayoutChangeEvent}. + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class BaseComponent extends AbstractRectCache implements Component, LayoutChangeListener, Enableable { + + private Rect source; + private boolean visible = true; + private boolean enabled = true; + private int indirectDisableLevel = 0; + + private Num alphaMul = Num.ONE; + + + public BaseComponent() { + enableCaching(false); + } + + + @Override + public void setRect(RectBound rect) + { + this.source = new RectProxy(rect); + } + + + @Override + public final boolean isVisible() + { + return visible; + } + + + @Override + public final void setVisible(boolean visible) + { + this.visible = visible; + } + + + @Override + public final Rect getCacheSource() + { + return source.round(); // round to avoid visual artifacts in fonts and such + } + + + @Override + public final void render() + { + if (!isVisible()) return; + + Color.pushAlpha(alphaMul); + renderComponent(); + Color.popAlpha(); + } + + + @Override + public final void onLayoutChanged() + { + try { + poll(); + } catch (final NullPointerException e) { + Log.e("Component is missing a bounding rect, at: " + Support.str(getClass())); + } + } + + + @Override + public final void onConstraintChanged() + { + updateLayout(); + } + + + @Override + public final boolean isMouseOver() + { + return App.input().getMousePos().isInside(this); + } + + + /** + * Draw the component (it's visible) + */ + protected abstract void renderComponent(); + + + @Override + @Stub + public void updateLayout() + { + } + + + @Override + public void setEnabled(boolean yes) + { + enabled = yes; + } + + + @Override + public boolean isEnabled() + { + return enabled && isIndirectlyEnabled(); + } + + + @Override + public final void setAlpha(Num alpha) + { + this.alphaMul = alpha; + } + + + @Override + public final void setAlpha(double alpha) + { + this.alphaMul = Num.make(alpha); + } + + + @Override + public void setIndirectlyEnabled(boolean yes) + { + if (!yes) { + indirectDisableLevel++; + } else { + if (indirectDisableLevel > 0) indirectDisableLevel--; + } + } + + + @Override + public boolean isIndirectlyEnabled() + { + return indirectDisableLevel == 0; + } + + + @Override + public boolean isDirectlyEnabled() + { + return enabled; + } +} diff --git a/src/mightypork/gamecore/gui/components/Component.java b/src/mightypork/gamecore/gui/components/Component.java new file mode 100644 index 0000000..2375efc --- /dev/null +++ b/src/mightypork/gamecore/gui/components/Component.java @@ -0,0 +1,95 @@ +package mightypork.gamecore.gui.components; + + +import mightypork.utils.interfaces.Enableable; +import mightypork.utils.interfaces.Hideable; +import mightypork.utils.math.constraints.num.Num; + + +/** + * Basic UI component interface + * + * @author Ondřej Hruška (MightyPork) + */ +public interface Component extends Enableable, Hideable, PluggableRenderable { + + /** + * Render the component, if it is visible. + */ + @Override + void render(); + + + /** + * The bounding rect was changed. The component should now update any cached + * constraints derived from it. + */ + void updateLayout(); + + + /** + * @return true if mouse is currently over the component + */ + boolean isMouseOver(); + + + /** + * Set alpha multiplier for this and nested components + * + * @param alpha alpha multiplier (dynamic value) + */ + void setAlpha(Num alpha); + + + /** + * Set alpha multiplier for this and nested components + * + * @param alpha alpha multiplier (constant value) + */ + void setAlpha(double alpha); + + + /** + * Indirectly enable / disable, used for nested hierarchies.
+ * When component is twice indirectly disabled, it needs to be twice + * indirectly enabled to be enabled again. + * + * @param yes + */ + void setIndirectlyEnabled(boolean yes); + + + /** + * Check if the compionent is not indirectly disabled. May still be directly + * disabled. + * + * @return indirectly enabled + */ + boolean isIndirectlyEnabled(); + + + /** + * Check if the component is directly enabled (set by setEnabled()). May + * still be indirectly disabled. + * + * @return directly enabled + */ + boolean isDirectlyEnabled(); + + + /** + * Set directly enabled (must be both directly and indirectly enabled to be + * enabled completely) + */ + @Override + public void setEnabled(boolean yes); + + + /** + * Check if the component is both directly and indirectly enabled + * + * @return enabled + */ + @Override + public boolean isEnabled(); +} diff --git a/src/mightypork/gamecore/gui/components/DynamicWidthComponent.java b/src/mightypork/gamecore/gui/components/DynamicWidthComponent.java new file mode 100644 index 0000000..06e51b4 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/DynamicWidthComponent.java @@ -0,0 +1,7 @@ +package mightypork.gamecore.gui.components; + + +public interface DynamicWidthComponent extends Component { + + double computeWidth(double height); +} diff --git a/src/mightypork/gamecore/gui/components/InputComponent.java b/src/mightypork/gamecore/gui/components/InputComponent.java new file mode 100644 index 0000000..8f35f95 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/InputComponent.java @@ -0,0 +1,15 @@ +package mightypork.gamecore.gui.components; + + +import mightypork.utils.eventbus.clients.ToggleableClient; +import mightypork.utils.interfaces.Enableable; + + +public abstract class InputComponent extends BaseComponent implements Enableable, ToggleableClient { + + @Override + public boolean isListening() + { + return isEnabled(); + } +} diff --git a/src/mightypork/gamecore/gui/components/LayoutComponent.java b/src/mightypork/gamecore/gui/components/LayoutComponent.java new file mode 100644 index 0000000..22aeef7 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/LayoutComponent.java @@ -0,0 +1,122 @@ +package mightypork.gamecore.gui.components; + + +import java.util.Collection; +import java.util.LinkedList; + +import mightypork.utils.eventbus.clients.ClientHub; +import mightypork.utils.eventbus.clients.DelegatingList; +import mightypork.utils.math.constraints.rect.RectBound; + + +public abstract class LayoutComponent extends BaseComponent implements ClientHub { + + private final DelegatingList clientList; + final LinkedList components = new LinkedList<>(); + + + public LayoutComponent(RectBound context) { + this.clientList = new DelegatingList(); + setRect(context); + enableCaching(true); // layout is typically updated only when screen resizes. + } + + + public LayoutComponent() { + this(null); + } + + + @Override + public Collection getChildClients() + { + return clientList; + } + + + @Override + public boolean doesDelegate() + { + return clientList.doesDelegate(); + } + + + @Override + public boolean isListening() + { + return clientList.isListening(); + } + + + @Override + public void addChildClient(Object client) + { + clientList.add(client); + } + + + @Override + public void removeChildClient(Object client) + { + clientList.remove(client); + } + + + @Override + public void setEnabled(boolean yes) + { + if (isDirectlyEnabled() != yes) { + super.setEnabled(yes); + + for (final Component c : components) { + c.setIndirectlyEnabled(yes); + } + } + } + + + /** + * Connect to bus and add to element list + * + * @param component added component, whose context has already been set. + */ + protected final void attach(Component component) + { + if (component == null) return; + if (component == this) { + throw new IllegalArgumentException("Uruboros. (infinite recursion evaded)"); + } + + components.add(component); + addChildClient(component); + } + + + @Override + public void renderComponent() + { + for (final Component cmp : components) { + cmp.render(); + } + } + + + @Override + public void updateLayout() + { + for (final Component cmp : components) { + cmp.updateLayout(); + } + } + + + @Override + public void setIndirectlyEnabled(boolean yes) + { + super.setIndirectlyEnabled(yes); + + for (final Component cmp : components) { + cmp.setIndirectlyEnabled(yes); + } + } +} diff --git a/src/mightypork/gamecore/gui/components/LinearComponent.java b/src/mightypork/gamecore/gui/components/LinearComponent.java new file mode 100644 index 0000000..665d545 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/LinearComponent.java @@ -0,0 +1,77 @@ +package mightypork.gamecore.gui.components; + + +import mightypork.utils.math.constraints.num.Num; +import mightypork.utils.math.constraints.rect.Rect; +import mightypork.utils.math.constraints.rect.RectBound; +import mightypork.utils.math.constraints.vect.Vect; +import mightypork.utils.math.constraints.vect.proxy.VectAdapter; + + +public abstract class LinearComponent extends BaseComponent implements DynamicWidthComponent { + + private final Rect rect = new Rect() { + + @Override + public Vect size() + { + return new Vect() { + + @Override + public double x() + { + return computeWidth(y()); + } + + + @Override + public double y() + { + return height.value(); + } + + }; + } + + + @Override + public Vect origin() + { + return new VectAdapter() { + + @Override + protected Vect getSource() + { + return origin; + } + }; + } + }; + + private Vect origin; + private Num height; + + + public LinearComponent() { + super.setRect(rect); + } + + + @Override + public void setRect(RectBound rect) + { + throw new RuntimeException("Cannot assign a rect to a linear component. Set origin and height instead."); + } + + + public void setHeight(Num height) + { + this.height = height; + } + + + public void setOrigin(Vect origin) + { + this.origin = origin; + } +} diff --git a/src/mightypork/gamecore/gui/components/PluggableRenderable.java b/src/mightypork/gamecore/gui/components/PluggableRenderable.java new file mode 100644 index 0000000..e53c245 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/PluggableRenderable.java @@ -0,0 +1,28 @@ +package mightypork.gamecore.gui.components; + + +import mightypork.gamecore.graphics.Renderable; +import mightypork.utils.math.constraints.rect.PluggableRectBound; +import mightypork.utils.math.constraints.rect.Rect; +import mightypork.utils.math.constraints.rect.RectBound; + + +/** + * Renderable that can be assigned different context + * + * @author Ondřej Hruška (MightyPork) + */ +public interface PluggableRenderable extends Renderable, PluggableRectBound { + + @Override + void render(); + + + @Override + Rect getRect(); + + + @Override + void setRect(RectBound rect); + +} diff --git a/src/mightypork/gamecore/gui/components/input/ClickableComponent.java b/src/mightypork/gamecore/gui/components/input/ClickableComponent.java new file mode 100644 index 0000000..186c7ab --- /dev/null +++ b/src/mightypork/gamecore/gui/components/input/ClickableComponent.java @@ -0,0 +1,49 @@ +package mightypork.gamecore.gui.components.input; + + +import mightypork.gamecore.gui.Action; +import mightypork.gamecore.gui.ActionTrigger; +import mightypork.gamecore.gui.components.InputComponent; +import mightypork.gamecore.input.events.MouseButtonEvent; +import mightypork.gamecore.input.events.MouseButtonHandler; + + +public abstract class ClickableComponent extends InputComponent implements ActionTrigger, MouseButtonHandler { + + protected boolean btnDownOver; + private Action action; + + + @Override + public void setAction(Action action) + { + this.action = action; + } + + + protected void triggerAction() + { + if (action != null && isEnabled()) action.run(); + } + + + @Override + public void receive(MouseButtonEvent event) + { + if (!event.isButtonEvent()) return; + + if (event.isDown()) { + btnDownOver = event.isOver(this); + } + + if (event.isUp()) { + + if (btnDownOver && event.isOver(this)) { + triggerAction(); + event.consume(); + } + + btnDownOver = false; + } + } +} diff --git a/src/mightypork/gamecore/gui/components/input/ClickableWrapper.java b/src/mightypork/gamecore/gui/components/input/ClickableWrapper.java new file mode 100644 index 0000000..ac6b8aa --- /dev/null +++ b/src/mightypork/gamecore/gui/components/input/ClickableWrapper.java @@ -0,0 +1,63 @@ +package mightypork.gamecore.gui.components.input; + + +import java.util.Collection; + +import mightypork.gamecore.gui.components.Component; +import mightypork.utils.eventbus.clients.ClientList; +import mightypork.utils.eventbus.clients.DelegatingClient; + + +public class ClickableWrapper extends ClickableComponent implements DelegatingClient { + + private final Component wrapped; + private final ClientList list; + + + public ClickableWrapper(Component wrapped) { + this.wrapped = wrapped; + wrapped.setRect(this); + + list = new ClientList(wrapped); + } + + + @Override + public Collection getChildClients() + { + return list; + } + + + @Override + public boolean doesDelegate() + { + return true; + } + + + @Override + protected void renderComponent() + { + wrapped.render(); + } + + + @Override + public void setEnabled(boolean yes) + { + if (yes != super.isDirectlyEnabled()) { + super.setEnabled(yes); + wrapped.setIndirectlyEnabled(yes); + } + } + + + @Override + public void setIndirectlyEnabled(boolean yes) + { + super.setIndirectlyEnabled(yes); + wrapped.setIndirectlyEnabled(yes); + } + +} diff --git a/src/mightypork/gamecore/gui/components/input/TextButton.java b/src/mightypork/gamecore/gui/components/input/TextButton.java new file mode 100644 index 0000000..725c5bf --- /dev/null +++ b/src/mightypork/gamecore/gui/components/input/TextButton.java @@ -0,0 +1,77 @@ +package mightypork.gamecore.gui.components.input; + + +import mightypork.gamecore.core.App; +import mightypork.gamecore.graphics.fonts.IFont; +import mightypork.gamecore.gui.components.DynamicWidthComponent; +import mightypork.gamecore.gui.components.painters.TextPainter; +import mightypork.utils.math.AlignX; +import mightypork.utils.math.color.Color; +import mightypork.utils.math.color.pal.RGB; +import mightypork.utils.math.constraints.vect.Vect; +import mightypork.utils.math.constraints.vect.var.VectVar; + + +/** + * Menu-like button with shadow and push state + * + * @author Ondřej Hruška (MightyPork) + */ +public class TextButton extends ClickableComponent implements DynamicWidthComponent { + + public final TextPainter textPainter; + + private final VectVar offset = Vect.makeVar(); + + public Vect offsetPassive = height().div(16).toVectXY(); + public Vect offsetOver = height().div(20).toVectXY(); + public Vect offsetUnder = height().div(32).toVectXY(); + + private final Color color; + + private boolean hoverMove = true; + + + public TextButton(IFont font, String text, Color color) { + this.color = color; + + this.textPainter = new TextPainter(font, AlignX.CENTER, this.color, text); + this.textPainter.setRect(this); + this.textPainter.setShadow(RGB.BLACK_30, offset); + textPainter.setVPaddingPercent(5); + } + + + @Override + protected void renderComponent() + { + if (isMouseOver()) { + if (App.input().isMouseButtonDown(0)) { + offset.setTo(offsetUnder); + } else { + offset.setTo(hoverMove ? offsetOver : offsetPassive); + } + } else { + offset.setTo(offsetPassive); + } + + textPainter.render(); + } + + + /** + * Disable offset change on hover + */ + public void disableHoverEffect() + { + hoverMove = false; + } + + + @Override + public double computeWidth(double height) + { + return textPainter.computeWidth(height); + } + +} diff --git a/src/mightypork/gamecore/gui/components/layout/ColumnLayout.java b/src/mightypork/gamecore/gui/components/layout/ColumnLayout.java new file mode 100644 index 0000000..57e8849 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/ColumnLayout.java @@ -0,0 +1,42 @@ +package mightypork.gamecore.gui.components.layout; + + +import mightypork.gamecore.gui.components.Component; +import mightypork.utils.math.constraints.rect.RectBound; + + +public class ColumnLayout extends GridLayout { + + private int col = 0; + + + public ColumnLayout(int rows) { + this(null, rows); + } + + + public ColumnLayout(RectBound context, int cols) { + super(context, 1, cols); + } + + + public void add(final Component elem) + { + add(elem, 1); + } + + + public void add(final Component elem, int colSpan) + { + if (elem == null) return; + + put(elem, 0, col, 1, colSpan); + col += colSpan; + } + + + public void skip(int cols) + { + col += cols; + } +} diff --git a/src/mightypork/gamecore/gui/components/layout/ConstraintLayout.java b/src/mightypork/gamecore/gui/components/layout/ConstraintLayout.java new file mode 100644 index 0000000..b12c15b --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/ConstraintLayout.java @@ -0,0 +1,36 @@ +package mightypork.gamecore.gui.components.layout; + + +import mightypork.gamecore.gui.components.Component; +import mightypork.gamecore.gui.components.LayoutComponent; +import mightypork.utils.math.constraints.rect.RectBound; + + +/** + * Layout for components with arbitrary constraints. + * + * @author Ondřej Hruška (MightyPork) + */ +public class ConstraintLayout extends LayoutComponent { + + public ConstraintLayout() { + } + + + public ConstraintLayout(RectBound context) { + super(context); + } + + + /** + * Add a component to the layout.
+ * The component's rect must be set up manually. + * + * @param component + */ + public void add(Component component) + { + attach(component); + } + +} diff --git a/src/mightypork/gamecore/gui/components/layout/FlowColumnLayout.java b/src/mightypork/gamecore/gui/components/layout/FlowColumnLayout.java new file mode 100644 index 0000000..2162170 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/FlowColumnLayout.java @@ -0,0 +1,85 @@ +package mightypork.gamecore.gui.components.layout; + + +import mightypork.gamecore.gui.components.Component; +import mightypork.gamecore.gui.components.LayoutComponent; +import mightypork.utils.math.AlignX; +import mightypork.utils.math.constraints.num.Num; +import mightypork.utils.math.constraints.rect.Rect; +import mightypork.utils.math.constraints.rect.RectBound; + + +/** + * Holder with same-sized columns, aligned to left or right + * + * @author Ondřej Hruška (MightyPork) + */ +public class FlowColumnLayout extends LayoutComponent { + + private int col = 0; + private Num elementWidth; + private final AlignX align; + + + /** + * @param context context + * @param elementWidth width of all elements + * @param align component align. Legal values are LEFT and RIGHT. + */ + public FlowColumnLayout(RectBound context, Num elementWidth, AlignX align) { + super(context); + this.elementWidth = elementWidth; + this.align = align; + + if (align != AlignX.LEFT && align != AlignX.RIGHT) { + throw new IllegalArgumentException("Can align only left or right."); + } + } + + + /** + * make a new holder.
+ * Context must be assigned before rendering. + * + * @param elementWidth width of all elements + * @param align component align. Legal values are LEFT and RIGHT. + */ + public FlowColumnLayout(Num elementWidth, AlignX align) { + this(null, elementWidth, align); + } + + + /** + * Add an item + * + * @param elem + */ + public void add(final Component elem) + { + if (elem == null) return; + + final Rect r; + + switch (align) { + case LEFT: + r = leftEdge().growRight(elementWidth).moveX(elementWidth.mul(col++)); + break; + case RIGHT: + r = rightEdge().growLeft(elementWidth).moveX(elementWidth.mul(-(col++))); + break; + default: + throw new IllegalArgumentException("Bad align."); + } + + elem.setRect(r); + + attach(elem); + } + + + public void setElementWidth(Num elementWidth) + { + this.elementWidth = elementWidth; + } + +} diff --git a/src/mightypork/gamecore/gui/components/layout/FlowRowLayout.java b/src/mightypork/gamecore/gui/components/layout/FlowRowLayout.java new file mode 100644 index 0000000..580ca69 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/FlowRowLayout.java @@ -0,0 +1,84 @@ +package mightypork.gamecore.gui.components.layout; + + +import mightypork.gamecore.gui.components.Component; +import mightypork.gamecore.gui.components.LayoutComponent; +import mightypork.utils.math.AlignY; +import mightypork.utils.math.constraints.num.Num; +import mightypork.utils.math.constraints.rect.Rect; +import mightypork.utils.math.constraints.rect.RectBound; + + +/** + * Holder with same-sized rows, aligned to top or bottom + * + * @author Ondřej Hruška (MightyPork) + */ +public class FlowRowLayout extends LayoutComponent { + + private int row = 0; + private Num elementHeight; + private final AlignY align; + + + /** + * @param context context + * @param elementHeight height of all elements + * @param align component align. Legal values are TOP and BOTTOM. + */ + public FlowRowLayout(RectBound context, Num elementHeight, AlignY align) { + super(context); + this.elementHeight = elementHeight; + this.align = align; + + if (align != AlignY.TOP && align != AlignY.BOTTOM) { + throw new IllegalArgumentException("Can align only to top or bottom."); + } + } + + + /** + * make a new holder.
+ * Context must be assigned before rendering. + * + * @param elementHeight height of all elements + * @param align component align. Legal values are TOP and BOTTOM. + */ + public FlowRowLayout(Num elementHeight, AlignY align) { + this(null, elementHeight, align); + } + + + /** + * Add an item + * + * @param elem + */ + public void add(final Component elem) + { + if (elem == null) return; + + final Rect r; + + switch (align) { + case TOP: + r = topEdge().growDown(elementHeight).moveY(elementHeight.mul(row++)); + break; + case BOTTOM: + r = bottomEdge().growUp(elementHeight).moveY(elementHeight.mul(-(row++))); + break; + default: + throw new IllegalArgumentException("Bad align."); + } + + elem.setRect(r); + + attach(elem); + } + + + public void setElementHeight(Num elementHeight) + { + this.elementHeight = elementHeight; + } +} diff --git a/src/mightypork/gamecore/gui/components/layout/GridLayout.java b/src/mightypork/gamecore/gui/components/layout/GridLayout.java new file mode 100644 index 0000000..74cdc12 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/GridLayout.java @@ -0,0 +1,78 @@ +package mightypork.gamecore.gui.components.layout; + + +import mightypork.gamecore.gui.components.Component; +import mightypork.gamecore.gui.components.LayoutComponent; +import mightypork.utils.math.constraints.rect.RectBound; +import mightypork.utils.math.constraints.rect.builders.TiledRect; + + +/** + * Holder with table cells + * + * @author Ondřej Hruška (MightyPork) + */ +public class GridLayout extends LayoutComponent { + + private final TiledRect tiler; + + + /** + * @param context context + * @param rows number of rows + * @param cols number of columns + */ + public GridLayout(RectBound context, int rows, int cols) { + super(context); + this.tiler = tiles(cols, rows); + } + + + /** + * make a new holder.
+ * Context must be assigned before rendering. + * + * @param rows number of rows + * @param cols number of columns + */ + public GridLayout(int rows, int cols) { + this(null, rows, cols); + } + + + /** + * Add a row to the holder. + * + * @param row row (one-based) + * @param column column (one-based) + * @param elem added component + */ + public void put(Component elem, int row, int column) + { + if (elem == null) return; + + elem.setRect(tiler.tile(column, row)); + + attach(elem); + } + + + /** + * Put with span + * + * @param elem + * @param row + * @param column + * @param rowspan + * @param colspan + */ + public void put(Component elem, int row, int column, int rowspan, int colspan) + { + if (elem == null) return; + + elem.setRect(tiler.span(column, row, colspan, rowspan)); + + attach(elem); + } + +} diff --git a/src/mightypork/gamecore/gui/components/layout/NullComponent.java b/src/mightypork/gamecore/gui/components/layout/NullComponent.java new file mode 100644 index 0000000..f04964b --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/NullComponent.java @@ -0,0 +1,18 @@ +package mightypork.gamecore.gui.components.layout; + + +import mightypork.gamecore.gui.components.BaseComponent; + + +/** + * Invisible component that does nothing at all; Null object pattern + * + * @author Ondřej Hruška (MightyPork) + */ +public class NullComponent extends BaseComponent { + + @Override + protected void renderComponent() + { + } +} diff --git a/src/mightypork/gamecore/gui/components/layout/RowLayout.java b/src/mightypork/gamecore/gui/components/layout/RowLayout.java new file mode 100644 index 0000000..400c06f --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/RowLayout.java @@ -0,0 +1,42 @@ +package mightypork.gamecore.gui.components.layout; + + +import mightypork.gamecore.gui.components.Component; +import mightypork.utils.math.constraints.rect.RectBound; + + +public class RowLayout extends GridLayout { + + private int row = 0; + + + public RowLayout(int rows) { + this(null, rows); + } + + + public RowLayout(RectBound context, int rows) { + super(context, rows, 1); + } + + + public void add(final Component elem) + { + add(elem, 1); + } + + + public void add(final Component elem, int rowSpan) + { + if (elem == null) return; + + put(elem, row, 0, rowSpan, 1); + row += rowSpan; + } + + + public void skip(int rows) + { + row += rows; + } +} diff --git a/src/mightypork/gamecore/gui/components/layout/linear/AbstractLinearWrapper.java b/src/mightypork/gamecore/gui/components/layout/linear/AbstractLinearWrapper.java new file mode 100644 index 0000000..f7ef0ed --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/linear/AbstractLinearWrapper.java @@ -0,0 +1,78 @@ +package mightypork.gamecore.gui.components.layout.linear; + + +import java.util.Collection; + +import mightypork.gamecore.gui.components.Component; +import mightypork.gamecore.gui.components.LinearComponent; +import mightypork.utils.eventbus.clients.ClientList; +import mightypork.utils.eventbus.clients.DelegatingClient; + + +/** + * Converts a component into a linear component + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class AbstractLinearWrapper extends LinearComponent implements DelegatingClient { + + protected final Component wrapped; + private final ClientList list; + + + /** + * @param wrapped wrapped component. Can be null. + */ + public AbstractLinearWrapper(Component wrapped) { + this.wrapped = wrapped; + if (wrapped != null) { + if (wrapped instanceof LinearComponent) { + ((LinearComponent) wrapped).setHeight(height()); + ((LinearComponent) wrapped).setOrigin(origin()); + } else { + wrapped.setRect(this); + } + } + + list = new ClientList(wrapped); + } + + + @Override + protected void renderComponent() + { + if (wrapped != null) wrapped.render(); + } + + + @Override + public Collection getChildClients() + { + return list; + } + + + @Override + public boolean doesDelegate() + { + return true; + } + + + @Override + public void setEnabled(boolean yes) + { + if (yes != super.isDirectlyEnabled()) { + super.setEnabled(yes); + wrapped.setIndirectlyEnabled(yes); + } + } + + + @Override + public void setIndirectlyEnabled(boolean yes) + { + super.setIndirectlyEnabled(yes); + wrapped.setIndirectlyEnabled(yes); + } +} diff --git a/src/mightypork/gamecore/gui/components/layout/linear/LinearGap.java b/src/mightypork/gamecore/gui/components/layout/linear/LinearGap.java new file mode 100644 index 0000000..a5bf526 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/linear/LinearGap.java @@ -0,0 +1,24 @@ +package mightypork.gamecore.gui.components.layout.linear; + + +import mightypork.gamecore.gui.components.layout.NullComponent; +import mightypork.utils.math.constraints.num.Num; + + +/** + * Gap in linear layout + * + * @author Ondřej Hruška (MightyPork) + */ +public class LinearGap extends LinearRectangle { + + public LinearGap(Num width) { + super(new NullComponent(), width); + } + + + public LinearGap(double heightPercent) { + this(Num.ZERO); + setWidth(height().perc(heightPercent)); + } +} diff --git a/src/mightypork/gamecore/gui/components/layout/linear/LinearLayout.java b/src/mightypork/gamecore/gui/components/layout/linear/LinearLayout.java new file mode 100644 index 0000000..d97adb6 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/linear/LinearLayout.java @@ -0,0 +1,93 @@ +package mightypork.gamecore.gui.components.layout.linear; + + +import mightypork.gamecore.gui.components.DynamicWidthComponent; +import mightypork.gamecore.gui.components.LayoutComponent; +import mightypork.gamecore.gui.components.LinearComponent; +import mightypork.utils.math.AlignX; +import mightypork.utils.math.constraints.num.Num; +import mightypork.utils.math.constraints.num.batch.NumSum; +import mightypork.utils.math.constraints.rect.RectBound; +import mightypork.utils.math.constraints.vect.Vect; +import mightypork.utils.math.constraints.vect.proxy.VectAdapter; + + +/** + * Layout that aligns elements while taking into account their actual + * dimensions.
+ * Useful eg. for buttons that stretch based on text length. + * + * @author Ondřej Hruška (MightyPork) + */ +public class LinearLayout extends LayoutComponent { + + public LinearLayout(AlignX align) { + this.align = align; + } + + + public LinearLayout(RectBound context, AlignX align) { + super(context); + this.align = align; + } + + private final NumSum totalWidth = new NumSum(); + + private final Vect leftAlignOrigin = LinearLayout.this.origin(); + private final Vect centerAlignOrigin = LinearLayout.this.topCenter().sub(totalWidth.half(), Num.ZERO); + private final Vect rightAlignOrigin = LinearLayout.this.topRight().sub(totalWidth, Num.ZERO); + + private final Vect leftMostOrigin = new VectAdapter() { + + @Override + protected Vect getSource() + { + switch (align) { + default: + case LEFT: + return leftAlignOrigin; + case CENTER: + return centerAlignOrigin; + case RIGHT: + return rightAlignOrigin; + } + } + }; + + private Vect nextOrigin = leftMostOrigin; + + private AlignX align = AlignX.LEFT; + + + public void add(DynamicWidthComponent dwcomp) + { + add(new LinearWrapper(dwcomp)); + } + + + public void add(LinearComponent lincomp) + { + lincomp.setHeight(height()); + lincomp.setOrigin(nextOrigin); + nextOrigin = nextOrigin.add(lincomp.width(), Num.ZERO); + totalWidth.addSummand(lincomp.width()); + attach(lincomp); + } + + + public void setAlign(AlignX align) + { + this.align = align; + } + + + /** + * Add a gap. + * + * @param heightPercent percent of height for gap width + */ + public void gap(double heightPercent) + { + add(new LinearGap(heightPercent)); + } +} diff --git a/src/mightypork/gamecore/gui/components/layout/linear/LinearRectangle.java b/src/mightypork/gamecore/gui/components/layout/linear/LinearRectangle.java new file mode 100644 index 0000000..9fa3fe8 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/linear/LinearRectangle.java @@ -0,0 +1,31 @@ +package mightypork.gamecore.gui.components.layout.linear; + + +import mightypork.gamecore.gui.components.Component; +import mightypork.utils.math.constraints.num.Num; + + +public class LinearRectangle extends AbstractLinearWrapper { + + private Num width; + + + public LinearRectangle(Component wrapped, Num width) { + super(wrapped); + this.width = width; + } + + + public void setWidth(Num width) + { + this.width = width; + } + + + @Override + public double computeWidth(double height) + { + return this.width.value(); + } + +} diff --git a/src/mightypork/gamecore/gui/components/layout/linear/LinearSquare.java b/src/mightypork/gamecore/gui/components/layout/linear/LinearSquare.java new file mode 100644 index 0000000..bb3a813 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/linear/LinearSquare.java @@ -0,0 +1,20 @@ +package mightypork.gamecore.gui.components.layout.linear; + + +import mightypork.gamecore.gui.components.Component; + + +public class LinearSquare extends AbstractLinearWrapper { + + public LinearSquare(Component wrapped) { + super(wrapped); + } + + + @Override + public double computeWidth(double height) + { + return height; + } + +} diff --git a/src/mightypork/gamecore/gui/components/layout/linear/LinearWrapper.java b/src/mightypork/gamecore/gui/components/layout/linear/LinearWrapper.java new file mode 100644 index 0000000..0acd7d8 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/layout/linear/LinearWrapper.java @@ -0,0 +1,20 @@ +package mightypork.gamecore.gui.components.layout.linear; + + +import mightypork.gamecore.gui.components.DynamicWidthComponent; + + +public class LinearWrapper extends AbstractLinearWrapper { + + public LinearWrapper(DynamicWidthComponent wrapped) { + super(wrapped); + } + + + @Override + public double computeWidth(double height) + { + return ((DynamicWidthComponent) wrapped).computeWidth(height); + } + +} diff --git a/src/mightypork/gamecore/gui/components/painters/ImagePainter.java b/src/mightypork/gamecore/gui/components/painters/ImagePainter.java new file mode 100644 index 0000000..f51b639 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/painters/ImagePainter.java @@ -0,0 +1,46 @@ +package mightypork.gamecore.gui.components.painters; + + +import mightypork.gamecore.core.App; +import mightypork.gamecore.graphics.textures.TxQuad; +import mightypork.gamecore.gui.components.BaseComponent; +import mightypork.gamecore.gui.components.DynamicWidthComponent; + + +/** + * Draws image in given rect + * + * @author Ondřej Hruška (MightyPork) + */ +public class ImagePainter extends BaseComponent implements DynamicWidthComponent { + + private TxQuad txQuad; + + + /** + * @param txQuad drawn image + */ + public ImagePainter(TxQuad txQuad) { + this.txQuad = txQuad; + } + + + @Override + public void renderComponent() + { + App.gfx().quad(this, txQuad); + } + + + @Override + public double computeWidth(double height) + { + return (height / txQuad.uvs.height().value()) * txQuad.uvs.width().value(); + } + + + public void setTxQuad(TxQuad txQuad) + { + this.txQuad = txQuad; + } +} diff --git a/src/mightypork/gamecore/gui/components/painters/QuadPainter.java b/src/mightypork/gamecore/gui/components/painters/QuadPainter.java new file mode 100644 index 0000000..57b9d13 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/painters/QuadPainter.java @@ -0,0 +1,62 @@ +package mightypork.gamecore.gui.components.painters; + + +import mightypork.gamecore.core.App; +import mightypork.gamecore.gui.components.BaseComponent; +import mightypork.utils.annotations.FactoryMethod; +import mightypork.utils.math.color.Color; +import mightypork.utils.math.color.Grad; + + +/** + * Draws image in given rect + * + * @author Ondřej Hruška (MightyPork) + */ +public class QuadPainter extends BaseComponent { + + @FactoryMethod + public static QuadPainter gradH(Color colorLeft, Color colorRight) + { + return new QuadPainter(colorLeft, colorRight, colorRight, colorLeft); + } + + + @FactoryMethod + public static QuadPainter gradV(Color colorTop, Color colorBottom) + { + return new QuadPainter(colorTop, colorTop, colorBottom, colorBottom); + } + + private final Grad grad; + + + /** + * Painter with solid color + * + * @param color + */ + public QuadPainter(Color color) { + this.grad = new Grad(color, color, color, color); + } + + + /** + * Painter with coloured vertices. + * + * @param leftTop + * @param rightTop + * @param leftBottom + * @param rightBottom + */ + public QuadPainter(Color leftTop, Color rightTop, Color leftBottom, Color rightBottom) { + this.grad = new Grad(leftTop, rightTop, rightBottom, leftBottom); + } + + + @Override + public void renderComponent() + { + App.gfx().quad(getRect(), grad); + } +} diff --git a/src/mightypork/gamecore/gui/components/painters/TextPainter.java b/src/mightypork/gamecore/gui/components/painters/TextPainter.java new file mode 100644 index 0000000..59b5c65 --- /dev/null +++ b/src/mightypork/gamecore/gui/components/painters/TextPainter.java @@ -0,0 +1,163 @@ +package mightypork.gamecore.gui.components.painters; + + +import mightypork.gamecore.core.App; +import mightypork.gamecore.graphics.fonts.FontRenderer; +import mightypork.gamecore.graphics.fonts.IFont; +import mightypork.gamecore.gui.components.BaseComponent; +import mightypork.gamecore.gui.components.DynamicWidthComponent; +import mightypork.utils.math.AlignX; +import mightypork.utils.math.color.Color; +import mightypork.utils.math.color.pal.RGB; +import mightypork.utils.math.constraints.num.Num; +import mightypork.utils.math.constraints.rect.Rect; +import mightypork.utils.math.constraints.vect.Vect; +import mightypork.utils.string.StringProvider; +import mightypork.utils.string.StringWrapper; + + +/** + * Text painting component. + * + * @author Ondřej Hruška (MightyPork) + */ +public class TextPainter extends BaseComponent implements DynamicWidthComponent { + + private static final boolean DEBUG_FONT_RENDER = false; + private final FontRenderer font; + private Color color; + private AlignX align; + private StringProvider text; + private boolean shadow; + + private double yPaddingPerc = 0; + + private Color shadowColor = RGB.BLACK; + private Vect shadowOffset = Vect.make(2, 2); + + + /** + * @param font font to use + */ + public TextPainter(IFont font) { + this(font, AlignX.LEFT, RGB.WHITE); + } + + + public TextPainter(IFont font, Color color, String text) { + this(font, AlignX.LEFT, color, new StringWrapper(text)); + } + + + public TextPainter(IFont font, Color color, StringProvider text) { + this(font, AlignX.LEFT, color, text); + } + + + public TextPainter(IFont font, Color color) { + this(font, AlignX.LEFT, color, (StringProvider) null); + } + + + public TextPainter(IFont font, AlignX align, Color color, String text) { + this(font, align, color, new StringWrapper(text)); + } + + + public TextPainter(IFont font, AlignX align, Color color, StringProvider text) { + this.font = new FontRenderer(font); + this.color = color; + this.align = align; + this.text = text; + } + + + public TextPainter(IFont font, AlignX align, Color color) { + this(font, align, color, (StringProvider) null); + } + + + @Override + public void renderComponent() + { + if (text == null) return; + + final String str = text.getString(); + + final Num shrY = height().perc(yPaddingPerc); + + final Rect rect = getRect().shrink(Num.ZERO, shrY); + + if (shadow) { + font.draw(str, rect.round(), align, shadowColor); + } + + final Rect r = (shadow ? rect.move(shadowOffset.neg()) : rect).round(); + font.draw(str, r, align, color); + + if (DEBUG_FONT_RENDER) App.gfx().quad(r, RGB.PINK.withAlpha(0.4)); + } + + + public void setShadow(Color color, Vect offset) + { + setShadow(true); + setShadowColor(color); + setShadowOffset(offset); + } + + + public void setShadow(boolean shadow) + { + this.shadow = shadow; + } + + + public void setShadowColor(Color shadowColor) + { + this.shadowColor = shadowColor; + } + + + public void setShadowOffset(Vect shadowOffset) + { + this.shadowOffset = shadowOffset; + } + + + public void setColor(Color color) + { + this.color = color; + } + + + public void setAlign(AlignX align) + { + this.align = align; + } + + + public void setText(String text) + { + this.text = new StringWrapper(text); + } + + + public void setText(StringProvider text) + { + this.text = text; + } + + + public void setVPaddingPercent(double percY) + { + yPaddingPerc = percY; + } + + + @Override + public double computeWidth(double height) + { + return font.getWidth(this.text.getString(), height * ((100 - yPaddingPerc * 2) / 100D)); + } +} diff --git a/src/mightypork/gamecore/gui/events/LayoutChangeEvent.java b/src/mightypork/gamecore/gui/events/LayoutChangeEvent.java new file mode 100644 index 0000000..0a9e378 --- /dev/null +++ b/src/mightypork/gamecore/gui/events/LayoutChangeEvent.java @@ -0,0 +1,30 @@ +package mightypork.gamecore.gui.events; + + +import mightypork.utils.eventbus.BusEvent; +import mightypork.utils.eventbus.events.flags.DirectEvent; +import mightypork.utils.eventbus.events.flags.NonConsumableEvent; +import mightypork.utils.eventbus.events.flags.NonRejectableEvent; + + +/** + * Intended use is to notify UI component sub-clients that they should poll + * their cached constraints. + * + * @author Ondřej Hruška (MightyPork) + */ +@DirectEvent +@NonConsumableEvent +@NonRejectableEvent +public class LayoutChangeEvent extends BusEvent { + + public LayoutChangeEvent() { + } + + + @Override + public void handleBy(LayoutChangeListener handler) + { + handler.onLayoutChanged(); + } +} diff --git a/src/mightypork/gamecore/gui/events/LayoutChangeListener.java b/src/mightypork/gamecore/gui/events/LayoutChangeListener.java new file mode 100644 index 0000000..4daedee --- /dev/null +++ b/src/mightypork/gamecore/gui/events/LayoutChangeListener.java @@ -0,0 +1,7 @@ +package mightypork.gamecore.gui.events; + + +public interface LayoutChangeListener { + + public void onLayoutChanged(); +} diff --git a/src/mightypork/gamecore/gui/events/ScreenRequest.java b/src/mightypork/gamecore/gui/events/ScreenRequest.java new file mode 100644 index 0000000..c48fc8e --- /dev/null +++ b/src/mightypork/gamecore/gui/events/ScreenRequest.java @@ -0,0 +1,33 @@ +package mightypork.gamecore.gui.events; + + +import mightypork.utils.eventbus.BusEvent; +import mightypork.utils.eventbus.events.flags.SingleReceiverEvent; + + +/** + * Request to change screen + * + * @author Ondřej Hruška (MightyPork) + */ +@SingleReceiverEvent +public class ScreenRequest extends BusEvent { + + private final String scrName; + + + /** + * @param screenKey screen name + */ + public ScreenRequest(String screenKey) { + scrName = screenKey; + } + + + @Override + public void handleBy(ScreenRequestListener handler) + { + handler.showScreen(scrName); + } + +} diff --git a/src/mightypork/gamecore/gui/events/ScreenRequestListener.java b/src/mightypork/gamecore/gui/events/ScreenRequestListener.java new file mode 100644 index 0000000..e4a1d06 --- /dev/null +++ b/src/mightypork/gamecore/gui/events/ScreenRequestListener.java @@ -0,0 +1,15 @@ +package mightypork.gamecore.gui.events; + + +/** + * {@link ScreenRequest} listener + * + * @author Ondřej Hruška (MightyPork) + */ +public interface ScreenRequestListener { + + /** + * @param key screen to show + */ + void showScreen(String key); +} diff --git a/src/mightypork/gamecore/gui/events/ViewportChangeEvent.java b/src/mightypork/gamecore/gui/events/ViewportChangeEvent.java new file mode 100644 index 0000000..b013926 --- /dev/null +++ b/src/mightypork/gamecore/gui/events/ViewportChangeEvent.java @@ -0,0 +1,44 @@ +package mightypork.gamecore.gui.events; + + +import mightypork.utils.eventbus.BusEvent; +import mightypork.utils.eventbus.events.flags.NonConsumableEvent; +import mightypork.utils.eventbus.events.flags.NotLoggedEvent; +import mightypork.utils.math.constraints.vect.Vect; + + +/** + * Screen resolution or mode was changed + * + * @author Ondřej Hruška (MightyPork) + */ +@NonConsumableEvent +@NotLoggedEvent +public class ViewportChangeEvent extends BusEvent { + + private final Vect screenSize; + + + /** + * @param size new screen size + */ + public ViewportChangeEvent(Vect size) { + this.screenSize = size; + } + + + /** + * @return new screen size + */ + public Vect getScreenSize() + { + return screenSize; + } + + + @Override + public void handleBy(ViewportChangeListener handler) + { + handler.onViewportChanged(this); + } +} diff --git a/src/mightypork/gamecore/gui/events/ViewportChangeListener.java b/src/mightypork/gamecore/gui/events/ViewportChangeListener.java new file mode 100644 index 0000000..078c177 --- /dev/null +++ b/src/mightypork/gamecore/gui/events/ViewportChangeListener.java @@ -0,0 +1,17 @@ +package mightypork.gamecore.gui.events; + + +/** + * {@link ViewportChangeEvent} listener + * + * @author Ondřej Hruška (MightyPork) + */ +public interface ViewportChangeListener { + + /** + * Handle event + * + * @param event + */ + void onViewportChanged(ViewportChangeEvent event); +} diff --git a/src/mightypork/gamecore/gui/screens/LayeredScreen.java b/src/mightypork/gamecore/gui/screens/LayeredScreen.java new file mode 100644 index 0000000..d9233a5 --- /dev/null +++ b/src/mightypork/gamecore/gui/screens/LayeredScreen.java @@ -0,0 +1,112 @@ +package mightypork.gamecore.gui.screens; + + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import mightypork.utils.eventbus.clients.DelegatingClient; + + +/** + * Screen with multiple instances of {@link ScreenLayer} + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class LayeredScreen extends Screen { + + /** + * Wrapper for delegating client, to use custom client ordering. + * + * @author Ondřej Hruška (MightyPork) + */ + private class LayersClient implements DelegatingClient { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Collection getChildClients() + { + return layersByEventPriority; + } + + + @Override + public boolean doesDelegate() + { + return true; + } + + } + + private final List layersByZIndex = new ArrayList<>(); + private final List layersByEventPriority = new ArrayList<>(); + + private final LayersClient layersClient = new LayersClient(); + + + public LayeredScreen() { + addChildClient(layersClient); + } + + + @Override + protected void renderScreen() + { + for (final ScreenLayer layer : layersByZIndex) { + if (layer.isVisible()) layer.render(); + } + } + + + /** + * Add a layer to the screen. + * + * @param layer + */ + protected void addLayer(ScreenLayer layer) + { + this.layersByZIndex.add(layer); + this.layersByEventPriority.add(layer); + + Collections.sort(layersByEventPriority, new Comparator() { + + @Override + public int compare(Overlay o1, Overlay o2) + { + return o2.getEventPriority() - o1.getEventPriority(); + } + + }); + + Collections.sort(layersByZIndex, new Comparator() { + + @Override + public int compare(Overlay o1, Overlay o2) + { + return o1.getZIndex() - o2.getZIndex(); + } + + }); + } + + + @Override + protected void onScreenEnter() + { + for (final ScreenLayer layer : layersByEventPriority) { + layer.onScreenEnter(); + } + } + + + @Override + protected void onScreenLeave() + { + for (final ScreenLayer layer : layersByEventPriority) { + layer.onScreenLeave(); + } + } + +} diff --git a/src/mightypork/gamecore/gui/screens/Overlay.java b/src/mightypork/gamecore/gui/screens/Overlay.java new file mode 100644 index 0000000..c997972 --- /dev/null +++ b/src/mightypork/gamecore/gui/screens/Overlay.java @@ -0,0 +1,222 @@ +package mightypork.gamecore.gui.screens; + + +import java.util.ArrayList; +import java.util.Collection; + +import mightypork.gamecore.core.App; +import mightypork.gamecore.graphics.Renderable; +import mightypork.gamecore.gui.components.layout.ConstraintLayout; +import mightypork.gamecore.gui.events.LayoutChangeListener; +import mightypork.gamecore.input.KeyBinder; +import mightypork.gamecore.input.KeyBindingPool; +import mightypork.gamecore.input.KeyStroke; +import mightypork.gamecore.input.Trigger; +import mightypork.utils.annotations.Stub; +import mightypork.utils.eventbus.clients.BusNode; +import mightypork.utils.interfaces.Enableable; +import mightypork.utils.interfaces.Hideable; +import mightypork.utils.interfaces.Updateable; +import mightypork.utils.math.color.Color; +import mightypork.utils.math.constraints.num.Num; +import mightypork.utils.math.constraints.vect.Vect; + + +/** + * Abstract overlay.
+ * Overlay is connected to event bus and is renderable. + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class Overlay extends BusNode implements Comparable, Updateable, Renderable, KeyBinder, Hideable, Enableable, LayoutChangeListener { + + private boolean visible = true; + private boolean enabled = true; + + private final KeyBindingPool keybindings = new KeyBindingPool(); + + /** Root layout, rendered and attached to the event bus. */ + protected final ConstraintLayout root; + + /** Constraint: Mouse position. */ + protected final Vect mouse; + + /** Extra rendered items (outside root) */ + protected final Collection rendered = new ArrayList<>(); + + /** Extra updated items (outside root - those can just implement Updateable) */ + protected final Collection updated = new ArrayList<>(); + private Num alphaMul = Num.ONE; + + + public Overlay() { + + this.mouse = App.input().getMousePos(); + + this.root = new ConstraintLayout(App.gfx().getRect()); + addChildClient(root); + addChildClient(keybindings); + + rendered.add(root); + } + + + @Override + public final void bindKey(KeyStroke stroke, Trigger edge, Runnable task) + { + keybindings.bindKey(stroke, edge, task); + } + + + @Override + public final void unbindKey(KeyStroke stroke) + { + keybindings.unbindKey(stroke); + } + + + @Override + public final boolean isVisible() + { + return visible; + } + + + @Override + public void setVisible(boolean visible) + { + if (visible != this.visible) { + this.visible = visible; + root.setVisible(visible); + } + } + + + @Override + public void setEnabled(boolean yes) + { + if (enabled != yes) { + this.enabled = yes; + root.setEnabled(yes); + } + } + + + @Override + public boolean isEnabled() + { + return enabled; + } + + + /** + * Get rendering layer + * + * @return higher = on top. + */ + @Stub + public abstract int getZIndex(); + + + /** + * Get event bus listening priority - useful to block incoming events. + * + * @return higher = first. + */ + public int getEventPriority() + { + return getZIndex(); + } + + + /** + * Render the overlay. The caller MUST check for visibility himself. + */ + @Override + public void render() + { + if (!isVisible()) return; + + Color.pushAlpha(alphaMul); + for (final Renderable r : rendered) { + r.render(); + } + + Color.popAlpha(); + } + + + @Override + public void update(double delta) + { + if (!isEnabled()) return; + + for (final Updateable u : updated) { + u.update(delta); + } + } + + + @Override + public int compareTo(Overlay o) + { + return o.getEventPriority() - getEventPriority(); + } + + + /** + *

+ * Screen size changed. + *

+ *

+ * Layouts / components should listen for this event and update their cached + * constraints; components added to root or directly to this overlay as + * child clients will receive the event. + *

+ */ + @Override + @Stub + public void onLayoutChanged() + { + } + + + public void setAlpha(Num alpha) + { + this.alphaMul = alpha; + } + + + public void setAlpha(double alpha) + { + this.alphaMul = Num.make(alpha); + } + + + public void show() + { + setVisible(true); + setEnabled(true); + } + + + public void hide() + { + setVisible(false); + setEnabled(false); + } + + + @Override + public boolean isListening() + { + return (isVisible() || isEnabled()); + } + + + @Override + public boolean doesDelegate() + { + return isListening(); + } +} diff --git a/src/mightypork/gamecore/gui/screens/Screen.java b/src/mightypork/gamecore/gui/screens/Screen.java new file mode 100644 index 0000000..29d659a --- /dev/null +++ b/src/mightypork/gamecore/gui/screens/Screen.java @@ -0,0 +1,153 @@ +package mightypork.gamecore.gui.screens; + + +import mightypork.gamecore.core.App; +import mightypork.gamecore.graphics.Renderable; +import mightypork.gamecore.gui.events.LayoutChangeEvent; +import mightypork.gamecore.gui.events.LayoutChangeListener; +import mightypork.gamecore.input.KeyBinder; +import mightypork.gamecore.input.KeyBindingPool; +import mightypork.gamecore.input.KeyStroke; +import mightypork.gamecore.input.Trigger; +import mightypork.utils.annotations.Stub; +import mightypork.utils.eventbus.clients.BusNode; +import mightypork.utils.math.constraints.rect.Rect; +import mightypork.utils.math.constraints.rect.RectBound; + + +/** + * Screen class. + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class Screen extends BusNode implements Renderable, RectBound, KeyBinder, LayoutChangeListener { + + private final KeyBindingPool keybindings = new KeyBindingPool(); + + private volatile boolean active; + private volatile boolean needSetupViewport = false; + + + public Screen() { + + // disable events initially + setListening(false); + + addChildClient(keybindings); + } + + + private void fireLayoutChangeEvent() + { + App.bus().sendDirectToChildren(this, new LayoutChangeEvent()); + } + + + @Override + public final void bindKey(KeyStroke stroke, Trigger edge, Runnable task) + { + keybindings.bindKey(stroke, edge, task); + } + + + @Override + public final void unbindKey(KeyStroke stroke) + { + keybindings.unbindKey(stroke); + } + + + /** + * Prepare for being shown + * + * @param shown true to show, false to hide + */ + public final void setActive(boolean shown) + { + if (shown) { + active = true; + needSetupViewport = true; + + fireLayoutChangeEvent(); + onScreenEnter(); + + // enable events + setListening(true); + + } else { + onScreenLeave(); + + active = false; + + // disable events + setListening(false); + } + } + + + /** + * @return true if screen is the current screen + */ + public final boolean isActive() + { + return active; + } + + + @Override + public void onLayoutChanged() + { + if (!isActive()) return; + + needSetupViewport = true; + } + + + @Override + public final Rect getRect() + { + return App.gfx().getRect(); + } + + + @Override + public void render() + { + if (!isActive()) return; + + if (needSetupViewport) { + App.gfx().setupProjection(); + } + + App.gfx().pushState(); + + renderScreen(); + + App.gfx().popState(); + } + + + /** + * Called when the screen becomes active + */ + @Stub + protected void onScreenEnter() + { + } + + + /** + * Called when the screen is no longer active + */ + @Stub + protected void onScreenLeave() + { + } + + + /** + * Render screen contents (context is ready for 2D rendering) + */ + protected abstract void renderScreen(); + +} diff --git a/src/mightypork/gamecore/gui/screens/ScreenLayer.java b/src/mightypork/gamecore/gui/screens/ScreenLayer.java new file mode 100644 index 0000000..6a7c7af --- /dev/null +++ b/src/mightypork/gamecore/gui/screens/ScreenLayer.java @@ -0,0 +1,50 @@ +package mightypork.gamecore.gui.screens; + + +import mightypork.utils.annotations.Stub; + + +/** + * Screen display layer + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class ScreenLayer extends Overlay { + + private final Screen screen; + + + /** + * @param screen parent screen + */ + public ScreenLayer(Screen screen) { + this.screen = screen; + } + + + /** + * @return parent screen instance + */ + protected final Screen getScreen() + { + return screen; + } + + + /** + * Called when the screen becomes active + */ + @Stub + protected void onScreenEnter() + { + } + + + /** + * Called when the screen is no longer active + */ + @Stub + protected void onScreenLeave() + { + } +} diff --git a/src/mightypork/gamecore/gui/screens/ScreenRegistry.java b/src/mightypork/gamecore/gui/screens/ScreenRegistry.java new file mode 100644 index 0000000..80b3ac7 --- /dev/null +++ b/src/mightypork/gamecore/gui/screens/ScreenRegistry.java @@ -0,0 +1,106 @@ +package mightypork.gamecore.gui.screens; + + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeSet; + +import mightypork.gamecore.core.App; +import mightypork.gamecore.graphics.Renderable; +import mightypork.gamecore.gui.events.LayoutChangeEvent; +import mightypork.gamecore.gui.events.ScreenRequestListener; +import mightypork.gamecore.gui.events.ViewportChangeEvent; +import mightypork.gamecore.gui.events.ViewportChangeListener; +import mightypork.utils.eventbus.clients.BusNode; +import mightypork.utils.logging.Log; + + +/** + * Game screens holder; Takes care of rendering and screen requests. + * + * @author Ondřej Hruška (MightyPork) + */ +public class ScreenRegistry extends BusNode implements ScreenRequestListener, ViewportChangeListener, Renderable { + + private final Map screens = new HashMap<>(); + private final Collection overlays = new TreeSet<>(); + private volatile Screen active = null; + + + /** + * Add a screen + * + * @param name screen key for calling + * @param screen added screen + */ + public void addScreen(String name, Screen screen) + { + screens.put(name, screen); + addChildClient(screen); + } + + + /** + * Add an overlay + * + * @param overlay added overlay + */ + public void addOverlay(Overlay overlay) + { + overlays.add(overlay); + addChildClient(overlay); + } + + + @Override + public void showScreen(String key) + { + Log.f3("Request to show screen \"" + key + "\""); + + // find screen to show + final Screen toShow = screens.get(key); + if (toShow == null) { + throw new RuntimeException("Screen " + key + " not defined."); + } + + // deactivate last screen + if (active != null) { + active.setActive(false); + } + + // activate new screen + toShow.setActive(true); + + active = toShow; + + fireLayoutUpdateEvent(); + } + + + @Override + public void render() + { + if (active != null) { + active.render(); + + for (final Overlay overlay : overlays) { + if (overlay.isVisible()) overlay.render(); + } + } + } + + + @Override + public void onViewportChanged(ViewportChangeEvent event) + { + if (active != null) fireLayoutUpdateEvent(); + } + + + private void fireLayoutUpdateEvent() + { + App.bus().sendDirectToChildren(this, new LayoutChangeEvent()); + } + +} diff --git a/src/mightypork/gamecore/gui/screens/impl/CrossfadeOverlay.java b/src/mightypork/gamecore/gui/screens/impl/CrossfadeOverlay.java new file mode 100644 index 0000000..b89179e --- /dev/null +++ b/src/mightypork/gamecore/gui/screens/impl/CrossfadeOverlay.java @@ -0,0 +1,83 @@ +package mightypork.gamecore.gui.screens.impl; + + +import mightypork.gamecore.core.App; +import mightypork.gamecore.gui.components.painters.QuadPainter; +import mightypork.gamecore.gui.events.ScreenRequest; +import mightypork.gamecore.gui.screens.Overlay; +import mightypork.utils.math.animation.Easing; +import mightypork.utils.math.animation.NumAnimated; +import mightypork.utils.math.color.pal.RGB; +import mightypork.utils.math.timing.TimedTask; + + +/** + * Overlay used for cross-fading between screens + * + * @author Ondřej Hruška (MightyPork) + */ +public class CrossfadeOverlay extends Overlay { + + private static final double T_IN = 0.4; + private static final double T_OUT = 0.6; + + NumAnimated alpha = new NumAnimated(0); + String requestedScreenName; + + TimedTask revealTask = new TimedTask() { + + @Override + public void run() + { + if (requestedScreenName == null) { + App.shutdown(); + } else { + App.bus().send(new ScreenRequest(requestedScreenName)); + } + alpha.setEasing(Easing.SINE_OUT); + alpha.fadeOut(T_OUT); + } + }; + + + public CrossfadeOverlay() { + final QuadPainter qp = new QuadPainter(RGB.BLACK); // TODO allow custom colors + qp.setRect(root); + root.add(qp); + + updated.add(alpha); + updated.add(revealTask); + + setAlpha(alpha); + } + + + @Override + public int getZIndex() + { + return 10000; // not too high, so app can put something on top + } + + + public void goToScreen(String screen, boolean fromDark) + { + requestedScreenName = screen; + + if (screen == null) { + // going for halt + App.audio().fadeOutAllLoops(); + } + + if (fromDark) { + alpha.setTo(1); + revealTask.run(); + } else { + revealTask.start(T_IN); + + alpha.setEasing(Easing.SINE_IN); + alpha.fadeIn(T_IN); + + } + } + +} diff --git a/src/mightypork/gamecore/gui/screens/impl/CrossfadeRequest.java b/src/mightypork/gamecore/gui/screens/impl/CrossfadeRequest.java new file mode 100644 index 0000000..29982e2 --- /dev/null +++ b/src/mightypork/gamecore/gui/screens/impl/CrossfadeRequest.java @@ -0,0 +1,45 @@ +package mightypork.gamecore.gui.screens.impl; + + +import mightypork.utils.eventbus.BusEvent; +import mightypork.utils.eventbus.events.flags.SingleReceiverEvent; + + +/** + * @author Ondřej Hruška (MightyPork) + */ +@SingleReceiverEvent +public class CrossfadeRequest extends BusEvent { + + private final String screen; + private final boolean fromDark; + + + /** + * @param screen screen key to show. Null = exit the app. + * @param fromDark true to fade from full black (ie. start of the game) + */ + public CrossfadeRequest(String screen, boolean fromDark) { + super(); + this.screen = screen; + this.fromDark = fromDark; + } + + + /** + * @param screen screen key to show. Null = exit the app. + */ + public CrossfadeRequest(String screen) { + super(); + this.screen = screen; + this.fromDark = false; + } + + + @Override + public void handleBy(CrossfadeOverlay handler) + { + handler.goToScreen(screen, fromDark); + } + +} diff --git a/src/mightypork/gamecore/gui/screens/impl/FadingLayer.java b/src/mightypork/gamecore/gui/screens/impl/FadingLayer.java new file mode 100644 index 0000000..82e87cc --- /dev/null +++ b/src/mightypork/gamecore/gui/screens/impl/FadingLayer.java @@ -0,0 +1,151 @@ +package mightypork.gamecore.gui.screens.impl; + + +import mightypork.gamecore.gui.screens.Screen; +import mightypork.gamecore.gui.screens.ScreenLayer; +import mightypork.utils.annotations.Stub; +import mightypork.utils.math.animation.Easing; +import mightypork.utils.math.animation.NumAnimated; +import mightypork.utils.math.timing.TimedTask; + + +/** + * Layer that smoothly appears/disappears when shown/hidden + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class FadingLayer extends ScreenLayer { + + private final NumAnimated numa; + private final TimedTask hideTimer = new TimedTask() { + + @Override + public void run() + { + FadingLayer.super.hide(); + fadingOut = false; + onHideFinished(); + } + }; + + private final TimedTask showTimer = new TimedTask() { + + @Override + public void run() + { + fadingIn = false; + onShowFinished(); + } + }; + + private boolean fadingIn = false; + private boolean fadingOut = false; + + + /** + * Create with default fading time and effect + * + * @param screen + */ + public FadingLayer(Screen screen) { + this(screen, new NumAnimated(1, Easing.QUADRATIC_OUT, 0.3)); + } + + + /** + * @param screen + * @param easingAnim the animation num + */ + public FadingLayer(Screen screen, NumAnimated easingAnim) { + super(screen); + + numa = easingAnim; + + updated.add(numa); + updated.add(hideTimer); + updated.add(showTimer); + + setAlpha(numa); + } + + + /** + * Called after the fade-out was completed + */ + @Stub + protected void onHideFinished() + { + } + + + /** + * Called after the fade-in was completed + */ + @Stub + protected void onShowFinished() + { + } + + + /** + * Show with fading + */ + @Override + public void show() + { + if (fadingIn) return; + + if (!isVisible() || fadingOut) { + super.show(); + numa.fadeIn(); + hideTimer.stop(); + showTimer.start(numa.getDefaultDuration()); + + fadingOut = false; + fadingIn = true; + } + } + + + /** + * Hide without fading + */ + public void hideImmediate() + { + hideTimer.stop(); + numa.setTo(0); + super.hide(); + onHideFinished(); + } + + + /** + * Show without fading + */ + public void showImmediate() + { + hideTimer.stop(); + numa.setTo(1); + super.show(); + onShowFinished(); + } + + + /** + * Hide with fading + */ + @Override + public void hide() + { + if (fadingOut) return; + + if (isVisible()) { + numa.fadeOut(); + hideTimer.start(numa.getDefaultDuration()); + + fadingOut = true; + fadingIn = false; + } + } + +} diff --git a/src/mightypork/gamecore/gui/screens/impl/LayerColor.java b/src/mightypork/gamecore/gui/screens/impl/LayerColor.java new file mode 100644 index 0000000..b1b67a2 --- /dev/null +++ b/src/mightypork/gamecore/gui/screens/impl/LayerColor.java @@ -0,0 +1,31 @@ +package mightypork.gamecore.gui.screens.impl; + + +import mightypork.gamecore.gui.components.painters.QuadPainter; +import mightypork.gamecore.gui.screens.Screen; +import mightypork.gamecore.gui.screens.ScreenLayer; +import mightypork.utils.math.color.Color; + + +public class LayerColor extends ScreenLayer { + + private final int zIndex; + + + public LayerColor(Screen screen, Color color, int zIndex) { + super(screen); + + final QuadPainter qp = new QuadPainter(color); + qp.setRect(root); + root.add(qp); + this.zIndex = zIndex; + } + + + @Override + public int getZIndex() + { + return this.zIndex; + } + +} diff --git a/src/mightypork/gamecore/input/InputModule.java b/src/mightypork/gamecore/input/InputModule.java new file mode 100644 index 0000000..ce83ec9 --- /dev/null +++ b/src/mightypork/gamecore/input/InputModule.java @@ -0,0 +1,101 @@ +package mightypork.gamecore.input; + + +import mightypork.gamecore.core.BackendModule; +import mightypork.utils.math.constraints.vect.Vect; + + +/** + * Abstract input module.
+ * An input module takes care of dispatching mouse and keyboard events, provides + * access to mouse position, key states etc.
+ * The input module also takes care of calling App.shutdown() when the user + * requests exit (eg. clicks the titlebar close button) + * + * @author Ondřej Hruška (MightyPork) + */ +public abstract class InputModule extends BackendModule implements KeyBinder { + + protected KeyBindingPool keybindings; + + + @Override + public final void init() + { + initKeyCodes(); + initDevices(); + + keybindings = new KeyBindingPool(); + addChildClient(keybindings); + } + + + /** + * Initialize key codes for keys in {@link Keys} + */ + protected abstract void initKeyCodes(); + + + /** + * Initialize input devices (set up infrastructure for getting the input) + */ + protected abstract void initDevices(); + + + @Override + public void bindKey(KeyStroke stroke, Trigger edge, Runnable task) + { + keybindings.bindKey(stroke, edge, task); + } + + + @Override + public void unbindKey(KeyStroke stroke) + { + keybindings.unbindKey(stroke); + } + + + /** + * Get absolute mouse position. Should always return the same Vect instance + * (use a VectVar or similar). + * + * @return mouse position + */ + public abstract Vect getMousePos(); + + + /** + * Check if mouse is inside window + * + * @return true if mouse is inside window. + */ + public abstract boolean isMouseInside(); + + + /** + * Trap mouse cursor in the window / release it + * + * @param grab true to grab, false to release + */ + public abstract void grabMouse(boolean grab); + + + /** + * Check if key is down. The key comes from the Keys class, so the code is + * the one assigned in initKeyCodes() + * + * @param key key to check + * @return is down + */ + public abstract boolean isKeyDown(Key key); + + + /** + * Check mouse button state + * + * @param button button to test (0 left, 1 right, 2 middle, 3,4,5... extra) + * @return true if the button exists and is down + */ + public abstract boolean isMouseButtonDown(int button); +} diff --git a/src/mightypork/gamecore/input/Key.java b/src/mightypork/gamecore/input/Key.java new file mode 100644 index 0000000..66b6695 --- /dev/null +++ b/src/mightypork/gamecore/input/Key.java @@ -0,0 +1,114 @@ +package mightypork.gamecore.input; + + +import java.util.HashSet; +import java.util.Set; + +import mightypork.gamecore.core.App; + + +/** + * Abstraction above a physical keyboard key.
+ * Provides name, aliases, and the {@link InputModule} may assign it a numeric + * code that corresponds to the underlying keyboard system. + * + * @author Ondřej Hruška (MightyPork) + */ +public class Key { + + private int code = -1; + private final String name; + private final Set aliases = new HashSet<>(1); + + + /** + * Create a key. Note that both name and aliases are converted to uppercase, + * and all underscores are ignored when the aliases are matched. + * + * @param name key name (primary alias) + * @param aliases extra aliases (used for matching) + */ + public Key(String name, String... aliases) { + + // assign name and aliases, converting both to uppercase + + this.name = name; + this.aliases.add(prepareForMatch(name)); + + for (String al : aliases) { + this.aliases.add(prepareForMatch(al)); + } + } + + + public boolean isDown() + { + return App.input().isKeyDown(this); + } + + + /** + * Set a key code. This can be used by the {@link InputModule} to store a + * numeric code in the key. + * + * @param code a code to assign + */ + public void setCode(int code) + { + this.code = code; + } + + + /** + * Check if the provided alias matches this key.
+ * Both the primary alias and the extra aliases are considered. + * + * @param alias + * @return true if matches (this is the key) + */ + public boolean matches(String alias) + { + if (alias == null) return false; + return aliases.contains(prepareForMatch(alias)); + } + + + private String prepareForMatch(String matched) + { + return matched.toUpperCase().replace("_", ""); + } + + + /** + * Get key name (primary alias). + * + * @return name (uppercase) + */ + public String getName() + { + return name; + } + + + /** + * Get the numeric code assigned to this key. If none is assigned, the value + * is -1. + * + * @return numeric key code. + */ + public int getCode() + { + return code; + } + + + /** + * Get if this key is not a NONE or undefined key. + * + * @return true if the key is defined. + */ + public boolean isDefined() + { + return code > 0; + } +} diff --git a/src/mightypork/gamecore/input/KeyBinder.java b/src/mightypork/gamecore/input/KeyBinder.java new file mode 100644 index 0000000..9730e0e --- /dev/null +++ b/src/mightypork/gamecore/input/KeyBinder.java @@ -0,0 +1,31 @@ +package mightypork.gamecore.input; + + +import mightypork.gamecore.gui.Action; + + +/** + * Can bind events to keys. + * + * @author Ondřej Hruška (MightyPork) + */ +public interface KeyBinder { + + /** + * Bind handler to a keystroke, replace current handler if any + * + * @param edge trigger edge + * @param stroke trigger keystroke + * @param task handler; can be {@link Runnable} or {@link Action} + */ + void bindKey(KeyStroke stroke, Trigger edge, Runnable task); + + + /** + * Remove handler from a keystroke (id any) + * + * @param stroke stroke + */ + void unbindKey(KeyStroke stroke); + +} diff --git a/src/mightypork/gamecore/input/KeyBinding.java b/src/mightypork/gamecore/input/KeyBinding.java new file mode 100644 index 0000000..0e5baec --- /dev/null +++ b/src/mightypork/gamecore/input/KeyBinding.java @@ -0,0 +1,70 @@ +package mightypork.gamecore.input; + + +import mightypork.gamecore.input.events.KeyEvent; +import mightypork.gamecore.input.events.KeyEventHandler; + + +/** + * Key binding, trigger activated by a keystroke event + * + * @author Ondřej Hruška (MightyPork) + */ +public class KeyBinding implements KeyEventHandler { + + private final KeyStroke keystroke; + private Runnable handler; + private final Trigger edge; + private boolean wasDown = false; + + + /** + * @param edge trigger edge + * @param stroke trigger keystroke + * @param handler action + */ + public KeyBinding(KeyStroke stroke, Trigger edge, Runnable handler) { + this.keystroke = stroke; + this.handler = handler; + this.edge = edge; + wasDown = stroke.isDown(); + } + + + /** + * Check for equality of keystroke + * + * @param stroke other keystroke + * @return true if keystrokes are equal (cannot co-exist) + */ + public boolean matches(KeyStroke stroke) + { + return this.keystroke.equals(stroke); + } + + + /** + * @param handler event handler + */ + public void setHandler(Runnable handler) + { + this.handler = handler; + } + + + @Override + public void receive(KeyEvent event) + { + final boolean nowDown = keystroke.isDown(); + + boolean trigger = false; + trigger |= (edge == Trigger.FALLING && (!wasDown && nowDown)); + trigger |= (edge == Trigger.RISING && (wasDown && !nowDown)); + wasDown = nowDown; + + // run handler when event was met + if (trigger) { + handler.run(); + } + } +} diff --git a/src/mightypork/gamecore/input/KeyBindingPool.java b/src/mightypork/gamecore/input/KeyBindingPool.java new file mode 100644 index 0000000..2abcc23 --- /dev/null +++ b/src/mightypork/gamecore/input/KeyBindingPool.java @@ -0,0 +1,71 @@ +package mightypork.gamecore.input; + + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import mightypork.gamecore.input.events.KeyEvent; +import mightypork.gamecore.input.events.KeyEventHandler; +import mightypork.utils.logging.Log; + + +/** + * Key binding pool + * + * @author Ondřej Hruška (MightyPork) + */ +public class KeyBindingPool implements KeyBinder, KeyEventHandler { + + private final Set bindings = new HashSet<>(); + + + /** + * Bind handler to a keystroke, replace current handler if any + * + * @param stroke trigger keystroke + * @param task handler + */ + @Override + public void bindKey(KeyStroke stroke, Trigger edge, Runnable task) + { + for (final KeyBinding kb : bindings) { + if (kb.matches(stroke)) { + Log.w("Duplicate KeyBinding (" + stroke + "), replacing handler."); + kb.setHandler(task); + return; + } + } + + bindings.add(new KeyBinding(stroke, edge, task)); + } + + + /** + * Remove handler from keystroke (id any) + * + * @param stroke stroke + */ + @Override + public void unbindKey(KeyStroke stroke) + { + final Iterator iter = bindings.iterator(); + + while (iter.hasNext()) { + final KeyBinding kb = iter.next(); + if (kb.matches(stroke)) { + iter.remove(); + return; + } + } + } + + + @Override + public void receive(KeyEvent event) + { + for (final KeyBinding kb : bindings) { + kb.receive(event); + } + } +} diff --git a/src/mightypork/gamecore/input/KeyStroke.java b/src/mightypork/gamecore/input/KeyStroke.java new file mode 100644 index 0000000..0f0da13 --- /dev/null +++ b/src/mightypork/gamecore/input/KeyStroke.java @@ -0,0 +1,137 @@ +package mightypork.gamecore.input; + + +import mightypork.utils.string.StringUtil; + + +/** + * Key stroke + * + * @author Ondřej Hruška (MightyPork) + */ +public class KeyStroke { + + private byte mod; + private Key key; + + + /** + * Create a Key Stroke + * + * @param key key code + * @param modmask modifiers + */ + public KeyStroke(Key key, int modmask) { + setTo(key, modmask); + } + + + /** + * Change to...
+ * (KeyStroke is mutable, so that upon changing it in Config, all existing + * key bindings are updated automatically.) + * + * @param key key code + * @param modmask modifiers + */ + public void setTo(Key key, int modmask) + { + this.key = key; + this.mod = (byte) (modmask | Keys.keyToMod(key)); // for mods alone + } + + + /** + * Create a new keystroke without modifiers + * + * @param key key + */ + public KeyStroke(Key key) { + this(key, Keys.MOD_NONE); + } + + + /** + * Get if the key is down and modifiers match + * + * @return true if the key is currently down & modifiers match + */ + public boolean isDown() + { + return key.isDown() && (Keys.getActiveMod() == mod); + } + + + public String saveToString() + { + return Keys.modToString(mod) + "+" + key.getName(); + } + + + public static KeyStroke createFromString(String dataString) + { + final KeyStroke ks = new KeyStroke(Keys.NONE, Keys.MOD_NONE); + ks.loadFromString(dataString); + return ks; + } + + + public void loadFromString(String dataString) + { + final String dataString1 = dataString.toUpperCase().replace('-', '+').replaceAll("[^A-Z0-9_+]", ""); + + if (dataString1.contains("+")) { + + final String keyStr = StringUtil.fromLastChar(dataString1, '+'); + final String modStr = StringUtil.toLastChar(dataString1, '+'); + + setTo(Keys.stringToKey(keyStr), Keys.stringToMod(modStr)); + + } else { + setTo(Keys.stringToKey(dataString1), Keys.MOD_NONE); + } + } + + + public Key getKey() + { + return key; + } + + + public byte getMod() + { + return mod; + } + + + @Override + public String toString() + { + return saveToString(); + } + + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + key.getCode(); + result = prime * result + mod; + return result; + } + + + @Override + public boolean equals(Object obj) + { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + final KeyStroke other = (KeyStroke) obj; + if (key.getCode() != other.key.getCode()) return false; + if (mod != other.mod) return false; + return true; + } +} diff --git a/src/mightypork/gamecore/input/Keys.java b/src/mightypork/gamecore/input/Keys.java new file mode 100644 index 0000000..957c22f --- /dev/null +++ b/src/mightypork/gamecore/input/Keys.java @@ -0,0 +1,374 @@ +package mightypork.gamecore.input; + + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import mightypork.gamecore.core.App; +import mightypork.utils.logging.Log; + + +/** + * Key constants & translation table. + * + * @author Ondřej Hruška (MightyPork) + */ +public class Keys { + + //@formatter:off + public static final Key NONE = new Key("NONE", "NULL"); + + public static final Key NUM_0 = new Key("0", "ZERO"); + public static final Key NUM_1 = new Key("1", "ONE"); + public static final Key NUM_2 = new Key("2", "TWO"); + public static final Key NUM_3 = new Key("3", "THREE"); + public static final Key NUM_4 = new Key("4", "FOUR"); + public static final Key NUM_5 = new Key("5", "FIVE"); + public static final Key NUM_6 = new Key("6", "SIX"); + public static final Key NUM_7 = new Key("7", "SEVEN"); + public static final Key NUM_8 = new Key("8", "EIGHT"); + public static final Key NUM_9 = new Key("9", "NINE"); + + public static final Key Q = new Key("Q"); + public static final Key W = new Key("W"); + public static final Key E = new Key("E"); + public static final Key R = new Key("R"); + public static final Key T = new Key("T"); + public static final Key Y = new Key("Y"); + public static final Key U = new Key("U"); + public static final Key I = new Key("I"); + public static final Key O = new Key("O"); + public static final Key P = new Key("P"); + public static final Key A = new Key("A"); + public static final Key S = new Key("S"); + public static final Key D = new Key("D"); + public static final Key F = new Key("F"); + public static final Key G = new Key("G"); + public static final Key H = new Key("H"); + public static final Key J = new Key("J"); + public static final Key K = new Key("K"); + public static final Key L = new Key("L"); + public static final Key Z = new Key("Z"); + public static final Key X = new Key("X"); + public static final Key C = new Key("C"); + public static final Key V = new Key("V"); + public static final Key B = new Key("B"); + public static final Key N = new Key("N"); + public static final Key M = new Key("M"); + + public static final Key MINUS = new Key("MINUS", "DASH"); + public static final Key EQUALS = new Key("EQUALS"); + public static final Key SLASH = new Key("SLASH"); + public static final Key BACKSLASH = new Key("BACKSLASH"); + public static final Key BRACKET_LEFT = new Key("LBRACKET", "LEFT_BRACKET"); + public static final Key BRACKET_RIGHT = new Key("RBRACKET", "RIGHT_BRACKET"); + public static final Key SEMICOLON = new Key("SEMICOLON"); + public static final Key APOSTROPHE = new Key("APOSTROPHE", "APOS"); + public static final Key GRAVE = new Key("GRAVE", "ACCENT"); + public static final Key COMMA = new Key("COMMA"); + public static final Key PERIOD = new Key("PERIOD", "DOT", "POINT"); + + public static final Key SPACE = new Key("SPACE", "SPACEBAR"); + public static final Key BACKSPACE = new Key("BACKSPACE", "BACK"); + public static final Key TAB = new Key("TAB", "TABULATOR", "INDENT"); + public static final Key ESCAPE = new Key("ESC", "ESCAPE"); + + // those probably can't be used + public static final Key APPS = new Key("APPS"); + public static final Key POWER = new Key("POWER"); + public static final Key SLEEP = new Key("SLEEP"); + public static final Key MENU = new Key("MENU"); + + public static final Key F1 = new Key("F1"); + public static final Key F2 = new Key("F2"); + public static final Key F3 = new Key("F3"); + public static final Key F4 = new Key("F4"); + public static final Key F5 = new Key("F5"); + public static final Key F6 = new Key("F6"); + public static final Key F7 = new Key("F7"); + public static final Key F8 = new Key("F8"); + public static final Key F9 = new Key("F9"); + public static final Key F10 = new Key("F10"); + public static final Key F11 = new Key("F11"); + public static final Key F12 = new Key("F12"); + public static final Key F13 = new Key("F13"); + public static final Key F14 = new Key("F14"); + public static final Key F15 = new Key("F15"); + + // probably not possible to bind to those. + public static final Key CAPS_LOCK = new Key("CAPSLOCK", "CAPS", "CAPITAL"); + public static final Key SCROLL_LOCK = new Key("SCROLL", "SCROLL_LOCK"); + public static final Key NUM_LOCK = new Key("NUMLOCK"); + + public static final Key NUMPAD_MINUS = new Key("SUBTRACT", "NUMPAD_MINUS", "NUMPAD_SUBTRACT"); + public static final Key NUMPAD_PLUSS = new Key("ADD", "NUMPAD_PLUS", "NUMPAD_ADD"); + public static final Key NUMPAD_0 = new Key("NUMPAD_0"); + public static final Key NUMPAD_1 = new Key("NUMPAD_1"); + public static final Key NUMPAD_2 = new Key("NUMPAD_2"); + public static final Key NUMPAD_3 = new Key("NUMPAD_3"); + public static final Key NUMPAD_4 = new Key("NUMPAD_4"); + public static final Key NUMPAD_5 = new Key("NUMPAD_5"); + public static final Key NUMPAD_6 = new Key("NUMPAD_6"); + public static final Key NUMPAD_7 = new Key("NUMPAD_7"); + public static final Key NUMPAD_8 = new Key("NUMPAD_8"); + public static final Key NUMPAD_9 = new Key("NUMPAD_9"); + public static final Key NUMPAD_DECIMAL = new Key("DECIMAL", "NUMPAD_DECIMAL", "NUMPAD_PERIOD", "NUMPAD_POINT"); + public static final Key NUMPAD_ENTER = new Key("NUMPAD_ENTER", "NUMPADRETURN", "NUMPAD_RETURN"); + public static final Key NUMPAD_DIVIDE = new Key("DIVIDE", "NUMPAD_DIVIDE", "NUMPAD_SLASH"); + public static final Key NUMPAD_MULTIPLY = new Key("MULTIPLY", "NUMPAD_MULTIPLY", "NUMPAD_ASTERISK"); + + public static final Key CONTROL_LEFT = new Key("LCONTROL", "LEFT_CONTROL", "LCTRL", "LEFT_CTRL"); + public static final Key CONTROL_RIGHT = new Key("RCONTROL", "RIGHT_CONTROL", "RCTRL", "RIGHT_CTRL"); + public static final Key ALT_LEFT = new Key("LALT", "LMENU", "LEFT_MENU"); + public static final Key ALT_RIGHT = new Key("RALT", "RMENU", "RIGHT_MENU"); + public static final Key SHIFT_LEFT = new Key("LSHIFT", "LEFT_SHIFT"); + public static final Key SHIFT_RIGHT = new Key("RSHIFT", "RIGHT_SHIFT"); + public static final Key META_LEFT = new Key("LMETA", "LEFT_META", "LWIN", "LEFT_WIN"); + public static final Key META_RIGHT = new Key("RMETA", "RIGHT_META", "RWIN", "RIGHT_WIN"); + + public static final Key UP = new Key("UP", "ARROW_UP"); + public static final Key DOWN = new Key("DOWN", "ARROW_DOWN"); + public static final Key LEFT = new Key("LEFT", "ARROW_LEFT"); + public static final Key RIGHT = new Key("RIGHT", "ARROW_RIGHT"); + + public static final Key HOME = new Key("HOME"); + public static final Key END = new Key("END"); + + public static final Key PAGE_UP = new Key("PAGE_UP", "PGUP", "PRIOR"); + public static final Key PAGE_DOWN = new Key("PAGE_DOWN", "PGDN", "NEXT"); + + public static final Key RETURN = new Key("ENTER", "RETURN", "CR"); + public static final Key PAUSE = new Key("PAUSE", "BREAK"); + public static final Key INSERT = new Key("INSERT"); + public static final Key DELETE = new Key("DELETE"); + public static final Key SYSRQ = new Key("SYSRQ"); // wtf is this anyway? + + // here go modifier bits + public static final byte MOD_NONE = 0; + public static final byte MOD_ALT = 1; + public static final byte MOD_CONTROL = 2; + public static final byte MOD_SHIFT = 4; + public static final byte MOD_META = 8; + //@formatter:on + + private static Map lookupByCode = new HashMap<>(100); + private static List keyList = new ArrayList<>(100); + + static { + // define none key + NONE.setCode(0); + + // Use reflection to find keys + Field[] fields = Keys.class.getFields(); + try { + for (Field field : fields) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers) && Modifier.isFinal(modifiers) && field.getType().equals(Key.class)) { + + keyList.add((Key) field.get(null)); + } + } + } catch (Exception e) {} + } + + + /** + * Build lookup table by key codes + */ + private static void buildCodeLookupTable() + { + lookupByCode.clear(); + + lookupByCode.put(NONE.getCode(), NONE); + + for (Key k : keyList) { + if (!k.isDefined()) continue; + if (!lookupByCode.containsKey(k.getCode())) { + lookupByCode.put(k.getCode(), k); + } + } + + if (lookupByCode.size() == 1) { + // NONE alone + Log.w("Key codes are not ininitialized."); + } + } + + + /** + * Convert a key name to a key code. + * + * @param keyStr key name + * @return the key, or NONE if none matches + */ + public static Key stringToKey(String keyStr) + { + for (Key k : keyList) { + if (k.matches(keyStr)) return k; + } + + Log.w("No such key: " + keyStr); + + return NONE; + } + + + /** + * Convert a mod description to a mod mask. A mod description is a string + * containing CTRL,ALT,SHIFT,META, as in CTRL+ALT.
+ * If none of the mod identifiers are found in the description, a MOD_NONE + * is returned.
+ * This method is used for parsing keystroke, together with nameToKey(). + * + * @param modStr mod description (eg. CTRL+ALT) + * @return mod mask + */ + public static int stringToMod(String modStr) + { + int mod_mask = MOD_NONE; + + modStr = modStr.toUpperCase(); + + if (modStr.contains("CTRL")) { + mod_mask |= MOD_CONTROL; + } + + if (modStr.contains("ALT")) { + mod_mask |= MOD_ALT; + } + + if (modStr.contains("SHIFT")) { + mod_mask |= MOD_SHIFT; + } + + if (modStr.contains("META") || modStr.contains("WIN")) { + mod_mask |= MOD_META; + } + + return mod_mask; + } + + + /** + * Convert a mod mask to a mod description, in a format recognized by + * stringToMod() - joining mods by +. + * + * @param modMask mod mask + * @return mods as string (CTRL+ALT) + */ + public static String modToString(int modMask) + { + String s = ""; + + if ((modMask & MOD_CONTROL) != 0) { + s += "CTRL"; + } + + if ((modMask & MOD_ALT) != 0) { + if (!s.isEmpty()) s += "+"; + s += "ALT"; + } + + if ((modMask & MOD_SHIFT) != 0) { + if (!s.isEmpty()) s += "+"; + s += "SHIFT"; + } + + if ((modMask & MOD_META) != 0) { + if (!s.isEmpty()) s += "+"; + s += "META"; + } + + return s; + } + + + /** + * Get a {@link Key} for key code. + * + * @param keyCode code + * @return key instance, or NONE if no key matches. + */ + public static Key codeToKey(int keyCode) + { + if (lookupByCode.isEmpty()) buildCodeLookupTable(); + + Key k = lookupByCode.get(keyCode); + + if (k == null) { + Log.w("No key for code: " + keyCode); + k = NONE; + } + + return k; + } + + + /** + * Convert a key to mod mask, in case the key is one of the mod keys. + * + * @param key the key + * @return mod mask corresponding to the key + */ + public static int keyToMod(Key key) + { + + if (key == SHIFT_LEFT || key == SHIFT_RIGHT) return MOD_SHIFT; + + if (key == CONTROL_LEFT || key == CONTROL_RIGHT) return MOD_CONTROL; + if (key == ALT_LEFT || key == ALT_RIGHT) return MOD_ALT; + if (key == META_LEFT || key == META_RIGHT) return MOD_META; + + return MOD_NONE; + } + + + /** + * Get if the given key is down (call it's "isDown()" method).
+ * This method is here just for completeness, since the getActiveMod() is + * also here. + * + * @param key the key to check + * @return true if the key is down + */ + public static boolean isKeyDown(Key key) + { + return key.isDown(); + } + + + /** + * Get currently active key modifiers + * + * @return active mod mask (mod bits ored) + */ + public static int getActiveMod() + { + int mods = 0; + + InputModule inp = App.input(); + + if (inp.isKeyDown(Keys.ALT_LEFT) || inp.isKeyDown(Keys.ALT_RIGHT)) { + mods |= Keys.MOD_ALT; + } + + if (inp.isKeyDown(Keys.SHIFT_LEFT) || inp.isKeyDown(Keys.SHIFT_RIGHT)) { + mods |= Keys.MOD_SHIFT; + } + + if (inp.isKeyDown(Keys.CONTROL_LEFT) || inp.isKeyDown(Keys.CONTROL_RIGHT)) { + mods |= Keys.MOD_CONTROL; + } + + if (inp.isKeyDown(Keys.META_LEFT) || inp.isKeyDown(Keys.META_RIGHT)) { + mods |= Keys.MOD_META; + } + + return mods; + } + +} diff --git a/src/mightypork/gamecore/input/Trigger.java b/src/mightypork/gamecore/input/Trigger.java new file mode 100644 index 0000000..5ac5216 --- /dev/null +++ b/src/mightypork/gamecore/input/Trigger.java @@ -0,0 +1,13 @@ +package mightypork.gamecore.input; + + +/** + * Type of keystroke (falling / rising edge) + */ +public enum Trigger +{ + /** Activated by falling edge (press) */ + FALLING, + /** Activated by rising edge (release) */ + RISING; +} diff --git a/src/mightypork/gamecore/input/events/KeyEvent.java b/src/mightypork/gamecore/input/events/KeyEvent.java new file mode 100644 index 0000000..4c97912 --- /dev/null +++ b/src/mightypork/gamecore/input/events/KeyEvent.java @@ -0,0 +1,84 @@ +package mightypork.gamecore.input.events; + + +import mightypork.utils.eventbus.BusEvent; +import mightypork.utils.eventbus.events.flags.NotLoggedEvent; + +import org.lwjgl.input.Keyboard; + + +/** + * A keyboard event + * + * @author Ondřej Hruška (MightyPork) + */ +@NotLoggedEvent +public class KeyEvent extends BusEvent { + + private final int key; + private final boolean down; + private final char c; + + + /** + * @param key key that triggered the event. Can be KEY_NONE. + * @param c typed char (can be zero char) + * @param down true = pressed, false = released. + */ + public KeyEvent(int key, char c, boolean down) { + this.key = key; + this.c = c; + this.down = down; + } + + + /** + * @return key code (see {@link org.lwjgl.input.Keyboard}) + */ + public int getKey() + { + return key; + } + + + /** + * @return true if key was just pressed + */ + public boolean isDown() + { + return down; + } + + + /** + * @return true if key was just released + */ + public boolean isUp() + { + return !down; + } + + + /** + * @return event character (if any) + */ + public char getChar() + { + return c; + } + + + @Override + public void handleBy(KeyEventHandler keh) + { + keh.receive(this); + } + + + @Override + public String toString() + { + return Keyboard.getKeyName(key) + ":" + (down ? "DOWN" : "UP"); + } + +} diff --git a/src/mightypork/gamecore/input/events/KeyEventHandler.java b/src/mightypork/gamecore/input/events/KeyEventHandler.java new file mode 100644 index 0000000..5d781b2 --- /dev/null +++ b/src/mightypork/gamecore/input/events/KeyEventHandler.java @@ -0,0 +1,17 @@ +package mightypork.gamecore.input.events; + + +/** + * {@link KeyEvent} listener + * + * @author Ondřej Hruška (MightyPork) + */ +public interface KeyEventHandler { + + /** + * Handle an event + * + * @param event event + */ + void receive(KeyEvent event); +} diff --git a/src/mightypork/gamecore/input/events/MouseButtonEvent.java b/src/mightypork/gamecore/input/events/MouseButtonEvent.java new file mode 100644 index 0000000..3696afe --- /dev/null +++ b/src/mightypork/gamecore/input/events/MouseButtonEvent.java @@ -0,0 +1,125 @@ +package mightypork.gamecore.input.events; + + +import mightypork.utils.eventbus.BusEvent; +import mightypork.utils.eventbus.events.flags.NotLoggedEvent; +import mightypork.utils.math.constraints.rect.RectBound; +import mightypork.utils.math.constraints.vect.Vect; +import mightypork.utils.math.constraints.vect.VectConst; + + +/** + * Mouse button / wheel event triggered + * + * @author Ondřej Hruška (MightyPork) + */ +@NotLoggedEvent +public class MouseButtonEvent extends BusEvent { + + public static final int BUTTON_LEFT = 0; + public static final int BUTTON_MIDDLE = 1; + public static final int BUTTON_RIGHT = 2; + + private final int button; + private final int wheeld; + private final VectConst pos; + private final boolean down; + + + /** + * Mouse button event + * + * @param pos event position + * @param button button id + * @param down button pressed + * @param wheeld wheel change + */ + public MouseButtonEvent(Vect pos, int button, boolean down, int wheeld) { + this.button = button; + this.down = down; + this.pos = pos.freeze(); + this.wheeld = wheeld; + } + + + /** + * @return true if the event was caused by a button state change + */ + public boolean isButtonEvent() + { + return button != -1; + } + + + /** + * @return true if the event was caused by a wheel change + */ + public boolean isWheelEvent() + { + return wheeld != 0; + } + + + /** + * @return button id or -1 if none was pressed + */ + public int getButton() + { + return button; + } + + + /** + * @return number of steps the wheel changed since last event + */ + public int getWheelDelta() + { + return wheeld; + } + + + /** + * @return mouse position when the event occurred + */ + public VectConst getPos() + { + return pos; + } + + + /** + * @return true if button was just pressed + */ + public boolean isDown() + { + return button != -1 && down; + } + + + /** + * @return true if button was just released + */ + public boolean isUp() + { + return button != -1 && !down; + } + + + /** + * Get if event happened over a rect + * + * @param rect rect region + * @return was over + */ + public boolean isOver(RectBound rect) + { + return rect.getRect().contains(pos); + } + + + @Override + public void handleBy(MouseButtonHandler handler) + { + handler.receive(this); + } +} diff --git a/src/mightypork/gamecore/input/events/MouseButtonHandler.java b/src/mightypork/gamecore/input/events/MouseButtonHandler.java new file mode 100644 index 0000000..6d59f7a --- /dev/null +++ b/src/mightypork/gamecore/input/events/MouseButtonHandler.java @@ -0,0 +1,17 @@ +package mightypork.gamecore.input.events; + + +/** + * {@link MouseButtonEvent} listener + * + * @author Ondřej Hruška (MightyPork) + */ +public interface MouseButtonHandler { + + /** + * Handle an event + * + * @param event event + */ + void receive(MouseButtonEvent event); +} diff --git a/src/mightypork/gamecore/input/events/MouseMotionEvent.java b/src/mightypork/gamecore/input/events/MouseMotionEvent.java new file mode 100644 index 0000000..5131644 --- /dev/null +++ b/src/mightypork/gamecore/input/events/MouseMotionEvent.java @@ -0,0 +1,56 @@ +package mightypork.gamecore.input.events; + + +import mightypork.utils.eventbus.BusEvent; +import mightypork.utils.eventbus.events.flags.NotLoggedEvent; +import mightypork.utils.math.constraints.vect.Vect; +import mightypork.utils.math.constraints.vect.VectConst; + + +/** + * Mouse moved + * + * @author Ondřej Hruška (MightyPork) + */ +@NotLoggedEvent +public class MouseMotionEvent extends BusEvent { + + private final VectConst move; + private final VectConst pos; + + + /** + * @param pos end pos + * @param move move vector + */ + public MouseMotionEvent(Vect pos, Vect move) { + this.move = move.freeze(); + this.pos = pos.freeze(); + } + + + /** + * @return movement since last {@link MouseMotionEvent} + */ + public VectConst getMove() + { + return move; + } + + + /** + * @return current mouse position + */ + public VectConst getPos() + { + return pos; + } + + + @Override + public void handleBy(MouseMotionHandler keh) + { + keh.receive(this); + } + +} diff --git a/src/mightypork/gamecore/input/events/MouseMotionHandler.java b/src/mightypork/gamecore/input/events/MouseMotionHandler.java new file mode 100644 index 0000000..cbdabe7 --- /dev/null +++ b/src/mightypork/gamecore/input/events/MouseMotionHandler.java @@ -0,0 +1,17 @@ +package mightypork.gamecore.input.events; + + +/** + * {@link MouseMotionEvent} listener + * + * @author Ondřej Hruška (MightyPork) + */ +public interface MouseMotionHandler { + + /** + * Handle an event + * + * @param event event + */ + void receive(MouseMotionEvent event); +} diff --git a/src/mightypork/gamecore/resources/BaseDeferredResource.java b/src/mightypork/gamecore/resources/BaseDeferredResource.java new file mode 100644 index 0000000..e0ac880 --- /dev/null +++ b/src/mightypork/gamecore/resources/BaseDeferredResource.java @@ -0,0 +1,135 @@ +package mightypork.gamecore.resources; + + +import java.io.IOException; + +import mightypork.utils.annotations.Alias; +import mightypork.utils.interfaces.Destroyable; +import mightypork.utils.logging.Log; +import mightypork.utils.math.timing.Profiler; +import mightypork.utils.string.StringUtil; + + +/** + * Deferred resource abstraction. + * + * @author Ondřej Hruška (MightyPork) + */ +@Alias(name = "Resource") +public abstract class BaseDeferredResource implements DeferredResource, Destroyable { + + private final String resource; + private volatile boolean loadFailed = false; + private volatile boolean loadAttempted = false; + + + /** + * @param resource resource path / name; this string is later used in + * loadResource() + */ + public BaseDeferredResource(String resource) { + this.resource = resource; + } + + + @Override + public synchronized final void load() + { + if (!loadFailed && loadAttempted) return; + +// +// if (loadFailed) return; +// if (loadAttempted) return; +// + + loadAttempted = true; + loadFailed = false; + + try { + if (resource == null) { + throw new NullPointerException("Resource string cannot be null for non-null resource."); + } + + final long time = Profiler.begin(); + Log.f3("(res) + Load: " + this); + loadResource(resource); + Log.f3("(res) - Done: " + this + " in " + Profiler.endStr(time)); + + } catch (final Throwable t) { + loadFailed = true; + Log.e("(res) Failed to load: " + this, t); + } + } + + + @Override + public synchronized final boolean isLoaded() + { + return loadAttempted && !loadFailed; + } + + + /** + * Check if the resource is loaded; if not, try to do so. + * + * @return true if it's loaded now. + */ + public synchronized final boolean ensureLoaded() + { + if (isLoaded()) { + return true; + } else { + if (loadFailed) return false; + + Log.f3("(res) !! Loading on access: " + this); + load(); + } + + return isLoaded(); + } + + + /** + * Load the resource. Called from load() - once only. + * + * @param resource the path / name of a resource + * @throws IOException when some problem prevented the resource from being + * loaded. + */ + protected abstract void loadResource(String resource) throws IOException; + + + @Override + public abstract void destroy(); + + + @Override + public String toString() + { + return StringUtil.fromLastChar(resource, '/'); + } + + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((resource == null) ? 0 : resource.hashCode()); + return result; + } + + + @Override + public boolean equals(Object obj) + { + if (this == obj) return true; + if (obj == null) return false; + if (!(obj instanceof BaseDeferredResource)) return false; + final BaseDeferredResource other = (BaseDeferredResource) obj; + if (resource == null) { + if (other.resource != null) return false; + } else if (!resource.equals(other.resource)) return false; + return true; + } +} diff --git a/src/mightypork/gamecore/resources/DeferredResource.java b/src/mightypork/gamecore/resources/DeferredResource.java new file mode 100644 index 0000000..4636bee --- /dev/null +++ b/src/mightypork/gamecore/resources/DeferredResource.java @@ -0,0 +1,23 @@ +package mightypork.gamecore.resources; + + +/** + * Deferred resource + * + * @author Ondřej Hruška (MightyPork) + */ +public interface DeferredResource { + + /** + * Load the actual resource, if not loaded yet. + */ + void load(); + + + /** + * Check if resource was successfully loaded. + * + * @return true if already loaded + */ + boolean isLoaded(); +} diff --git a/src/mightypork/gamecore/resources/Res.java b/src/mightypork/gamecore/resources/Res.java new file mode 100644 index 0000000..523be6a --- /dev/null +++ b/src/mightypork/gamecore/resources/Res.java @@ -0,0 +1,100 @@ +package mightypork.gamecore.resources; + + +import mightypork.gamecore.audio.SoundRegistry; +import mightypork.gamecore.audio.players.EffectPlayer; +import mightypork.gamecore.audio.players.LoopPlayer; +import mightypork.gamecore.graphics.fonts.FontRegistry; +import mightypork.gamecore.graphics.fonts.IFont; +import mightypork.gamecore.graphics.textures.ITexture; +import mightypork.gamecore.graphics.textures.TextureRegistry; +import mightypork.gamecore.graphics.textures.TxQuad; +import mightypork.gamecore.graphics.textures.TxSheet; + + +/** + * Static resource repository + * + * @author Ondřej Hruška (MightyPork) + */ +public final class Res { + + private static TextureRegistry textures; + private static SoundRegistry sounds; + private static FontRegistry fonts; + + private static boolean initialized = false; + + + /** + * Load on behalf of given base app + * + * @param app app access + */ + public static void init() + { + if (initialized) return; + initialized = true; + + textures = new TextureRegistry(); + sounds = new SoundRegistry(); + fonts = new FontRegistry(); + } + + + public static ITexture getTexture(String key) + { + return textures.getTexture(key); + } + + + /** + * Get a texture sheet by key + * + * @param key + * @return sheet + */ + public static TxSheet getTxSheet(String key) + { + return textures.getSheet(key); + } + + + /** + * Get a texture quad by key + * + * @param key + * @return quad + */ + public static TxQuad getTxQuad(String key) + { + return textures.getQuad(key); + } + + + public static LoopPlayer getSoundLoop(String key) + { + return sounds.getLoop(key); + } + + + public static EffectPlayer getSoundEffect(String key) + { + return sounds.getEffect(key); + } + + + public static IFont getFont(String key) + { + return fonts.getFont(key); + } + + + public static void load(ResourceSetup binder) + { + binder.addFonts(fonts); + binder.addTextures(textures); + binder.addSounds(sounds); + } + +} diff --git a/src/mightypork/gamecore/resources/ResourceSetup.java b/src/mightypork/gamecore/resources/ResourceSetup.java new file mode 100644 index 0000000..5e7682f --- /dev/null +++ b/src/mightypork/gamecore/resources/ResourceSetup.java @@ -0,0 +1,38 @@ +package mightypork.gamecore.resources; + + +import mightypork.gamecore.audio.SoundRegistry; +import mightypork.gamecore.graphics.fonts.FontRegistry; +import mightypork.gamecore.graphics.textures.TextureRegistry; + + +/** + * Resource binder; used by apps to specify what resources are to be loaded. + * + * @author Ondřej Hruška (MightyPork) + */ +public interface ResourceSetup { + + /** + * Add fonts to load. + * + * @param fonts font registry + */ + void addFonts(FontRegistry fonts); + + + /** + * Add sounds to load. + * + * @param sounds sound registry + */ + void addSounds(SoundRegistry sounds); + + + /** + * Add textures to load + * + * @param textures texture registry + */ + void addTextures(TextureRegistry textures); +} diff --git a/src/mightypork/gamecore/resources/loading/AsyncResourceLoader.java b/src/mightypork/gamecore/resources/loading/AsyncResourceLoader.java new file mode 100644 index 0000000..efd5410 --- /dev/null +++ b/src/mightypork/gamecore/resources/loading/AsyncResourceLoader.java @@ -0,0 +1,126 @@ +package mightypork.gamecore.resources.loading; + + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; + +import mightypork.gamecore.core.App; +import mightypork.gamecore.core.events.MainLoopRequest; +import mightypork.gamecore.resources.DeferredResource; +import mightypork.utils.Reflect; +import mightypork.utils.Support; +import mightypork.utils.interfaces.Destroyable; +import mightypork.utils.logging.Log; + + +/** + * Asynchronous resource loading thread. + * + * @author Ondřej Hruška (MightyPork) + */ +public class AsyncResourceLoader extends Thread implements ResourceLoader, Destroyable { + + private final ExecutorService exs = Executors.newFixedThreadPool(2); + + private final LinkedBlockingQueue toLoad = new LinkedBlockingQueue<>(); + private volatile boolean stopped; + private volatile boolean mainLoopQueuing = true; + + + @Override + public synchronized void init() + { + App.bus().subscribe(this); // FIXME bad + setDaemon(true); + super.start(); + } + + + public void enableMainLoopQueuing(boolean yes) + { + mainLoopQueuing = yes; + } + + + public AsyncResourceLoader() { + super("Deferred loader"); + } + + + @Override + public void loadResource(final DeferredResource resource) + { + if (resource.isLoaded()) return; + + // textures & fonts needs to be loaded in main thread + if (Reflect.hasAnnotation(resource, MustLoadInRenderingContext.class)) { + + if (!mainLoopQueuing) { + // just let it be + } else { + Log.f3("(loader) Delegating to main thread: " + Support.str(resource)); + + App.bus().send(new MainLoopRequest(new Runnable() { + + @Override + public void run() + { + resource.load(); + } + }, false)); + } + + return; + } + + toLoad.add(resource); + } + + + @Override + public void run() + { + Log.f3("Asynchronous resource loader started."); + + while (!stopped) { + + try { + final DeferredResource def = toLoad.take(); + if (def == null) continue; + + if (!def.isLoaded()) { + + Log.f3("(loader) Scheduling... " + Support.str(def)); + + exs.submit(new Runnable() { + + @Override + public void run() + { + if (!def.isLoaded()) { + def.load(); + } + } + }); + } + + } catch (final InterruptedException ignored) { + // + } + + } + } + + + // apparently, destroy method exists on thread :/ + @SuppressWarnings("deprecation") + @Override + public void destroy() + { + Log.f3("Stopping resource loader thread."); + stopped = true; + exs.shutdownNow(); + } + +} diff --git a/src/mightypork/gamecore/resources/loading/MustLoadInRenderingContext.java b/src/mightypork/gamecore/resources/loading/MustLoadInRenderingContext.java new file mode 100644 index 0000000..64497a7 --- /dev/null +++ b/src/mightypork/gamecore/resources/loading/MustLoadInRenderingContext.java @@ -0,0 +1,18 @@ +package mightypork.gamecore.resources.loading; + + +import java.lang.annotation.*; + + +/** + * Resource that is texture-based and therefore needs to be loaded in the main + * thread (ie. main loop). + * + * @author Ondřej Hruška (MightyPork) + */ + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +@Documented +public @interface MustLoadInRenderingContext {} diff --git a/src/mightypork/gamecore/resources/loading/ResourceLoadRequest.java b/src/mightypork/gamecore/resources/loading/ResourceLoadRequest.java new file mode 100644 index 0000000..a940adc --- /dev/null +++ b/src/mightypork/gamecore/resources/loading/ResourceLoadRequest.java @@ -0,0 +1,33 @@ +package mightypork.gamecore.resources.loading; + + +import mightypork.gamecore.resources.DeferredResource; +import mightypork.utils.eventbus.BusEvent; +import mightypork.utils.eventbus.events.flags.SingleReceiverEvent; + + +/** + * Request to load a deferred resource. + * + * @author Ondřej Hruška (MightyPork) + */ +@SingleReceiverEvent +public class ResourceLoadRequest extends BusEvent { + + private final DeferredResource resource; + + + /** + * @param resource resource to load + */ + public ResourceLoadRequest(DeferredResource resource) { + this.resource = resource; + } + + + @Override + public void handleBy(ResourceLoader handler) + { + handler.loadResource(resource); + } +} diff --git a/src/mightypork/gamecore/resources/loading/ResourceLoader.java b/src/mightypork/gamecore/resources/loading/ResourceLoader.java new file mode 100644 index 0000000..b06a669 --- /dev/null +++ b/src/mightypork/gamecore/resources/loading/ResourceLoader.java @@ -0,0 +1,29 @@ +package mightypork.gamecore.resources.loading; + + +import mightypork.gamecore.resources.DeferredResource; + + +/** + * {@link ResourceLoadRequest} listener + * + * @author Ondřej Hruška (MightyPork) + */ +public interface ResourceLoader { + + /** + * Load a resource + * + * @param resource + */ + void loadResource(DeferredResource resource); + + + /** + * Initialize the loader (Join the bus, start a stread etc) + * + * @param app app the loader works for. The event bus must already be + * initialized. + */ + void init(); +}